diff --git a/cli/src/main.rs b/cli/src/main.rs index d929690..2f647be 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -107,7 +107,7 @@ enum Commands { limit: usize, /// YT Client used to fetch player data #[clap(long)] - client_type: Option, + client_type: Option>, /// Pot token to circumvent bot detection #[clap(long)] pot: Option, @@ -145,7 +145,7 @@ enum Commands { player: bool, /// YT Client used to fetch player data #[clap(long)] - client_type: Option, + client_type: Option, }, /// Search YouTube Search { @@ -260,7 +260,7 @@ enum MusicSearchCategory { } #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] -enum PlayerType { +enum ClientTypeArg { Desktop, Tv, TvEmbed, @@ -310,14 +310,14 @@ impl From for search_filter::Order { } } -impl From for ClientType { - fn from(value: PlayerType) -> Self { +impl From for ClientType { + fn from(value: ClientTypeArg) -> Self { match value { - PlayerType::Desktop => Self::Desktop, - PlayerType::TvEmbed => Self::TvHtml5Embed, - PlayerType::Tv => Self::Tv, - PlayerType::Android => Self::Android, - PlayerType::Ios => Self::Ios, + ClientTypeArg::Desktop => Self::Desktop, + ClientTypeArg::TvEmbed => Self::TvHtml5Embed, + ClientTypeArg::Tv => Self::Tv, + ClientTypeArg::Android => Self::Android, + ClientTypeArg::Ios => Self::Ios, } } } @@ -424,11 +424,11 @@ async fn download_video( dl: &Downloader, id: &str, target: &DownloadTarget, - client_type: Option, + client_types: Option<&[ClientType]>, ) { let mut q = target.apply(dl.id(id)); - if let Some(client_type) = client_type { - q = q.client_type(client_type.into()); + if let Some(client_types) = client_types { + q = q.client_types(client_types); } let res = q.download().await; if let Err(e) = res { @@ -441,7 +441,7 @@ async fn download_videos( videos: Vec, target: &DownloadTarget, parallel: usize, - client_type: Option, + client_types: Option<&[ClientType]>, multi: MultiProgress, ) -> anyhow::Result<()> { // Indicatif setup @@ -467,8 +467,8 @@ async fn download_videos( let n_failed = n_failed.clone(); let mut q = target.apply(dl.video(video)); - if let Some(client_type) = client_type { - q = q.client_type(client_type.into()); + if let Some(client_types) = client_types { + q = q.client_types(client_types); } async move { @@ -589,9 +589,11 @@ async fn run() -> anyhow::Result<()> { } let dl = dl.stream_filter(filter).build(); + let cts = client_type.map(|c| c.into_iter().map(ClientType::from).collect::>()); + match url_target { UrlTarget::Video { id, .. } => { - download_video(&dl, &id, &target, client_type).await; + download_video(&dl, &id, &target, cts.as_deref()).await; } UrlTarget::Channel { id } => { target.assert_dir(); @@ -604,7 +606,7 @@ async fn run() -> anyhow::Result<()> { .take(limit) .map(|v| DownloadVideo::from_entity(&v)) .collect(); - download_videos(&dl, videos, &target, parallel, client_type, multi).await?; + download_videos(&dl, videos, &target, parallel, cts.as_deref(), multi).await?; } UrlTarget::Playlist { id } => { target.assert_dir(); @@ -629,7 +631,7 @@ async fn run() -> anyhow::Result<()> { .map(|v| DownloadVideo::from_entity(&v)) .collect() }; - download_videos(&dl, videos, &target, parallel, client_type, multi).await?; + download_videos(&dl, videos, &target, parallel, cts.as_deref(), multi).await?; } UrlTarget::Album { id } => { target.assert_dir(); @@ -640,7 +642,7 @@ async fn run() -> anyhow::Result<()> { .take(limit) .map(|v| DownloadVideo::from_track(&v)) .collect(); - download_videos(&dl, videos, &target, parallel, client_type, multi).await?; + download_videos(&dl, videos, &target, parallel, cts.as_deref(), multi).await?; } } } diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index 16d93a6..df9d430 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -74,6 +74,7 @@ pub struct DownloaderBuilder { audio_tag: bool, #[cfg(feature = "audiotag")] crop_cover: bool, + client_types: Option>, pot: Option, } @@ -104,6 +105,8 @@ struct DownloaderInner { /// Crop YT thumbnails to ensure square album covers #[cfg(feature = "audiotag")] crop_cover: bool, + /// Client types for fetching videos + client_types: Option>, /// Pot token to circumvent bot detection pot: Option, } @@ -123,8 +126,8 @@ pub struct DownloadQuery { filter: Option, /// Target video format video_format: Option, - /// ClientType type for fetching videos - client_type: Option, + /// Client types for fetching videos + client_types: Option>, /// Pot token to circumvent bot detection pot: Option, } @@ -292,6 +295,7 @@ impl Default for DownloaderBuilder { audio_tag: false, #[cfg(feature = "audiotag")] crop_cover: false, + client_types: None, pot: None, } } @@ -390,6 +394,23 @@ impl DownloaderBuilder { self } + /// Set the [`ClientType`] used to fetch the YT player + #[must_use] + pub fn client_type(mut self, client_type: ClientType) -> Self { + self.client_types = Some(vec![client_type]); + self + } + + /// Set a list of client types used to fetch the YT player + /// + /// The clients are used in the given order. If a client cannot fetch the requested video, + /// an attempt is made with the next one. + #[must_use] + pub fn client_types>>(mut self, client_types: T) -> Self { + self.client_types = Some(client_types.into()); + self + } + /// Set the `pot` token to circumvent bot detection /// /// YouTube has implemented the token to prevent other clients from downloading YouTube videos. @@ -438,6 +459,7 @@ impl DownloaderBuilder { audio_tag: self.audio_tag, #[cfg(feature = "audiotag")] crop_cover: self.crop_cover, + client_types: self.client_types, pot: self.pot, }), } @@ -472,7 +494,7 @@ impl Downloader { progress: None, filter: None, video_format: None, - client_type: None, + client_types: None, pot: None, } } @@ -609,7 +631,17 @@ impl DownloadQuery { /// Set the [`ClientType`] used to fetch the YT player #[must_use] pub fn client_type(mut self, client_type: ClientType) -> Self { - self.client_type = Some(client_type); + self.client_types = Some(vec![client_type]); + self + } + + /// Set a list of client types used to fetch the YT player + /// + /// The clients are used in the given order. If a client cannot fetch the requested video, + /// an attempt is made with the next one. + #[must_use] + pub fn client_types>>(mut self, client_types: T) -> Self { + self.client_types = Some(client_types.into()); self } @@ -710,16 +742,20 @@ impl DownloadQuery { }; #[cfg(feature = "indicatif")] if let Some(pb) = pb { - pb.set_message(format!( - "Fetching player data for {}{}", - self.video.name.as_deref().unwrap_or_default(), - attempt_suffix - )) + if let Some(n) = &self.video.name { + pb.set_message(format!("Fetching player data for {n}{attempt_suffix}")); + } else { + pb.set_message(format!("Fetching player data{attempt_suffix}")); + } } let q = self.dl.i.rp.query(); - let player_data = match self.client_type { - Some(client_type) => q.player_from_client(&self.video.id, client_type).await?, + let player_data = match self + .client_types + .as_ref() + .or(self.dl.i.client_types.as_ref()) + { + Some(client_types) => q.player_from_clients(&self.video.id, client_types).await?, None => q.player(&self.video.id).await?, }; let user_agent = q.user_agent(player_data.client_type);