use crate::models::{CookieSource, VideoMetadata}; use serde::Deserialize; use std::process::Command; #[derive(Debug, Deserialize)] pub struct YtDlpJson { pub title: String, pub duration: Option, pub fps: Option, pub thumbnail: Option, } pub fn parse_ytdlp_json(json_str: &str) -> Result { serde_json::from_str::(json_str) .map_err(|e| format!("Failed to parse yt-dlp JSON: {e}")) } pub fn resolve(url: &str, cookie_source: &CookieSource) -> Result { let cookie_args = cookie_source.to_ytdlp_args(); let mut meta_cmd = Command::new("yt-dlp"); meta_cmd.arg("--dump-json").arg("--no-download"); for arg in &cookie_args { meta_cmd.arg(arg); } meta_cmd.arg(url); let meta_output = meta_cmd .output() .map_err(|e| format!("Failed to run yt-dlp: {e}"))?; if !meta_output.status.success() { let stderr = String::from_utf8_lossy(&meta_output.stderr); return Err(format!("yt-dlp metadata failed: {stderr}")); } let json_str = String::from_utf8_lossy(&meta_output.stdout); let meta = parse_ytdlp_json(&json_str)?; let mut stream_cmd = Command::new("yt-dlp"); stream_cmd.arg("-g").arg("--no-download"); for arg in &cookie_args { stream_cmd.arg(arg); } stream_cmd.arg(url); let stream_output = stream_cmd .output() .map_err(|e| format!("Failed to get stream URL: {e}"))?; if !stream_output.status.success() { let stderr = String::from_utf8_lossy(&stream_output.stderr); return Err(format!("yt-dlp stream URL failed: {stderr}")); } let stream_url = String::from_utf8_lossy(&stream_output.stdout) .lines() .next() .unwrap_or("") .trim() .to_string(); Ok(VideoMetadata { url: url.to_string(), title: meta.title, duration: meta.duration.unwrap_or(0.0), fps: meta.fps.unwrap_or(30.0), thumbnail_url: meta.thumbnail, stream_url, }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_ytdlp_json_valid() { let json = r#"{"title": "Test Video", "duration": 125.5, "fps": 29.97, "thumbnail": "https://i.ytimg.com/vi/abc/hqdefault.jpg"}"#; let result = parse_ytdlp_json(json).unwrap(); assert_eq!(result.title, "Test Video"); assert!((result.duration.unwrap() - 125.5).abs() < 0.001); assert!((result.fps.unwrap() - 29.97).abs() < 0.001); assert_eq!( result.thumbnail.unwrap(), "https://i.ytimg.com/vi/abc/hqdefault.jpg" ); } #[test] fn test_parse_ytdlp_json_missing_optional_fields() { let json = r#"{"title": "Minimal Video"}"#; let result = parse_ytdlp_json(json).unwrap(); assert_eq!(result.title, "Minimal Video"); assert!(result.duration.is_none()); assert!(result.fps.is_none()); assert!(result.thumbnail.is_none()); } #[test] fn test_parse_ytdlp_json_invalid() { let result = parse_ytdlp_json("not json"); assert!(result.is_err()); } }