2020// Example usage: dash-mpd-cli --timeout 5 --output=/tmp/foo.mp4 https://v.redd.it/zv89llsvexdz/DASHPlaylist.mpd
2121
2222use std:: env;
23+ use std:: io:: { self , Write } ;
2324use std:: path:: Path ;
2425use std:: net:: IpAddr ;
2526use std:: str:: FromStr ;
@@ -45,6 +46,13 @@ mod cookies;
4546use crate :: cookies:: { list_cookie_sources, read_browser_cookies} ;
4647
4748
49+ #[ derive( Debug , PartialEq ) ]
50+ enum ProgressType {
51+ None ,
52+ Bar ,
53+ Json ,
54+ }
55+
4856struct DownloadProgressBar {
4957 bar : ProgressBar ,
5058}
@@ -61,17 +69,43 @@ impl DownloadProgressBar {
6169}
6270
6371impl ProgressObserver for DownloadProgressBar {
64- fn update ( & self , percent : u32 , message : & str ) {
72+ fn update ( & self , percent : u32 , bandwidth : u64 , message : & str ) {
73+ let msg = if bandwidth > 500_000 {
74+ format ! ( "{message} ({:.1} MB/s)" , bandwidth as f64 / 1e6 )
75+ } else {
76+ format ! ( "{message} ({:3} kB/s)" , bandwidth as f64 / 1e3 )
77+ } ;
6578 if percent <= 100 {
6679 self . bar . set_position ( percent. into ( ) ) ;
67- self . bar . set_message ( message . to_string ( ) ) ;
80+ self . bar . set_message ( msg ) ;
6881 }
6982 if percent == 100 {
7083 self . bar . finish_with_message ( "Done" ) ;
7184 }
7285 }
7386}
7487
88+ struct DownloadProgressJson {
89+ }
90+
91+ impl DownloadProgressJson {
92+ pub fn new ( ) -> Self {
93+ Self { }
94+ }
95+ }
96+
97+ // Prints newline-delimited JSON to stderr.
98+ impl ProgressObserver for DownloadProgressJson {
99+ fn update ( & self , percent : u32 , bandwidth : u64 , message : & str ) {
100+ eprint ! ( "{{\" type\" : \" progress\" , \" percent\" : {percent}, \" bandwidth\" : {bandwidth}, \" message\" : \" " ) ;
101+ for str in json_escape:: escape_str ( message) {
102+ eprint ! ( "{str}" ) ;
103+ }
104+ eprintln ! ( "\" }}" ) ;
105+ let _ = io:: stderr ( ) . flush ( ) ;
106+ }
107+ }
108+
75109
76110// Check whether a newer release is available on GitHub.
77111async fn check_newer_version ( ) -> Result < ( ) > {
@@ -102,26 +136,6 @@ async fn check_newer_version() -> Result<()> {
102136
103137#[ tokio:: main]
104138async fn main ( ) -> Result < ( ) > {
105- let time_fmt = time:: format_description:: parse ( "[hour]:[minute]:[second]" ) . unwrap ( ) ;
106- let time_offset = time:: UtcOffset :: current_local_offset ( )
107- . unwrap_or ( time:: UtcOffset :: UTC ) ;
108- let timer = tracing_subscriber:: fmt:: time:: OffsetTime :: new ( time_offset, time_fmt) ;
109- // Logs of level >= INFO go to stdout, otherwise (warnings and errors) to stderr.
110- let stderr = std:: io:: stderr. with_max_level ( Level :: WARN ) ;
111- let fmt_layer = tracing_subscriber:: fmt:: layer ( )
112- . map_writer ( move |w| stderr. or_else ( w) )
113- . compact ( )
114- . with_target ( false )
115- . with_timer ( timer) ;
116- let filter_layer = EnvFilter :: try_from_default_env ( )
117- // The sqlx crate is used by the decrypt-cookies crate
118- . or_else ( |_| EnvFilter :: try_new ( "info,reqwest=warn,hyper=warn,h2=warn,sqlx=warn" ) )
119- . context ( "initializing logging" ) ?;
120- tracing_subscriber:: registry ( )
121- . with ( filter_layer)
122- . with ( fmt_layer)
123- . init ( ) ;
124-
125139 #[ allow( unused_mut) ]
126140 let mut clap = clap:: Command :: new ( "dash-mpd-cli" )
127141 . about ( "Download content from an MPEG-DASH streaming media manifest." )
@@ -374,6 +388,11 @@ async fn main () -> Result<()> {
374388 . action ( ArgAction :: SetTrue )
375389 . num_args ( 0 )
376390 . help ( "Disable the progress bar" ) )
391+ . arg ( Arg :: new ( "progress" )
392+ . long ( "progress" )
393+ . value_name ( "PROGRESS-TYPE" )
394+ . num_args ( 1 )
395+ . help ( "Progress=json to print machine-readable progress information to stdout." ) )
377396 . arg ( Arg :: new ( "no-xattr" )
378397 . long ( "no-xattr" )
379398 . action ( ArgAction :: SetTrue )
@@ -462,6 +481,43 @@ async fn main () -> Result<()> {
462481 // TODO: add --mtime arg (Last-modified header)
463482 let matches = clap. get_matches ( ) ;
464483
484+ let time_fmt = time:: format_description:: parse ( "[hour]:[minute]:[second]" ) . unwrap ( ) ;
485+ let time_offset = time:: UtcOffset :: current_local_offset ( )
486+ . unwrap_or ( time:: UtcOffset :: UTC ) ;
487+ let timer = tracing_subscriber:: fmt:: time:: OffsetTime :: new ( time_offset, time_fmt) ;
488+ // Logs of level >= INFO go to stdout, otherwise (warnings and errors) to stderr.
489+ let stderr = std:: io:: stderr. with_max_level ( Level :: WARN ) ;
490+ let filter_layer = EnvFilter :: try_from_default_env ( )
491+ // The sqlx crate is used by the decrypt-cookies crate
492+ . or_else ( |_| EnvFilter :: try_new ( "info,reqwest=warn,hyper=warn,h2=warn,sqlx=warn" ) )
493+ . context ( "initializing logging" ) ?;
494+ if matches. get_one :: < String > ( "progress" )
495+ . is_some_and ( |p| p. eq ( "json" ) )
496+ {
497+ // Display logs in NDJSON format
498+ let fmt_layer = tracing_subscriber:: fmt:: layer ( )
499+ . json ( )
500+ . map_writer ( move |w| stderr. or_else ( w) )
501+ . compact ( )
502+ . with_target ( false )
503+ . with_ansi ( false )
504+ . with_timer ( timer) ;
505+ tracing_subscriber:: registry ( )
506+ . with ( filter_layer)
507+ . with ( fmt_layer)
508+ . init ( ) ;
509+ } else {
510+ let fmt_layer = tracing_subscriber:: fmt:: layer ( )
511+ . map_writer ( move |w| stderr. or_else ( w) )
512+ . compact ( )
513+ . with_target ( false )
514+ . with_timer ( timer) ;
515+ tracing_subscriber:: registry ( )
516+ . with ( filter_layer)
517+ . with ( fmt_layer)
518+ . init ( ) ;
519+ } ;
520+
465521 if ! matches. get_flag ( "no-version-check" ) {
466522 let _ = check_newer_version ( ) . await ;
467523 }
@@ -592,8 +648,21 @@ async fn main () -> Result<()> {
592648 if let Some ( url) = matches. get_one :: < String > ( "referer" ) {
593649 dl = dl. with_referer ( url. clone ( ) ) ;
594650 }
595- if !matches. get_flag ( "no-progress" ) && !matches. get_flag ( "quiet" ) {
596- dl = dl. add_progress_observer ( Arc :: new ( DownloadProgressBar :: new ( ) ) ) ;
651+ let mut progress_type = ProgressType :: Bar ;
652+ if matches. get_flag ( "no-progress" ) || matches. get_flag ( "quiet" ) {
653+ progress_type = ProgressType :: None ;
654+ }
655+ if let Some ( ptype) = matches. get_one :: < String > ( "progress" ) {
656+ if ptype. eq ( "json" ) {
657+ progress_type = ProgressType :: Json ;
658+ } else if !ptype. eq ( "bar" ) {
659+ warn ! ( "Ignoring invalid value for --progress" ) ;
660+ }
661+ }
662+ match progress_type {
663+ ProgressType :: Bar => dl = dl. add_progress_observer ( Arc :: new ( DownloadProgressBar :: new ( ) ) ) ,
664+ ProgressType :: Json => dl = dl. add_progress_observer ( Arc :: new ( DownloadProgressJson :: new ( ) ) ) ,
665+ ProgressType :: None => { } ,
597666 }
598667 if let Some ( seconds) = matches. get_one :: < u8 > ( "sleep-requests" ) {
599668 dl = dl. sleep_between_requests ( * seconds) ;
0 commit comments