#![warn(missing_docs)]
pub(crate) mod jsi;
pub mod download;
pub mod error;
pub mod extractor;
pub mod format;
#[cfg(feature = "ffmpeg")]
pub mod postprocess;
pub mod types;
use std::future::IntoFuture;
use std::path::{Path, PathBuf};
use std::pin::Pin;
pub use download::{DownloadOptions, Progress};
pub use error::{Error, Result};
pub use extractor::{Extractor, ExtractorContext, Registry};
pub use format::FormatSelector;
pub use types::*;
use download::{Downloader, ProgressCallback};
use extractor::youtube::YoutubeExtractor;
pub struct Ytdown {
ctx: ExtractorContext,
registry: Registry,
downloader: Downloader,
#[cfg(feature = "ffmpeg")]
ffmpeg_binary: PathBuf,
}
pub struct YtdownBuilder {
user_agent: Option<String>,
client: Option<reqwest::Client>,
extractors: Vec<Box<dyn Extractor>>,
#[cfg(feature = "ffmpeg")]
ffmpeg_binary: PathBuf,
}
impl Default for YtdownBuilder {
fn default() -> Self {
Self {
user_agent: None,
client: None,
extractors: vec![Box::new(YoutubeExtractor::new())],
#[cfg(feature = "ffmpeg")]
ffmpeg_binary: PathBuf::from("ffmpeg"),
}
}
}
impl YtdownBuilder {
pub fn user_agent(mut self, ua: &str) -> Self {
self.user_agent = Some(ua.to_string());
self
}
pub fn client(mut self, c: reqwest::Client) -> Self {
self.client = Some(c);
self
}
#[cfg(feature = "ffmpeg")]
pub fn ffmpeg_binary(mut self, p: impl Into<PathBuf>) -> Self {
self.ffmpeg_binary = p.into();
self
}
pub fn extractor(mut self, e: Box<dyn Extractor>) -> Self {
self.extractors.push(e);
self
}
pub fn clear_extractors(mut self) -> Self {
self.extractors.clear();
self
}
pub fn build(self) -> Result<Ytdown> {
let http = match self.client {
Some(c) => c,
None => {
let mut builder = reqwest::Client::builder();
if let Some(ua) = &self.user_agent {
builder = builder.user_agent(ua);
}
builder.build().map_err(|source| Error::Network {
stage: "client-build",
source,
})?
}
};
Ok(Ytdown {
ctx: ExtractorContext::new(http.clone()),
registry: Registry::new(self.extractors),
downloader: Downloader::new(http),
#[cfg(feature = "ffmpeg")]
ffmpeg_binary: self.ffmpeg_binary,
})
}
}
impl Ytdown {
pub fn builder() -> YtdownBuilder {
YtdownBuilder::default()
}
pub async fn resolve(&self, url: &str) -> Result<MediaInfo> {
self.registry.resolve(&self.ctx, url).await
}
pub fn download<'a>(&'a self, format: &Format, dest: impl AsRef<Path>) -> DownloadBuilder<'a> {
DownloadBuilder {
downloader: &self.downloader,
url: format.url.clone(),
dest: dest.as_ref().to_path_buf(),
options: DownloadOptions::default(),
}
}
#[cfg(feature = "ffmpeg")]
pub async fn download_merged(
&self,
video: &Format,
audio: &Format,
dest: impl AsRef<Path>,
) -> Result<()> {
let dest = dest.as_ref();
let video_tmp = sibling_tmp(dest, "video.part");
let audio_tmp = sibling_tmp(dest, "audio.part");
self.downloader
.download(&video.url, &video_tmp, DownloadOptions::default())
.await?;
self.downloader
.download(&audio.url, &audio_tmp, DownloadOptions::default())
.await?;
let merger = postprocess::FfmpegMerger::with_binary(self.ffmpeg_binary.clone());
merger.merge(&video_tmp, &audio_tmp, dest).await?;
let _ = tokio::fs::remove_file(&video_tmp).await;
let _ = tokio::fs::remove_file(&audio_tmp).await;
Ok(())
}
}
#[cfg(feature = "ffmpeg")]
fn sibling_tmp(dest: &Path, suffix: &str) -> PathBuf {
let stem = dest
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("ytdown");
let name = format!(".{stem}.{suffix}");
match dest.parent() {
Some(parent) => parent.join(name),
None => PathBuf::from(name),
}
}
pub struct DownloadBuilder<'a> {
downloader: &'a Downloader,
url: String,
dest: PathBuf,
options: DownloadOptions,
}
impl<'a> DownloadBuilder<'a> {
pub fn progress(mut self, f: impl Fn(Progress) + Send + Sync + 'static) -> Self {
let cb: ProgressCallback = std::sync::Arc::new(f);
self.options.progress = Some(cb);
self
}
pub fn concurrency(mut self, n: usize) -> Self {
self.options.concurrency = n;
self
}
pub fn chunk_size(mut self, bytes: u64) -> Self {
self.options.chunk_size = bytes;
self
}
pub fn retries(mut self, n: u32) -> Self {
self.options.retries = n;
self
}
pub fn resume(mut self, yes: bool) -> Self {
self.options.resume = yes;
self
}
}
impl<'a> IntoFuture for DownloadBuilder<'a> {
type Output = Result<()>;
type IntoFuture = Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
self.downloader
.download(&self.url, &self.dest, self.options)
.await
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_setters_reach_download_options() {
let yt = Ytdown::builder().build().expect("build");
let fmt = Format {
url: "https://example.invalid/x".into(),
..Format::default()
};
let builder = yt
.download(&fmt, "out.bin")
.concurrency(8)
.chunk_size(1234)
.retries(7)
.resume(false);
assert_eq!(builder.options.concurrency, 8);
assert_eq!(builder.options.chunk_size, 1234);
assert_eq!(builder.options.retries, 7);
assert!(!builder.options.resume);
}
#[tokio::test]
async fn clear_extractors_removes_defaults() {
let yt = Ytdown::builder().clear_extractors().build().expect("build");
let err = yt
.resolve("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
.await
.unwrap_err();
assert!(matches!(err, Error::UnsupportedUrl(_)));
}
}