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]
|
[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"
|
||||||
@@ -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
@@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user