feat: retry with different client after 403 error
This commit is contained in:
parent
1a62471ad7
commit
edeb8191b4
3 changed files with 56 additions and 23 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Reference in a new issue