feat: add streaming output for progress monitoring and update README

This commit is contained in:
nvrl committed 2026-03-22 12:19:46 +01:00
1 parent 46f51fa98d
commit f5d2083248
3 files changed
+89 -7

No files matched your search

+1 -1
View File
@@ -12,6 +12,6 @@ readme = "README.md"
[dependencies] [dependencies]
regex = "1.11.1" regex = "1.11.1"
thiserror = "2.0.11" thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["process", "rt", "rt-multi-thread", "macros", "fs"] } tokio = { version = "1.43.0", features = ["process", "rt", "rt-multi-thread", "macros", "fs", "io-util"] }
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138" serde_json = "1.0.138"
+22
View File
@@ -65,5 +65,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
``` ```
### Streaming Output (Progress Bars)
If you want to track the progress of a long-running download, you can stream the standard output line-by-line:
```rust
use ytd_rs::YtDlp;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut process = YtDlp::new("https://www.youtube.com/watch?v=uTO0KnDsVH0")
.download_process()
.await?;
while let Some(line) = process.next_line().await? {
// e.g. "[download] 10.0% of 50.00MiB at 1.00MiB/s ETA 00:50"
println!("yt-dlp output: {}", line);
}
process.wait().await?;
println!("Download finished successfully!");
Ok(())
}
```
## License ## License
MIT MIT
+66 -6
View File
@@ -25,7 +25,8 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Stdio; use std::process::Stdio;
use tokio::fs; use tokio::fs;
use tokio::process::Command; use tokio::io::{AsyncBufReadExt, BufReader, Lines};
use tokio::process::{ChildStdout, Command};
pub mod error; pub mod error;
@@ -62,6 +63,36 @@ pub struct VideoInfo {
pub extra: serde_json::Value, pub extra: serde_json::Value,
} }
/// A running yt-dlp process that allows streaming standard output (useful for progress bars).
pub struct YtDlpChild {
child: tokio::process::Child,
stdout: Option<Lines<BufReader<ChildStdout>>>,
}
impl YtDlpChild {
/// Returns the next line of output from the process. Returns `None` if EOF is reached.
pub async fn next_line(&mut self) -> Result<Option<String>> {
if let Some(stdout) = &mut self.stdout {
match stdout.next_line().await {
Ok(line) => Ok(line),
Err(e) => Err(YtDlpError::IOError(e)),
}
} else {
Ok(None)
}
}
/// Waits for the process to complete and checks its status.
pub async fn wait(mut self) -> Result<()> {
let status = self.child.wait().await?;
if status.success() {
Ok(())
} else {
Err(YtDlpError::Failure("Process failed".to_string()))
}
}
}
/// The main builder for configuring and running yt-dlp. /// The main builder for configuring and running yt-dlp.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct YtDlp { pub struct YtDlp {
@@ -226,7 +257,7 @@ impl YtDlp {
/// Executes yt-dlp and returns the standard output. /// Executes yt-dlp and returns the standard output.
pub async fn download(&self) -> Result<YtDlpResult> { pub async fn download(&self) -> Result<YtDlpResult> {
let output = self.spawn_yt_dlp(false).await?; let output = self.spawn_yt_dlp(false).await?.wait_with_output().await?;
if !output.status.success() { if !output.status.success() {
return Err(YtDlpError::Failure(String::from_utf8(output.stderr)?)); return Err(YtDlpError::Failure(String::from_utf8(output.stderr)?));
@@ -235,9 +266,21 @@ impl YtDlp {
Ok(YtDlpResult::new(String::from_utf8(output.stdout)?)) Ok(YtDlpResult::new(String::from_utf8(output.stdout)?))
} }
/// Executes yt-dlp as a continuous process, allowing line-by-line output streaming.
/// Automatically adds `--newline` so progress updates are written on new lines.
pub async fn download_process(&self) -> Result<YtDlpChild> {
let mut clone = self.clone();
clone = clone.arg("--newline");
let mut child = clone.spawn_yt_dlp(false).await?;
let stdout = child.stdout.take().map(|stdout| BufReader::new(stdout).lines());
Ok(YtDlpChild { child, stdout })
}
/// Executes yt-dlp with --dump-json and parses the output into VideoInfo. /// Executes yt-dlp with --dump-json and parses the output into VideoInfo.
pub async fn get_info(&self) -> Result<Vec<VideoInfo>> { pub async fn get_info(&self) -> Result<Vec<VideoInfo>> {
let output = self.spawn_yt_dlp(true).await?; let output = self.spawn_yt_dlp(true).await?.wait_with_output().await?;
if !output.status.success() { if !output.status.success() {
return Err(YtDlpError::Failure(String::from_utf8(output.stderr)?)); return Err(YtDlpError::Failure(String::from_utf8(output.stderr)?));
@@ -255,7 +298,7 @@ impl YtDlp {
Ok(infos) Ok(infos)
} }
async fn spawn_yt_dlp(&self, dump_json: bool) -> Result<std::process::Output> { async fn spawn_yt_dlp(&self, dump_json: bool) -> Result<tokio::process::Child> {
if let Some(ref path) = self.output_dir { if let Some(ref path) = self.output_dir {
if !path.exists() { if !path.exists() {
fs::create_dir_all(path).await?; fs::create_dir_all(path).await?;
@@ -287,8 +330,7 @@ impl YtDlp {
cmd.arg(link); cmd.arg(link);
} }
let output = cmd.spawn()?.wait_with_output().await?; Ok(cmd.spawn()?)
Ok(output)
} }
} }
@@ -313,4 +355,22 @@ mod tests {
println!("yt-dlp version: {}", version.output()); println!("yt-dlp version: {}", version.output());
Ok(()) Ok(())
} }
#[tokio::test]
async fn test_download_process() -> Result<()> {
let mut process = YtDlp::new("").arg("--version").download_process().await?;
let mut lines = Vec::new();
while let Some(line) = process.next_line().await? {
lines.push(line);
}
process.wait().await?;
assert!(!lines.is_empty());
assert!(regex::Regex::new(r"\d{4}\.\d{2}\.\d{2}")
.unwrap()
.is_match(&lines[0]));
Ok(())
}
} }