2021-06-10 18:16:31 +01:00
use std ::{ fs , num ::NonZeroUsize , path ::Path } ;
2021-02-06 09:59:03 +00:00
2021-04-29 17:58:37 +01:00
use chrono ::{ DateTime , Local } ;
2021-06-01 10:23:22 +01:00
use clap ::{ App , AppSettings , Arg , ArgMatches } ;
2021-06-06 11:20:08 +01:00
use flexi_logger ::LevelFilter as LogLevel ;
2021-06-10 18:16:31 +01:00
use itertools ::Itertools ;
2021-06-06 11:20:08 +01:00
type Error = crate ::errors ::CliError < AppConfigBuilderError > ;
2021-04-29 17:58:37 +01:00
2021-06-01 10:23:22 +01:00
const DEFAULT_MAX_CONN : usize = 8 ;
2020-05-16 08:09:44 +01:00
2021-06-01 10:23:22 +01:00
#[ derive(derive_builder::Builder) ]
pub struct AppConfig {
/// Urls for store in epub
pub urls : Vec < String > ,
pub max_conn : usize ,
/// Path to file of multiple articles into a single epub
pub merged : Option < String > ,
pub output_directory : Option < String > ,
pub log_level : LogLevel ,
pub can_disable_progress_bar : bool ,
pub start_time : DateTime < Local > ,
pub is_logging_to_file : bool ,
2021-06-17 15:32:53 +01:00
pub inline_toc : bool ,
2021-06-01 10:23:22 +01:00
}
impl AppConfig {
pub fn init_with_cli ( ) -> Result < AppConfig , Error > {
let app = App ::new ( " paperoni " )
2020-11-24 06:58:50 +00:00
. settings ( & [
AppSettings ::ArgRequiredElseHelp ,
AppSettings ::UnifiedHelpMessage ,
] )
2021-04-17 10:08:24 +01:00
. version ( clap ::crate_version! ( ) )
2020-11-24 06:58:50 +00:00
. about (
2021-04-30 04:55:02 +01:00
" Paperoni is a CLI tool made in Rust for downloading web articles as EPUBs " ,
2020-11-24 06:58:50 +00:00
)
. arg (
Arg ::with_name ( " urls " )
. help ( " Urls of web articles " )
. multiple ( true ) ,
)
2021-02-01 08:28:07 +00:00
. arg (
Arg ::with_name ( " file " )
. short ( " f " )
. long ( " file " )
. help ( " Input file containing links " )
. takes_value ( true ) ,
2021-02-11 10:51:21 +00:00
)
2021-06-01 10:23:22 +01:00
. arg (
Arg ::with_name ( " output_directory " )
2021-06-08 05:42:30 +01:00
. long ( " output-dir " )
2021-06-01 10:23:22 +01:00
. short ( " o " )
2021-06-06 11:20:08 +01:00
. help ( " Directory to store output epub documents " )
2021-06-01 10:23:22 +01:00
. conflicts_with ( " output_name " )
. takes_value ( true ) ,
)
2021-02-11 10:51:21 +00:00
. arg (
Arg ::with_name ( " output_name " )
. long ( " merge " )
. help ( " Merge multiple articles into a single epub " )
. long_help ( " Merge multiple articles into a single epub that will be given the name provided " )
2021-06-01 10:23:22 +01:00
. conflicts_with ( " output_directory " )
2021-02-11 10:51:21 +00:00
. takes_value ( true ) ,
2021-02-21 09:40:17 +00:00
) . arg (
2021-06-08 05:42:30 +01:00
Arg ::with_name ( " max-conn " )
2021-02-21 09:40:17 +00:00
. long ( " max_conn " )
. help ( " The maximum number of concurrent HTTP connections when downloading articles. Default is 8 " )
. long_help ( " The maximum number of concurrent HTTP connections when downloading articles. Default is 8. \n NOTE: It is advised to use as few connections as needed i.e between 1 and 50. Using more connections can end up overloading your network card with too many concurrent requests. " )
2021-04-24 13:50:43 +01:00
. takes_value ( true ) )
. arg (
2021-04-29 17:58:37 +01:00
Arg ::with_name ( " verbosity " )
. short ( " v " )
. multiple ( true )
2021-04-30 04:55:02 +01:00
. help ( " Enables logging of events and set the verbosity level. Use --help to read on its usage " )
2021-04-29 17:58:37 +01:00
. long_help (
" This takes upto 4 levels of verbosity in the following order.
- Error ( - v )
- Warn ( - vv )
- Info ( - vvv )
- Debug ( - vvvv )
When this flag is passed , it disables the progress bars and logs to stderr .
If you would like to send the logs to a file ( and enable progress bars ) , pass the log - to - file flag . "
)
. takes_value ( false ) )
. arg (
Arg ::with_name ( " log-to-file " )
. long ( " log-to-file " )
. help ( " Enables logging of events to a file located in .paperoni/logs with a default log level of debug. Use -v to specify the logging level " )
2021-06-17 15:32:53 +01:00
. takes_value ( false ) )
. arg (
Arg ::with_name ( " inline-toc " )
. long ( " inline-toc " )
. requires ( " output_name " )
. help ( " Add an inlined Table of Contents page at the start of the merged article. " )
. long_help ( " Add an inlined Table of Contents page at the start of the merged article. This does not affect the Table of Contents navigation " )
) ;
2021-04-29 17:58:37 +01:00
2021-06-01 10:23:22 +01:00
Self ::try_from ( app . get_matches ( ) )
2021-02-06 09:59:03 +00:00
}
2021-06-01 10:23:22 +01:00
fn init_merge_file ( self ) -> Result < Self , Error > {
self . merged
. as_deref ( )
. map ( fs ::File ::create )
. transpose ( )
. err ( )
. map ( | err | Err ( Error ::InvalidOutputPath ( err . to_string ( ) ) ) )
. unwrap_or ( Ok ( self ) )
2021-02-11 10:51:21 +00:00
}
2021-04-29 17:58:37 +01:00
2021-06-01 10:23:22 +01:00
fn init_logger ( self ) -> Result < Self , Error > {
2021-06-06 11:20:08 +01:00
use crate ::logs ;
logs ::init_logger ( self . log_level , & self . start_time , self . is_logging_to_file )
. map ( | _ | self )
. map_err ( Error ::LogError )
2021-04-29 17:58:37 +01:00
}
2021-02-06 09:59:03 +00:00
}
2021-06-01 10:23:22 +01:00
use std ::convert ::TryFrom ;
impl < ' a > TryFrom < ArgMatches < ' a > > for AppConfig {
type Error = Error ;
fn try_from ( arg_matches : ArgMatches < ' a > ) -> Result < Self , Self ::Error > {
AppConfigBuilder ::default ( )
. urls ( {
let url_filter = | url : & str | {
let url = url . trim ( ) ;
if ! url . is_empty ( ) {
Some ( url . to_owned ( ) )
} else {
None
}
} ;
2021-06-06 11:20:08 +01:00
let direct_urls = arg_matches
. values_of ( " urls " )
2021-06-10 18:16:31 +01:00
. and_then ( | urls | urls . map ( url_filter ) . collect ::< Option < Vec < _ > > > ( ) )
. unwrap_or ( Vec ::new ( ) ) ;
2021-06-06 11:20:08 +01:00
let file_urls = arg_matches
. value_of ( " file " )
. map ( fs ::read_to_string )
. transpose ( ) ?
2021-06-10 18:16:31 +01:00
. and_then ( | content | content . lines ( ) . map ( url_filter ) . collect ::< Option < Vec < _ > > > ( ) )
. unwrap_or ( Vec ::new ( ) ) ;
let urls = [ direct_urls , file_urls ]
. concat ( )
. into_iter ( )
. unique ( )
. collect_vec ( ) ;
if ! urls . is_empty ( ) {
Ok ( urls )
} else {
Err ( Error ::NoUrls )
2021-06-01 10:23:22 +01:00
}
} ? )
2021-06-08 05:42:30 +01:00
. max_conn ( match arg_matches . value_of ( " max-conn " ) {
2021-06-01 10:23:22 +01:00
Some ( max_conn ) = > max_conn . parse ::< NonZeroUsize > ( ) ? . get ( ) ,
None = > DEFAULT_MAX_CONN ,
} )
. merged ( arg_matches . value_of ( " output_name " ) . map ( ToOwned ::to_owned ) )
. can_disable_progress_bar (
arg_matches . is_present ( " verbosity " ) & & ! arg_matches . is_present ( " log-to-file " ) ,
)
. log_level ( match arg_matches . occurrences_of ( " verbosity " ) {
2021-06-09 05:26:52 +01:00
0 = > {
if ! arg_matches . is_present ( " log-to-file " ) {
LogLevel ::Off
} else {
LogLevel ::Debug
}
}
2021-06-01 10:23:22 +01:00
1 = > LogLevel ::Error ,
2 = > LogLevel ::Warn ,
3 = > LogLevel ::Info ,
4 ..= u64 ::MAX = > LogLevel ::Debug ,
} )
2021-06-06 11:20:08 +01:00
. is_logging_to_file ( arg_matches . is_present ( " log-to-file " ) )
2021-06-17 15:32:53 +01:00
. inline_toc ( arg_matches . is_present ( " inline-toc " ) )
2021-06-01 10:23:22 +01:00
. output_directory (
arg_matches
. value_of ( " output_directory " )
. map ( | output_directory | {
let path = Path ::new ( output_directory ) ;
if ! path . exists ( ) {
Err ( Error ::OutputDirectoryNotExists )
} else if ! path . is_dir ( ) {
Err ( Error ::WrongOutputDirectory )
} else {
Ok ( output_directory . to_owned ( ) )
}
} )
. transpose ( ) ? ,
)
. start_time ( Local ::now ( ) )
. try_init ( )
2021-04-29 17:58:37 +01:00
}
2021-06-01 10:23:22 +01:00
}
2021-04-29 17:58:37 +01:00
2021-06-01 10:23:22 +01:00
impl AppConfigBuilder {
pub fn try_init ( & self ) -> Result < AppConfig , Error > {
2021-06-06 11:20:08 +01:00
self . build ( )
. map_err ( Error ::AppBuildError ) ?
. init_logger ( ) ?
. init_merge_file ( )
2021-04-24 13:50:43 +01:00
}
2020-05-16 08:09:44 +01:00
}