mirror of
https://github.com/narrrl/ytd-rs.git
synced 2026-11-03 16:41:27 +01:00
feat: add streaming output for progress monitoring and update README
This commit is contained in:
1 parent
46f51fa98d
commit
f5d2083248
3 files changed
+89
-7
No files matched your search
+1
-1
@@ -12,6 +12,6 @@ readme = "README.md"
|
||||
[dependencies]
|
||||
regex = "1.11.1"
|
||||
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_json = "1.0.138"
|
||||
@@ -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
|
||||
MIT
|
||||
+66
-6
@@ -25,7 +25,8 @@ use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use tokio::fs;
|
||||
use tokio::process::Command;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader, Lines};
|
||||
use tokio::process::{ChildStdout, Command};
|
||||
|
||||
pub mod error;
|
||||
|
||||
@@ -62,6 +63,36 @@ pub struct VideoInfo {
|
||||
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.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct YtDlp {
|
||||
@@ -226,7 +257,7 @@ impl YtDlp {
|
||||
|
||||
/// Executes yt-dlp and returns the standard output.
|
||||
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() {
|
||||
return Err(YtDlpError::Failure(String::from_utf8(output.stderr)?));
|
||||
@@ -235,9 +266,21 @@ impl YtDlp {
|
||||
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.
|
||||
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() {
|
||||
return Err(YtDlpError::Failure(String::from_utf8(output.stderr)?));
|
||||
@@ -255,7 +298,7 @@ impl YtDlp {
|
||||
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 !path.exists() {
|
||||
fs::create_dir_all(path).await?;
|
||||
@@ -287,8 +330,7 @@ impl YtDlp {
|
||||
cmd.arg(link);
|
||||
}
|
||||
|
||||
let output = cmd.spawn()?.wait_with_output().await?;
|
||||
Ok(output)
|
||||
Ok(cmd.spawn()?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,4 +355,22 @@ mod tests {
|
||||
println!("yt-dlp version: {}", version.output());
|
||||
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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user