feat: retry with different client after 403 error

This commit is contained in:
ThetaDev 2024-08-10 03:11:49 +02:00
parent 1a62471ad7
commit edeb8191b4
3 changed files with 56 additions and 23 deletions

View file

@ -94,21 +94,21 @@ enum Commands {
#[clap(short, long)] #[clap(short, long)]
resolution: Option<u32>, resolution: Option<u32>,
/// Download only the audio track /// Download only the audio track
#[clap(long)] #[clap(short, long)]
audio: bool, audio: bool,
/// Number of videos downloaded in parallel /// Number of videos downloaded in parallel
#[clap(short, long, default_value_t = 8)] #[clap(short, long, default_value_t = 8)]
parallel: usize, parallel: usize,
/// Use YouTube Music for downloading playlists /// Use YouTube Music for downloading playlists
#[clap(long)] #[clap(short, long)]
music: bool, music: bool,
/// Limit the number of videos to download /// Limit the number of videos to download
#[clap(long, default_value_t = 1000)] #[clap(short, long, default_value_t = 1000)]
limit: usize, limit: usize,
/// YT Client used to fetch player data /// YT Client used to fetch player data
#[clap(long)] #[clap(short, long)]
client_type: Option<Vec<ClientTypeArg>>, client_type: Option<Vec<ClientTypeArg>>,
/// Pot token to circumvent bot detection /// `pot` token to circumvent bot detection
#[clap(long)] #[clap(long)]
pot: Option<String>, pot: Option<String>,
}, },
@ -123,16 +123,16 @@ enum Commands {
#[clap(long)] #[clap(long)]
pretty: bool, pretty: bool,
/// Output as text /// Output as text
#[clap(long)] #[clap(short, long)]
txt: bool, txt: bool,
/// Limit the number of items to fetch /// Limit the number of items to fetch
#[clap(long, default_value_t = 20)] #[clap(short, long, default_value_t = 20)]
limit: usize, limit: usize,
/// Channel tab /// Channel tab
#[clap(long, default_value = "videos")] #[clap(long, default_value = "videos")]
tab: ChannelTab, tab: ChannelTab,
/// Use YouTube Music /// Use YouTube Music
#[clap(long)] #[clap(short, long)]
music: bool, music: bool,
/// Get comments /// Get comments
#[clap(long)] #[clap(long)]
@ -144,7 +144,7 @@ enum Commands {
#[clap(long)] #[clap(long)]
player: bool, player: bool,
/// YT Client used to fetch player data /// YT Client used to fetch player data
#[clap(long)] #[clap(short, long)]
client_type: Option<ClientTypeArg>, client_type: Option<ClientTypeArg>,
}, },
/// Search YouTube /// Search YouTube
@ -158,10 +158,10 @@ enum Commands {
#[clap(long)] #[clap(long)]
pretty: bool, pretty: bool,
/// Output as text /// Output as text
#[clap(long)] #[clap(short, long)]
txt: bool, txt: bool,
/// Limit the number of items to fetch /// Limit the number of items to fetch
#[clap(long, default_value_t = 20)] #[clap(short, long, default_value_t = 20)]
limit: usize, limit: usize,
/// Filter results by item type /// Filter results by item type
#[clap(long)] #[clap(long)]
@ -179,7 +179,7 @@ enum Commands {
#[clap(long)] #[clap(long)]
channel: Option<String>, channel: Option<String>,
/// YouTube Music search filter /// YouTube Music search filter
#[clap(long)] #[clap(short, long)]
music: Option<MusicSearchCategory>, music: Option<MusicSearchCategory>,
}, },
/// Get a YouTube visitor data cookie /// Get a YouTube visitor data cookie

View file

@ -19,7 +19,7 @@ use rand::Rng;
use regex::Regex; use regex::Regex;
use reqwest::{header, Client, StatusCode, Url}; use reqwest::{header, Client, StatusCode, Url};
use rustypipe::{ use rustypipe::{
client::{ClientType, RustyPipe}, client::{ClientType, RustyPipe, DEFAULT_PLAYER_CLIENT_ORDER},
model::{ model::{
traits::{FileFormat, YtEntity}, traits::{FileFormat, YtEntity},
AudioCodec, TrackItem, VideoCodec, VideoPlayer, AudioCodec, TrackItem, VideoCodec, VideoPlayer,
@ -667,6 +667,7 @@ impl DownloadQuery {
#[tracing::instrument(skip(self), level="error", fields(id = self.video.id))] #[tracing::instrument(skip(self), level="error", fields(id = self.video.id))]
pub async fn download(&self) -> Result<DownloadResult> { pub async fn download(&self) -> Result<DownloadResult> {
let mut last_err = None; let mut last_err = None;
let mut failed_client = None;
// Progress bar // Progress bar
#[cfg(feature = "indicatif")] #[cfg(feature = "indicatif")]
@ -683,14 +684,19 @@ impl DownloadQuery {
let err = match self let err = match self
.download_attempt( .download_attempt(
n, n,
failed_client,
#[cfg(feature = "indicatif")] #[cfg(feature = "indicatif")]
&pb, &pb,
) )
.await .await
{ {
Ok(res) => return Ok(res), Ok(res) => return Ok(res),
Err(DownloadError::Forbidden(c)) => {
failed_client = Some(c);
DownloadError::Forbidden(c)
}
Err(DownloadError::Http(e)) => { Err(DownloadError::Http(e)) => {
if !e.is_timeout() && e.status() != Some(StatusCode::FORBIDDEN) { if !e.is_timeout() {
return Err(DownloadError::Http(e)); return Err(DownloadError::Http(e));
} }
DownloadError::Http(e) DownloadError::Http(e)
@ -710,6 +716,7 @@ impl DownloadQuery {
async fn download_attempt( async fn download_attempt(
&self, &self,
#[allow(unused_variables)] n: u32, #[allow(unused_variables)] n: u32,
failed_client: Option<ClientType>,
#[cfg(feature = "indicatif")] pb: &Option<ProgressBar>, #[cfg(feature = "indicatif")] pb: &Option<ProgressBar>,
) -> Result<DownloadResult> { ) -> Result<DownloadResult> {
let filter = self.filter.as_ref().unwrap_or(&self.dl.i.filter); 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 q = self.dl.i.rp.query();
let player_data = match self
.client_types let mut client_types = Cow::Borrowed(
.as_ref() self.client_types
.or(self.dl.i.client_types.as_ref()) .as_ref()
{ .or(self.dl.i.client_types.as_ref())
Some(client_types) => q.player_from_clients(&self.video.id, client_types).await?, .map(Vec::as_slice)
None => q.player(&self.video.id).await?, .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 user_agent = q.user_agent(player_data.client_type);
let pot = if matches!( let pot = if matches!(
player_data.client_type, player_data.client_type,
@ -848,7 +869,15 @@ impl DownloadQuery {
#[cfg(feature = "indicatif")] #[cfg(feature = "indicatif")]
pb.clone(), 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")] #[cfg(feature = "indicatif")]
if let Some(pb) = &pb { if let Some(pb) = &pb {

View file

@ -1,6 +1,7 @@
use std::{borrow::Cow, collections::BTreeMap, path::PathBuf}; use std::{borrow::Cow, collections::BTreeMap, path::PathBuf};
use reqwest::Url; use reqwest::Url;
use rustypipe::client::ClientType;
/// Error from the video downloader /// Error from the video downloader
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -12,6 +13,9 @@ pub enum DownloadError {
/// Error from the HTTP client /// Error from the HTTP client
#[error("http error: {0}")] #[error("http error: {0}")]
Http(#[from] reqwest::Error), Http(#[from] reqwest::Error),
/// 403 error trying to download video
#[error("YouTube returned 403 error")]
Forbidden(ClientType),
/// File IO error /// File IO error
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),