use crate::models::{CookieSource, VideoMetadata}; use crate::services::{download_manager, subtitle_downloader, video_resolver}; use serde::Serialize; use std::io::{BufRead, BufReader}; use tauri::ipc::Channel; #[tauri::command] pub async fn resolve_url( url: String, cookie_source: CookieSource, ) -> Result { tokio::task::spawn_blocking(move || video_resolver::resolve(&url, &cookie_source)) .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 }, } fn variant_dir(variant: &str) -> String { std::env::temp_dir() .join("video-clipper") .join(variant) .to_string_lossy() .to_string() } #[tauri::command] pub async fn start_download( url: String, cookie_source: CookieSource, format_spec: String, variant: String, on_event: Channel, ) -> Result<(), String> { tokio::task::spawn_blocking(move || { let output_dir = variant_dir(&variant); std::fs::create_dir_all(&output_dir) .map_err(|e| format!("Failed to create dir: {e}"))?; eprintln!( "[video-clipper:download:{}] starting — format='{}' dir='{}'", variant, format_spec, output_dir ); let (mut child, _template) = download_manager::start_download(&url, &cookie_source, &output_dir, &format_spec)?; let Some(stdout) = child.stdout.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(stdout); let mut last_file_path: Option = 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:") { 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); } } else if line.contains("[Merger] Merging formats into") { let trimmed = line .trim_start_matches("[Merger] Merging formats into ") .trim() .trim_matches('"') .to_string(); if !trimmed.is_empty() { let _ = on_event.send(DownloadEvent::FilePath { path: trimmed.clone(), }); last_file_path = Some(trimmed); } } else if line.contains("has already been downloaded") { if let Some(rest) = line.strip_prefix("[download] ") { if let Some(path) = rest.strip_suffix(" has already been downloaded") { let trimmed = path.trim().to_string(); if !trimmed.is_empty() { 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(); eprintln!( "[video-clipper:download:{}] finished — success={}, path='{}'", variant, status.success(), path ); let _ = on_event.send(DownloadEvent::Finished { success: status.success(), path, }); Ok(()) }) .await .map_err(|e| format!("Task failed: {e}"))? } /// Check if a video with the given title exists in the variant's download dir. #[tauri::command] pub async fn check_cached_download(title: String, variant: String) -> Option { let dir = variant_dir(&variant); let dir_path = std::path::Path::new(&dir); if !dir_path.is_dir() { return None; } let entries = std::fs::read_dir(dir_path).ok()?; for entry in entries.flatten() { let path = entry.path(); if path.is_file() { if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { if stem == title { let path_str = path.to_string_lossy().to_string(); if let Ok(meta) = std::fs::metadata(&path_str) { if meta.len() > 0 { eprintln!( "[video-clipper:cache:{}] found: '{}'", variant, path_str ); return Some(path_str); } } } } } } None } /// Download English VTT subtitles for a video URL. /// Returns the path to the .vtt file, or an error string. #[tauri::command] pub async fn download_subtitles( url: String, cookie_source: CookieSource, is_auto: bool, ) -> Result { tokio::task::spawn_blocking(move || { let output_dir = std::env::temp_dir() .join("video-clipper") .join("subtitles") .to_string_lossy() .to_string(); eprintln!( "[video-clipper:subtitles] downloading (auto={}) to '{}'", is_auto, output_dir ); let result = subtitle_downloader::download_subtitles(&url, &cookie_source, &output_dir, is_auto); match &result { Ok(path) => eprintln!("[video-clipper:subtitles] done: '{}'", path), Err(e) => eprintln!("[video-clipper:subtitles] FAILED: {}", e), } result }) .await .map_err(|e| format!("Task failed: {e}"))? }