1- use crate :: model:: { Feed , FeedGroup , Group , Item , ModelExt } ;
2- use crate :: state:: State ;
31use anyhow:: { anyhow, Context , Result } ;
42use async_std:: prelude:: FutureExt ;
3+ use futures:: stream:: { self , StreamExt } ;
4+ use log:: { info, warn} ;
55use prettytable:: { cell, format, row, Table } ;
66use std:: path:: PathBuf ;
77use structopt:: StructOpt ;
88
9+ use crate :: model:: { Feed , FeedGroup , Group , Item , ModelExt } ;
10+ use crate :: opml;
11+ use crate :: remote:: RemoteFeed ;
12+ use crate :: state:: State ;
13+
914#[ derive( Debug , StructOpt ) ]
1015pub enum FeedCommand {
1116 /// Lists all feeds
@@ -23,6 +28,9 @@ pub enum FeedCommand {
2328
2429 /// Crawls a feed manually
2530 Crawl { id : u32 } ,
31+
32+ /// Imports OPML file
33+ Import { file : PathBuf } ,
2634}
2735
2836impl FeedCommand {
@@ -53,26 +61,14 @@ impl FeedCommand {
5361 return Err ( anyhow ! ( "Feed `{}` already exists!" , url) ) ;
5462 }
5563
56- let bytes = surf:: get ( & url)
57- . await
58- . map_err ( |err| anyhow ! ( "unable to fetch {}: {:?}" , & url, err) ) ?
59- . body_bytes ( )
60- . await ?;
61- let raw_feed = feed_rs:: parser:: parse ( & bytes[ ..] ) ?;
64+ let remote = RemoteFeed :: new ( & url) . await ?;
65+
6266 let feed = Feed :: new (
63- raw_feed
64- . title
65- . map ( |t| t. content )
67+ remote
68+ . get_title ( )
6669 . ok_or_else ( || anyhow ! ( "Feed doesn't have a title" ) ) ?,
6770 url. clone ( ) ,
68- raw_feed
69- . links
70- . iter ( )
71- . map ( |l| l. href . as_str ( ) )
72- . filter ( |& link| link != url)
73- . next ( )
74- . map ( |l| l. to_string ( ) )
75- . unwrap_or ( url) ,
71+ remote. get_site_url ( ) . unwrap_or ( url) ,
7672 ) ;
7773 let feed = {
7874 let conn = state. db . get ( ) ?;
@@ -111,12 +107,81 @@ impl FeedCommand {
111107 Ok ( ( ) )
112108 }
113109
110+ async fn import ( state : State , file : PathBuf ) -> Result < ( ) > {
111+ let imports = opml:: from_file ( & file) ?;
112+
113+ let imports: Vec < _ > = stream:: iter ( imports)
114+ . then ( |( group, feeds) | async move {
115+ // normalize feeds
116+ let feeds = stream:: iter ( feeds)
117+ . filter_map ( |mut feed| async move {
118+ if let Err ( e) = feed. update ( ) . await {
119+ warn ! ( "failed to update feed {}: {:?}" , feed, e) ;
120+ }
121+
122+ if let Err ( e) = feed. validate ( ) {
123+ warn ! ( "invalid feed ({}): {:?}" , feed, e) ;
124+ None
125+ } else {
126+ Some ( feed)
127+ }
128+ } )
129+ . map ( Feed :: from)
130+ . collect :: < Vec < Feed > > ( )
131+ . await ;
132+
133+ ( group, feeds)
134+ } )
135+ . collect ( )
136+ . await ;
137+
138+ let conn = state. db . get ( ) ?;
139+ for ( group, feeds) in imports. into_iter ( ) {
140+ let group = group. and_then ( |title| {
141+ if let Ok ( group) = Group :: get_by_name ( & conn, & title) {
142+ Some ( group)
143+ } else {
144+ let group = Group :: new ( title. clone ( ) ) ;
145+ match group. insert ( & conn) {
146+ Ok ( group) => Some ( group) ,
147+ Err ( e) => {
148+ warn ! ( "unable to create group {}: {:?}" , title, e) ;
149+ None
150+ }
151+ }
152+ }
153+ } ) ;
154+
155+ for feed in feeds {
156+ let feed = match feed. insert ( & conn) {
157+ Err ( e) => {
158+ warn ! ( "unable to create feed: {:?}" , e) ;
159+ continue ;
160+ }
161+ Ok ( feed) => feed,
162+ } ;
163+
164+ if let Some ( group) = group. as_ref ( ) {
165+ if let Err ( e) = group. add_feed ( & conn, feed) {
166+ warn ! ( "unable to add feed to group {:?}: {:?}" , group, e) ;
167+ continue ;
168+ }
169+ }
170+ }
171+ }
172+
173+ info ! ( "import completed." ) ;
174+
175+ Ok ( ( ) )
176+ }
177+
114178 async fn run ( self , state : State ) -> Result < ( ) > {
115179 match self {
116180 Self :: List => Self :: list ( state) ,
117181 Self :: Add { url, group } => Self :: add ( state, url, group) . await ,
118182 Self :: Delete { id } => Self :: delete ( state, id) ,
119183 Self :: Crawl { id } => Self :: crawl ( state, id) . await ,
184+ Self :: Import { file } => Self :: import ( state, file) . await ,
120185 }
121186 }
122187}
@@ -260,6 +325,9 @@ pub struct Options {
260325 ) ]
261326 database : PathBuf ,
262327
328+ #[ structopt( long) ]
329+ debug : bool ,
330+
263331 #[ structopt( subcommand) ]
264332 command : SubCommand ,
265333}
@@ -287,6 +355,12 @@ impl Options {
287355 let pool = crate :: model:: get_pool ( & self . database ) ?;
288356 let state = crate :: state:: State :: new ( pool) ;
289357
358+ if self . debug {
359+ femme:: with_level ( log:: LevelFilter :: Debug ) ;
360+ } else {
361+ femme:: with_level ( log:: LevelFilter :: Info ) ;
362+ }
363+
290364 match self . command {
291365 SubCommand :: Feed ( cmd) => cmd. run ( state) . await ,
292366 SubCommand :: Group ( cmd) => cmd. run ( state) . await ,
0 commit comments