Skip to content

Commit 5c0e0c8

Browse files
committed
LDJSON progressbar functionality
1 parent 504a3b1 commit 5c0e0c8

2 files changed

Lines changed: 106 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [0.2.31] - Unreleased
4+
5+
New commandline option `--progress` which accepts options `bar` (the default), `none` and `json`. If
6+
option `json` is selected, machine-readable progress information is printed to stderr as
7+
newline-delimited JSON of the form
8+
9+
`{"type": "progress", "percent": 75, "bandwidth": 2266856, "message": "Fetching video segments)"}`
10+
11+
When the `json` option is selected, logging is modified also to use a partly JSON format, of the
12+
form
13+
14+
`18:03:33 INFO {"message":"Preparing download for period 0 (#1)"}`
15+
316

417
## [0.2.30] - 2026-01-25
518

src/main.rs

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// Example usage: dash-mpd-cli --timeout 5 --output=/tmp/foo.mp4 https://v.redd.it/zv89llsvexdz/DASHPlaylist.mpd
2121

2222
use std::env;
23+
use std::io::{self, Write};
2324
use std::path::Path;
2425
use std::net::IpAddr;
2526
use std::str::FromStr;
@@ -45,6 +46,13 @@ mod cookies;
4546
use 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+
4856
struct DownloadProgressBar {
4957
bar: ProgressBar,
5058
}
@@ -61,17 +69,43 @@ impl DownloadProgressBar {
6169
}
6270

6371
impl 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.
77111
async fn check_newer_version() -> Result<()> {
@@ -102,26 +136,6 @@ async fn check_newer_version() -> Result<()> {
102136

103137
#[tokio::main]
104138
async 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

Comments
 (0)