feat: add background download manager with progress reporting
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
use crate::models::{CookieSource, VideoMetadata};
|
||||
use crate::services::video_resolver;
|
||||
use crate::services::{download_manager, video_resolver};
|
||||
use serde::Serialize;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use tauri::ipc::Channel;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn resolve_url(
|
||||
@@ -10,3 +13,76 @@ pub async fn resolve_url(
|
||||
.await
|
||||
.map_err(|e| format!("Task failed: {e}"))?
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
|
||||
pub enum DownloadEvent {
|
||||
Progress { percent: f64 },
|
||||
FilePath { path: String },
|
||||
Finished { success: bool, path: String },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_download(
|
||||
url: String,
|
||||
cookie_source: CookieSource,
|
||||
on_event: Channel<DownloadEvent>,
|
||||
) -> Result<(), String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let temp_dir = std::env::temp_dir()
|
||||
.join("video-clipper")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
std::fs::create_dir_all(&temp_dir)
|
||||
.map_err(|e| format!("Failed to create temp dir: {e}"))?;
|
||||
|
||||
let (mut child, _template) =
|
||||
download_manager::start_download(&url, &cookie_source, &temp_dir)?;
|
||||
|
||||
let Some(stderr) = child.stderr.take() else {
|
||||
let status = child
|
||||
.wait()
|
||||
.map_err(|e| format!("Download failed: {e}"))?;
|
||||
let _ = on_event.send(DownloadEvent::Finished {
|
||||
success: status.success(),
|
||||
path: String::new(),
|
||||
});
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut last_file_path: Option<String> = None;
|
||||
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
if let Some(pct) = download_manager::parse_progress_line(&line) {
|
||||
let _ = on_event.send(DownloadEvent::Progress { percent: pct });
|
||||
}
|
||||
|
||||
if line.contains("[download] Destination:")
|
||||
|| line.contains("[Merger] Merging formats into")
|
||||
{
|
||||
if let Some(path) = line.split(": ").nth(1) {
|
||||
let trimmed = path.trim().trim_matches('"').to_string();
|
||||
let _ = on_event.send(DownloadEvent::FilePath {
|
||||
path: trimmed.clone(),
|
||||
});
|
||||
last_file_path = Some(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.map_err(|e| format!("Download failed: {e}"))?;
|
||||
let path = last_file_path.unwrap_or_default();
|
||||
let _ = on_event.send(DownloadEvent::Finished {
|
||||
success: status.success(),
|
||||
path,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Task failed: {e}"))?
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub fn run() {
|
||||
dependencies::check_dependencies,
|
||||
dependencies::install_dependency,
|
||||
video::resolve_url,
|
||||
video::start_download,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -1 +1,71 @@
|
||||
// Implemented in Task 5
|
||||
use crate::models::CookieSource;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
pub fn parse_progress_line(line: &str) -> Option<f64> {
|
||||
if !line.contains("[download]") || !line.contains('%') {
|
||||
return None;
|
||||
}
|
||||
let pct_str = line.split_whitespace().find(|s| s.ends_with('%'))?;
|
||||
pct_str
|
||||
.trim_end_matches('%')
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.map(|p| p / 100.0)
|
||||
}
|
||||
|
||||
pub fn start_download(
|
||||
url: &str,
|
||||
cookie_source: &CookieSource,
|
||||
temp_dir: &str,
|
||||
) -> Result<(Child, String), String> {
|
||||
let output_template = format!("{}/%(title)s.%(ext)s", temp_dir);
|
||||
|
||||
let mut cmd = Command::new("yt-dlp");
|
||||
cmd.arg("-o")
|
||||
.arg(&output_template)
|
||||
.arg("--newline")
|
||||
.arg("--no-part")
|
||||
.arg("-c");
|
||||
|
||||
for arg in cookie_source.to_ytdlp_args() {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
cmd.arg(url)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
let child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start yt-dlp: {e}"))?;
|
||||
|
||||
Ok((child, output_template))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_progress_percentage() {
|
||||
let line = "[download] 45.2% of ~100.00MiB at 5.00MiB/s ETA 00:10";
|
||||
assert!((parse_progress_line(line).unwrap() - 0.452).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_progress_100_percent() {
|
||||
let line = "[download] 100% of 50.00MiB in 00:05";
|
||||
assert!((parse_progress_line(line).unwrap() - 1.0).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_progress_non_progress_line() {
|
||||
let line = "[youtube] Extracting URL: https://www.youtube.com/watch?v=abc";
|
||||
assert!(parse_progress_line(line).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_progress_empty_line() {
|
||||
assert!(parse_progress_line("").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user