#![doc = include_str!("../README.md")]
use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
#[cfg(cache)]
use cache::{CacheConfig, CacheLayer};
use crate::client::deps::{Libraries, LibraryInstaller};
use crate::download::manager::ManagerConfig;
use crate::error::Result;
use crate::executor::Executor;
use crate::extractor::{ExtractorConfig, ExtractorName};
#[cfg(cache)]
pub mod cache;
pub mod error;
pub mod executor;
pub mod metadata;
pub use metadata::PlaylistMetadata;
pub mod model;
pub mod utils;
pub mod client;
pub mod download;
pub mod extractor;
pub mod events;
#[cfg(feature = "hooks")]
#[doc(hidden)]
pub use async_trait;
#[cfg(feature = "statistics")]
pub mod stats;
#[cfg(any(feature = "live-recording", feature = "live-streaming"))]
pub mod live;
pub mod macros;
pub mod prelude;
pub use client::streams::selection::VideoSelection;
pub use client::{DownloadBuilder, DownloaderBuilder};
pub use download::{DownloadManager, DownloadPriority, DownloadStatus};
pub use model::utils::{AllTraits, CommonTraits};
use crate::model::Video;
#[derive(Debug)]
pub struct Downloader {
pub(crate) youtube_extractor: extractor::Youtube,
pub(crate) generic_extractor: extractor::Generic,
pub(crate) libraries: Libraries,
pub(crate) output_dir: PathBuf,
pub(crate) args: Vec<String>,
pub(crate) user_agent: Option<String>,
pub(crate) timeout: Duration,
pub(crate) proxy: Option<client::proxy::ProxyConfig>,
#[cfg(cache)]
pub(crate) cache: Option<Arc<CacheLayer>>,
pub(crate) download_manager: Arc<DownloadManager>,
pub(crate) cancellation_token: tokio_util::sync::CancellationToken,
pub(crate) event_bus: events::EventBus,
#[cfg(feature = "hooks")]
pub(crate) hook_registry: Option<events::HookRegistry>,
#[cfg(feature = "webhooks")]
pub(crate) webhook_delivery: Option<events::WebhookDelivery>,
#[cfg(feature = "statistics")]
pub(crate) statistics: Arc<stats::StatisticsTracker>,
}
impl Downloader {
pub fn builder(libraries: Libraries, output_dir: impl Into<PathBuf>) -> DownloaderBuilder {
DownloaderBuilder::new(libraries, output_dir)
}
pub fn with_download_manager_config(
libraries: Libraries,
output_dir: impl Into<PathBuf>,
download_manager_config: ManagerConfig,
) -> DownloaderBuilder {
Self::builder(libraries, output_dir).with_download_manager_config(download_manager_config)
}
pub fn download<'a>(&'a self, video: &'a Video, output: impl Into<PathBuf>) -> DownloadBuilder<'a> {
DownloadBuilder::new(self, video, output)
}
#[cfg(feature = "live-recording")]
pub fn record_live<'a>(&'a self, video: &'a Video, output: impl Into<PathBuf>) -> live::LiveRecordingBuilder<'a> {
live::LiveRecordingBuilder::new(self, video, output)
}
#[cfg(feature = "live-streaming")]
pub fn stream_live<'a>(&'a self, video: &'a Video) -> live::LiveStreamBuilder<'a> {
live::LiveStreamBuilder::new(self, video)
}
pub async fn with_new_binaries(
executables_dir: impl Into<PathBuf>,
output_dir: impl Into<PathBuf>,
) -> Result<DownloaderBuilder> {
let executables_dir: PathBuf = executables_dir.into();
let output_dir: PathBuf = output_dir.into();
tracing::info!(
executables_dir = ?executables_dir,
output_dir = ?output_dir,
"📦 Installing dependencies"
);
let installer = LibraryInstaller::new(executables_dir.clone());
let youtube_path = executables_dir.join(utils::find_executable("yt-dlp"));
let ffmpeg_path = executables_dir.join(utils::find_executable("ffmpeg"));
let youtube_exists = youtube_path.exists();
let ffmpeg_exists = ffmpeg_path.exists();
tracing::debug!(
youtube_path = ?youtube_path,
youtube_exists = youtube_exists,
ffmpeg_path = ?ffmpeg_path,
ffmpeg_exists = ffmpeg_exists,
"📦 Checking for existing binaries"
);
let youtube = if youtube_exists {
tracing::debug!("📦 Using existing yt-dlp binary");
youtube_path
} else {
tracing::debug!("📦 Installing yt-dlp binary");
installer.install_youtube(None).await?
};
let ffmpeg = if ffmpeg_exists {
tracing::debug!("📦 Using existing ffmpeg binary");
ffmpeg_path
} else {
tracing::debug!("📦 Installing ffmpeg binary");
installer.install_ffmpeg(None).await?
};
tracing::info!(
youtube_path = ?youtube,
ffmpeg_path = ?ffmpeg,
"✅ Dependencies ready"
);
let libraries = Libraries::new(youtube, ffmpeg);
Ok(DownloaderBuilder::new(libraries, output_dir))
}
pub fn youtube_extractor(&self) -> &extractor::Youtube {
&self.youtube_extractor
}
pub fn generic_extractor(&self) -> &extractor::Generic {
&self.generic_extractor
}
pub fn libraries(&self) -> &Libraries {
&self.libraries
}
pub fn output_dir(&self) -> &Path {
&self.output_dir
}
pub fn args(&self) -> &[String] {
&self.args
}
pub fn user_agent(&self) -> Option<&str> {
self.user_agent.as_deref()
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn proxy(&self) -> Option<&client::proxy::ProxyConfig> {
self.proxy.as_ref()
}
#[cfg(cache)]
pub fn cache(&self) -> Option<&Arc<CacheLayer>> {
self.cache.as_ref()
}
pub fn download_manager(&self) -> &Arc<DownloadManager> {
&self.download_manager
}
pub fn event_bus(&self) -> &events::EventBus {
&self.event_bus
}
pub fn set_user_agent(&mut self, user_agent: impl AsRef<str>) -> &mut Self {
tracing::debug!(user_agent = user_agent.as_ref(), "🔧 Setting user agent");
self.user_agent = Some(user_agent.as_ref().to_string());
self
}
pub fn append_args(&mut self, mut args: Vec<String>) -> &mut Self {
tracing::debug!(arg_count = args.len(), "🔧 Appending custom yt-dlp arguments");
self.args.append(&mut args);
self
}
pub fn set_args(&mut self, args: Vec<String>) -> &mut Self {
tracing::debug!(arg_count = args.len(), "🔧 Setting custom yt-dlp arguments");
self.args = args;
self
}
pub fn set_timeout(&mut self, timeout: Duration) -> &mut Self {
tracing::debug!(timeout = ?timeout, "🔧 Setting command execution timeout");
self.timeout = timeout;
self
}
pub fn add_arg(&mut self, arg: impl AsRef<str>) -> &mut Self {
tracing::debug!(arg = arg.as_ref(), "🔧 Adding custom yt-dlp argument");
self.args.push(arg.as_ref().to_string());
self
}
pub fn set_cookies(&mut self, path: impl AsRef<Path>) -> &mut Self {
tracing::debug!(cookies_path = ?path.as_ref(), "🔧 Configuring cookie authentication");
let s = path.as_ref().display().to_string();
self.youtube_extractor.with_cookies(path.as_ref());
self.generic_extractor.with_cookies(path.as_ref());
self.args.push(format!("--cookies={}", s));
self
}
pub fn set_cookies_from_browser(&mut self, browser: impl AsRef<str>) -> &mut Self {
tracing::debug!(browser = browser.as_ref(), "🔧 Configuring browser cookie extraction");
let b = browser.as_ref();
self.youtube_extractor.with_cookies_from_browser(b);
self.generic_extractor.with_cookies_from_browser(b);
self.args.push(format!("--cookies-from-browser={}", b));
self
}
pub fn set_netrc(&mut self) -> &mut Self {
tracing::debug!("🔧 Configuring .netrc authentication");
self.youtube_extractor.with_netrc();
self.generic_extractor.with_netrc();
self.args.push("--netrc".to_string());
self
}
pub async fn update_downloader(&self) -> Result<()> {
tracing::info!("🔄 Updating yt-dlp binary");
let args = vec!["--update"];
let executor = Executor::new(self.libraries.youtube.clone(), utils::to_owned(args), self.timeout);
executor.execute().await?;
Ok(())
}
#[cfg(cache)]
pub async fn set_cache(&mut self, config: CacheConfig) -> Result<&mut Self> {
tracing::debug!(config = %config, "🔍 Enabling cache layer");
let layer = CacheLayer::from_config(&config).await?;
self.cache = Some(Arc::new(layer));
tracing::debug!("✅ Cache layer enabled");
Ok(self)
}
pub fn shutdown(&self) {
tracing::info!("🛑 Initiating graceful shutdown");
self.cancellation_token.cancel();
}
pub fn is_shutdown_requested(&self) -> bool {
self.cancellation_token.is_cancelled()
}
pub async fn detect_extractor(&self, url: &str) -> Result<ExtractorName> {
tracing::debug!(url = url, "📡 Detecting extractor for URL");
let extractor = extractor::detector::detect_extractor_type(url, &self.libraries.youtube).await?;
tracing::debug!(
url = url,
extractor = ?extractor,
"📡 Extractor detected"
);
Ok(extractor)
}
}
impl Clone for Downloader {
fn clone(&self) -> Self {
let youtube_extractor = self.youtube_extractor.clone();
let generic_extractor = self.generic_extractor.clone();
Self {
youtube_extractor,
generic_extractor,
libraries: self.libraries.clone(),
output_dir: self.output_dir.clone(),
args: self.args.clone(),
user_agent: self.user_agent.clone(),
timeout: self.timeout,
proxy: self.proxy.clone(),
#[cfg(cache)]
cache: self.cache.clone(),
download_manager: self.download_manager.clone(),
cancellation_token: self.cancellation_token.clone(),
event_bus: self.event_bus.clone(),
#[cfg(feature = "hooks")]
hook_registry: self.hook_registry.clone(),
#[cfg(feature = "webhooks")]
webhook_delivery: self.webhook_delivery.clone(),
#[cfg(feature = "statistics")]
statistics: self.statistics.clone(),
}
}
}
impl Display for Downloader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Downloader(output_dir={}, timeout={}s, proxy={})",
self.output_dir.display(),
self.timeout.as_secs(),
self.proxy.is_some()
)
}
}