Compare commits
No commits in common. "03c4d3c392386e06f2673f0e0783e22d10087989" and "d36ba595dab0bbaef1012ebfa8930fc0e6bf8167" have entirely different histories.
03c4d3c392
...
d36ba595da
22 changed files with 179 additions and 396 deletions
|
@ -41,7 +41,7 @@ rustls-tls-native-roots = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustypipe = { workspace = true, features = ["rss"] }
|
rustypipe.workspace = true
|
||||||
rustypipe-downloader.workspace = true
|
rustypipe-downloader.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
267
cli/src/main.rs
267
cli/src/main.rs
|
@ -1,12 +1,7 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![warn(clippy::todo, clippy::dbg_macro)]
|
#![warn(clippy::todo, clippy::dbg_macro)]
|
||||||
|
|
||||||
use std::{
|
use std::{path::PathBuf, str::FromStr, time::Duration};
|
||||||
path::PathBuf,
|
|
||||||
str::FromStr,
|
|
||||||
sync::{atomic::AtomicUsize, Arc},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::stream::{self, StreamExt};
|
||||||
|
@ -94,23 +89,20 @@ 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(short, long)]
|
#[clap(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(short, long)]
|
#[clap(long)]
|
||||||
music: bool,
|
music: bool,
|
||||||
/// Limit the number of videos to download
|
/// Limit the number of videos to download
|
||||||
#[clap(short, long, default_value_t = 1000)]
|
#[clap(long, default_value_t = 1000)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
/// YT Client used to fetch player data
|
/// YT Client used to fetch player data
|
||||||
#[clap(short, long)]
|
|
||||||
client_type: Option<Vec<ClientTypeArg>>,
|
|
||||||
/// `pot` token to circumvent bot detection
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pot: Option<String>,
|
client_type: Option<PlayerType>,
|
||||||
},
|
},
|
||||||
/// Extract video, playlist, album or channel data
|
/// Extract video, playlist, album or channel data
|
||||||
Get {
|
Get {
|
||||||
|
@ -123,20 +115,17 @@ enum Commands {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pretty: bool,
|
pretty: bool,
|
||||||
/// Output as text
|
/// Output as text
|
||||||
#[clap(short, long)]
|
#[clap(long)]
|
||||||
txt: bool,
|
txt: bool,
|
||||||
/// Limit the number of items to fetch
|
/// Limit the number of items to fetch
|
||||||
#[clap(short, long, default_value_t = 20)]
|
#[clap(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(short, long)]
|
|
||||||
music: bool,
|
|
||||||
/// Use the RSS feed of a channel
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
rss: bool,
|
music: bool,
|
||||||
/// Get comments
|
/// Get comments
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
comments: Option<CommentsOrder>,
|
comments: Option<CommentsOrder>,
|
||||||
|
@ -147,8 +136,8 @@ 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(short, long)]
|
#[clap(long)]
|
||||||
client_type: Option<ClientTypeArg>,
|
client_type: Option<PlayerType>,
|
||||||
},
|
},
|
||||||
/// Search YouTube
|
/// Search YouTube
|
||||||
Search {
|
Search {
|
||||||
|
@ -161,10 +150,10 @@ enum Commands {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pretty: bool,
|
pretty: bool,
|
||||||
/// Output as text
|
/// Output as text
|
||||||
#[clap(short, long)]
|
#[clap(long)]
|
||||||
txt: bool,
|
txt: bool,
|
||||||
/// Limit the number of items to fetch
|
/// Limit the number of items to fetch
|
||||||
#[clap(short, long, default_value_t = 20)]
|
#[clap(long, default_value_t = 20)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
/// Filter results by item type
|
/// Filter results by item type
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
|
@ -182,10 +171,9 @@ enum Commands {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
channel: Option<String>,
|
channel: Option<String>,
|
||||||
/// YouTube Music search filter
|
/// YouTube Music search filter
|
||||||
#[clap(short, long)]
|
#[clap(long)]
|
||||||
music: Option<MusicSearchCategory>,
|
music: Option<MusicSearchCategory>,
|
||||||
},
|
},
|
||||||
/// Get a YouTube visitor data cookie
|
|
||||||
Vdata,
|
Vdata,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +251,7 @@ enum MusicSearchCategory {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
|
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
|
||||||
enum ClientTypeArg {
|
enum PlayerType {
|
||||||
Desktop,
|
Desktop,
|
||||||
Tv,
|
Tv,
|
||||||
TvEmbed,
|
TvEmbed,
|
||||||
|
@ -313,14 +301,14 @@ impl From<SearchOrder> for search_filter::Order {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ClientTypeArg> for ClientType {
|
impl From<PlayerType> for ClientType {
|
||||||
fn from(value: ClientTypeArg) -> Self {
|
fn from(value: PlayerType) -> Self {
|
||||||
match value {
|
match value {
|
||||||
ClientTypeArg::Desktop => Self::Desktop,
|
PlayerType::Desktop => Self::Desktop,
|
||||||
ClientTypeArg::TvEmbed => Self::TvHtml5Embed,
|
PlayerType::TvEmbed => Self::TvHtml5Embed,
|
||||||
ClientTypeArg::Tv => Self::Tv,
|
PlayerType::Tv => Self::Tv,
|
||||||
ClientTypeArg::Android => Self::Android,
|
PlayerType::Android => Self::Android,
|
||||||
ClientTypeArg::Ios => Self::Ios,
|
PlayerType::Ios => Self::Ios,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -427,11 +415,11 @@ async fn download_video(
|
||||||
dl: &Downloader,
|
dl: &Downloader,
|
||||||
id: &str,
|
id: &str,
|
||||||
target: &DownloadTarget,
|
target: &DownloadTarget,
|
||||||
client_types: Option<&[ClientType]>,
|
client_type: Option<PlayerType>,
|
||||||
) {
|
) {
|
||||||
let mut q = target.apply(dl.id(id));
|
let mut q = target.apply(dl.id(id));
|
||||||
if let Some(client_types) = client_types {
|
if let Some(client_type) = client_type {
|
||||||
q = q.client_types(client_types);
|
q = q.client_type(client_type.into());
|
||||||
}
|
}
|
||||||
let res = q.download().await;
|
let res = q.download().await;
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
|
@ -444,9 +432,9 @@ async fn download_videos(
|
||||||
videos: Vec<DownloadVideo>,
|
videos: Vec<DownloadVideo>,
|
||||||
target: &DownloadTarget,
|
target: &DownloadTarget,
|
||||||
parallel: usize,
|
parallel: usize,
|
||||||
client_types: Option<&[ClientType]>,
|
client_type: Option<PlayerType>,
|
||||||
multi: MultiProgress,
|
multi: MultiProgress,
|
||||||
) -> anyhow::Result<()> {
|
) {
|
||||||
// Indicatif setup
|
// Indicatif setup
|
||||||
let main = multi.add(ProgressBar::new(
|
let main = multi.add(ProgressBar::new(
|
||||||
videos.len().try_into().unwrap_or_default(),
|
videos.len().try_into().unwrap_or_default(),
|
||||||
|
@ -460,38 +448,27 @@ async fn download_videos(
|
||||||
);
|
);
|
||||||
main.tick();
|
main.tick();
|
||||||
|
|
||||||
let n_failed = Arc::new(AtomicUsize::default());
|
|
||||||
|
|
||||||
stream::iter(videos)
|
stream::iter(videos)
|
||||||
.for_each_concurrent(parallel, |video| {
|
.for_each_concurrent(parallel, |video| {
|
||||||
let dl = dl.clone();
|
let dl = dl.clone();
|
||||||
let main = main.clone();
|
let main = main.clone();
|
||||||
let id = video.id().to_owned();
|
let id = video.id().to_owned();
|
||||||
let n_failed = n_failed.clone();
|
|
||||||
|
|
||||||
let mut q = target.apply(dl.video(video));
|
let mut q = target.apply(dl.video(video));
|
||||||
if let Some(client_types) = client_types {
|
if let Some(client_type) = client_type {
|
||||||
q = q.client_types(client_types);
|
q = q.client_type(client_type.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
if let Err(e) = q.download().await {
|
if let Err(e) = q.download().await {
|
||||||
if !matches!(e, DownloadError::Exists(_)) {
|
if !matches!(e, DownloadError::Exists(_)) {
|
||||||
tracing::error!("[{id}]: {e}");
|
tracing::error!("[{id}]: {e}");
|
||||||
n_failed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
main.inc(1);
|
main.inc(1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let n_failed = n_failed.load(std::sync::atomic::Ordering::Relaxed);
|
|
||||||
if n_failed > 0 {
|
|
||||||
anyhow::bail!("{n_failed} downloads failed");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stderr writer that suspends the progress bars before printing logs
|
/// Stderr writer that suspends the progress bars before printing logs
|
||||||
|
@ -518,14 +495,6 @@ impl std::io::Write for ProgWriter {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
if let Err(e) = run().await {
|
|
||||||
println!("{}", "Error:".red().bold());
|
|
||||||
println!("{}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run() -> anyhow::Result<()> {
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let multi = MultiProgress::new();
|
let multi = MultiProgress::new();
|
||||||
|
|
||||||
|
@ -555,7 +524,7 @@ async fn run() -> anyhow::Result<()> {
|
||||||
if let Some(country) = cli.country {
|
if let Some(country) = cli.country {
|
||||||
rp = rp.country(Country::from_str(&country.to_ascii_uppercase()).expect("invalid country"));
|
rp = rp.country(Country::from_str(&country.to_ascii_uppercase()).expect("invalid country"));
|
||||||
}
|
}
|
||||||
let rp = rp.build()?;
|
let rp = rp.build().unwrap();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Download {
|
Commands::Download {
|
||||||
|
@ -567,9 +536,8 @@ async fn run() -> anyhow::Result<()> {
|
||||||
music,
|
music,
|
||||||
limit,
|
limit,
|
||||||
client_type,
|
client_type,
|
||||||
pot,
|
|
||||||
} => {
|
} => {
|
||||||
let url_target = rp.query().resolve_string(&id, false).await?;
|
let url_target = rp.query().resolve_string(&id, false).await.unwrap();
|
||||||
|
|
||||||
let mut filter = StreamFilter::new();
|
let mut filter = StreamFilter::new();
|
||||||
if let Some(res) = resolution {
|
if let Some(res) = resolution {
|
||||||
|
@ -587,21 +555,20 @@ async fn run() -> anyhow::Result<()> {
|
||||||
dl = dl.audio_tag().crop_cover();
|
dl = dl.audio_tag().crop_cover();
|
||||||
filter = filter.no_video();
|
filter = filter.no_video();
|
||||||
}
|
}
|
||||||
if let Some(pot) = pot {
|
|
||||||
dl = dl.pot(pot);
|
|
||||||
}
|
|
||||||
let dl = dl.stream_filter(filter).build();
|
let dl = dl.stream_filter(filter).build();
|
||||||
|
|
||||||
let cts = client_type.map(|c| c.into_iter().map(ClientType::from).collect::<Vec<_>>());
|
|
||||||
|
|
||||||
match url_target {
|
match url_target {
|
||||||
UrlTarget::Video { id, .. } => {
|
UrlTarget::Video { id, .. } => {
|
||||||
download_video(&dl, &id, &target, cts.as_deref()).await;
|
download_video(&dl, &id, &target, client_type).await;
|
||||||
}
|
}
|
||||||
UrlTarget::Channel { id } => {
|
UrlTarget::Channel { id } => {
|
||||||
target.assert_dir();
|
target.assert_dir();
|
||||||
let mut channel = rp.query().channel_videos(id).await?;
|
let mut channel = rp.query().channel_videos(id).await.unwrap();
|
||||||
channel.content.extend_limit(&rp.query(), limit).await?;
|
channel
|
||||||
|
.content
|
||||||
|
.extend_limit(&rp.query(), limit)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let videos = channel
|
let videos = channel
|
||||||
.content
|
.content
|
||||||
.items
|
.items
|
||||||
|
@ -609,13 +576,17 @@ async fn run() -> anyhow::Result<()> {
|
||||||
.take(limit)
|
.take(limit)
|
||||||
.map(|v| DownloadVideo::from_entity(&v))
|
.map(|v| DownloadVideo::from_entity(&v))
|
||||||
.collect();
|
.collect();
|
||||||
download_videos(&dl, videos, &target, parallel, cts.as_deref(), multi).await?;
|
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||||
}
|
}
|
||||||
UrlTarget::Playlist { id } => {
|
UrlTarget::Playlist { id } => {
|
||||||
target.assert_dir();
|
target.assert_dir();
|
||||||
let videos = if music {
|
let videos = if music {
|
||||||
let mut playlist = rp.query().music_playlist(id).await?;
|
let mut playlist = rp.query().music_playlist(id).await.unwrap();
|
||||||
playlist.tracks.extend_limit(&rp.query(), limit).await?;
|
playlist
|
||||||
|
.tracks
|
||||||
|
.extend_limit(&rp.query(), limit)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
playlist
|
playlist
|
||||||
.tracks
|
.tracks
|
||||||
.items
|
.items
|
||||||
|
@ -624,8 +595,12 @@ async fn run() -> anyhow::Result<()> {
|
||||||
.map(|v| DownloadVideo::from_track(&v))
|
.map(|v| DownloadVideo::from_track(&v))
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
let mut playlist = rp.query().playlist(id).await?;
|
let mut playlist = rp.query().playlist(id).await.unwrap();
|
||||||
playlist.videos.extend_limit(&rp.query(), limit).await?;
|
playlist
|
||||||
|
.videos
|
||||||
|
.extend_limit(&rp.query(), limit)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
playlist
|
playlist
|
||||||
.videos
|
.videos
|
||||||
.items
|
.items
|
||||||
|
@ -634,18 +609,18 @@ async fn run() -> anyhow::Result<()> {
|
||||||
.map(|v| DownloadVideo::from_entity(&v))
|
.map(|v| DownloadVideo::from_entity(&v))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
download_videos(&dl, videos, &target, parallel, cts.as_deref(), multi).await?;
|
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||||
}
|
}
|
||||||
UrlTarget::Album { id } => {
|
UrlTarget::Album { id } => {
|
||||||
target.assert_dir();
|
target.assert_dir();
|
||||||
let album = rp.query().music_album(id).await?;
|
let album = rp.query().music_album(id).await.unwrap();
|
||||||
let videos = album
|
let videos = album
|
||||||
.tracks
|
.tracks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.take(limit)
|
.take(limit)
|
||||||
.map(|v| DownloadVideo::from_track(&v))
|
.map(|v| DownloadVideo::from_track(&v))
|
||||||
.collect();
|
.collect();
|
||||||
download_videos(&dl, videos, &target, parallel, cts.as_deref(), multi).await?;
|
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -657,23 +632,22 @@ async fn run() -> anyhow::Result<()> {
|
||||||
limit,
|
limit,
|
||||||
tab,
|
tab,
|
||||||
music,
|
music,
|
||||||
rss,
|
|
||||||
comments,
|
comments,
|
||||||
lyrics,
|
lyrics,
|
||||||
player,
|
player,
|
||||||
client_type,
|
client_type,
|
||||||
} => {
|
} => {
|
||||||
let target = rp.query().resolve_string(&id, false).await?;
|
let target = rp.query().resolve_string(&id, false).await.unwrap();
|
||||||
|
|
||||||
match target {
|
match target {
|
||||||
UrlTarget::Video { id, .. } => {
|
UrlTarget::Video { id, .. } => {
|
||||||
if lyrics {
|
if lyrics {
|
||||||
let details = rp.query().music_details(&id).await?;
|
let details = rp.query().music_details(&id).await.unwrap();
|
||||||
match details.lyrics_id {
|
match details.lyrics_id {
|
||||||
Some(lyrics_id) => {
|
Some(lyrics_id) => {
|
||||||
let lyrics = rp.query().music_lyrics(lyrics_id).await?;
|
let lyrics = rp.query().music_lyrics(lyrics_id).await.unwrap();
|
||||||
if txt {
|
if txt {
|
||||||
println!("{}\n\n{}", lyrics.body, lyrics.footer.blue());
|
println!("{}\n\n{}", lyrics.body, lyrics.footer);
|
||||||
} else {
|
} else {
|
||||||
print_data(&lyrics, format, pretty);
|
print_data(&lyrics, format, pretty);
|
||||||
}
|
}
|
||||||
|
@ -681,26 +655,21 @@ async fn run() -> anyhow::Result<()> {
|
||||||
None => eprintln!("no lyrics found"),
|
None => eprintln!("no lyrics found"),
|
||||||
}
|
}
|
||||||
} else if music {
|
} else if music {
|
||||||
let details = rp.query().music_details(&id).await?;
|
let details = rp.query().music_details(&id).await.unwrap();
|
||||||
if txt {
|
if txt {
|
||||||
if details.track.is_video {
|
if details.track.is_video {
|
||||||
anstream::println!("{}", "[MV]".on_green().black());
|
println!("[MV]");
|
||||||
} else {
|
} else {
|
||||||
anstream::println!("{}", "[Track]".on_green().black());
|
println!("[Track]");
|
||||||
}
|
}
|
||||||
anstream::print!(
|
print!("{} [{}]", details.track.name, details.track.id);
|
||||||
"{} [{}]",
|
|
||||||
details.track.name.green().bold(),
|
|
||||||
details.track.id
|
|
||||||
);
|
|
||||||
print_duration(details.track.duration);
|
print_duration(details.track.duration);
|
||||||
println!();
|
println!();
|
||||||
print_artists(&details.track.artists);
|
print_artists(&details.track.artists);
|
||||||
println!();
|
println!();
|
||||||
if !details.track.is_video {
|
if !details.track.is_video {
|
||||||
anstream::println!(
|
println!(
|
||||||
"{} {}",
|
"Album: {}",
|
||||||
"Album:".blue(),
|
|
||||||
details
|
details
|
||||||
.track
|
.track
|
||||||
.album
|
.album
|
||||||
|
@ -710,7 +679,7 @@ async fn run() -> anyhow::Result<()> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if let Some(view_count) = details.track.view_count {
|
if let Some(view_count) = details.track.view_count {
|
||||||
anstream::println!("{} {}", "Views:".blue(), view_count);
|
println!("Views: {view_count}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print_data(&details, format, pretty);
|
print_data(&details, format, pretty);
|
||||||
|
@ -720,20 +689,26 @@ async fn run() -> anyhow::Result<()> {
|
||||||
rp.query().player_from_client(&id, client_type.into()).await
|
rp.query().player_from_client(&id, client_type.into()).await
|
||||||
} else {
|
} else {
|
||||||
rp.query().player(&id).await
|
rp.query().player(&id).await
|
||||||
}?;
|
}
|
||||||
|
.unwrap();
|
||||||
print_data(&player, format, pretty);
|
print_data(&player, format, pretty);
|
||||||
} else {
|
} else {
|
||||||
let mut details = rp.query().video_details(&id).await?;
|
let mut details = rp.query().video_details(&id).await.unwrap();
|
||||||
|
|
||||||
match comments {
|
match comments {
|
||||||
Some(CommentsOrder::Top) => {
|
Some(CommentsOrder::Top) => {
|
||||||
details.top_comments.extend_limit(rp.query(), limit).await?;
|
details
|
||||||
|
.top_comments
|
||||||
|
.extend_limit(rp.query(), limit)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
Some(CommentsOrder::Latest) => {
|
Some(CommentsOrder::Latest) => {
|
||||||
details
|
details
|
||||||
.latest_comments
|
.latest_comments
|
||||||
.extend_limit(rp.query(), limit)
|
.extend_limit(rp.query(), limit)
|
||||||
.await?;
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
@ -811,7 +786,7 @@ async fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
UrlTarget::Channel { id } => {
|
UrlTarget::Channel { id } => {
|
||||||
if music {
|
if music {
|
||||||
let artist = rp.query().music_artist(&id, true).await?;
|
let artist = rp.query().music_artist(&id, true).await.unwrap();
|
||||||
if txt {
|
if txt {
|
||||||
anstream::println!(
|
anstream::println!(
|
||||||
"{}\n{} [{}]",
|
"{}\n{} [{}]",
|
||||||
|
@ -861,31 +836,6 @@ async fn run() -> anyhow::Result<()> {
|
||||||
} else {
|
} else {
|
||||||
print_data(&artist, format, pretty);
|
print_data(&artist, format, pretty);
|
||||||
}
|
}
|
||||||
} else if rss {
|
|
||||||
let rss = rp.query().channel_rss(&id).await?;
|
|
||||||
|
|
||||||
if txt {
|
|
||||||
anstream::println!(
|
|
||||||
"{}\n{} [{}]\n{} {}",
|
|
||||||
"[Channel RSS]".on_green().black(),
|
|
||||||
rss.name.green().bold(),
|
|
||||||
rss.id,
|
|
||||||
"Created on:".blue(),
|
|
||||||
rss.create_date,
|
|
||||||
);
|
|
||||||
if let Some(v) = rss.videos.first() {
|
|
||||||
anstream::println!(
|
|
||||||
"{} {} [{}]",
|
|
||||||
"Latest video:".blue(),
|
|
||||||
v.publish_date,
|
|
||||||
v.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
print_entities(&rss.videos);
|
|
||||||
} else {
|
|
||||||
print_data(&rss, format, pretty);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
match tab {
|
match tab {
|
||||||
ChannelTab::Videos | ChannelTab::Shorts | ChannelTab::Live => {
|
ChannelTab::Videos | ChannelTab::Shorts | ChannelTab::Live => {
|
||||||
|
@ -896,9 +846,13 @@ async fn run() -> anyhow::Result<()> {
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let mut channel =
|
let mut channel =
|
||||||
rp.query().channel_videos_tab(&id, video_tab).await?;
|
rp.query().channel_videos_tab(&id, video_tab).await.unwrap();
|
||||||
|
|
||||||
channel.content.extend_limit(rp.query(), limit).await?;
|
channel
|
||||||
|
.content
|
||||||
|
.extend_limit(rp.query(), limit)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
if txt {
|
if txt {
|
||||||
anstream::print!(
|
anstream::print!(
|
||||||
|
@ -920,7 +874,7 @@ async fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChannelTab::Playlists => {
|
ChannelTab::Playlists => {
|
||||||
let channel = rp.query().channel_playlists(&id).await?;
|
let channel = rp.query().channel_playlists(&id).await.unwrap();
|
||||||
|
|
||||||
if txt {
|
if txt {
|
||||||
anstream::println!(
|
anstream::println!(
|
||||||
|
@ -940,7 +894,7 @@ async fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChannelTab::Info => {
|
ChannelTab::Info => {
|
||||||
let info = rp.query().channel_info(&id).await?;
|
let info = rp.query().channel_info(&id).await.unwrap();
|
||||||
|
|
||||||
if txt {
|
if txt {
|
||||||
anstream::println!(
|
anstream::println!(
|
||||||
|
@ -976,8 +930,12 @@ async fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
UrlTarget::Playlist { id } => {
|
UrlTarget::Playlist { id } => {
|
||||||
if music {
|
if music {
|
||||||
let mut playlist = rp.query().music_playlist(&id).await?;
|
let mut playlist = rp.query().music_playlist(&id).await.unwrap();
|
||||||
playlist.tracks.extend_limit(rp.query(), limit).await?;
|
playlist
|
||||||
|
.tracks
|
||||||
|
.extend_limit(rp.query(), limit)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
if txt {
|
if txt {
|
||||||
anstream::println!(
|
anstream::println!(
|
||||||
"{}\n{} [{}]\n{} {}",
|
"{}\n{} [{}]\n{} {}",
|
||||||
|
@ -1001,8 +959,12 @@ async fn run() -> anyhow::Result<()> {
|
||||||
print_data(&playlist, format, pretty);
|
print_data(&playlist, format, pretty);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut playlist = rp.query().playlist(&id).await?;
|
let mut playlist = rp.query().playlist(&id).await.unwrap();
|
||||||
playlist.videos.extend_limit(rp.query(), limit).await?;
|
playlist
|
||||||
|
.videos
|
||||||
|
.extend_limit(rp.query(), limit)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
if txt {
|
if txt {
|
||||||
anstream::println!(
|
anstream::println!(
|
||||||
"{}\n{} [{}]\n{} {}",
|
"{}\n{} [{}]\n{} {}",
|
||||||
|
@ -1031,7 +993,7 @@ async fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UrlTarget::Album { id } => {
|
UrlTarget::Album { id } => {
|
||||||
let album = rp.query().music_album(&id).await?;
|
let album = rp.query().music_album(&id).await.unwrap();
|
||||||
if txt {
|
if txt {
|
||||||
anstream::print!(
|
anstream::print!(
|
||||||
"{}\n{} [{}] ({:?}",
|
"{}\n{} [{}] ({:?}",
|
||||||
|
@ -1074,8 +1036,8 @@ async fn run() -> anyhow::Result<()> {
|
||||||
} => match music {
|
} => match music {
|
||||||
None => match channel {
|
None => match channel {
|
||||||
Some(channel) => {
|
Some(channel) => {
|
||||||
rustypipe::validate::channel_id(&channel)?;
|
rustypipe::validate::channel_id(&channel).unwrap();
|
||||||
let res = rp.query().channel_search(&channel, &query).await?;
|
let res = rp.query().channel_search(&channel, &query).await.unwrap();
|
||||||
print_data(&res, format, pretty);
|
print_data(&res, format, pretty);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -1087,8 +1049,9 @@ async fn run() -> anyhow::Result<()> {
|
||||||
let mut res = rp
|
let mut res = rp
|
||||||
.query()
|
.query()
|
||||||
.search_filter::<YouTubeItem, _>(&query, &filter)
|
.search_filter::<YouTubeItem, _>(&query, &filter)
|
||||||
.await?;
|
.await
|
||||||
res.items.extend_limit(rp.query(), limit).await?;
|
.unwrap();
|
||||||
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
|
|
||||||
if txt {
|
if txt {
|
||||||
if let Some(corr) = res.corrected_query {
|
if let Some(corr) = res.corrected_query {
|
||||||
|
@ -1101,27 +1064,27 @@ async fn run() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(MusicSearchCategory::All) => {
|
Some(MusicSearchCategory::All) => {
|
||||||
let res = rp.query().music_search_main(&query).await?;
|
let res = rp.query().music_search_main(&query).await.unwrap();
|
||||||
print_music_search(&res, format, pretty, txt);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::Tracks) => {
|
Some(MusicSearchCategory::Tracks) => {
|
||||||
let mut res = rp.query().music_search_tracks(&query).await?;
|
let mut res = rp.query().music_search_tracks(&query).await.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await?;
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_music_search(&res, format, pretty, txt);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::Videos) => {
|
Some(MusicSearchCategory::Videos) => {
|
||||||
let mut res = rp.query().music_search_videos(&query).await?;
|
let mut res = rp.query().music_search_videos(&query).await.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await?;
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_music_search(&res, format, pretty, txt);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::Artists) => {
|
Some(MusicSearchCategory::Artists) => {
|
||||||
let mut res = rp.query().music_search_artists(&query).await?;
|
let mut res = rp.query().music_search_artists(&query).await.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await?;
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_music_search(&res, format, pretty, txt);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::Albums) => {
|
Some(MusicSearchCategory::Albums) => {
|
||||||
let mut res = rp.query().music_search_albums(&query).await?;
|
let mut res = rp.query().music_search_albums(&query).await.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await?;
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_music_search(&res, format, pretty, txt);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => {
|
Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => {
|
||||||
|
@ -1131,15 +1094,15 @@ async fn run() -> anyhow::Result<()> {
|
||||||
&query,
|
&query,
|
||||||
music == Some(MusicSearchCategory::PlaylistsCommunity),
|
music == Some(MusicSearchCategory::PlaylistsCommunity),
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
res.items.extend_limit(rp.query(), limit).await?;
|
.unwrap();
|
||||||
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_music_search(&res, format, pretty, txt);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Commands::Vdata => {
|
Commands::Vdata => {
|
||||||
let vd = rp.query().get_visitor_data().await?;
|
let vd = rp.query().get_visitor_data().await.unwrap();
|
||||||
println!("{vd}");
|
println!("{vd}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@ use futures::stream::{self, StreamExt};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::{header, Client, StatusCode, Url};
|
use reqwest::{header, Client, StatusCode};
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::{ClientType, RustyPipe, DEFAULT_PLAYER_CLIENT_ORDER},
|
client::{ClientType, RustyPipe},
|
||||||
model::{
|
model::{
|
||||||
traits::{FileFormat, YtEntity},
|
traits::{FileFormat, YtEntity},
|
||||||
AudioCodec, TrackItem, VideoCodec, VideoPlayer,
|
AudioCodec, TrackItem, VideoCodec, VideoPlayer,
|
||||||
|
@ -74,8 +74,6 @@ pub struct DownloaderBuilder {
|
||||||
audio_tag: bool,
|
audio_tag: bool,
|
||||||
#[cfg(feature = "audiotag")]
|
#[cfg(feature = "audiotag")]
|
||||||
crop_cover: bool,
|
crop_cover: bool,
|
||||||
client_types: Option<Vec<ClientType>>,
|
|
||||||
pot: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DownloaderInner {
|
struct DownloaderInner {
|
||||||
|
@ -105,10 +103,6 @@ struct DownloaderInner {
|
||||||
/// Crop YT thumbnails to ensure square album covers
|
/// Crop YT thumbnails to ensure square album covers
|
||||||
#[cfg(feature = "audiotag")]
|
#[cfg(feature = "audiotag")]
|
||||||
crop_cover: bool,
|
crop_cover: bool,
|
||||||
/// Client types for fetching videos
|
|
||||||
client_types: Option<Vec<ClientType>>,
|
|
||||||
/// Pot token to circumvent bot detection
|
|
||||||
pot: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download query
|
/// Download query
|
||||||
|
@ -126,10 +120,8 @@ pub struct DownloadQuery {
|
||||||
filter: Option<StreamFilter>,
|
filter: Option<StreamFilter>,
|
||||||
/// Target video format
|
/// Target video format
|
||||||
video_format: Option<DownloadVideoFormat>,
|
video_format: Option<DownloadVideoFormat>,
|
||||||
/// Client types for fetching videos
|
/// ClientType type for fetching videos
|
||||||
client_types: Option<Vec<ClientType>>,
|
client_type: Option<ClientType>,
|
||||||
/// Pot token to circumvent bot detection
|
|
||||||
pot: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Video to be downloaded
|
/// Video to be downloaded
|
||||||
|
@ -295,8 +287,6 @@ impl Default for DownloaderBuilder {
|
||||||
audio_tag: false,
|
audio_tag: false,
|
||||||
#[cfg(feature = "audiotag")]
|
#[cfg(feature = "audiotag")]
|
||||||
crop_cover: false,
|
crop_cover: false,
|
||||||
client_types: None,
|
|
||||||
pot: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -394,38 +384,6 @@ impl DownloaderBuilder {
|
||||||
self
|
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<T: Into<Vec<ClientType>>>(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.
|
|
||||||
/// The token is generated using YouTube's botguard. Therefore you need a full browser environment
|
|
||||||
/// to obtain one.
|
|
||||||
///
|
|
||||||
/// The Invidious project has created a script to extract this token: <https://github.com/iv-org/youtube-trusted-session-generator>
|
|
||||||
///
|
|
||||||
/// The `pot` token is only used for the [`ClientType::Desktop`] and [`ClientType::DesktopMusic`] clients.
|
|
||||||
#[must_use]
|
|
||||||
pub fn pot<S: Into<String>>(mut self, pot: S) -> Self {
|
|
||||||
self.pot = Some(pot.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new, configured [`Downloader`] instance
|
/// Create a new, configured [`Downloader`] instance
|
||||||
pub fn build(self) -> Downloader {
|
pub fn build(self) -> Downloader {
|
||||||
self.build_with_client(
|
self.build_with_client(
|
||||||
|
@ -459,8 +417,6 @@ impl DownloaderBuilder {
|
||||||
audio_tag: self.audio_tag,
|
audio_tag: self.audio_tag,
|
||||||
#[cfg(feature = "audiotag")]
|
#[cfg(feature = "audiotag")]
|
||||||
crop_cover: self.crop_cover,
|
crop_cover: self.crop_cover,
|
||||||
client_types: self.client_types,
|
|
||||||
pot: self.pot,
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -494,8 +450,7 @@ impl Downloader {
|
||||||
progress: None,
|
progress: None,
|
||||||
filter: None,
|
filter: None,
|
||||||
video_format: None,
|
video_format: None,
|
||||||
client_types: None,
|
client_type: None,
|
||||||
pot: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -631,32 +586,7 @@ impl DownloadQuery {
|
||||||
/// Set the [`ClientType`] used to fetch the YT player
|
/// Set the [`ClientType`] used to fetch the YT player
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn client_type(mut self, client_type: ClientType) -> Self {
|
pub fn client_type(mut self, client_type: ClientType) -> Self {
|
||||||
self.client_types = Some(vec![client_type]);
|
self.client_type = Some(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<T: Into<Vec<ClientType>>>(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.
|
|
||||||
/// The token is generated using YouTube's botguard. Therefore you need a full browser environment
|
|
||||||
/// to obtain one.
|
|
||||||
///
|
|
||||||
/// The Invidious project has created a script to extract this token: <https://github.com/iv-org/youtube-trusted-session-generator>
|
|
||||||
///
|
|
||||||
/// The `pot` token is only used for the [`ClientType::Desktop`] and [`ClientType::DesktopMusic`] clients.
|
|
||||||
#[must_use]
|
|
||||||
pub fn pot<S: Into<String>>(mut self, pot: S) -> Self {
|
|
||||||
self.pot = Some(pot.into());
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -664,10 +594,9 @@ impl DownloadQuery {
|
||||||
///
|
///
|
||||||
/// If no download path is set, the video is downloaded to the current directory
|
/// If no download path is set, the video is downloaded to the current directory
|
||||||
/// with a filename created by this template: `{track} {title} [{id}]`.
|
/// with a filename created by this template: `{track} {title} [{id}]`.
|
||||||
#[tracing::instrument(skip(self), level="error", fields(id = self.video.id))]
|
#[tracing::instrument(skip(self), 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")]
|
||||||
|
@ -684,19 +613,14 @@ 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() {
|
if !e.is_timeout() && e.status() != Some(StatusCode::FORBIDDEN) {
|
||||||
return Err(DownloadError::Http(e));
|
return Err(DownloadError::Http(e));
|
||||||
}
|
}
|
||||||
DownloadError::Http(e)
|
DownloadError::Http(e)
|
||||||
|
@ -716,7 +640,6 @@ 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);
|
||||||
|
@ -749,45 +672,19 @@ impl DownloadQuery {
|
||||||
};
|
};
|
||||||
#[cfg(feature = "indicatif")]
|
#[cfg(feature = "indicatif")]
|
||||||
if let Some(pb) = pb {
|
if let Some(pb) = pb {
|
||||||
if let Some(n) = &self.video.name {
|
pb.set_message(format!(
|
||||||
pb.set_message(format!("Fetching player data for {n}{attempt_suffix}"));
|
"Fetching player data for {}{}",
|
||||||
} else {
|
self.video.name.as_deref().unwrap_or_default(),
|
||||||
pb.set_message(format!("Fetching player data{attempt_suffix}"));
|
attempt_suffix
|
||||||
}
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
let q = self.dl.i.rp.query();
|
let q = self.dl.i.rp.query();
|
||||||
|
let player_data = match self.client_type {
|
||||||
let mut client_types = Cow::Borrowed(
|
Some(client_type) => q.player_from_client(&self.video.id, client_type).await?,
|
||||||
self.client_types
|
None => q.player(&self.video.id).await?,
|
||||||
.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,
|
|
||||||
ClientType::Desktop | ClientType::DesktopMusic
|
|
||||||
) {
|
|
||||||
self.pot.as_deref().or(self.dl.i.pot.as_deref())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
let user_agent = q.user_agent(player_data.client_type);
|
||||||
|
|
||||||
// Select streams to download
|
// Select streams to download
|
||||||
let (video, audio) = player_data.select_video_audio_stream(filter);
|
let (video, audio) = player_data.select_video_audio_stream(filter);
|
||||||
|
@ -865,19 +762,10 @@ impl DownloadQuery {
|
||||||
&downloads,
|
&downloads,
|
||||||
&self.dl.i.http,
|
&self.dl.i.http,
|
||||||
&user_agent,
|
&user_agent,
|
||||||
pot,
|
|
||||||
#[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 {
|
||||||
|
@ -1118,7 +1006,6 @@ async fn download_single_file(
|
||||||
output: &Path,
|
output: &Path,
|
||||||
http: &Client,
|
http: &Client,
|
||||||
user_agent: &str,
|
user_agent: &str,
|
||||||
pot: Option<&str>,
|
|
||||||
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Check if file is already downloaded
|
// Check if file is already downloaded
|
||||||
|
@ -1215,7 +1102,6 @@ async fn download_single_file(
|
||||||
size.unwrap(),
|
size.unwrap(),
|
||||||
offset,
|
offset,
|
||||||
user_agent,
|
user_agent,
|
||||||
pot,
|
|
||||||
#[cfg(feature = "indicatif")]
|
#[cfg(feature = "indicatif")]
|
||||||
pb,
|
pb,
|
||||||
)
|
)
|
||||||
|
@ -1323,7 +1209,6 @@ async fn download_chunks_by_header(
|
||||||
// Use the `range` url parameter to download a stream in chunks.
|
// Use the `range` url parameter to download a stream in chunks.
|
||||||
// This ist used by YouTube's web player. The file size
|
// This ist used by YouTube's web player. The file size
|
||||||
// must be known beforehand (it is included in the stream url).
|
// must be known beforehand (it is included in the stream url).
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn download_chunks_by_param(
|
async fn download_chunks_by_param(
|
||||||
http: &Client,
|
http: &Client,
|
||||||
file: &mut File,
|
file: &mut File,
|
||||||
|
@ -1331,7 +1216,6 @@ async fn download_chunks_by_param(
|
||||||
size: u64,
|
size: u64,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
user_agent: &str,
|
user_agent: &str,
|
||||||
pot: Option<&str>,
|
|
||||||
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut offset = offset;
|
let mut offset = offset;
|
||||||
|
@ -1344,15 +1228,8 @@ async fn download_chunks_by_param(
|
||||||
let range = get_download_range(offset, Some(size));
|
let range = get_download_range(offset, Some(size));
|
||||||
tracing::debug!("Fetching range {}-{}", range.start, range.end);
|
tracing::debug!("Fetching range {}-{}", range.start, range.end);
|
||||||
|
|
||||||
let mut urlp =
|
|
||||||
Url::parse_with_params(url, [("range", &format!("{}-{}", range.start, range.end))])
|
|
||||||
.map_err(|e| DownloadError::Progressive(format!("url parsing: {e}").into()))?;
|
|
||||||
if let Some(pot) = pot {
|
|
||||||
urlp.query_pairs_mut().append_pair("pot", pot);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = http
|
let res = http
|
||||||
.get(urlp)
|
.get(format!("{}&range={}-{}", url, range.start, range.end))
|
||||||
.header(header::USER_AGENT, user_agent)
|
.header(header::USER_AGENT, user_agent)
|
||||||
.header(header::ORIGIN, "https://www.youtube.com")
|
.header(header::ORIGIN, "https://www.youtube.com")
|
||||||
.header(header::REFERER, "https://www.youtube.com/")
|
.header(header::REFERER, "https://www.youtube.com/")
|
||||||
|
@ -1400,7 +1277,6 @@ async fn download_streams(
|
||||||
downloads: &Vec<StreamDownload>,
|
downloads: &Vec<StreamDownload>,
|
||||||
http: &Client,
|
http: &Client,
|
||||||
user_agent: &str,
|
user_agent: &str,
|
||||||
pot: Option<&str>,
|
|
||||||
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let n = downloads.len();
|
let n = downloads.len();
|
||||||
|
@ -1412,7 +1288,6 @@ async fn download_streams(
|
||||||
&d.file,
|
&d.file,
|
||||||
http,
|
http,
|
||||||
user_agent,
|
user_agent,
|
||||||
pot,
|
|
||||||
#[cfg(feature = "indicatif")]
|
#[cfg(feature = "indicatif")]
|
||||||
pb.clone(),
|
pb.clone(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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)]
|
||||||
|
@ -13,9 +12,6 @@ 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),
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
# About the new `pot` token
|
|
||||||
|
|
||||||
YouTube has implemented a new method to prevent downloaders and alternative clients from accessing
|
|
||||||
their videos. Now requests to YouTube's video servers require a `pot` URL parameter.
|
|
||||||
|
|
||||||
It is currently only required in the web player. The YTM and embedded player sends the token, too, but does not require it (this may change in the future).
|
|
||||||
|
|
||||||
The TV player does not use the token at all and is currently the best workaround. The only downside
|
|
||||||
is that the TV player does not return any video metadata like title and description text.
|
|
||||||
|
|
||||||
The first part of a video file (range: 0-1007959 bytes) can be downloaded without the token.
|
|
||||||
Requesting more of the file requires the pot token to be set, otherwise YouTube responds with a 403
|
|
||||||
error.
|
|
||||||
|
|
||||||
The pot token is base64-formatted and usually starts with a M
|
|
||||||
|
|
||||||
`MnToZ2brHmyo0ehfKtK_EWUq60dPYDXksNX_UsaniM_Uj6zbtiIZujCHY02hr7opxB_n3XHetJQCBV9cnNHovuhvDqrjfxsKR-sjn-eIxqv3qOZKphvyDpQzlYBnT2AXK41R-ti6iPonrvlvKIASNmYX2lhsEg==`
|
|
||||||
|
|
||||||
The token is generated from YouTubes Botguard script. The token is bound to the visitor data cookie
|
|
||||||
used to fetch the player data.
|
|
||||||
|
|
||||||
This feature has been A/B-tested for a few weeks. During that time, refetching the player in case
|
|
||||||
of a 403 download error often made things work again. As of 08.08.2024 this new feature seems to be
|
|
||||||
stabilized and retrying requests does not work any more.
|
|
||||||
|
|
||||||
## Getting a `pot` token
|
|
||||||
|
|
||||||
You need a real browser environment to run YouTube's botguard and obtain a pot token. The Invidious project has created a script to
|
|
||||||
<https://github.com/iv-org/youtube-trusted-session-generator/tree/master>.
|
|
||||||
The script opens YouTube's embedded video player, starts playback and extracts the visitor data
|
|
|
@ -82,7 +82,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the videos from a YouTube channel
|
/// Get the videos from a YouTube channel
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn channel_videos<S: AsRef<str> + Debug>(
|
pub async fn channel_videos<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
@ -94,7 +94,7 @@ impl RustyPipeQuery {
|
||||||
/// Get a ordered list of videos from a YouTube channel
|
/// Get a ordered list of videos from a YouTube channel
|
||||||
///
|
///
|
||||||
/// This function does not return channel metadata.
|
/// This function does not return channel metadata.
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn channel_videos_order<S: AsRef<str> + Debug>(
|
pub async fn channel_videos_order<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
@ -105,7 +105,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the videos of the given tab (Shorts, Livestreams) from a YouTube channel
|
/// Get the videos of the given tab (Shorts, Livestreams) from a YouTube channel
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn channel_videos_tab<S: AsRef<str> + Debug>(
|
pub async fn channel_videos_tab<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
@ -118,7 +118,7 @@ impl RustyPipeQuery {
|
||||||
/// Get a ordered list of videos from the given tab (Shorts, Livestreams) of a YouTube channel
|
/// Get a ordered list of videos from the given tab (Shorts, Livestreams) of a YouTube channel
|
||||||
///
|
///
|
||||||
/// This function does not return channel metadata.
|
/// This function does not return channel metadata.
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn channel_videos_tab_order<S: AsRef<str> + Debug>(
|
pub async fn channel_videos_tab_order<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
@ -136,7 +136,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search the videos of a channel
|
/// Search the videos of a channel
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn channel_search<S: AsRef<str> + Debug, S2: AsRef<str> + Debug>(
|
pub async fn channel_search<S: AsRef<str> + Debug, S2: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
@ -152,7 +152,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the playlists of a channel
|
/// Get the playlists of a channel
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn channel_playlists<S: AsRef<str> + Debug>(
|
pub async fn channel_playlists<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
@ -177,7 +177,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get additional metadata from the *About* tab of a channel
|
/// Get additional metadata from the *About* tab of a channel
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn channel_info<S: AsRef<str> + Debug>(
|
pub async fn channel_info<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
|
|
@ -18,7 +18,7 @@ impl RustyPipeQuery {
|
||||||
/// for checking a lot of channels or implementing a subscription feed.
|
/// for checking a lot of channels or implementing a subscription feed.
|
||||||
///
|
///
|
||||||
/// The downside of using the RSS feed is that it does not provide video durations.
|
/// The downside of using the RSS feed is that it does not provide video durations.
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn channel_rss<S: AsRef<str> + Debug>(
|
pub async fn channel_rss<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
channel_id: S,
|
channel_id: S,
|
||||||
|
|
|
@ -216,17 +216,6 @@ static CLIENT_VERSION_REGEX: Lazy<Regex> =
|
||||||
static VISITOR_DATA_REGEX: Lazy<Regex> =
|
static VISITOR_DATA_REGEX: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r#""visitorData":"([\w\d_\-%]+?)""#).unwrap());
|
Lazy::new(|| Regex::new(r#""visitorData":"([\w\d_\-%]+?)""#).unwrap());
|
||||||
|
|
||||||
/// Default order of client types when fetching player data
|
|
||||||
///
|
|
||||||
/// The order may change in the future in case YouTube applies changes to their
|
|
||||||
/// platform that disable a client or make it less reliable.
|
|
||||||
pub const DEFAULT_PLAYER_CLIENT_ORDER: &[ClientType] = &[
|
|
||||||
ClientType::Tv,
|
|
||||||
ClientType::TvHtml5Embed,
|
|
||||||
ClientType::Android,
|
|
||||||
ClientType::Ios,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// The RustyPipe client used to access YouTube's API
|
/// The RustyPipe client used to access YouTube's API
|
||||||
///
|
///
|
||||||
/// RustyPipe uses an [`Arc`] internally, so if you are using the client
|
/// RustyPipe uses an [`Arc`] internally, so if you are using the client
|
||||||
|
|
|
@ -32,7 +32,7 @@ struct FormData {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the YouTube Music charts for a given country
|
/// Get the YouTube Music charts for a given country
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_charts(&self, country: Option<Country>) -> Result<MusicCharts, Error> {
|
pub async fn music_charts(&self, country: Option<Country>) -> Result<MusicCharts, Error> {
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QCharts {
|
let request_body = QCharts {
|
||||||
|
|
|
@ -40,7 +40,7 @@ struct QRadio<'a> {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the metadata of a YouTube music track
|
/// Get the metadata of a YouTube music track
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_details<S: AsRef<str> + Debug>(
|
pub async fn music_details<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
video_id: S,
|
video_id: S,
|
||||||
|
@ -68,7 +68,7 @@ impl RustyPipeQuery {
|
||||||
/// Get the lyrics of a YouTube music track
|
/// Get the lyrics of a YouTube music track
|
||||||
///
|
///
|
||||||
/// The `lyrics_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
/// The `lyrics_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_lyrics<S: AsRef<str> + Debug>(&self, lyrics_id: S) -> Result<Lyrics, Error> {
|
pub async fn music_lyrics<S: AsRef<str> + Debug>(&self, lyrics_id: S) -> Result<Lyrics, Error> {
|
||||||
let lyrics_id = lyrics_id.as_ref();
|
let lyrics_id = lyrics_id.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
|
@ -90,7 +90,7 @@ impl RustyPipeQuery {
|
||||||
/// Get related items (tracks, playlists, artists) to a YouTube Music track
|
/// Get related items (tracks, playlists, artists) to a YouTube Music track
|
||||||
///
|
///
|
||||||
/// The `related_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
/// The `related_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_related<S: AsRef<str> + Debug>(
|
pub async fn music_related<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
related_id: S,
|
related_id: S,
|
||||||
|
@ -115,7 +115,7 @@ impl RustyPipeQuery {
|
||||||
/// Get a YouTube Music radio (a dynamically generated playlist)
|
/// Get a YouTube Music radio (a dynamically generated playlist)
|
||||||
///
|
///
|
||||||
/// The `radio_id` can be obtained using [`RustyPipeQuery::music_artist`] to get an artist's radio.
|
/// The `radio_id` can be obtained using [`RustyPipeQuery::music_artist`] to get an artist's radio.
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_radio<S: AsRef<str> + Debug>(
|
pub async fn music_radio<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
radio_id: S,
|
radio_id: S,
|
||||||
|
@ -146,7 +146,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a YouTube Music radio (a dynamically generated playlist) for a track
|
/// Get a YouTube Music radio (a dynamically generated playlist) for a track
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_radio_track<S: AsRef<str> + Debug>(
|
pub async fn music_radio_track<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
video_id: S,
|
video_id: S,
|
||||||
|
@ -156,7 +156,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a YouTube Music radio (a dynamically generated playlist) for a playlist
|
/// Get a YouTube Music radio (a dynamically generated playlist) for a playlist
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_radio_playlist<S: AsRef<str> + Debug>(
|
pub async fn music_radio_playlist<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
playlist_id: S,
|
playlist_id: S,
|
||||||
|
|
|
@ -13,7 +13,7 @@ use super::{
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get a list of moods and genres from YouTube Music
|
/// Get a list of moods and genres from YouTube Music
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
@ -32,7 +32,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the playlists from a YouTube Music genre
|
/// Get the playlists from a YouTube Music genre
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_genre<S: AsRef<str> + Debug>(
|
pub async fn music_genre<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
genre_id: S,
|
genre_id: S,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use super::{response, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQue
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the new albums that were released on YouTube Music
|
/// Get the new albums that were released on YouTube Music
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_new_albums(&self) -> Result<Vec<AlbumItem>, Error> {
|
pub async fn music_new_albums(&self) -> Result<Vec<AlbumItem>, Error> {
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
@ -30,7 +30,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the new music videos that were released on YouTube Music
|
/// Get the new music videos that were released on YouTube Music
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_new_videos(&self) -> Result<Vec<TrackItem>, Error> {
|
pub async fn music_new_videos(&self) -> Result<Vec<TrackItem>, Error> {
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
|
|
@ -22,7 +22,7 @@ use super::{
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get a playlist from YouTube Music
|
/// Get a playlist from YouTube Music
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_playlist<S: AsRef<str> + Debug>(
|
pub async fn music_playlist<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
playlist_id: S,
|
playlist_id: S,
|
||||||
|
@ -54,7 +54,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an album from YouTube Music
|
/// Get an album from YouTube Music
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_album<S: AsRef<str> + Debug>(
|
pub async fn music_album<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
album_id: S,
|
album_id: S,
|
||||||
|
|
|
@ -126,7 +126,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YouTube Music search suggestions
|
/// Get YouTube Music search suggestions
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn music_search_suggestion<S: AsRef<str> + Debug>(
|
pub async fn music_search_suggestion<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
|
|
|
@ -14,7 +14,7 @@ use super::{response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyP
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get more YouTube items from the given continuation token and endpoint
|
/// Get more YouTube items from the given continuation token and endpoint
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn continuation<T: FromYtItem, S: AsRef<str> + Debug>(
|
pub async fn continuation<T: FromYtItem, S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
ctoken: S,
|
ctoken: S,
|
||||||
|
|
|
@ -25,7 +25,6 @@ use super::{
|
||||||
player::{self, Format},
|
player::{self, Format},
|
||||||
},
|
},
|
||||||
ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
||||||
DEFAULT_PLAYER_CLIENT_ORDER,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -66,7 +65,7 @@ struct QContentPlaybackContext<'a> {
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get YouTube player data (video/audio streams + basic metadata)
|
/// Get YouTube player data (video/audio streams + basic metadata)
|
||||||
pub async fn player<S: AsRef<str> + Debug>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
pub async fn player<S: AsRef<str> + Debug>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
||||||
self.player_from_clients(video_id, DEFAULT_PLAYER_CLIENT_ORDER)
|
self.player_from_clients(video_id, &[ClientType::Desktop, ClientType::TvHtml5Embed])
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +113,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YouTube player data (video/audio streams + basic metadata) using the specified client
|
/// Get YouTube player data (video/audio streams + basic metadata) using the specified client
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn player_from_client<S: AsRef<str> + Debug>(
|
pub async fn player_from_client<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
video_id: S,
|
video_id: S,
|
||||||
|
|
|
@ -17,7 +17,7 @@ use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, QBrowse, R
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get a YouTube playlist
|
/// Get a YouTube playlist
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn playlist<S: AsRef<str> + Debug>(&self, playlist_id: S) -> Result<Playlist, Error> {
|
pub async fn playlist<S: AsRef<str> + Debug>(&self, playlist_id: S) -> Result<Playlist, Error> {
|
||||||
let playlist_id = playlist_id.as_ref();
|
let playlist_id = playlist_id.as_ref();
|
||||||
// YTM playlists require visitor data for continuations to work
|
// YTM playlists require visitor data for continuations to work
|
||||||
|
|
|
@ -24,7 +24,7 @@ struct QSearch<'a> {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Search YouTube
|
/// Search YouTube
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn search<T: FromYtItem, S: AsRef<str> + Debug>(
|
pub async fn search<T: FromYtItem, S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
|
@ -48,7 +48,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search YouTube using the given [`SearchFilter`]
|
/// Search YouTube using the given [`SearchFilter`]
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn search_filter<T: FromYtItem, S: AsRef<str> + Debug>(
|
pub async fn search_filter<T: FromYtItem, S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
|
@ -73,7 +73,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YouTube search suggestions
|
/// Get YouTube search suggestions
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn search_suggestion<S: AsRef<str> + Debug>(
|
pub async fn search_suggestion<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
query: S,
|
query: S,
|
||||||
|
|
|
@ -16,7 +16,7 @@ use super::{
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the videos from the YouTube startpage
|
/// Get the videos from the YouTube startpage
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn startpage(&self) -> Result<Paginator<VideoItem>, Error> {
|
pub async fn startpage(&self) -> Result<Paginator<VideoItem>, Error> {
|
||||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
|
@ -35,7 +35,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the videos from the YouTube trending page
|
/// Get the videos from the YouTube trending page
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn trending(&self) -> Result<Vec<VideoItem>, Error> {
|
pub async fn trending(&self) -> Result<Vec<VideoItem>, Error> {
|
||||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||||
let request_body = QBrowseParams {
|
let request_body = QBrowseParams {
|
||||||
|
|
|
@ -58,7 +58,7 @@ impl RustyPipeQuery {
|
||||||
/// );
|
/// );
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn resolve_url<S: AsRef<str> + Debug>(
|
pub async fn resolve_url<S: AsRef<str> + Debug>(
|
||||||
self,
|
self,
|
||||||
url: S,
|
url: S,
|
||||||
|
@ -236,7 +236,7 @@ impl RustyPipeQuery {
|
||||||
/// );
|
/// );
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn resolve_string<S: AsRef<str> + Debug>(
|
pub async fn resolve_string<S: AsRef<str> + Debug>(
|
||||||
self,
|
self,
|
||||||
s: S,
|
s: S,
|
||||||
|
|
|
@ -31,7 +31,7 @@ struct QVideo<'a> {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the metadata for a video
|
/// Get the metadata for a video
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn video_details<S: AsRef<str> + Debug>(
|
pub async fn video_details<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
video_id: S,
|
video_id: S,
|
||||||
|
@ -56,7 +56,7 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the comments for a video using the continuation token obtained from `rusty_pipe_query.video_details()`
|
/// Get the comments for a video using the continuation token obtained from `rusty_pipe_query.video_details()`
|
||||||
#[tracing::instrument(skip(self), level = "error")]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn video_comments<S: AsRef<str> + Debug>(
|
pub async fn video_comments<S: AsRef<str> + Debug>(
|
||||||
&self,
|
&self,
|
||||||
ctoken: S,
|
ctoken: S,
|
||||||
|
|
|
@ -137,12 +137,9 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
|
||||||
assert_eq!(audio.format, AudioFormat::Webm);
|
assert_eq!(audio.format, AudioFormat::Webm);
|
||||||
assert_eq!(audio.codec, AudioCodec::Opus);
|
assert_eq!(audio.codec, AudioCodec::Opus);
|
||||||
|
|
||||||
// Desktop client now requires pot token so the streams cannot be tested here
|
|
||||||
if client_type != ClientType::Desktop {
|
|
||||||
check_video_stream(video).await;
|
check_video_stream(video).await;
|
||||||
check_video_stream(audio).await;
|
check_video_stream(audio).await;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
assert!(player_data.expires_in_seconds > 10000);
|
assert!(player_data.expires_in_seconds > 10000);
|
||||||
}
|
}
|
||||||
|
@ -249,25 +246,19 @@ async fn get_player(
|
||||||
let details = player_data.details;
|
let details = player_data.details;
|
||||||
|
|
||||||
assert_eq!(details.id, id);
|
assert_eq!(details.id, id);
|
||||||
if let Some(n) = &details.name {
|
assert_eq!(details.name.expect("name"), name);
|
||||||
assert_eq!(n, name);
|
let desc = details.description.expect("description");
|
||||||
}
|
|
||||||
if let Some(desc) = &details.description {
|
|
||||||
assert!(desc.contains(description), "description: {desc}");
|
assert!(desc.contains(description), "description: {desc}");
|
||||||
}
|
|
||||||
assert_eq!(details.duration, duration);
|
assert_eq!(details.duration, duration);
|
||||||
assert_eq!(details.channel_id, channel_id);
|
assert_eq!(details.channel_id, channel_id);
|
||||||
if let Some(cn) = &details.channel_name {
|
assert_eq!(details.channel_name.expect("channel name"), channel_name);
|
||||||
assert_eq!(cn, channel_name);
|
assert_gte(details.view_count.expect("view count"), views, "views");
|
||||||
}
|
|
||||||
if let Some(vc) = details.view_count {
|
|
||||||
assert_gte(vc, views, "views");
|
|
||||||
}
|
|
||||||
assert_eq!(details.is_live, is_live);
|
assert_eq!(details.is_live, is_live);
|
||||||
assert_eq!(details.is_live_content, is_live_content);
|
assert_eq!(details.is_live_content, is_live_content);
|
||||||
|
|
||||||
if is_live {
|
if is_live {
|
||||||
assert!(player_data.hls_manifest_url.is_some() || player_data.dash_manifest_url.is_some());
|
assert!(player_data.hls_manifest_url.is_some());
|
||||||
|
assert!(player_data.dash_manifest_url.is_some());
|
||||||
} else {
|
} else {
|
||||||
assert!(!player_data.video_only_streams.is_empty());
|
assert!(!player_data.video_only_streams.is_empty());
|
||||||
assert!(!player_data.audio_streams.is_empty());
|
assert!(!player_data.audio_streams.is_empty());
|
||||||
|
|
Loading…
Reference in a new issue