use std::fmt;
use async_trait::async_trait;
use downcast_rs::{Downcast, impl_downcast};
use crate::error::Result;
use crate::model::Video;
use crate::model::playlist::Playlist;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ExtractorName {
Youtube,
Generic(Option<String>),
}
impl fmt::Display for ExtractorName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Youtube => f.write_str("Youtube"),
Self::Generic(Some(name)) => write!(f, "Generic(name={})", name),
Self::Generic(None) => f.write_str("Generic"),
}
}
}
#[async_trait]
pub trait VideoExtractor: Downcast + Send + Sync + fmt::Debug {
async fn fetch_video(&self, url: &str) -> Result<Video>;
async fn fetch_playlist(&self, url: &str) -> Result<Playlist>;
fn name(&self) -> ExtractorName;
fn supports_url(&self, url: &str) -> bool;
}
impl_downcast!(VideoExtractor);
pub trait ExtractorConfig: VideoExtractor {
fn args_mut(&mut self) -> &mut Vec<String>;
fn timeout_mut(&mut self) -> &mut Duration;
fn with_arg(&mut self, arg: String) -> &mut Self {
self.args_mut().push(arg);
self
}
fn with_timeout(&mut self, timeout: Duration) -> &mut Self {
*self.timeout_mut() = timeout;
self
}
fn with_cookies(&mut self, path: impl AsRef<Path>) -> &mut Self {
let cookie_path = path.as_ref().display().to_string();
self.with_arg(format!("--cookies={}", cookie_path))
}
fn with_cookies_from_browser(&mut self, browser: &str) -> &mut Self {
self.with_arg(format!("--cookies-from-browser={}", browser))
}
fn with_netrc(&mut self) -> &mut Self {
self.with_arg("--netrc".to_string())
}
}
pub mod detector;
pub mod generic;
pub mod youtube;
#[async_trait]
pub trait ExtractorBase: VideoExtractor {
fn executable_path(&self) -> PathBuf;
fn timeout(&self) -> Duration;
fn build_base_args(&self) -> Vec<String>;
async fn fetch_video_metadata(&self, url: &str) -> Result<Video> {
let mut args = self.build_base_args();
args.push(url.to_string());
execute_and_parse_video(self.executable_path(), &args, self.timeout()).await
}
async fn fetch_playlist_metadata(&self, url: &str) -> Result<Playlist> {
let mut args = self.build_base_args();
args.push("--flat-playlist".to_string());
args.push(url.to_string());
execute_and_parse_playlist(self.executable_path(), &args, self.timeout()).await
}
async fn log_and_fetch_video(&self, url: &str, extractor: &str) -> Result<Video> {
let result = self.fetch_video_metadata(url).await;
match &result {
Ok(video) => tracing::debug!(
url = url,
extractor = extractor,
video_id = video.id,
title = video.title,
format_count = video.formats.len(),
"✅ Video fetched successfully"
),
Err(e) => tracing::warn!(
url = url,
extractor = extractor,
error = %e,
"Failed to fetch video"
),
}
result
}
async fn log_and_fetch_playlist(&self, url: &str, extractor: &str) -> Result<Playlist> {
let result = self.fetch_playlist_metadata(url).await;
match &result {
Ok(playlist) => tracing::debug!(
url = url,
extractor = extractor,
playlist_id = playlist.id,
title = playlist.title,
entry_count = playlist.entries.len(),
"✅ Playlist fetched successfully"
),
Err(e) => tracing::warn!(
url = url,
extractor = extractor,
error = %e,
"Failed to fetch playlist"
),
}
result
}
}
macro_rules! impl_extractor_config {
($type:path) => {
impl $crate::extractor::ExtractorConfig for $type {
fn args_mut(&mut self) -> &mut Vec<String> {
&mut self.args
}
fn timeout_mut(&mut self) -> &mut std::time::Duration {
&mut self.timeout
}
}
};
}
use std::path::{Path, PathBuf};
use std::time::Duration;
pub use detector::detect_extractor_type;
pub use generic::Generic;
pub(crate) use impl_extractor_config;
pub use youtube::Youtube;
use crate::executor::Executor;
async fn execute_and_parse<T>(
executable_path: PathBuf,
args: &[String],
timeout: Duration,
label: &'static str,
) -> Result<T>
where
T: serde::de::DeserializeOwned + Send + 'static,
{
tracing::debug!(
executable = ?executable_path,
arg_count = args.len(),
timeout_secs = timeout.as_secs(),
"📡 Executing extractor for {label}"
);
let executor = Executor::new(executable_path.clone(), args.to_vec(), timeout);
let temp_dir = tempfile::tempdir()?;
let output_path = temp_dir.path().join(format!("{}_{}.json", label, uuid::Uuid::new_v4()));
tracing::debug!(
executable = ?executable_path,
output_path = ?output_path,
"📡 Redirecting yt-dlp output to temporary file"
);
let _output = executor.execute_to_file(&output_path).await?;
tracing::debug!(output_path = ?output_path, "⚙️ Opening output file for parsing");
let file = tokio::fs::File::open(&output_path).await?;
let file = file.into_std().await;
tracing::debug!("⚙️ Spawning blocking task for JSON parsing");
let result: T =
tokio::task::spawn_blocking(move || serde_json::from_reader(std::io::BufReader::new(file))).await??;
Ok(result)
}
pub async fn execute_and_parse_video(executable_path: PathBuf, args: &[String], timeout: Duration) -> Result<Video> {
let mut video: Video = execute_and_parse(executable_path, args, timeout, "video").await?;
tracing::debug!(
video_id = %video.id,
title = %video.title,
format_count = video.formats.len(),
"✅ Video parsed successfully"
);
for format in &mut video.formats {
format.video_id = Some(video.id.clone());
}
tracing::debug!(video_id = %video.id, "⚙️ Set video_id on all formats");
Ok(video)
}
pub async fn execute_and_parse_playlist(
executable_path: PathBuf,
args: &[String],
timeout: Duration,
) -> Result<Playlist> {
let playlist: Playlist = execute_and_parse(executable_path, args, timeout, "playlist").await?;
tracing::debug!(
playlist_id = %playlist.id,
title = %playlist.title,
entry_count = playlist.entries.len(),
"✅ Playlist parsed successfully"
);
Ok(playlist)
}