diff --git a/cli/src/main.rs b/cli/src/main.rs index 2f647be..124dee9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -94,21 +94,21 @@ enum Commands { #[clap(short, long)] resolution: Option, /// Download only the audio track - #[clap(long)] + #[clap(short, long)] audio: bool, /// Number of videos downloaded in parallel #[clap(short, long, default_value_t = 8)] parallel: usize, /// Use YouTube Music for downloading playlists - #[clap(long)] + #[clap(short, long)] music: bool, /// Limit the number of videos to download - #[clap(long, default_value_t = 1000)] + #[clap(short, long, default_value_t = 1000)] limit: usize, /// YT Client used to fetch player data - #[clap(long)] + #[clap(short, long)] client_type: Option>, - /// Pot token to circumvent bot detection + /// `pot` token to circumvent bot detection #[clap(long)] pot: Option, }, @@ -123,16 +123,16 @@ enum Commands { #[clap(long)] pretty: bool, /// Output as text - #[clap(long)] + #[clap(short, long)] txt: bool, /// Limit the number of items to fetch - #[clap(long, default_value_t = 20)] + #[clap(short, long, default_value_t = 20)] limit: usize, /// Channel tab #[clap(long, default_value = "videos")] tab: ChannelTab, /// Use YouTube Music - #[clap(long)] + #[clap(short, long)] music: bool, /// Get comments #[clap(long)] @@ -144,7 +144,7 @@ enum Commands { #[clap(long)] player: bool, /// YT Client used to fetch player data - #[clap(long)] + #[clap(short, long)] client_type: Option, }, /// Search YouTube @@ -158,10 +158,10 @@ enum Commands { #[clap(long)] pretty: bool, /// Output as text - #[clap(long)] + #[clap(short, long)] txt: bool, /// Limit the number of items to fetch - #[clap(long, default_value_t = 20)] + #[clap(short, long, default_value_t = 20)] limit: usize, /// Filter results by item type #[clap(long)] @@ -179,7 +179,7 @@ enum Commands { #[clap(long)] channel: Option, /// YouTube Music search filter - #[clap(long)] + #[clap(short, long)] music: Option, }, /// Get a YouTube visitor data cookie diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index df9d430..53c301c 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -19,7 +19,7 @@ use rand::Rng; use regex::Regex; use reqwest::{header, Client, StatusCode, Url}; use rustypipe::{ - client::{ClientType, RustyPipe}, + client::{ClientType, RustyPipe, DEFAULT_PLAYER_CLIENT_ORDER}, model::{ traits::{FileFormat, YtEntity}, AudioCodec, TrackItem, VideoCodec, VideoPlayer, @@ -667,6 +667,7 @@ impl DownloadQuery { #[tracing::instrument(skip(self), level="error", fields(id = self.video.id))] pub async fn download(&self) -> Result { let mut last_err = None; + let mut failed_client = None; // Progress bar #[cfg(feature = "indicatif")] @@ -683,14 +684,19 @@ impl DownloadQuery { let err = match self .download_attempt( n, + failed_client, #[cfg(feature = "indicatif")] &pb, ) .await { Ok(res) => return Ok(res), + Err(DownloadError::Forbidden(c)) => { + failed_client = Some(c); + DownloadError::Forbidden(c) + } Err(DownloadError::Http(e)) => { - if !e.is_timeout() && e.status() != Some(StatusCode::FORBIDDEN) { + if !e.is_timeout() { return Err(DownloadError::Http(e)); } DownloadError::Http(e) @@ -710,6 +716,7 @@ impl DownloadQuery { async fn download_attempt( &self, #[allow(unused_variables)] n: u32, + failed_client: Option, #[cfg(feature = "indicatif")] pb: &Option, ) -> Result { let filter = self.filter.as_ref().unwrap_or(&self.dl.i.filter); @@ -750,14 +757,28 @@ impl DownloadQuery { } let q = self.dl.i.rp.query(); - 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 mut client_types = Cow::Borrowed( + self.client_types + .as_ref() + .or(self.dl.i.client_types.as_ref()) + .map(Vec::as_slice) + .unwrap_or(DEFAULT_PLAYER_CLIENT_ORDER), + ); + + // If the last download failed, try another client if possible + if let Some(failed_client) = failed_client { + if let Some(pos) = client_types.iter().position(|c| c == &failed_client) { + let p2 = pos + 1; + if p2 < client_types.len() { + let mut v = client_types[p2..].to_vec(); + v.extend(&client_types[..p2]); + client_types = v.into(); + } + } + } + + let player_data = q.player_from_clients(&self.video.id, &client_types).await?; let user_agent = q.user_agent(player_data.client_type); let pot = if matches!( player_data.client_type, @@ -848,7 +869,15 @@ impl DownloadQuery { #[cfg(feature = "indicatif")] pb.clone(), ) - .await?; + .await + .map_err(|e| { + if let DownloadError::Http(e) = &e { + if e.status() == Some(StatusCode::FORBIDDEN) { + return DownloadError::Forbidden(player_data.client_type); + } + } + e + })?; #[cfg(feature = "indicatif")] if let Some(pb) = &pb { diff --git a/downloader/src/util.rs b/downloader/src/util.rs index 3934e2f..5069c96 100644 --- a/downloader/src/util.rs +++ b/downloader/src/util.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, collections::BTreeMap, path::PathBuf}; use reqwest::Url; +use rustypipe::client::ClientType; /// Error from the video downloader #[derive(thiserror::Error, Debug)] @@ -12,6 +13,9 @@ pub enum DownloadError { /// Error from the HTTP client #[error("http error: {0}")] Http(#[from] reqwest::Error), + /// 403 error trying to download video + #[error("YouTube returned 403 error")] + Forbidden(ClientType), /// File IO error #[error(transparent)] Io(#[from] std::io::Error),