From 8692ca81d972d0d2acf6fb4da79b9e0f5ebf4daf Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 17 Aug 2024 00:10:31 +0200 Subject: [PATCH 1/9] todo: update metadata --- README.md | 6 +- cli/README.md | 95 +++++- cli/src/main.rs | 697 +++++++++++++++++++++++-------------------- downloader/README.md | 10 +- 4 files changed, 477 insertions(+), 331 deletions(-) diff --git a/README.md b/README.md index 15f6876..9b5646c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) -[![Current crates.io version](https://img.shields.io/crates/v/smartcrop2.svg)](https://crates.io/crates/smartcrop2) +[![Current crates.io version](https://img.shields.io/crates/v/rustypipe.svg)](https://crates.io/crates/rustypipe) [![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](http://opensource.org/licenses/MIT) [![CI status](https://code.thetadev.de/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://code.thetadev.de/ThetaDev/rustypipe/actions/?workflow=ci.yaml) -Rust client for the public YouTube / YouTube Music API (Innertube), inspired by -[NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). +RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music API +(Innertube), inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). ## Features diff --git a/cli/README.md b/cli/README.md index e88277e..4cb7d8a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1 +1,94 @@ -# RustyPipe CLI +# ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) CLI + +[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-cli.svg)](https://crates.io/crates/rustypipe-cli) +[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](http://opensource.org/licenses/MIT) +[![CI status](https://code.thetadev.de/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://code.thetadev.de/ThetaDev/rustypipe/actions/?workflow=ci.yaml) + +The RustyPipe CLI is a powerful YouTube client for the command line. It allows you to +access most of the features of the RustyPipe crate: getting data from YouTube and +downloading videos. + +The following subcommands are included: + +## `get`: Fetch information + +You can call the get command with any YouTube entity ID or URL and RustyPipe will fetch +the associated metadata. It can fetch channels, playlists, albums and videos. + +**Usage:** `rustypipe get UC2TXq_t06Hjdr2g_KdKpHQg` + +- `-l`, `--limit` Limit the number of list items to fetch +- ``-t, --tab` Channel tab (options: **videos**, shorts, live, playlists, info) +- `-m, --music` Use the YouTube Music API +- `--rss`Fetch the RSS feed of a channel +- `--comments` Get comments (options: top, latest) +- `--lyrics` Get the lyrics for YTM tracks +- `--player` Get the player data instead of the video details when fetching videos +- `-c, --client-type` YT clients used to fetch player data (options: desktop, tv, + tv-embed, android, ios; if multiple clients are specified, they are attempted in + order) + +## `search`: Search YouTube + +With the search command you can search the entire YouTube platform or individual +channels. YouTube Music search is also supported. + +Note that search filters are only supported when searching YouTube. They have no effect +when searching YTM or individual channels. + +**Usage:** `rustypipe search "query"` + +### Options + +- `-l`, `--limit` Limit the number of list items to fetch + +- `--item-type` Filter results by item type +- `--length` Filter results by video length +- `--date` Filter results by upload date (options: hour, day, week, month, year) +- `--order` Sort search results (options: rating, date, views) +- `--channel` Channel ID for searching channel videos +- `-m, --music` Search YouTube Music in the given category (options: all, tracks, + videos, artists, albums, playlists-ytm, playlists-community) + +## `dl`: Download videos + +The downloader can download individual videos, playlists, albums and channels. Multiple +videos can be downloaded in parallel for improved performance. + +**Usage:** `rustypipe dl eRsGyueVLvQ` + +### Options + +- `-o`, `--output` Download to the given directory +- `--output-file` Download to the given file +- `--template` Download to a path determined by a template + +- `-r`, `--resolution` Video resolution (e.g. 720, 1080). Set to 0 for audio-only +- `-a`, `--audio` Download only the audio track and write track metadata + album cover +- `-p`, `--parallel` Number of videos downloaded in parallel (default: 8) +- `-m, --music` Use YouTube Music for downloading playlists +- `-l`, `--limit` Limit the number of videos to download (default: 1000) +- `-c`, `--client-type` YT clients used to fetch player data (options: desktop, tv, + tv-embed, android, ios; if multiple clients are specified, they are attempted in + order) +- `--pot` token to circumvent bot detection + +## `vdata`: Get visitor data + +You can use the vdata command to get a new visitor data cookie. This feature may come in +handy for testing and reproducing A/B tests. + +## Global options + +- **Proxy:** RustyPipe respects the environment variables `HTTP_PROXY`, `HTTPS_PROXY` + and `ALL_PROXY` +- **Logging:** You can change the log level with the `RUST_LOG` environment variable, it + is set to `info` by default +- **Visitor data:** A custom visitor data cookie can be used with the `--vdata` flag +- `--report` + +### Output format + +By default, the CLI outputs YouTube data in a human-readable text format. If you want to +store the data or process it with a script, you should choose a machine readable output +format. You can choose both JSON and YAML with the `-f, --format` flag. diff --git a/cli/src/main.rs b/cli/src/main.rs index 96e3925..5c4c249 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -49,10 +49,13 @@ struct Cli { #[derive(Parser)] #[group(multiple = false)] struct DownloadTarget { + /// Download to the given directory #[clap(short, long)] output: Option, + /// Download to the given file #[clap(long)] output_file: Option, + /// Download to a path determined by a template #[clap(long)] template: Option, } @@ -93,7 +96,7 @@ enum Commands { /// Video resolution (e.g. 720, 1080). Set to 0 for audio-only. #[clap(short, long)] resolution: Option, - /// Download only the audio track + /// Download only the audio track and write track information #[clap(short, long)] audio: bool, /// Number of videos downloaded in parallel @@ -117,24 +120,21 @@ enum Commands { /// ID or URL id: String, /// Output format - #[clap(long, value_parser, default_value = "json")] - format: Format, + #[clap(short, long, value_parser)] + format: Option, /// Pretty-print output #[clap(long)] pretty: bool, - /// Output as text - #[clap(short, long)] - txt: bool, /// Limit the number of items to fetch #[clap(short, long, default_value_t = 20)] limit: usize, /// Channel tab - #[clap(long, default_value = "videos")] + #[clap(short, long, default_value = "videos")] tab: ChannelTab, /// Use YouTube Music #[clap(short, long)] music: bool, - /// Use the RSS feed of a channel + /// Fetch the RSS feed of a channel #[clap(long)] rss: bool, /// Get comments @@ -148,21 +148,18 @@ enum Commands { player: bool, /// YT Client used to fetch player data #[clap(short, long)] - client_type: Option, + client_type: Option>, }, /// Search YouTube Search { /// Search query query: String, /// Output format - #[clap(long, value_parser, default_value = "json")] - format: Format, + #[clap(short, long, value_parser)] + format: Option, /// Pretty-print output #[clap(long)] pretty: bool, - /// Output as text - #[clap(short, long)] - txt: bool, /// Limit the number of items to fetch #[clap(short, long, default_value_t = 20)] limit: usize, @@ -181,7 +178,7 @@ enum Commands { /// Channel ID for searching channel videos #[clap(long)] channel: Option, - /// YouTube Music search filter + /// Search YouTube Music in the given category #[clap(short, long)] music: Option, }, @@ -189,8 +186,9 @@ enum Commands { Vdata, } -#[derive(Copy, Clone, ValueEnum)] +#[derive(Default, Copy, Clone, ValueEnum)] enum Format { + #[default] Json, Yaml, } @@ -388,17 +386,17 @@ fn print_duration(duration: Option) { fn print_music_search( data: &MusicSearchResult, - format: Format, + format: Option, pretty: bool, - txt: bool, ) { - if txt { - if let Some(corr) = &data.corrected_query { - anstream::println!("Did you mean `{}`?", corr.magenta()); + match format { + Some(format) => print_data(data, format, pretty), + None => { + if let Some(corr) = &data.corrected_query { + anstream::println!("Did you mean `{}`?", corr.magenta()); + } + print_entities(&data.items.items); } - print_entities(&data.items.items); - } else { - print_data(data, format, pretty) } } @@ -406,7 +404,7 @@ fn print_description(desc: Option) { if let Some(desc) = desc { if !desc.is_empty() { print_h2("Description"); - println!("{}", desc); + println!("{}", desc.trim()); } } } @@ -651,7 +649,6 @@ async fn run() -> anyhow::Result<()> { Commands::Get { id, format, - txt, pretty, limit, tab, @@ -671,56 +668,60 @@ async fn run() -> anyhow::Result<()> { match details.lyrics_id { Some(lyrics_id) => { let lyrics = rp.query().music_lyrics(lyrics_id).await?; - if txt { - println!("{}\n\n{}", lyrics.body, lyrics.footer.blue()); - } else { - print_data(&lyrics, format, pretty); + match format { + Some(format) => print_data(&lyrics, format, pretty), + None => println!("{}\n\n{}", lyrics.body, lyrics.footer.blue()), } } None => eprintln!("no lyrics found"), } } else if music { let details = rp.query().music_details(&id).await?; - if txt { - if details.track.is_video { - anstream::println!("{}", "[MV]".on_green().black()); - } else { - anstream::println!("{}", "[Track]".on_green().black()); + match format { + Some(format) => print_data(&details, format, pretty), + None => { + if details.track.is_video { + anstream::println!("{}", "[MV]".on_green().black()); + } else { + anstream::println!("{}", "[Track]".on_green().black()); + } + anstream::print!( + "{} [{}]", + details.track.name.green().bold(), + details.track.id + ); + print_duration(details.track.duration); + println!(); + print_artists(&details.track.artists); + println!(); + if !details.track.is_video { + anstream::println!( + "{} {}", + "Album:".blue(), + details + .track + .album + .as_ref() + .map(|b| b.id.as_str()) + .unwrap_or("None") + ) + } + if let Some(view_count) = details.track.view_count { + anstream::println!("{} {}", "Views:".blue(), view_count); + } } - anstream::print!( - "{} [{}]", - details.track.name.green().bold(), - details.track.id - ); - print_duration(details.track.duration); - println!(); - print_artists(&details.track.artists); - println!(); - if !details.track.is_video { - anstream::println!( - "{} {}", - "Album:".blue(), - details - .track - .album - .as_ref() - .map(|b| b.id.as_str()) - .unwrap_or("None") - ) - } - if let Some(view_count) = details.track.view_count { - anstream::println!("{} {}", "Views:".blue(), view_count); - } - } else { - print_data(&details, format, pretty); } } else if player { - let player = if let Some(client_type) = client_type { - rp.query().player_from_client(&id, client_type.into()).await + let player = if let Some(client_types) = client_type { + let cts = client_types + .into_iter() + .map(ClientType::from) + .collect::>(); + rp.query().player_from_clients(&id, &cts).await } else { rp.query().player(&id).await }?; - print_data(&player, format, pretty); + print_data(&player, format.unwrap_or_default(), pretty); } else { let mut details = rp.query().video_details(&id).await?; @@ -737,153 +738,160 @@ async fn run() -> anyhow::Result<()> { None => {} } - if txt { - anstream::println!( - "{}\n{} [{}]", - "[Video]".on_green().black(), - details.name.green().bold(), - details.id - ); - anstream::println!( - "{} {} [{}]", - "Channel:".blue(), - details.channel.name, - details.channel.id - ); - if let Some(subs) = details.channel.subscriber_count { - anstream::println!("{} {}", "Subscribers:".blue(), subs); - } - if let Some(date) = details.publish_date { - anstream::println!("{} {}", "Date:".blue(), date); - } - anstream::println!("{} {}", "Views:".blue(), details.view_count); - if let Some(likes) = details.like_count { - anstream::println!("{} {}", "Likes:".blue(), likes); - } - if let Some(comments) = details.top_comments.count { - anstream::println!("{} {}", "Comments:".blue(), comments); - } - if details.is_ccommons { - anstream::println!("{}", "Creative Commons".green()); - } - if details.is_live { - anstream::println!("{}", "Livestream".red()); - } - print_description(Some(details.description.to_plaintext())); - if !details.recommended.is_empty() { - print_h2("Recommended"); - print_entities(&details.recommended.items); - } - let comment_list = comments.map(|c| match c { - CommentsOrder::Top => &details.top_comments.items, - CommentsOrder::Latest => &details.latest_comments.items, - }); - if let Some(comment_list) = comment_list { - print_h2("Comments"); - for c in comment_list { - if let Some(author) = &c.author { - anstream::print!("{} [{}]", author.name.cyan(), author.id); - print_verification(author.verification); - } else { - anstream::print!("{}", "Unknown author".magenta()); + match format { + Some(format) => print_data(&details, format, pretty), + None => { + anstream::println!( + "{}\n{} [{}]", + "[Video]".on_green().black(), + details.name.green().bold(), + details.id + ); + anstream::println!( + "{} {} [{}]", + "Channel:".blue(), + details.channel.name, + details.channel.id + ); + if let Some(subs) = details.channel.subscriber_count { + anstream::println!("{} {}", "Subscribers:".blue(), subs); + } + if let Some(date) = details.publish_date { + anstream::println!("{} {}", "Date:".blue(), date); + } + anstream::println!("{} {}", "Views:".blue(), details.view_count); + if let Some(likes) = details.like_count { + anstream::println!("{} {}", "Likes:".blue(), likes); + } + if let Some(comments) = details.top_comments.count { + anstream::println!("{} {}", "Comments:".blue(), comments); + } + if details.is_ccommons { + anstream::println!("{}", "Creative Commons".green()); + } + if details.is_live { + anstream::println!("{}", "Livestream".red()); + } + print_description(Some(details.description.to_plaintext())); + if !details.recommended.is_empty() { + print_h2("Recommended"); + print_entities(&details.recommended.items); + } + let comment_list = comments.map(|c| match c { + CommentsOrder::Top => &details.top_comments.items, + CommentsOrder::Latest => &details.latest_comments.items, + }); + if let Some(comment_list) = comment_list { + print_h2("Comments"); + for c in comment_list { + if let Some(author) = &c.author { + anstream::print!( + "{} [{}]", + author.name.cyan(), + author.id + ); + print_verification(author.verification); + } else { + anstream::print!("{}", "Unknown author".magenta()); + } + if c.by_owner { + print!(" (Owner)"); + } + println!(); + println!("{}", c.text.to_plaintext()); + anstream::print!( + "{} {}", + "Likes:".blue(), + c.like_count.unwrap_or_default() + ); + if c.hearted { + anstream::print!(" {}", "♥".red()); + } + println!("\n"); } - if c.by_owner { - print!(" (Owner)"); - } - println!(); - println!("{}", c.text.to_plaintext()); - anstream::print!( - "{} {}", - "Likes:".blue(), - c.like_count.unwrap_or_default() - ); - if c.hearted { - anstream::print!(" {}", "♥".red()); - } - println!("\n"); } } - } else { - print_data(&details, format, pretty); } } } UrlTarget::Channel { id } => { if music { let artist = rp.query().music_artist(&id, true).await?; - if txt { - anstream::println!( - "{}\n{} [{}]", - "[Artist]".on_green().black(), - artist.name.green().bold(), - artist.id - ); - if let Some(subs) = artist.subscriber_count { - anstream::println!("{} {}", "Subscribers:".blue(), subs); - } - if let Some(url) = artist.wikipedia_url { - anstream::println!("{} {}", "Wikipedia:".blue(), url); - } - if let Some(id) = artist.tracks_playlist_id { - anstream::println!("{} {}", "All tracks:".blue(), id); - } - if let Some(id) = artist.videos_playlist_id { - anstream::println!("{} {}", "All videos:".blue(), id); - } - if let Some(id) = artist.radio_id { - anstream::println!("{} {}", "Radio:".blue(), id); - } - print_description(artist.description); - if !artist.albums.is_empty() { - print_h2("Albums"); - for b in artist.albums { - anstream::print!( - "[{}] {} ({:?}", - b.id, - b.name.bold(), - b.album_type - ); - if let Some(y) = b.year { - print!(", {y}"); + match format { + Some(format) => print_data(&artist, format, pretty), + None => { + anstream::println!( + "{}\n{} [{}]", + "[Artist]".on_green().black(), + artist.name.green().bold(), + artist.id + ); + if let Some(subs) = artist.subscriber_count { + anstream::println!("{} {}", "Subscribers:".blue(), subs); + } + if let Some(url) = artist.wikipedia_url { + anstream::println!("{} {}", "Wikipedia:".blue(), url); + } + if let Some(id) = artist.tracks_playlist_id { + anstream::println!("{} {}", "All tracks:".blue(), id); + } + if let Some(id) = artist.videos_playlist_id { + anstream::println!("{} {}", "All videos:".blue(), id); + } + if let Some(id) = artist.radio_id { + anstream::println!("{} {}", "Radio:".blue(), id); + } + print_description(artist.description); + if !artist.albums.is_empty() { + print_h2("Albums"); + for b in artist.albums { + anstream::print!( + "[{}] {} ({:?}", + b.id, + b.name.bold(), + b.album_type + ); + if let Some(y) = b.year { + print!(", {y}"); + } + println!(")"); } - println!(")"); + } + if !artist.playlists.is_empty() { + print_h2("Playlists"); + print_entities(&artist.playlists); + } + if !artist.similar_artists.is_empty() { + print_h2("Similar artists"); + print_entities(&artist.similar_artists); } } - if !artist.playlists.is_empty() { - print_h2("Playlists"); - print_entities(&artist.playlists); - } - if !artist.similar_artists.is_empty() { - print_h2("Similar artists"); - print_entities(&artist.similar_artists); - } - } else { - 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() { + match format { + Some(format) => print_data(&rss, format, pretty), + None => { anstream::println!( - "{} {} [{}]", - "Latest video:".blue(), - v.publish_date, - v.id + "{}\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); } - println!(); - print_entities(&rss.videos); - } else { - print_data(&rss, format, pretty); } } else { match tab { @@ -899,75 +907,94 @@ async fn run() -> anyhow::Result<()> { channel.content.extend_limit(rp.query(), limit).await?; - if txt { - anstream::print!( - "{}\n{} [{}]", - format!("[Channel {tab:?}]").on_green().black(), - channel.name.green().bold(), - channel.id - ); - print_verification(channel.verification); - println!(); - if let Some(subs) = channel.subscriber_count { - anstream::println!("{} {}", "Subscribers:".blue(), subs); + match format { + Some(format) => print_data(&channel, format, pretty), + None => { + anstream::print!( + "{}\n{} [{}]", + format!("[Channel {tab:?}]").on_green().black(), + channel.name.green().bold(), + channel.id + ); + print_verification(channel.verification); + println!(); + if let Some(subs) = channel.subscriber_count { + anstream::println!( + "{} {}", + "Subscribers:".blue(), + subs + ); + } + print_description(Some(channel.description)); + println!(); + print_entities(&channel.content.items); } - print_description(Some(channel.description)); - println!(); - print_entities(&channel.content.items); - } else { - print_data(&channel, format, pretty); } } ChannelTab::Playlists => { let channel = rp.query().channel_playlists(&id).await?; - if txt { - anstream::println!( - "{}\n{} [{}]", - format!("[Channel {tab:?}]").on_green().black(), - channel.name.green().bold(), - channel.id - ); - print_description(Some(channel.description)); - if let Some(subs) = channel.subscriber_count { - anstream::println!("{} {}", "Subscribers:".blue(), subs); + match format { + Some(format) => print_data(&channel, format, pretty), + None => { + anstream::println!( + "{}\n{} [{}]", + format!("[Channel {tab:?}]").on_green().black(), + channel.name.green().bold(), + channel.id + ); + print_description(Some(channel.description)); + if let Some(subs) = channel.subscriber_count { + anstream::println!( + "{} {}", + "Subscribers:".blue(), + subs + ); + } + println!(); + print_entities(&channel.content.items); } - println!(); - print_entities(&channel.content.items); - } else { - print_data(&channel, format, pretty); } } ChannelTab::Info => { let info = rp.query().channel_info(&id).await?; - if txt { - anstream::println!( - "{}\nID:{}", - "[Channel info]".on_green().black(), - info.id - ); - print_description(Some(info.description)); - if let Some(subs) = info.subscriber_count { - anstream::println!("{} {}", "Subscribers:".blue(), subs); - } - if let Some(vids) = info.video_count { - anstream::println!("{} {}", "Videos:".blue(), vids); - } - if let Some(views) = info.view_count { - anstream::println!("{} {}", "Views:".blue(), views); - } - if let Some(created) = info.create_date { - anstream::println!("{} {}", "Created on:".blue(), created); - } - if !info.links.is_empty() { - print_h2("Links"); - for (name, url) in &info.links { - anstream::println!("{} {}", name.blue(), url); + match format { + Some(format) => print_data(&info, format, pretty), + None => { + anstream::println!( + "{}\nID:{}", + "[Channel info]".on_green().black(), + info.id + ); + print_description(Some(info.description)); + if let Some(subs) = info.subscriber_count { + anstream::println!( + "{} {}", + "Subscribers:".blue(), + subs + ); + } + if let Some(vids) = info.video_count { + anstream::println!("{} {}", "Videos:".blue(), vids); + } + if let Some(views) = info.view_count { + anstream::println!("{} {}", "Views:".blue(), views); + } + if let Some(created) = info.create_date { + anstream::println!( + "{} {}", + "Created on:".blue(), + created + ); + } + if !info.links.is_empty() { + print_h2("Links"); + for (name, url) in &info.links { + anstream::println!("{} {}", name.blue(), url); + } } } - } else { - print_data(&info, format, pretty); } } } @@ -977,83 +1004,86 @@ async fn run() -> anyhow::Result<()> { if music { let mut playlist = rp.query().music_playlist(&id).await?; playlist.tracks.extend_limit(rp.query(), limit).await?; - if txt { - anstream::println!( - "{}\n{} [{}]\n{} {}", - "[MusicPlaylist]".on_green().black(), - playlist.name.green().bold(), - playlist.id, - "Tracks:".blue(), - playlist.track_count.unwrap_or_default(), - ); - if let Some(n) = playlist.channel_name() { - anstream::print!("{} {}", "Author:".blue(), n.bold()); - if let Some(id) = playlist.channel_id() { - print!(" [{id}]"); + match format { + Some(format) => print_data(&playlist, format, pretty), + None => { + anstream::println!( + "{}\n{} [{}]\n{} {}", + "[MusicPlaylist]".on_green().black(), + playlist.name.green().bold(), + playlist.id, + "Tracks:".blue(), + playlist.track_count.unwrap_or_default(), + ); + if let Some(n) = playlist.channel_name() { + anstream::print!("{} {}", "Author:".blue(), n.bold()); + if let Some(id) = playlist.channel_id() { + print!(" [{id}]"); + } + println!(); } + print_description(playlist.description.map(|d| d.to_plaintext())); println!(); + print_tracks(&playlist.tracks.items); } - print_description(playlist.description.map(|d| d.to_plaintext())); - println!(); - print_tracks(&playlist.tracks.items); - } else { - print_data(&playlist, format, pretty); } } else { let mut playlist = rp.query().playlist(&id).await?; playlist.videos.extend_limit(rp.query(), limit).await?; - if txt { - anstream::println!( - "{}\n{} [{}]\n{} {}", - "[Playlist]".on_green().black(), - playlist.name.green().bold(), - playlist.id, - "Videos:".blue(), - playlist.video_count, - ); - if let Some(n) = playlist.channel_name() { - anstream::print!("{} {}", "Author:".blue(), n.bold()); - if let Some(id) = playlist.channel_id() { - print!(" [{id}]"); + match format { + Some(format) => print_data(&playlist, format, pretty), + None => { + anstream::println!( + "{}\n{} [{}]\n{} {}", + "[Playlist]".on_green().black(), + playlist.name.green().bold(), + playlist.id, + "Videos:".blue(), + playlist.video_count, + ); + if let Some(n) = playlist.channel_name() { + anstream::print!("{} {}", "Author:".blue(), n.bold()); + if let Some(id) = playlist.channel_id() { + print!(" [{id}]"); + } + println!(); } + if let Some(last_update) = playlist.last_update { + anstream::println!("{} {}", "Last update:".blue(), last_update); + } + print_description(playlist.description.map(|d| d.to_plaintext())); println!(); + print_entities(&playlist.videos.items); } - if let Some(last_update) = playlist.last_update { - anstream::println!("{} {}", "Last update:".blue(), last_update); - } - print_description(playlist.description.map(|d| d.to_plaintext())); - println!(); - print_entities(&playlist.videos.items); - } else { - print_data(&playlist, format, pretty); } } } UrlTarget::Album { id } => { let album = rp.query().music_album(&id).await?; - if txt { - anstream::print!( - "{}\n{} [{}] ({:?}", - "[Album]".on_green().black(), - album.name.green().bold(), - album.id, - album.album_type - ); - if let Some(year) = album.year { - print!(", {year}"); - } - println!(")"); - if let Some(n) = album.channel_name() { - anstream::print!("{} {}", "Artist:".blue(), n); - if let Some(id) = album.channel_id() { - print!(" [{id}]"); + match format { + Some(format) => print_data(&album, format, pretty), + None => { + anstream::print!( + "{}\n{} [{}] ({:?}", + "[Album]".on_green().black(), + album.name.green().bold(), + album.id, + album.album_type + ); + if let Some(year) = album.year { + print!(", {year}"); } + println!(")"); + if let Some(n) = album.channel_name() { + anstream::print!("{} {}", "Artist:".blue(), n); + if let Some(id) = album.channel_id() { + print!(" [{id}]"); + } + } + print_description(album.description.map(|d| d.to_plaintext())); + println!(); + print_tracks(&album.tracks); } - print_description(album.description.map(|d| d.to_plaintext())); - println!(); - print_tracks(&album.tracks); - } else { - print_data(&album, format, pretty); } } } @@ -1062,7 +1092,6 @@ async fn run() -> anyhow::Result<()> { query, format, pretty, - txt, limit, item_type, length, @@ -1072,10 +1101,29 @@ async fn run() -> anyhow::Result<()> { music, } => match music { None => match channel { - Some(channel) => { - rustypipe::validate::channel_id(&channel)?; - let res = rp.query().channel_search(&channel, &query).await?; - print_data(&res, format, pretty); + Some(channel_id) => { + rustypipe::validate::channel_id(&channel_id)?; + let channel = rp.query().channel_search(&channel_id, &query).await?; + + match format { + Some(format) => print_data(&channel, format, pretty), + None => { + anstream::print!( + "{}\n{} [{}]", + "[Channel search]".on_green().black(), + channel.name.green().bold(), + channel.id + ); + print_verification(channel.verification); + println!(); + if let Some(subs) = channel.subscriber_count { + anstream::println!("{} {}", "Subscribers:".blue(), subs); + } + print_description(Some(channel.description)); + println!(); + print_entities(&channel.content.items); + } + } } None => { let filter = search_filter::SearchFilter::new() @@ -1089,39 +1137,40 @@ async fn run() -> anyhow::Result<()> { .await?; res.items.extend_limit(rp.query(), limit).await?; - if txt { - if let Some(corr) = res.corrected_query { - anstream::println!("Did you mean `{}`?", corr.magenta()); + match format { + Some(format) => print_data(&res, format, pretty), + None => { + if let Some(corr) = res.corrected_query { + anstream::println!("Did you mean `{}`?", corr.magenta()); + } + print_entities(&res.items.items); } - print_entities(&res.items.items); - } else { - print_data(&res, format, pretty); } } }, Some(MusicSearchCategory::All) => { let res = rp.query().music_search_main(&query).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::Tracks) => { let mut res = rp.query().music_search_tracks(&query).await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::Videos) => { let mut res = rp.query().music_search_videos(&query).await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::Artists) => { let mut res = rp.query().music_search_artists(&query).await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::Albums) => { let mut res = rp.query().music_search_albums(&query).await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => { let mut res = rp @@ -1132,7 +1181,7 @@ async fn run() -> anyhow::Result<()> { ) .await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } }, Commands::Vdata => { diff --git a/downloader/README.md b/downloader/README.md index 24a554a..05ec1a8 100644 --- a/downloader/README.md +++ b/downloader/README.md @@ -1,4 +1,8 @@ -# RustyPipe downloader +# ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) Downloader + +[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-downloader.svg)](https://crates.io/crates/rustypipe-downloader) +[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](http://opensource.org/licenses/MIT) +[![CI status](https://code.thetadev.de/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://code.thetadev.de/ThetaDev/rustypipe/actions/?workflow=ci.yaml) The downloader is a companion crate for RustyPipe that allows for easy and fast downloading of video and audio files. @@ -35,8 +39,8 @@ let dl = DownloaderBuilder::new() .build(); let filter_audio = StreamFilter::new().no_video(); -dl.id("ZeerrnuLi5E").stream_filter(filter_audio).to_file("audio.opus").download().await; +dl.id("eRsGyueVLvQ").stream_filter(filter_audio).to_file("audio.opus").download().await; let filter_video = StreamFilter::new().video_max_res(720); -dl.id("ZeerrnuLi5E").stream_filter(filter_video).to_file("video.mp4").download().await; +dl.id("eRsGyueVLvQ").stream_filter(filter_video).to_file("video.mp4").download().await; ``` From d0ae7961ba91d56c8b9a8d1c545875e869b818f5 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 17 Aug 2024 00:21:47 +0200 Subject: [PATCH 2/9] fix: player_from_clients: fall back to TvHtml5Embed client --- src/client/mod.rs | 8 ++------ src/client/player.rs | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 3bbb724..2d74f5d 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -220,12 +220,8 @@ static VISITOR_DATA_REGEX: Lazy = /// /// 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, -]; +pub const DEFAULT_PLAYER_CLIENT_ORDER: &[ClientType] = + &[ClientType::Tv, ClientType::Android, ClientType::Ios]; /// The RustyPipe client used to access YouTube's API /// diff --git a/src/client/player.rs b/src/client/player.rs index 430d33c..76b16a5 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -74,6 +74,9 @@ impl RustyPipeQuery { /// /// The clients are used in the given order. If a client cannot fetch the requested video, /// an attempt is made with the next one. + /// + /// If an age-restricted video is detected, it will automatically use the [`ClientType::TvHtml5Embed`] + /// since it is the only one that can circumvent age restrictions. pub async fn player_from_clients + Debug>( &self, video_id: S, @@ -81,9 +84,6 @@ impl RustyPipeQuery { ) -> Result { let video_id = video_id.as_ref(); let mut last_e = Error::Other("no clients".into()); - // Prefer to output age restriction error (e.g. if video cannot be played - // by Desktop because of age restriction and by TvHtml5Embed because it is non-embeddable) - let mut age_restricted_e = None; for client in clients { let res = self.player_from_client(video_id, *client).await; @@ -96,11 +96,17 @@ impl RustyPipeQuery { msg, } = &e { - age_restricted_e = - Some(Error::Extraction(ExtractionError::Unavailable { + if let Ok(res) = self + .player_from_client(video_id, ClientType::TvHtml5Embed) + .await + { + return Ok(res); + } else { + return Err(Error::Extraction(ExtractionError::Unavailable { reason: UnavailabilityReason::AgeRestricted, msg: msg.to_owned(), })); + } } last_e = Error::Extraction(e); } else { @@ -110,7 +116,7 @@ impl RustyPipeQuery { Err(e) => return Err(e), } } - Err(age_restricted_e.unwrap_or(last_e)) + Err(last_e) } /// Get YouTube player data (video/audio streams + basic metadata) using the specified client From 5a6b2c3a621f6b20c1324ea8b9c03426e3d8018b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 17 Aug 2024 00:42:47 +0200 Subject: [PATCH 3/9] fix: parsing channels without banner --- src/client/response/channel.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/client/response/channel.rs b/src/client/response/channel.rs index 4bff8f2..eaa2cc2 100644 --- a/src/client/response/channel.rs +++ b/src/client/response/channel.rs @@ -125,29 +125,35 @@ pub(crate) struct PageHeaderRenderer { pub page_header_view_model: PageHeaderRendererInner, } +#[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PageHeaderRendererInner { + /// Channel title (only used to extract verification badges) + #[serde_as(as = "DefaultOnError")] pub title: PhTitleView, + /// Channel avatar pub image: PhAvatarView, + /// Channel metadata (subscribers, video count) pub metadata: PhMetadataView, + #[serde(default)] pub banner: PhBannerView, } -#[derive(Debug, Deserialize)] +#[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PhTitleView { pub dynamic_text_view_model: PhTitleView2, } -#[derive(Debug, Deserialize)] +#[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PhTitleView2 { pub text: PhTitleView3, } #[serde_as] -#[derive(Debug, Deserialize)] +#[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PhTitleView3 { #[serde_as(as = "VecSkipError<_>")] @@ -242,7 +248,7 @@ pub(crate) struct PhMetadataRow { pub metadata_parts: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PhBannerView { pub image_banner_view_model: ImageView, From e6715700d950912031d5fbc1263f8770b6ffc49c Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 17 Aug 2024 02:44:47 +0200 Subject: [PATCH 4/9] feat!: update channel model, addd handle + video_count, remove tv/mobile banner --- cli/src/main.rs | 15 ++++- codegen/src/abtest.rs | 2 +- downloader/src/lib.rs | 2 +- src/client/channel.rs | 60 ++++++++++--------- src/client/response/channel.rs | 5 -- ...annel__tests__map_channel_livestreams.snap | 57 +----------------- ...channel__tests__map_channel_playlists.snap | 57 +----------------- ...t__channel__tests__map_channel_shorts.snap | 57 +----------------- ...ap_channel_shorts_20240129_pageheader.snap | 5 +- ..._map_channel_videos_20221011_richgrid.snap | 57 +----------------- ...map_channel_videos_20221011_richgrid2.snap | 57 +----------------- ...map_channel_videos_20230415_coachella.snap | 5 +- ...p_channel_videos_20240324_pageheader2.snap | 5 +- ...annel__tests__map_channel_videos_base.snap | 57 +----------------- ...nnel__tests__map_channel_videos_empty.snap | 5 +- ...annel__tests__map_channel_videos_live.snap | 57 +----------------- ...nnel__tests__map_channel_videos_music.snap | 57 +----------------- ...nel__tests__map_channel_videos_shorts.snap | 57 +----------------- ...l__tests__map_channel_videos_upcoming.snap | 57 +----------------- src/model/mod.rs | 11 ++-- src/util/mod.rs | 7 ++- src/validate.rs | 31 +++++++++- tests/youtube.rs | 10 +--- 23 files changed, 114 insertions(+), 619 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 5c4c249..ae173c7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -911,9 +911,10 @@ async fn run() -> anyhow::Result<()> { Some(format) => print_data(&channel, format, pretty), None => { anstream::print!( - "{}\n{} [{}]", + "{}\n{} {} [{}]", format!("[Channel {tab:?}]").on_green().black(), channel.name.green().bold(), + channel.handle.unwrap_or_default(), channel.id ); print_verification(channel.verification); @@ -925,6 +926,9 @@ async fn run() -> anyhow::Result<()> { subs ); } + if let Some(vids) = channel.video_count { + anstream::println!("{} {}", "Videos:".blue(), vids); + } print_description(Some(channel.description)); println!(); print_entities(&channel.content.items); @@ -938,9 +942,10 @@ async fn run() -> anyhow::Result<()> { Some(format) => print_data(&channel, format, pretty), None => { anstream::println!( - "{}\n{} [{}]", + "{}\n{} {} [{}]", format!("[Channel {tab:?}]").on_green().black(), channel.name.green().bold(), + channel.handle.unwrap_or_default(), channel.id ); print_description(Some(channel.description)); @@ -951,6 +956,9 @@ async fn run() -> anyhow::Result<()> { subs ); } + if let Some(vids) = channel.video_count { + anstream::println!("{} {}", "Videos:".blue(), vids); + } println!(); print_entities(&channel.content.items); } @@ -988,6 +996,9 @@ async fn run() -> anyhow::Result<()> { created ); } + if let Some(country) = info.country { + anstream::println!("{} {}", "Country:".blue(), country); + } if !info.links.is_empty() { print_h2("Links"); for (name, url) in &info.links { diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index f1d8cbb..be14c79 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -327,7 +327,7 @@ pub async fn channel_page_header(rp: &RustyPipeQuery) -> Result { let channel = rp .channel_videos_tab("UCh8gHdtzO2tXd593_bjErWg", ChannelVideoTab::Shorts) .await?; - Ok(channel.mobile_banner.is_empty() && channel.tv_banner.is_empty()) + Ok(channel.video_count.is_some()) } pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result { diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index 53c301c..5c65fb0 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -158,7 +158,7 @@ impl DownloadVideo { channel_id: video.channel_id().map(str::to_owned), channel_name: video .channel_name() - .map(|n| n.strip_suffix(" - Topic").unwrap_or(n).to_owned()), + .map(|n| n.strip_suffix("- Topic").unwrap_or(n).trim().to_owned()), album_id: None, album_name: None, track_nr: None, diff --git a/src/client/channel.rs b/src/client/channel.rs index a688ee3..854c728 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -353,18 +353,6 @@ impl MapResponse for response::ChannelAbout { } } -fn map_vanity_url(url: &str, id: &str) -> Option { - if url.contains(id) { - return None; - } - - Url::parse(url).ok().map(|mut parsed_url| { - // The vanity URL from YouTube is http for some reason - _ = parsed_url.set_scheme("https"); - parsed_url.to_string() - }) -} - struct MapChannelData { header: Option, metadata: Option, @@ -401,10 +389,16 @@ fn map_channel( ))); } - let vanity_url = metadata + let handle = metadata .vanity_channel_url .as_ref() - .and_then(|url| map_vanity_url(url, ctx.id)); + .and_then(|url| Url::parse(url).ok()) + .and_then(|url| { + url.path() + .strip_prefix('/') + .filter(|handle| util::CHANNEL_HANDLE_REGEX.is_match(handle)) + .map(str::to_owned) + }); let mut warnings = Vec::new(); Ok(MapResult { @@ -412,17 +406,16 @@ fn map_channel( response::channel::Header::C4TabbedHeaderRenderer(header) => Channel { id: metadata.external_id, name: metadata.title, + handle, subscriber_count: header.subscriber_count_text.and_then(|txt| { util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings) }), + video_count: None, avatar: header.avatar.into(), verification: header.badges.into(), description: metadata.description, tags: microformat.microformat_data_renderer.tags, - vanity_url, banner: header.banner.into(), - mobile_banner: header.mobile_banner.into(), - tv_banner: header.tv_banner.into(), has_shorts: d.has_shorts, has_live: d.has_live, visitor_data: d.visitor_data, @@ -443,21 +436,20 @@ fn map_channel( Channel { id: metadata.external_id, name: metadata.title, + handle, subscriber_count: hdata.as_ref().and_then(|hdata| { hdata.0.as_ref().and_then(|txt| { util::parse_large_numstr_or_warn(txt, ctx.lang, &mut warnings) }) }), + video_count: None, avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(), // Since the carousel header is only used for YT-internal channels or special events // (World Cup, Coachella, etc.) we can assume the channel to be verified verification: crate::model::Verification::Verified, description: metadata.description, tags: microformat.microformat_data_renderer.tags, - vanity_url, banner: Vec::new(), - mobile_banner: Vec::new(), - tv_banner: Vec::new(), has_shorts: d.has_shorts, has_live: d.has_live, visitor_data: d.visitor_data, @@ -468,19 +460,33 @@ fn map_channel( let hdata = header.content.page_header_view_model; // channel handle - subscriber count - video count let md_rows = hdata.metadata.content_metadata_view_model.metadata_rows; - let sub_part = if md_rows.len() > 1 { - md_rows.get(1).and_then(|md| md.metadata_parts.first()) + let (sub_part, vc_part) = if md_rows.len() > 1 { + let mp = &md_rows[1].metadata_parts; + (mp.first(), mp.get(1)) } else { - md_rows.first().and_then(|md| md.metadata_parts.get(1)) + ( + md_rows.first().and_then(|md| md.metadata_parts.get(1)), + None, + ) }; let subscriber_count = sub_part.and_then(|t| { util::parse_large_numstr_or_warn::(&t.text, ctx.lang, &mut warnings) }); + let video_count = + vc_part.and_then(|t| util::parse_numeric_or_warn(&t.text, &mut warnings)); Channel { id: metadata.external_id, name: metadata.title, + handle: handle.or_else(|| { + md_rows + .first() + .and_then(|md| md.metadata_parts.get(1)) + .map(|txt| txt.text.to_owned()) + .filter(|txt| util::CHANNEL_HANDLE_REGEX.is_match(txt)) + }), subscriber_count, + video_count, avatar: hdata .image .decorated_avatar_view_model @@ -491,10 +497,7 @@ fn map_channel( verification: hdata.title.into(), description: metadata.description, tags: microformat.microformat_data_renderer.tags, - vanity_url, banner: hdata.banner.image_banner_view_model.image.into(), - mobile_banner: Vec::new(), - tv_banner: Vec::new(), has_shorts: d.has_shorts, has_live: d.has_live, visitor_data: d.visitor_data, @@ -604,15 +607,14 @@ fn combine_channel_data(channel_data: Channel<()>, content: T) -> Channel Channel { id: channel_data.id, name: channel_data.name, + handle: channel_data.handle, subscriber_count: channel_data.subscriber_count, + video_count: channel_data.video_count, avatar: channel_data.avatar, verification: channel_data.verification, description: channel_data.description, tags: channel_data.tags, - vanity_url: channel_data.vanity_url, banner: channel_data.banner, - mobile_banner: channel_data.mobile_banner, - tv_banner: channel_data.tv_banner, has_shorts: channel_data.has_shorts, has_live: channel_data.has_live, visitor_data: channel_data.visitor_data, diff --git a/src/client/response/channel.rs b/src/client/response/channel.rs index eaa2cc2..c9b0440 100644 --- a/src/client/response/channel.rs +++ b/src/client/response/channel.rs @@ -95,11 +95,6 @@ pub(crate) struct HeaderRenderer { pub badges: Vec, #[serde(default)] pub banner: Thumbnails, - #[serde(default)] - pub mobile_banner: Thumbnails, - /// Fullscreen (16:9) channel banner - #[serde(default)] - pub tv_banner: Thumbnails, } #[serde_as] diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_livestreams.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_livestreams.snap index 830b5fd..a2c19fa 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_livestreams.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_livestreams.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UC2DjFE7Xf11URZqWBigcVOQ", name: "EEVblog", + handle: None, subscriber_count: Some(884000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj", @@ -55,7 +57,6 @@ Channel( "dumpster diving", "debunking", ], - vanity_url: Some("https://www.youtube.com/c/EevblogDave"), banner: [ Thumbnail( url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -88,60 +89,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: false, has_live: true, visitor_data: None, diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_playlists.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_playlists.snap index 1ccf5b6..7558c9f 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_playlists.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_playlists.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UC2DjFE7Xf11URZqWBigcVOQ", name: "EEVblog", + handle: None, subscriber_count: Some(881000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj", @@ -55,7 +57,6 @@ Channel( "dumpster diving", "debunking", ], - vanity_url: Some("https://www.youtube.com/c/EevblogDave"), banner: [ Thumbnail( url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -88,60 +89,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: false, has_live: false, visitor_data: Some("CgttaWpyTVpUN1AyZyioqr2ZBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_shorts.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_shorts.snap index e9e98bf..46c6acf 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_shorts.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_shorts.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UCh8gHdtzO2tXd593_bjErWg", name: "Doobydobap", + handle: Some("@Doobydobap"), subscriber_count: Some(3360000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.googleusercontent.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj", @@ -26,7 +28,6 @@ Channel( verification: Verified, description: "Hi, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n", tags: [], - vanity_url: Some("https://www.youtube.com/@Doobydobap"), banner: [ Thumbnail( url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -59,60 +60,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: true, has_live: false, visitor_data: Some("CgtHU1dvWkR4cGRfdyjMpt6iBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_shorts_20240129_pageheader.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_shorts_20240129_pageheader.snap index 47cf981..81a993a 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_shorts_20240129_pageheader.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_shorts_20240129_pageheader.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UCh8gHdtzO2tXd593_bjErWg", name: "Doobydobap", + handle: Some("@Doobydobap"), subscriber_count: Some(3740000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.googleusercontent.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s72-c-k-c0x00ffffff-no-rj", @@ -26,7 +28,6 @@ Channel( verification: Verified, description: "Hi, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n", tags: [], - vanity_url: Some("https://www.youtube.com/@Doobydobap"), banner: [ Thumbnail( url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -59,8 +60,6 @@ Channel( height: 424, ), ], - mobile_banner: [], - tv_banner: [], has_shorts: true, has_live: false, visitor_data: None, diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20221011_richgrid.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20221011_richgrid.snap index 6f0e23e..7fd71af 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20221011_richgrid.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20221011_richgrid.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UCh8gHdtzO2tXd593_bjErWg", name: "Doobydobap", + handle: None, subscriber_count: Some(2930000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj", @@ -26,7 +28,6 @@ Channel( verification: Verified, description: "Hi, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n", tags: [], - vanity_url: Some("https://www.youtube.com/c/Doobydobap"), banner: [ Thumbnail( url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -59,60 +60,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: true, has_live: false, visitor_data: Some("CgtQdE9zVVR3NVBDbyjz0ZKaBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20221011_richgrid2.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20221011_richgrid2.snap index e4efa91..09b224f 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20221011_richgrid2.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20221011_richgrid2.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UC2DjFE7Xf11URZqWBigcVOQ", name: "EEVblog", + handle: None, subscriber_count: Some(883000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj", @@ -55,7 +57,6 @@ Channel( "dumpster diving", "debunking", ], - vanity_url: Some("https://www.youtube.com/c/EevblogDave"), banner: [ Thumbnail( url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -88,60 +89,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: false, has_live: true, visitor_data: Some("Cgs4ZFVmMzVlU1dxbyiBqpeaBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20230415_coachella.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20230415_coachella.snap index a365792..7279d2e 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20230415_coachella.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20230415_coachella.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UCHF66aWLOxBW4l6VkSrS3cQ", name: "Coachella", + handle: Some("@Coachella"), subscriber_count: Some(2710000), + video_count: None, avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/RDZ6VWFjHEMFm_QcmCCf-yG_UiGo9YWXEmVRuiHSC8SvP02dgeBEtAjd4CnEKGLo0V2gGdIRDQ=s88-c-k-c0x00ffffff-no-rj-mo", @@ -31,10 +33,7 @@ Channel( "indio", "california", ], - vanity_url: Some("https://www.youtube.com/@Coachella"), banner: [], - mobile_banner: [], - tv_banner: [], has_shorts: true, has_live: true, visitor_data: Some("CgtjSUhDeVJ6SU5wNCj75uyhBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20240324_pageheader2.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20240324_pageheader2.snap index 42d984e..96428f6 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20240324_pageheader2.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_20240324_pageheader2.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UC2DjFE7Xf11URZqWBigcVOQ", name: "EEVblog", + handle: Some("@EEVblog"), subscriber_count: Some(933000), + video_count: Some(19), avatar: [ Thumbnail( url: "https://yt3.googleusercontent.com/ytc/AIdro_lagjGDfXbXlQXhznx3CDRitOBdxvebllQd_YP1ag=s72-c-k-c0x00ffffff-no-rj", @@ -55,7 +57,6 @@ Channel( "dumpster diving", "debunking", ], - vanity_url: Some("https://www.youtube.com/@EEVblog"), banner: [ Thumbnail( url: "https://yt3.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -88,8 +89,6 @@ Channel( height: 424, ), ], - mobile_banner: [], - tv_banner: [], has_shorts: true, has_live: true, visitor_data: None, diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_base.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_base.snap index cbf872c..53e191e 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_base.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_base.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UC2DjFE7Xf11URZqWBigcVOQ", name: "EEVblog", + handle: None, subscriber_count: Some(880000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj", @@ -55,7 +57,6 @@ Channel( "dumpster diving", "debunking", ], - vanity_url: Some("https://www.youtube.com/c/EevblogDave"), banner: [ Thumbnail( url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -88,60 +89,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: false, has_live: false, visitor_data: Some("CgszNU5rbDVZS2hMcyim4K2ZBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_empty.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_empty.snap index 780e6a6..8a490a0 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_empty.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_empty.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UCxBa895m48H5idw5li7h-0g", name: "Sebastian Figurroa", + handle: None, subscriber_count: None, + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/ytc/AMLnZu_hsZ1XlUXHzXsGNHJw0np79WhWZcC4j8eFdy-tiUCDBKAjJyJOzE5kXFRiqL2S=s48-c-k-c0x00ffffff-no-rj", @@ -26,10 +28,7 @@ Channel( verification: None, description: "", tags: [], - vanity_url: None, banner: [], - mobile_banner: [], - tv_banner: [], has_shorts: false, has_live: false, visitor_data: Some("Cgtvc2s4UllvTGl6byigxseZBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_live.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_live.snap index 4fa906c..fdafcc3 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_live.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_live.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UChs0pSaEoNLV4mevBFGaoKA", name: "The Good Life Radio x Sensual Musique", + handle: None, subscriber_count: Some(760000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/ytc/AMLnZu_V9mOdHaorjNFqGXCecFeOBZhDWB8tVYG_I8gJwA=s48-c-k-c0x00ffffff-no-rj", @@ -39,7 +41,6 @@ Channel( "tropical house", "house music", ], - vanity_url: Some("https://www.youtube.com/c/TheGoodLiferadio"), banner: [ Thumbnail( url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -72,60 +73,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: false, has_live: false, visitor_data: Some("CgtkYXJITElwYmd4OCj85a2ZBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_music.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_music.snap index 4d7ed05..0d4a4d1 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_music.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_music.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UC_vmjW5e1xEHhYjY2a0kK1A", name: "Oonagh - Topic", + handle: None, subscriber_count: None, + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/pqKv4iqSjmMKPxsMCeyklTbpROSyInGNR4XvD1DqKD0AlROlsHzvoAlTvtMTO1g1x2WxaQ2Enxw=s48-c-k-c0x00ffffff-no-rj", @@ -26,7 +28,6 @@ Channel( verification: None, description: "", tags: [], - vanity_url: None, banner: [ Thumbnail( url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -59,60 +60,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: false, has_live: false, visitor_data: Some("CgtCV1l2R2Rzb2ZSZyiu4a2ZBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_shorts.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_shorts.snap index 46369d0..c655530 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_shorts.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_shorts.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UCh8gHdtzO2tXd593_bjErWg", name: "Doobydobap", + handle: None, subscriber_count: Some(2840000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj", @@ -26,7 +28,6 @@ Channel( verification: Verified, description: "Hi, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n", tags: [], - vanity_url: Some("https://www.youtube.com/c/Doobydobap"), banner: [ Thumbnail( url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -59,60 +60,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: false, has_live: false, visitor_data: Some("CgtneXVRbGtSMWtlYyj75a2ZBg%3D%3D"), diff --git a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_upcoming.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_upcoming.snap index 97348c9..2b5a675 100644 --- a/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_upcoming.snap +++ b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_videos_upcoming.snap @@ -5,7 +5,9 @@ expression: map_res.c Channel( id: "UCcvfHa-GHSOHFAjU0-Ie57A", name: "Adam Something", + handle: None, subscriber_count: Some(947000), + video_count: None, avatar: [ Thumbnail( url: "https://yt3.ggpht.com/FzV47fzr2nc8_KOeUO2FSIH-daaxCZaPDGqrgC1_Qp0_zEn0DnKmi7PiMwcssTG4IEDL1XfdTIk=s48-c-k-c0x00ffffff-no-rj", @@ -43,7 +45,6 @@ Channel( "budapest", "eu", ], - vanity_url: Some("https://www.youtube.com/c/AdamSomething"), banner: [ Thumbnail( url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj", @@ -76,60 +77,6 @@ Channel( height: 424, ), ], - mobile_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 320, - height: 88, - ), - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 640, - height: 175, - ), - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 960, - height: 263, - ), - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 351, - ), - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj", - width: 1440, - height: 395, - ), - ], - tv_banner: [ - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 320, - height: 180, - ), - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 854, - height: 480, - ), - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1280, - height: 720, - ), - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 1920, - height: 1080, - ), - Thumbnail( - url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj", - width: 2120, - height: 1192, - ), - ], has_shorts: false, has_live: false, visitor_data: Some("Cgs4Ri1tLW1KNWozNCjGk8yZBg%3D%3D"), diff --git a/src/model/mod.rs b/src/model/mod.rs index 6ec4d2f..24d9b11 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -700,11 +700,15 @@ pub struct Channel { pub id: String, /// Channel name pub name: String, + /// YouTube channel handle (e.g. `@EEVblog`) + pub handle: Option, /// Channel subscriber count /// /// [`None`] if the subscriber count was hidden by the owner /// or could not be parsed. pub subscriber_count: Option, + /// Number of videos + pub video_count: Option, /// Channel avatar / profile picture pub avatar: Vec, /// Channel verification mark @@ -713,15 +717,8 @@ pub struct Channel { pub description: String, /// List of words to describe the topic of the channel pub tags: Vec, - /// Custom URL set by the channel owner - /// (e.g. ) - pub vanity_url: Option, /// Banner image shown above the channel pub banner: Vec, - /// Banner image shown above the channel (small format for mobile) - pub mobile_banner: Vec, - /// Banner image shown above the channel (16:9 fullscreen format for TV) - pub tv_banner: Vec, /// Does the channel have a *Shorts* tab? pub has_shorts: bool, /// Does the channel have a *Live* tab? diff --git a/src/util/mod.rs b/src/util/mod.rs index 013ccdd..121c1b0 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -32,9 +32,10 @@ pub static PLAYLIST_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(?:PL|RD|OLAK|UU)[A-Za-z0-9_-]{5,50}$").unwrap()); pub static ALBUM_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^MPREb_[A-Za-z0-9_-]{11}$").unwrap()); -pub static VANITY_PATH_REGEX: Lazy = Lazy::new(|| { - Regex::new(r"^/?(?:(?:c/|user/)?[A-z0-9]{1,100})|(?:@[A-z0-9-_.]{1,100})$").unwrap() -}); +pub static VANITY_PATH_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^/?(?:(?:c/|user/)?[A-z0-9]{1,100})|(?:@[\w\-\.·]{1,30})$").unwrap()); +pub static CHANNEL_HANDLE_REGEX: Lazy = + Lazy::new(|| Regex::new(r#"^@[\w\-\.·]{1,30}$"#).unwrap()); /// Separator string for YouTube Music subtitles pub const DOT_SEPARATOR: &str = " • "; diff --git a/src/validate.rs b/src/validate.rs index ccaa58a..7c43a61 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -11,7 +11,10 @@ //! - The validation functions of this module are meant vor validating specific data (video IDs, //! channel IDs, playlist IDs) and return [`true`] if the given input is valid -use crate::{error::Error, util}; +use crate::{ + error::Error, + util::{self, CHANNEL_HANDLE_REGEX}, +}; use once_cell::sync::Lazy; use regex::Regex; @@ -202,6 +205,32 @@ pub fn track_lyrics_id>(lyrics_id: S) -> Result<(), Error> { ) } +/// Validate the given channel handle +/// +/// YouTube channel handles can be up to 30 characters long and start with an `@`. +/// Allowed characters are letters and numbers (Unicode), underscores (`_`), hyphens (`-`), +/// full stops (`.`) and middle dots (`· U+00B7`) +/// +/// There are more fine-grained rules for specific scripts. Verifying these is not implemented. +/// +/// Reference: +/// +/// ``` +/// # use rustypipe::validate; +/// assert!(validate::channel_handle("@EEVBlog").is_ok()); +/// assert!(validate::channel_handle("@Āll·._-").is_ok()); +/// assert!(validate::channel_handle("@한국").is_ok()); +/// +/// assert!(validate::channel_handle("noat").is_err()); +/// assert!(validate::channel_handle("@no space").is_err()); +/// ``` +pub fn channel_handle>(channel_handle: S) -> Result<(), Error> { + check( + CHANNEL_HANDLE_REGEX.is_match(channel_handle.as_ref()), + "invalid channel handle", + ) +} + fn check(res: bool, msg: &'static str) -> Result<(), Error> { if res { Ok(()) diff --git a/tests/youtube.rs b/tests/youtube.rs index b6a0577..ca5f079 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -885,16 +885,13 @@ async fn channel_shorts(rp: RustyPipe) { // dbg!(&channel); assert_eq!(channel.id, "UCh8gHdtzO2tXd593_bjErWg"); assert_eq!(channel.name, "Doobydobap"); + assert_eq!(channel.handle.as_deref(), Some("@Doobydobap")); assert_gteo(channel.subscriber_count, 2_800_000, "subscribers"); assert!(!channel.avatar.is_empty(), "got no thumbnails"); assert_eq!(channel.verification, Verification::Verified); assert!(channel .description .contains("Hi, I\u{2019}m Tina, aka Doobydobap")); - assert_eq!( - channel.vanity_url.as_deref(), - Some("https://www.youtube.com/@Doobydobap") - ); assert!(!channel.banner.is_empty(), "got no banners"); assert!( @@ -994,15 +991,12 @@ async fn channel_search(rp: RustyPipe) { fn assert_channel_eevblog(channel: &Channel) { assert_eq!(channel.id, "UC2DjFE7Xf11URZqWBigcVOQ"); assert_eq!(channel.name, "EEVblog"); + assert_eq!(channel.handle.as_deref(), Some("@EEVblog")); assert_gteo(channel.subscriber_count, 880_000, "subscribers"); assert!(!channel.avatar.is_empty(), "got no thumbnails"); assert_eq!(channel.verification, Verification::Verified); assert_eq!(channel.description, "NO SCRIPT, NO FEAR, ALL OPINION\nAn off-the-cuff Video Blog about Electronics Engineering, for engineers, hobbyists, enthusiasts, hackers and Makers\nHosted by Dave Jones from Sydney Australia\n\nDONATIONS:\nBitcoin: 3KqyH1U3qrMPnkLufM2oHDU7YB4zVZeFyZ\nEthereum: 0x99ccc4d2654ba40744a1f678d9868ecb15e91206\nPayPal: david@alternatezone.com\n\nPatreon: https://www.patreon.com/eevblog\n\nEEVblog2: http://www.youtube.com/EEVblog2\nEEVdiscover: https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ\n\nEMAIL:\nAdvertising/Commercial: eevblog+business@gmail.com\nFan mail: eevblog+fan@gmail.com\nHate Mail: eevblog+hate@gmail.com\n\nI DON'T DO PAID VIDEO SPONSORSHIPS, DON'T ASK!\n\nPLEASE:\nDo NOT ask for personal advice on something, post it in the EEVblog forum.\nI read ALL email, but please don't be offended if I don't have time to reply, I get a LOT of email.\n\nMailbag\nPO Box 7949\nBaulkham Hills NSW 2153\nAUSTRALIA"); assert!(!channel.tags.is_empty(), "got no tags"); - assert_eq!( - channel.vanity_url.as_deref(), - Some("https://www.youtube.com/@EEVblog") - ); assert!(!channel.banner.is_empty(), "got no banners"); } From 1cffb27cc0b64929f9627f5839df2d73b81988a4 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 17 Aug 2024 02:59:56 +0200 Subject: [PATCH 5/9] feat!: add handle to ChannelItem, remove video_count --- codegen/src/abtest.rs | 2 +- src/client/response/video_item.rs | 10 ++--- ...p_search_20221121_AB3_channel_handles.snap | 40 +++++++++---------- ...nt__search__tests__map_search_default.snap | 2 +- src/model/mod.rs | 4 +- 5 files changed, 28 insertions(+), 30 deletions(-) diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index be14c79..201563a 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -179,7 +179,7 @@ pub async fn channel_handles_in_search_results(rp: &RustyPipeQuery) -> Result channel .subscriber_count - .map(|sc| sc > 100 && channel.video_count.is_none()) + .map(|sc| sc > 100 && channel.handle.is_some()) .unwrap_or_default(), _ => false, })) diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index 3d7a116..9b9a97a 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -610,28 +610,26 @@ impl YouTubeListMapper { fn map_channel(&mut self, channel: ChannelRenderer) -> ChannelItem { // channel handle instead of subscriber count (A/B test 3) - let (sc_txt, vc_text) = if channel + let (handle, sc_txt) = if channel .subscriber_count_text .as_ref() .map(|txt| txt.starts_with('@')) .unwrap_or_default() { - (channel.video_count_text, None) - } else { (channel.subscriber_count_text, channel.video_count_text) + } else { + (None, channel.subscriber_count_text) }; ChannelItem { id: channel.channel_id, name: channel.title, + handle, avatar: channel.thumbnail.into(), verification: channel.owner_badges.into(), subscriber_count: sc_txt.and_then(|txt| { util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings) }), - video_count: vc_text.and_then(|txt| { - util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings) - }), short_description: channel.description_snippet, } } diff --git a/src/client/snapshots/rustypipe__client__search__tests__map_search_20221121_AB3_channel_handles.snap b/src/client/snapshots/rustypipe__client__search__tests__map_search_20221121_AB3_channel_handles.snap index 361af9c..ec1fe51 100644 --- a/src/client/snapshots/rustypipe__client__search__tests__map_search_20221121_AB3_channel_handles.snap +++ b/src/client/snapshots/rustypipe__client__search__tests__map_search_20221121_AB3_channel_handles.snap @@ -9,6 +9,7 @@ SearchResult( Channel(ChannelItem( id: "UCMwePVHRpDdfeUcwtDZu2Dw", name: "Monstafluff Music", + handle: Some("@MonstafluffMusic"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/ytc/AMLnZu9YhTzdAoL6P4PYq51PCF076ITDrgLitxSDPqv6sw=s88-c-k-c0x00ffffff-no-rj-mo", @@ -23,12 +24,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(582000), - video_count: None, short_description: "Music Submissions: https://monstafluff.edmdistrict.com/", )), Channel(ChannelItem( id: "UCLxAS02eWvfZK4icRNzWD_g", name: "Music Travel Love", + handle: Some("@MusicTravelLove"), avatar: [ Thumbnail( url: "https://yt3.ggpht.com/ytc/AMLnZu9njNDLU_VtFjfGUaTArBp4AJFhJIxb_CxP7knf3A=s88-c-k-c0x00ffffff-no-rj-mo", @@ -43,12 +44,12 @@ SearchResult( ], verification: Artist, subscriber_count: Some(4030000), - video_count: None, short_description: "Welcome to the official Music Travel Love YouTube channel! We travel the world making music, friends, videos and memories!", )), Channel(ChannelItem( id: "UCxKxjNPyL9UO5LRWHzp5JxA", name: "Black&White Music", + handle: Some("@blackwhitemusic5836"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/FDjW2-Cb6tFbtNv02D1UX4XtvP7P3eEWB93hGimeP4pb2TadVhAgxSVMZLZDp5NiBWGLT5eprA=s88-c-k-c0x00ffffff-no-rj-mo", @@ -63,12 +64,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(167000), - video_count: None, short_description: "MUSIC IN HARMONY WITH YOUR LIFE!!! If any producer, label, artist or photographer has an issue with any of the music or\u{a0}...", )), Channel(ChannelItem( id: "UCGIygiYkKxn7g7fFNFdXskg", name: "HAEVN MUSIC", + handle: Some("@HAEVNMUSIC"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/EYlGIfqhvwtfkCyi5vpqfY_kDHr6L3OeCmkudNiAyhvz6UCnTZQOQaM-8PelFDGofdIqeF7Mb4E=s88-c-k-c0x00ffffff-no-rj-mo", @@ -83,12 +84,12 @@ SearchResult( ], verification: Artist, subscriber_count: Some(411000), - video_count: None, short_description: "The official YouTube channel of HAEVN Music. Receiving a piano from his grandfather had a great impact on Jorrit\'s life.", )), Channel(ChannelItem( id: "UClvNJkDHdc1gvFGN_Fr_qPw", name: "Artemis Music", + handle: Some("@artemismusic1000"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/rGXIwYAhI49rKBQmw_pKFMv9yEt4euHnmXOE0OOCD6ApdQXGnuPmEv7TK7cDjrjt0rUXYHuw=s88-c-k-c0x00ffffff-no-rj-mo", @@ -103,12 +104,12 @@ SearchResult( ], verification: None, subscriber_count: Some(31200), - video_count: None, short_description: "Hello and welcome to \"Artemis Music\"! Music can play an effective role in helping us lead a better and more productive life.", )), Channel(ChannelItem( id: "UC5r3j8tQsB3MYZiwQFGKrdA", name: "Disco Music", + handle: Some("@discomusic9273"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/5nqhAdf26KoSKbfUB8kvhJo6rpMQw3XS345h8ZNmeXScqlB1KjJAM0T371r3QcS1mA1LZg9B1Po=s88-c-k-c0x00ffffff-no-rj-mo", @@ -123,12 +124,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(372000), - video_count: None, short_description: "Music is the only language in which you cannot say a mean or sarcastic thing. Have fun listening to music.", )), Channel(ChannelItem( id: "UCNZYpcqym8gHcNg2GWcC6nQ", name: "S!X - Music", + handle: Some("@s1x-music"), avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/ytc/AMLnZu_1NOzbZUJWZjtmD4NTsb9BR-TNIAzNoajv0TisvQ=s88-c-k-c0x00ffffff-no-rj-mo", @@ -143,12 +144,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(178000), - video_count: None, short_description: "S!X - Music is an independent Hip-Hop label. Soundcloud : https://soundcloud.com/s1xmusic Facebook\u{a0}...", )), Channel(ChannelItem( id: "UCoEryX-WO7IHBGqTAC5r9Zw", name: "Shake Music", + handle: Some("@ShakeMusic"), avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/ytc/AMLnZu9fMXUALsloNUJ_wLpqCS0ovprvc5W-XwfrpmWqIw=s88-c-k-c0x00ffffff-no-rj-mo", @@ -163,12 +164,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(1040000), - video_count: None, short_description: "Welcome to Shake Music, a Trap & Bass Channel / Record Label dedicated to bringing you the best tracks. All tracks on Shake\u{a0}...", )), Channel(ChannelItem( id: "UCTJ9Qg-1vBu2pP_YrWUfGnQ", name: "Miracle Music", + handle: Some("@miraclemusic2328"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/3RMarDSmUSIexCXWCpMUkqV64uiHDXTidBLwsObHstx5-AbB8h_n8Zy1W9JymURd7ivzlDEGFw=s88-c-k-c0x00ffffff-no-rj-mo", @@ -183,12 +184,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(822000), - video_count: None, short_description: "Welcome to Miracle Music! On this channel you will find a wide variety of different Deep House, Tropical House, Chill Out, EDM,.", )), Channel(ChannelItem( id: "UCp6_KuNhT0kcFk-jXw9Tivg", name: "Magic Music", + handle: Some("@MagicMusicGroup"), avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/ytc/AMLnZu-fgSc_lceD4fRL_y0b3MKd2k54DF-laDAR3Avbuw=s88-c-k-c0x00ffffff-no-rj-mo", @@ -203,12 +204,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(4620000), - video_count: None, short_description: "", )), Channel(ChannelItem( id: "UCe55Gy-hFDvLZp8C8BZhBnw", name: "Nightblue Music", + handle: Some("@NightblueMusic"), avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/ytc/AMLnZu-29SYt5qpqMP9Xi2A98mqL8ymI5Lg7Vzx-qpY09w=s88-c-k-c0x00ffffff-no-rj-mo", @@ -223,12 +224,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(1050000), - video_count: None, short_description: "BRINGING YOU ONLY THE BEST EDM - TRAP Submit your own track for promotion here:\u{a0}...", )), Channel(ChannelItem( id: "UC2fVSthyWxWSjsiEAHPzriQ", name: "Mr_MoMo Music", + handle: Some("@MrMoMoMusic"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/7YG4jSrhx_Mfi2TsV0rJFlFARaR8kl7ilcIyzs6gSeNjwn-J88DvDWD8PSNd5o03qJRzpvhs=s88-c-k-c0x00ffffff-no-rj-mo", @@ -243,12 +244,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(709000), - video_count: None, short_description: "Hey there! I am Mr MoMo My channel focus on Japan music, lofi, trap & bass type beat and Japanese instrumental. I mindfully\u{a0}...", )), Channel(ChannelItem( id: "UCN31w7dRjjz8CeP0GfSIo8A", name: "Danit Music Official", + handle: Some("@danitmusicofficial5734"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/ytc/AMLnZu9rUKtDsY-aSoE5WEwAQxvQTXiuAPYMBoJQ2mYTUA=s88-c-k-c0x00ffffff-no-rj-mo", @@ -263,12 +264,12 @@ SearchResult( ], verification: None, subscriber_count: Some(54400), - video_count: None, short_description: "", )), Channel(ChannelItem( id: "UCpEHWiTMk1eEBAdzBnAb3rA", name: "Energy Transformation Relaxing Music ", + handle: Some("@energytransformationrelaxi5596"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/RR7upyAvT7N0_qlZWfLlDSRPhLufX4W4X6-qahWvuvDCLn2cWCs0yh_HXB2iwGbk_MTwSqwWEQ=s88-c-k-c0x00ffffff-no-rj-mo", @@ -283,12 +284,12 @@ SearchResult( ], verification: None, subscriber_count: Some(3590), - video_count: None, short_description: "Welcome to our Energy Transformation Relaxing Music . This chakra music channel will focus on developing the best chakra\u{a0}...", )), Channel(ChannelItem( id: "UCqswUMaC5yWUrkQszr8fuBA", name: "Nonstop Music", + handle: Some("@nonstopmusic9993"), avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/ytc/AMLnZu9vLN62RxNbnpa20r5XreWRlVjHXbHf7BMcvSBxoQ=s88-c-k-c0x00ffffff-no-rj-mo", @@ -303,12 +304,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(416000), - video_count: None, short_description: "Nonstop Music - Home of 1h videos of your favourite songs and mixes. Nonstop Genres: Pop • Chillout • Tropical House • Deep\u{a0}...", )), Channel(ChannelItem( id: "UChO8h2G8UjOVc081rgYU8XQ", name: "Vibe Music", + handle: Some("@vibemusic."), avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/ytc/AMLnZu9Br5pt87kuDLRFbh1MqMXeFlCLbUrwFlDIzU4s=s88-c-k-c0x00ffffff-no-rj-mo", @@ -323,12 +324,12 @@ SearchResult( ], verification: Verified, subscriber_count: Some(3000000), - video_count: None, short_description: "Vibe Music strives to bring the best lyric videos of popular Rap & Hip Hop songs. Be sure to Subscribe to see new videos we\u{a0}...", )), Channel(ChannelItem( id: "UClV8b2EhIhIASKw-etzegyw", name: "Suits Music", + handle: Some("@SuitsMusic"), avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/ytc/AMLnZu9Aj5RtZZMdK_B_YD-8rOfi9c5ddFw5t1s4GYEeOQ=s88-c-k-c0x00ffffff-no-rj-mo", @@ -343,12 +344,12 @@ SearchResult( ], verification: None, subscriber_count: Some(120000), - video_count: None, short_description: "", )), Channel(ChannelItem( id: "UCI2hwz3r5phXpOtViIA5inA", name: "Rock Music Collection", + handle: Some("@rockmusiccollection4332"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/kB4gWvROUIWFuJN8xwIqmPl1QV2_gXMat6COAJjXZT07E3xomc4b2JwGtDg05t1MmhgqImSifhc=s88-c-k-c0x00ffffff-no-rj-mo", @@ -363,12 +364,12 @@ SearchResult( ], verification: None, subscriber_count: Some(81700), - video_count: None, short_description: "", )), Channel(ChannelItem( id: "UC9w8My3S7h-bQZ-4R-0ZPsw", name: "Helios Music", + handle: Some("@heliosmusic55"), avatar: [ Thumbnail( url: "//yt3.ggpht.com/bi08T8zuYI1PlbM8M5fyZzjVvNJRJFFcQoonRQvS30opJ-OqGIq5OPrZ19qga29PIAit7OO3=s88-c-k-c0x00ffffff-no-rj-mo", @@ -383,12 +384,12 @@ SearchResult( ], verification: None, subscriber_count: Some(53000), - video_count: None, short_description: "Welcome to my channel - Helios Music. I created this channel to help people have the most relaxing, refreshing and comfortable\u{a0}...", )), Channel(ChannelItem( id: "UC_ODKC5gTs2LvdHXDRdDm0w", name: "Music On", + handle: Some("@MilanPavlovic91"), avatar: [ Thumbnail( url: "//yt3.googleusercontent.com/ytc/AMLnZu8lUOYw4RdRwQf2Kz8RCExSmuWC78oetXF7VL67SA=s88-c-k-c0x00ffffff-no-rj-mo", @@ -403,7 +404,6 @@ SearchResult( ], verification: None, subscriber_count: Some(129000), - video_count: None, short_description: "Music On (UNOFFICIAL CHANNEL)", )), ], diff --git a/src/client/snapshots/rustypipe__client__search__tests__map_search_default.snap b/src/client/snapshots/rustypipe__client__search__tests__map_search_default.snap index e0ac9fd..3198cc7 100644 --- a/src/client/snapshots/rustypipe__client__search__tests__map_search_default.snap +++ b/src/client/snapshots/rustypipe__client__search__tests__map_search_default.snap @@ -9,6 +9,7 @@ SearchResult( Channel(ChannelItem( id: "UCh8gHdtzO2tXd593_bjErWg", name: "Doobydobap", + handle: None, avatar: [ Thumbnail( url: "//yt3.ggpht.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s88-c-k-c0x00ffffff-no-rj-mo", @@ -23,7 +24,6 @@ SearchResult( ], verification: Verified, subscriber_count: Some(2920000), - video_count: Some(219), short_description: "Hi, I\'m Tina, aka Doobydobap! Food is the medium I use to tell stories and connect with people who share the same passion as I\u{a0}...", )), Video(VideoItem( diff --git a/src/model/mod.rs b/src/model/mod.rs index 24d9b11..7adaeac 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -870,6 +870,8 @@ pub struct ChannelItem { pub id: String, /// Channel name pub name: String, + /// YouTube channel handle (e.g. `@EEVblog`) + pub handle: Option, /// Channel avatar/profile picture pub avatar: Vec, /// Channel verification mark @@ -878,8 +880,6 @@ pub struct ChannelItem { /// /// [`None`] if hidden by the owner or not present. pub subscriber_count: Option, - /// Number of videos from the channel - pub video_count: Option, /// Abbreviated channel description pub short_description: String, } From ee3ae40395263c5989784c7e00038ff13bc1151a Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 17 Aug 2024 03:26:20 +0200 Subject: [PATCH 6/9] fix: get TV client version --- src/client/mod.rs | 68 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 2d74f5d..7546d8b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -203,9 +203,9 @@ const DISABLE_PRETTY_PRINT_PARAMETER: &str = "prettyPrint=false"; // Desktop client const DESKTOP_CLIENT_VERSION: &str = "2.20230126.00.00"; -const TVHTML5_CLIENT_VERSION: &str = "2.0"; -const TV_CLIENT_VERSION: &str = "7.20240724.13.00"; const DESKTOP_MUSIC_CLIENT_VERSION: &str = "1.20230123.01.01"; +const TV_CLIENT_VERSION: &str = "7.20240724.13.00"; +const TVHTML5_CLIENT_VERSION: &str = "2.0"; // Mobile client const MOBILE_CLIENT_VERSION: &str = "18.03.33"; @@ -372,14 +372,20 @@ impl Default for RustyPipeOpts { struct CacheHolder { desktop_client: RwLock>, music_client: RwLock>, + tv_client: RwLock>, deobf: RwLock>, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[serde(default)] struct CacheData { + #[serde(skip_serializing_if = "CacheEntry::is_none")] desktop_client: CacheEntry, + #[serde(skip_serializing_if = "CacheEntry::is_none")] music_client: CacheEntry, + #[serde(skip_serializing_if = "CacheEntry::is_none")] + tv_client: CacheEntry, + #[serde(skip_serializing_if = "CacheEntry::is_none")] deobf: CacheEntry, } @@ -430,6 +436,10 @@ impl CacheEntry { CacheEntry::None => None, } } + + fn is_none(&self) -> bool { + matches!(self, Self::None) + } } impl From for CacheEntry { @@ -521,6 +531,7 @@ impl RustyPipeBuilder { cache: CacheHolder { desktop_client: RwLock::new(cdata.desktop_client), music_client: RwLock::new(cdata.music_client), + tv_client: RwLock::new(cdata.tv_client), deobf: RwLock::new(cdata.deobf), }, default_opts: self.default_opts, @@ -815,6 +826,12 @@ impl RustyPipe { .await } + /// Extract the current version of the YouTube TV client from the website. + async fn extract_tv_client_version(&self) -> Result { + self.extract_client_version(None, YOUTUBE_TV_URL, YOUTUBE_TV_URL, Some(TV_UA)) + .await + } + async fn extract_client_version( &self, sw_url: Option<&str>, @@ -933,6 +950,37 @@ impl RustyPipe { } } + /// Get the current version of the YouTube TV client from the following sources + /// + /// 1. from cache + /// 2. from the YouTube TV website + /// 3. fall back to the hardcoded version + async fn get_tv_client_version(&self) -> String { + // Write lock here to prevent concurrent tasks from fetching the same data + let mut tv_client = self.inner.cache.tv_client.write().await; + + match tv_client.get() { + Some(cdata) => cdata.version.clone(), + None => { + tracing::debug!("getting TV client version"); + match self.extract_tv_client_version().await { + Ok(version) => { + *tv_client = CacheEntry::from(ClientData { + version: version.clone(), + }); + drop(tv_client); + self.store_cache().await; + version + } + Err(e) => { + tracing::warn!("{}, falling back to hardcoded TV client version", e); + DESKTOP_MUSIC_CLIENT_VERSION.to_owned() + } + } + } + } + } + /// Get deobfuscation data (either from cache or extracted from YouTube's JavaScript code) async fn get_deobf_data(&self) -> Result { // Write lock here to prevent concurrent tasks from fetching the same data @@ -974,6 +1022,7 @@ impl RustyPipe { let cdata = CacheData { desktop_client: self.inner.cache.desktop_client.read().await.clone(), music_client: self.inner.cache.music_client.read().await.clone(), + tv_client: self.inner.cache.tv_client.read().await.clone(), deobf: self.inner.cache.deobf.read().await.clone(), }; @@ -1197,7 +1246,7 @@ impl RustyPipeQuery { ClientType::Tv => YTContext { client: ClientInfo { client_name: "TVHTML5", - client_version: Cow::Borrowed(TV_CLIENT_VERSION), + client_version: Cow::Owned(self.client.get_tv_client_version().await), client_screen: Some("WATCH"), platform: "TV", device_model: Some("SmartTV"), @@ -1689,21 +1738,28 @@ mod tests { } #[tokio::test] - async fn t_extract_desktop_client_version() { + async fn extract_desktop_client_version() { let rp = RustyPipe::new(); let version = rp.extract_desktop_client_version().await.unwrap(); assert!(get_major_version(&version) >= 2); } #[tokio::test] - async fn t_extract_music_client_version() { + async fn extract_music_client_version() { let rp = RustyPipe::new(); let version = rp.extract_music_client_version().await.unwrap(); assert!(get_major_version(&version) >= 1); } #[tokio::test] - async fn t_get_visitor_data() { + async fn extract_tv_client_version() { + let rp = RustyPipe::new(); + let version = rp.extract_tv_client_version().await.unwrap(); + assert!(get_major_version(&version) >= 7); + } + + #[tokio::test] + async fn get_visitor_data() { let rp = RustyPipe::new(); let visitor_data = rp.get_visitor_data().await.unwrap(); From a3a1d9abf3e7e3ee2020b76d287ba006f85eb240 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 17 Aug 2024 04:33:07 +0200 Subject: [PATCH 7/9] test: add downloader test --- Cargo.toml | 2 +- Justfile | 2 +- downloader/Cargo.toml | 6 ++ downloader/src/error.rs | 54 ++++++++++++++++++ downloader/src/lib.rs | 3 +- downloader/src/util.rs | 54 +----------------- downloader/tests/tests.rs | 113 ++++++++++++++++++++++++++++++++++++++ tests/youtube.rs | 1 + 8 files changed, 180 insertions(+), 55 deletions(-) create mode 100644 downloader/src/error.rs create mode 100644 downloader/tests/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 9c05861..7da8952 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ keywords.workspace = true categories.workspace = true description = "Client for the public YouTube / YouTube Music API (Innertube), inspired by NewPipe" -include = ["/src", "README.md", "LICENSE", "!snapshots"] +include = ["/src", "README.md", "CHANGELOG.md", "LICENSE", "!snapshots"] [workspace] members = [".", "codegen", "downloader", "cli"] diff --git a/Justfile b/Justfile index 5fd6b75..e4d0194 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,6 @@ test: # cargo test --features=rss - cargo nextest run --features=rss --no-fail-fast --failure-output final --retries 1 + cargo nextest run --workspace --features=rss --no-fail-fast --failure-output final --retries 1 unittest: cargo nextest run --features=rss --no-fail-fast --failure-output final --lib diff --git a/downloader/Cargo.toml b/downloader/Cargo.toml index b04d087..7582dfa 100644 --- a/downloader/Cargo.toml +++ b/downloader/Cargo.toml @@ -48,3 +48,9 @@ time.workspace = true lofty = { version = "0.21.0", optional = true } image = { version = "0.25.0", optional = true } smartcrop2 = { version = "0.3.0", optional = true } + +[dev-dependencies] +path_macro.workspace = true +rstest.workspace = true +serde_json.workspace = true +temp_testdir = "0.2.3" diff --git a/downloader/src/error.rs b/downloader/src/error.rs new file mode 100644 index 0000000..8970dd4 --- /dev/null +++ b/downloader/src/error.rs @@ -0,0 +1,54 @@ +use std::{borrow::Cow, path::PathBuf}; + +use rustypipe::client::ClientType; + +/// Error from the video downloader +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum DownloadError { + /// RustyPipe error + #[error("{0}")] + RustyPipe(#[from] rustypipe::error::Error), + /// 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), + /// FFmpeg returned an error + #[error("FFmpeg error: {0}")] + Ffmpeg(Cow<'static, str>), + /// Error parsing ranges for progressive download + #[error("Progressive download error: {0}")] + Progressive(Cow<'static, str>), + /// Video could not be downloaded because of invalid player data + #[error("input error: {0}")] + Input(Cow<'static, str>), + /// Download target already exists + #[error("file {0} already exists")] + Exists(PathBuf), + #[cfg(feature = "audiotag")] + /// Audio tagging error + #[error("Audio tag error: {0}")] + AudioTag(Cow<'static, str>), + /// Other error + #[error("error: {0}")] + Other(Cow<'static, str>), +} + +#[cfg(feature = "audiotag")] +impl From for DownloadError { + fn from(value: lofty::error::LoftyError) -> Self { + Self::AudioTag(value.to_string().into()) + } +} + +#[cfg(feature = "audiotag")] +impl From for DownloadError { + fn from(value: image::ImageError) -> Self { + Self::AudioTag(value.to_string().into()) + } +} diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index 5c65fb0..a1483d4 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -1,6 +1,7 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs, clippy::todo, clippy::dbg_macro)] +mod error; mod util; use std::{ @@ -42,7 +43,7 @@ use rustypipe::model::{richtext::ToPlaintext, VideoDetails, VideoPlayerDetails}; #[cfg(feature = "audiotag")] use time::{Date, OffsetDateTime}; -pub use util::DownloadError; +pub use error::DownloadError; type Result = core::result::Result; diff --git a/downloader/src/util.rs b/downloader/src/util.rs index 5069c96..5f87339 100644 --- a/downloader/src/util.rs +++ b/downloader/src/util.rs @@ -1,58 +1,8 @@ -use std::{borrow::Cow, collections::BTreeMap, path::PathBuf}; +use std::collections::BTreeMap; use reqwest::Url; -use rustypipe::client::ClientType; -/// Error from the video downloader -#[derive(thiserror::Error, Debug)] -#[non_exhaustive] -pub enum DownloadError { - /// RustyPipe error - #[error("{0}")] - RustyPipe(#[from] rustypipe::error::Error), - /// 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), - /// FFmpeg returned an error - #[error("FFmpeg error: {0}")] - Ffmpeg(Cow<'static, str>), - /// Error parsing ranges for progressive download - #[error("Progressive download error: {0}")] - Progressive(Cow<'static, str>), - /// Video could not be downloaded because of invalid player data - #[error("input error: {0}")] - Input(Cow<'static, str>), - /// Download target already exists - #[error("file {0} already exists")] - Exists(PathBuf), - #[cfg(feature = "audiotag")] - /// Audio tagging error - #[error("Audio tag error: {0}")] - AudioTag(Cow<'static, str>), - /// Other error - #[error("error: {0}")] - Other(Cow<'static, str>), -} - -#[cfg(feature = "audiotag")] -impl From for DownloadError { - fn from(value: lofty::error::LoftyError) -> Self { - Self::AudioTag(value.to_string().into()) - } -} - -#[cfg(feature = "audiotag")] -impl From for DownloadError { - fn from(value: image::ImageError) -> Self { - Self::AudioTag(value.to_string().into()) - } -} +use crate::DownloadError; /// Split an URL into its base string and parameter map /// diff --git a/downloader/tests/tests.rs b/downloader/tests/tests.rs new file mode 100644 index 0000000..3fc035d --- /dev/null +++ b/downloader/tests/tests.rs @@ -0,0 +1,113 @@ +use std::{fs, os::unix::fs::MetadataExt, path::Path, process::Command}; + +use path_macro::path; +use rstest::{fixture, rstest}; +use rustypipe::{client::RustyPipe, model::AudioCodec, param::StreamFilter}; +use rustypipe_downloader::Downloader; +use temp_testdir::TempDir; + +/// Get a new RusttyPipe instance +#[fixture] +fn rp() -> RustyPipe { + let vdata = std::env::var("YT_VDATA").ok(); + RustyPipe::builder() + .strict() + .storage_dir(path!(env!("CARGO_MANIFEST_DIR") / "..")) + .visitor_data_opt(vdata) + .build() + .unwrap() +} + +#[rstest] +#[tokio::test] +async fn download_video(rp: RustyPipe) { + let td = TempDir::default(); + let td_path = td.to_path_buf(); + + let dl = Downloader::builder().rustypipe(&rp).build(); + + let res = dl + .id("UXqq0ZvbOnk") + .to_dir(&td_path) + .stream_filter(StreamFilter::new().video_max_res(480)) + .download() + .await + .unwrap(); + + assert_eq!( + res.dest, + path!(td_path / "CHARGE - Blender Open Movie [UXqq0ZvbOnk].mp4") + ); + assert_eq!(res.player_data.details.id, "UXqq0ZvbOnk"); +} + +#[rstest] +#[tokio::test] +async fn download_music(rp: RustyPipe) { + let td = TempDir::default(); + let td_path = td.to_path_buf(); + + let dl = Downloader::builder() + .audio_tag() + .crop_cover() + .rustypipe(&rp) + .build(); + + let res = dl + .id("bVtv3st8bgc") + .to_dir(&td_path) + .stream_filter( + StreamFilter::new() + .no_video() + .audio_codecs([AudioCodec::Opus]), + ) + .download() + .await + .unwrap(); + + assert_eq!( + res.dest, + path!(td_path / "Lord of the Riffs [bVtv3st8bgc].opus") + ); + assert_eq!(res.player_data.details.id, "bVtv3st8bgc"); + let fm = fs::metadata(&res.dest).unwrap(); + assert_gte(fm.size(), 6_000_000, "file size"); + assert_audio_meta( + &res.dest, + "Lord of the Riffs", + "Alexander Nakarada - CreatorChords", + "Lord of the Riffs", + "2022-02-05", + ); +} + +/// Assert that number A is greater than or equal to number B +#[track_caller] +fn assert_gte(a: T, b: T, msg: &str) { + assert!(a >= b, "expected >= {b} {msg}, got {a}"); +} + +#[track_caller] +fn assert_audio_meta(p: &Path, title: &str, artist: &str, album: &str, date: &str) { + let res = Command::new("ffprobe") + .args([ + "-loglevel", + "error", + "-show_entries", + "stream_tags", + "-of", + "json", + ]) + .arg(p) + .output() + .unwrap(); + if !res.status.success() { + panic!("ffprobe error\n{}", String::from_utf8_lossy(&res.stderr)) + } + let res_json = serde_json::from_slice::(&res.stdout).unwrap(); + let tags = &res_json["streams"][0]["tags"]; + assert_eq!(tags["TITLE"].as_str(), Some(title)); + assert_eq!(tags["ARTIST"].as_str(), Some(artist)); + assert_eq!(tags["ALBUM"].as_str(), Some(album)); + assert_eq!(tags["DATE"].as_str(), Some(date)); +} diff --git a/tests/youtube.rs b/tests/youtube.rs index ca5f079..4e5f656 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -2697,6 +2697,7 @@ fn rp(lang: Language) -> RustyPipe { let vdata = std::env::var("YT_VDATA").ok(); RustyPipe::builder() .strict() + .storage_dir(env!("CARGO_MANIFEST_DIR")) .lang(lang) .visitor_data_opt(vdata) .build() From 640e954073fee11fa5a3f8ffaf9008824732c96c Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 00:43:02 +0200 Subject: [PATCH 8/9] feat: remove startpage --- codegen/src/download_testfiles.rs | 25 - src/client/pagination.rs | 1 - src/client/response/mod.rs | 1 - src/client/response/trends.rs | 9 +- ...ent__pagination__tests__map_startpage.snap | 884 ------------------ ..._client__trends__tests__map_startpage.snap | 784 ---------------- src/client/trends.rs | 103 +- tests/youtube.rs | 12 - 8 files changed, 6 insertions(+), 1813 deletions(-) delete mode 100644 src/client/snapshots/rustypipe__client__pagination__tests__map_startpage.snap delete mode 100644 src/client/snapshots/rustypipe__client__trends__tests__map_startpage.snap diff --git a/codegen/src/download_testfiles.rs b/codegen/src/download_testfiles.rs index d5fd42e..1a580d2 100644 --- a/codegen/src/download_testfiles.rs +++ b/codegen/src/download_testfiles.rs @@ -38,8 +38,6 @@ pub async fn download_testfiles() { search_cont().await; search_playlists().await; search_empty().await; - startpage().await; - startpage_cont().await; trending().await; music_playlist().await; @@ -448,29 +446,6 @@ async fn search_empty() { .unwrap(); } -async fn startpage() { - let json_path = path!(*TESTFILES_DIR / "trends" / "startpage.json"); - if json_path.exists() { - return; - } - - let rp = rp_testfile(&json_path); - rp.query().startpage().await.unwrap(); -} - -async fn startpage_cont() { - let json_path = path!(*TESTFILES_DIR / "trends" / "startpage_cont.json"); - if json_path.exists() { - return; - } - - let rp = RustyPipe::new(); - let startpage = rp.query().startpage().await.unwrap(); - - let rp = rp_testfile(&json_path); - startpage.next(rp.query()).await.unwrap(); -} - async fn trending() { let json_path = path!(*TESTFILES_DIR / "trends" / "trending_videos.json"); if json_path.exists() { diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 1251574..e8f53dc 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -355,7 +355,6 @@ mod tests { #[rstest] #[case::search("search", path!("search" / "cont.json"))] - #[case::startpage("startpage", path!("trends" / "startpage_cont.json"))] #[case::recommendations("recommendations", path!("video_details" / "recommendations.json"))] fn map_continuation_items(#[case] name: &str, #[case] path: PathBuf) { let json_path = path!(*TESTFILES / path); diff --git a/src/client/response/mod.rs b/src/client/response/mod.rs index 85392cd..e8b7b52 100644 --- a/src/client/response/mod.rs +++ b/src/client/response/mod.rs @@ -34,7 +34,6 @@ pub(crate) use player::Player; pub(crate) use playlist::Playlist; pub(crate) use search::Search; pub(crate) use search::SearchSuggestion; -pub(crate) use trends::Startpage; pub(crate) use trends::Trending; pub(crate) use url_endpoint::ResolvedUrl; pub(crate) use video_details::VideoComments; diff --git a/src/client/response/trends.rs b/src/client/response/trends.rs index f35472d..07f4b9a 100644 --- a/src/client/response/trends.rs +++ b/src/client/response/trends.rs @@ -1,13 +1,6 @@ use serde::Deserialize; -use super::{video_item::YouTubeListRendererWrap, ResponseContext, Tab, TwoColumnBrowseResults}; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct Startpage { - pub contents: Contents, - pub response_context: ResponseContext, -} +use super::{video_item::YouTubeListRendererWrap, Tab, TwoColumnBrowseResults}; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src/client/snapshots/rustypipe__client__pagination__tests__map_startpage.snap b/src/client/snapshots/rustypipe__client__pagination__tests__map_startpage.snap deleted file mode 100644 index 171c57f..0000000 --- a/src/client/snapshots/rustypipe__client__pagination__tests__map_startpage.snap +++ /dev/null @@ -1,884 +0,0 @@ ---- -source: src/client/pagination.rs -expression: map_res.c ---- -Paginator( - count: None, - items: [ - Video(VideoItem( - id: "mRmlXh7Hams", - name: "Extra 3 vom 12.10.2022 im NDR | extra 3 | NDR", - duration: Some(1839), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/mRmlXh7Hams/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAbO4lI0dDo_r85A1fi9XQS0rNiOQ", - width: 480, - height: 270, - ), - ], - channel: Some(ChannelTag( - id: "UCjhkuC_Pi85wGjnB0I1ydxw", - name: "extra 3", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/N2TrlnZnU3cYFrRcXmQhQ77IriCxoEl-XTapCJQ9UkEHEkb0gMYVASjewV5Rg1P0HPUOebRoYw=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("2 days ago"), - view_count: Some(585257), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Niedersachsen nach der Wahl: Schuld ist immer die Ampel | Die Grünen: Partei der erneuerbaren Prinzipien | Verhütung? Ist Frauensache! | Youtube: Handwerk mit goldenem Boden - Christian Ehring..."), - )), - Video(VideoItem( - id: "LsXC5r64Pvc", - name: "Most Rarest Plays In Baseball History", - duration: Some(1975), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/LsXC5r64Pvc/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB2KXmgKxrJVUy3Naqi_R-R2X92FA", - width: 480, - height: 270, - ), - ], - channel: Some(ChannelTag( - id: "UCRfKJZ7LHueFudiDgAJDr9Q", - name: "Top All Sports", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu_dYWlP21FumM8m8ZxkKiTNaF9E68a2fnFnBo_q=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("3 weeks ago"), - view_count: Some(985521), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("#baseball #mlb #mlbb"), - )), - Video(VideoItem( - id: "dwPmd1GqQHE", - name: "90S RAP & HIPHOP MIX - Notorious B I G , Dr Dre, 50 Cent, Snoop Dogg, 2Pac, DMX, Lil Jon and more", - duration: Some(5457), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/dwPmd1GqQHE/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAAyGcLGzFkfdEmqqohpxZsGOM9Kw", - width: 480, - height: 270, - ), - ], - channel: Some(ChannelTag( - id: "UCKICAAGtBLJJ5zRdIxn_B4g", - name: "#Hip Hop 2022", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/fD5u3Lvkxe7oD0J3VlZ_Ih9BWtxT10wc68XWzSbVt02L88J2QrqO4FaK2xrsOoejD1GpBE7VAaA=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("5 months ago"), - view_count: Some(1654055), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: None, - )), - Video(VideoItem( - id: "qxI-Ob8lpLE", - name: "Schlatt\'s Chips Tier List", - duration: Some(1071), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/qxI-Ob8lpLE/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBtEO5eB17tODb5Ek9GRoQwwVGtvA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/qxI-Ob8lpLE/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAwDt0sa98qoI5O8u0kHJY7FbTrZg", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UC2mP7il3YV7TxM_3m6U0bwA", - name: "jschlattLIVE", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/Rr0aOvzRYLCyIDtIhIgkAYdQeagRlGDPzRuWoLrwGakM4VdnHPZHeSfUbiV-pJKmFbJ8LL9r5g=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("1 year ago"), - view_count: Some(9029628), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Schlatt ranks every chip ever made.\nCREATE YOUR OWN TIER LIST: https://tiermaker.com/create/chips-for-big-guy-1146620\n\nSubscribe to me on Twitch:\nhttps://twitch.tv/jschlatt\n\nFollow me on Twitter:..."), - )), - Video(VideoItem( - id: "qmrzTUmZ4UU", - name: "850€ für den Verrat am System - UCS AT-AT LEGO® Star Wars 75313", - duration: Some(2043), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/qmrzTUmZ4UU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAsI3VS-wxnt1s_zS4M_YbVrV1pAg", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/qmrzTUmZ4UU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBYk7w0qGeW4kZchFr-tbydELUChQ", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UC_EZd3lsmxudu3IQzpTzOgw", - name: "Held der Steine Inh. Thomas Panke", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu8g9hFxZ2HD4P9pDsUxoAvkHwbZoTVNr3yw12i8YA=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("6 days ago"), - view_count: Some(600516), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Star Wars - erschienen 2021 - 6749 Teile\n\nDieses Set bei Amazon*:\nhttps://amzn.to/3yu9dHX\n\nErwähnt im Video*:\nTassen https://bit.ly/HdSBausteinecke\nBig Boy https://bit.ly/BBLokBigBoy\nBurg..."), - )), - Video(VideoItem( - id: "4q4vpQCIZ6w", - name: "🌉 Manhattan Jazz 💖 l Relaxing Jazz Piano Music l Background Music", - duration: Some(23229), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/4q4vpQCIZ6w/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD4DKjgt5VJBRX2pH_KzI4Ru9AMaQ", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/4q4vpQCIZ6w/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDMm9yeUF-9LH2rhU7jaQ6td05cMg", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCBnMxlW70f0SB4ZTJx124lw", - name: "몽키비지엠 MONKEYBGM", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/x8_XLvrLdd-Cs6z7Cmob2eZmqvbzmYdOdf6b7jLMry1z1YhdExnuqEhwRrYveu4X2airLfbv=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("6 months ago"), - view_count: Some(2343407), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("- Please Subscribe!\n\n🔺Disney OST Collection part 1 \n ➡\u{fe0f} https://youtu.be/lrzKFu85nhE\n\n🔺Disney OST Collection part 2 \n ➡\u{fe0f} https://youtu.be/EtE09lowIbk\n\n🔺Studio Ghibli..."), - )), - Video(VideoItem( - id: "Z_k31kqZxaE", - name: "1 in 1,000,000 NBA Moments", - duration: Some(567), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/Z_k31kqZxaE/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCovxnIKW7TCP3XBcG4x-Acw10OBA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/Z_k31kqZxaE/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBa52Ie0cfnzg44jnkfTGzrCsVfOw", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCpyoYVlp67N16Lg1_N4VnVw", - name: "dime", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/HwpHaCaatHTI3N1imp5ZszL8_raSsxBq60UHScSpXC6e6VySeOlZ8Y3msYgum4vzCH5jmCxLvEU=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("1 month ago"), - view_count: Some(4334298), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("• Instagram - https://instagram.com/dime_nba\n• TikTok - https://tiktok.com/@dime_nba\n\ndime is a Swedish brand, founded in 2022. We produce some of the most entertaining NBA content on YouTube..."), - )), - Video(VideoItem( - id: "zE-a5eqvlv8", - name: "Dua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me", - duration: None, - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/zE-a5eqvlv8/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAbIAO-SIuWTC9f2AKu6Yp9nB0BwQ", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/zE-a5eqvlv8/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDHdbRp6yOt4qkQk31BoFv6keTBYQ", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCX-USfenzQlhrEJR1zD5IYw", - name: "Deep Mood.", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/8WO05hff9bGjmlyPFo_PJRMIfHEoUvN_KbTcWRVX2yqeUO3fLgkz0K4MA6W95s3_NKdNUAwjow=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: None, - view_count: Some(889), - is_live: true, - is_short: false, - is_upcoming: false, - short_description: Some("#Summermix #DeepHouse #DeepHouseSummerMix\nDua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me\n\n🎵 All songs in this spotify playlist: https://spoti.fi/2TJ4Dyj\nSubmit..."), - )), - Video(VideoItem( - id: "gNlOk0LXi5M", - name: "Soll ich dir 1g GOLD schenken? oder JEMAND anderen DOPPELT?", - duration: Some(704), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/gNlOk0LXi5M/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAy3JbiDcqUTwF6NS69UnX715q90w", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/gNlOk0LXi5M/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDICPl-Jsul5nnhrac2s01gueUCDA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCqcWNPTUVATZt0Dlr2jV0Wg", - name: "Mois", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/uHDIV2MwZnJRX8guX2KfFr4-gdxXK5x9nH0tz456hcBn0DH7LurNQbkAPjP5tSKg1Tqu07y9nKw=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("8 days ago"), - view_count: Some(463834), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Je mehr Menschen mich abonnieren desto mehr Menschen werde ich glücklich machen \n\n24 std ab, viel Glück \n\nhttps://I-Clip.com/?sPartner=Mois"), - )), - Video(VideoItem( - id: "dbMvZjs8Yc8", - name: "Brad Pitt- Die Revanche eines Sexsymbols | Doku HD | ARTE", - duration: Some(3137), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/dbMvZjs8Yc8/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB6HnYSCQFmEQ1V5qlFf5fblOpv-g", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/dbMvZjs8Yc8/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD-AoMr1H_6EvzuWvg2whMDmbtY4A", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCsygZtQQSplGF6JA3XWvsdg", - name: "Irgendwas mit ARTE und Kultur", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu9_FXs7hsEndpcy9C4D_ZsM1xZzbLLThDQIL4-Dxg=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("5 days ago"), - view_count: Some(293878), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Vom „People“-Magazin wurde er mehrfach zum „Sexiest Man Alive“ gekrönt. Aber sein Aussehen ist nicht alles: In 30 Jahren Karriere drehte Brad Pitt eine Vielzahl herausragender Filme...."), - )), - Video(VideoItem( - id: "mFxi3lOAcFs", - name: "Craziest Soviet Machines You Won\'t Believe Exist - Part 1", - duration: Some(1569), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/mFxi3lOAcFs/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCgPz_lsa3ENFNi2sC_uraWrUIuBQ", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/mFxi3lOAcFs/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA2u97RbHNrNVp_Cb5m0DSvA0P02g", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCkQO3QsgTpNTsOw6ujimT5Q", - name: "BE AMAZED", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu_vmgpzJxLlR_1RA68cz8iITuzYLFFbPBvg5ULJlQ=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("1 year ago"), - view_count: Some(14056843), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Coming up are some crazy Soviet-era machines you won\'t believe exist!\nPart 2: https://youtu.be/MBZVOJrhuHY\nSuggest a topic here to be turned into a video: http://bit.ly/2kwqhuh\nSubscribe for..."), - )), - Video(VideoItem( - id: "eu7ubm7g59E", - name: "People Hated Me For Using This Slab", - duration: Some(1264), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/eu7ubm7g59E/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCg_b-6U2Pux_tZqAY8jkIa1JoTew", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/eu7ubm7g59E/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA9WwjUr_EpS3PPYNG3e4N8EEr9oA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UC6I0KzAD7uFTL1qzxyunkvA", - name: "Blacktail Studio", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu8jg6Uevc1qmfbksQ_xdJ0dF37PmZVFHkyNhouBTA=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("3 months ago"), - view_count: Some(2845035), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Some people were furious I used this slab, and I actually understand why. \nBlacktail bow tie jig (limited first run): https://www.blacktailstudio.com/bowtie-jig\nBlacktail epoxy table workshop:..."), - )), - Video(VideoItem( - id: "TRGHIN2PGIA", - name: "Christian Bale Breaks Down His Most Iconic Characters | GQ", - duration: Some(1381), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/TRGHIN2PGIA/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAMxhmIbADGzAlH1jNl6RN-ZU0eEQ", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/TRGHIN2PGIA/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDxo3aBHktmxUOEuSdXJVHmlcR4-Q", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCsEukrAd64fqA7FjwkmZ_Dw", - name: "GQ", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu-gTmA2HcJO9Y5kYl4IUKG-jZ8QtojL8qaQiyW9kA=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("9 days ago"), - view_count: Some(8044465), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Christian Bale breaks down a few of his most iconic characters from \'American Psycho,\' \'The Dark Knight\' Trilogy, \'The Fighter,\' \'The Machinist,\' \'The Big Short,\' \'Vice,\' \'Empire of the Sun,\'..."), - )), - Video(VideoItem( - id: "w3tENzcssDU", - name: "NFL Trick Plays But They Get Increasingly Higher IQ", - duration: Some(599), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/w3tENzcssDU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCZHp6o6cV9HNNJXPlI1FKi6S58qg", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/w3tENzcssDU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBH4K8b0AfAgX0MvL4oHlbianG8xQ", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCJka5SDh36_N4pjJd69efkg", - name: "Savage Brick Sports", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu_s0H6HPGb4LYTxkE6fH1Cp5Mp8jfeOaMluW2A03Q=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("3 months ago"), - view_count: Some(1172372), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("NFL Trick Plays But They Get Increasingly Higher IQ\nCredit to CoshReport for starting this trend.\n\n(if any of the links don\'t work, check most recent video)\nTalkSports Discord: https://discord.gg/n..."), - )), - Video(VideoItem( - id: "gUAd2XXzH7w", - name: "⚓\u{fe0f}Found ABANDONED SHIP!!! Big CRUISE SHIP on a desert island☠\u{fe0f} Where did the people go?!?", - duration: Some(2949), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/gUAd2XXzH7w/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDaBSyUxw88zjCr_Az868dEnhMrug", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/gUAd2XXzH7w/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAvfP1QR12y5cY8mvtg7Qqvl2XuTA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UClUZos7yKYtrmr0-azaD8pw", - name: "Kreosan English", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/Rzi1oOWYL20M028wSLcD4eEkByC7kWGcBpr6WBAx0aGC9UAlIcGB_-D4rI_wkMsOHe9VnRWL3Q=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("1 month ago"), - view_count: Some(1883533), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("We are preparing a continuation of the cruise ship for you! Very soon you will be able to see the next part. If you would like to help us make a video:\n\n► Support us - https://www.patreon.com/k..."), - )), - Video(VideoItem( - id: "YpGjaJ1ettI", - name: "[Working BGM] Comfortable music that makes you feel positive -- Morning Mood -- Daily Routine", - duration: Some(3651), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/YpGjaJ1ettI/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDjAMJifo4Bg-vXUdHXyWYRHSf-Sw", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/YpGjaJ1ettI/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAx95bizFu4fxePN4qbMdKIoNDCug", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCpxY9-3iB5Hyho31uBgzh7w", - name: "Daily Routine", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/uci2aPM5XOEgdMt2h9aHMiN-K1-TmJQQPRdWvprNrpJpyZSLI9z0zFzyXQeQ1mNIQWl2QrjX3Rc=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("2 months ago"), - view_count: Some(1465389), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Hello everyone. It\'s me again. I will stay at home and study . It\'s full of fun energy today, so it\'s ready to spread to everyone with hilarious music. 🔥🔥🔥\nHave fun together 😊😊😊..."), - )), - Video(VideoItem( - id: "rPAhFD8hKxQ", - name: "Survival Camping 9ft/3m Under Snow - Giant Winter Bushcraft Shelter and Quinzee", - duration: Some(1301), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/rPAhFD8hKxQ/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCY0Xhznr6RKZ-EG1G5C1M34h8ugA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/rPAhFD8hKxQ/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBiANoEaNfk7eMjCAxapIK5NiYmmQ", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCfpCQ89W9wjkHc8J_6eTbBg", - name: "Outdoor Boys", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu8v_ZMJTqxqU7M__w8nHHaygAyOvsqCnFeIhjQxFw=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("6 months ago"), - view_count: Some(20488431), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Solo winter camping and bushcraft 9 feet (3 meters) under the snow. I hiked high up into the mountains during a snow storm with 30 mph/48 kmh winds to build a deep snow bushcraft survival shelter..."), - )), - Video(VideoItem( - id: "2rye4u-cCNk", - name: "Pink Panther Fights Off Pests | 54 Minute Compilation | The Pink Panther Show", - duration: Some(3158), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/2rye4u-cCNk/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCi4Tt2tz-kk-cumb7SEfzzgixj5A", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/2rye4u-cCNk/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD4QbHfCufvmol1UNj5wqmOtjZNvw", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCFeUyPY6W8qX8w2o6oSiRmw", - name: "Official Pink Panther", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu-htKBt4jUDwmnm0r-ojGjHZMy9-H92Q1pRoAfkgw=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("11 months ago"), - view_count: Some(27357653), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("(1) Pink Pest Control\n(2) Pink-a-Boo\n(3) Little Beaux Pink\n(4) The Pink Package Plot\n(5) Come On In! The Water\'s Pink\n(6) Psychedelic Pink\n(7) Pink Posies\n(8) G.I. Pink\n\nThe Pink Panther is..."), - )), - Video(VideoItem( - id: "O0xAlfSaBNQ", - name: "FC Nantes vs. SC Freiburg – Highlights & Tore | UEFA Europa League", - duration: Some(326), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/O0xAlfSaBNQ/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDe-1NUODMNivJw5r5J5Wd16PMsqA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/O0xAlfSaBNQ/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAMD0BFcC-x_UYe-F5q5y4GPcGnWA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UC8WYi3XQXsf-6FNvqoEvxag", - name: "RTL Sport", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/E1ZL4Cnc8ej3MeHR0To12hetHWrlhcupsz0nFyZmEJoWvLvJo9aOXvPOWmNMWn9tJLoMB3duRg=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("11 hours ago"), - view_count: Some(117395), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("UEFA Europa League: https://www.rtlplus.com/shows/uefa-europa-league-19818?utm_source=youtube&utm_medium=editorial&utm_campaign=beschreibung&utm_term=rtlsport \nFC Nantes vs. SC Freiburg –..."), - )), - Video(VideoItem( - id: "Mhs9Sbnw19o", - name: "Dramatisches Duell: 400 Jahre altes Kästchen erzielt zig-fachen Wunschpreis! | Bares für Rares XXL", - duration: Some(744), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/Mhs9Sbnw19o/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBkxXdE8JNS0S6_Dhl-aY7FRmbL9g", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/Mhs9Sbnw19o/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAqbRhx4fQfK_2mVGNX_0_dZQt0YQ", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UC53bIpnef1pwAx69ERmmOLA", - name: "Bares für Rares", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu-ZyE4lblLYyk8iis1xoH_v64_tmhWca2Z6wmsVexk=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("11 days ago"), - view_count: Some(836333), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Du hast Schätze im Keller, die du unseren Expert*innen präsentieren möchtest? Hier geht\'s zum Bewerbungsformular: kurz.zdf.de/lSJ/\n\nEin einmaliges Bieterduell treibt den Preis für dieses..."), - )), - Video(VideoItem( - id: "Bzzp5Cay7DI", - name: "Sweet Jazz - Cool autumn Bossa Nova & October Jazz Positive Mood", - duration: None, - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/Bzzp5Cay7DI/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAKcYaDyG1yocH1e2_BIyl5FGKWPw", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/Bzzp5Cay7DI/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBOaXPCJec4XuaFyJ1-6dcnJWEmrg", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCoGlllJE7aYe_VzIGP3s_wA", - name: "Smooth Jazz Music", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/babJ-iwY1cNs3mE2CnDiBSf0IjePgGuCLNLvLGcepj6tzXNLbSAQA7rQho35fKv9qFxEVIWdCw=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: None, - view_count: Some(1216), - is_live: true, - is_short: false, - is_upcoming: false, - short_description: Some("Sweet Jazz - Cool autumn Bossa Nova & October Jazz Positive Mood\nhttps://youtu.be/Bzzp5Cay7DI\n********************************************\nSounds available on: Jazz Bossa Nova\nOFFICIAL VIDEO:..."), - )), - Video(VideoItem( - id: "SlskTqc9CEc", - name: "The Chick-Fil-A Full Menu Challenge", - duration: Some(613), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/SlskTqc9CEc/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBjDpJq0J5r8jvLwIQG2HCvsoj8nw", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/SlskTqc9CEc/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCnwo-jiD8xsP29kf6a5jMwIqHPEA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCd1fLoVFooPeWqCEYVUJZqg", - name: "Matt Stonie", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu9Of1-RwNeaBY6nulF3DECzDcAdZRbC_aOvZHPedw=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("3 years ago"), - view_count: Some(39286403), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Good Video? Like/Fav & Share!!\n\nTBH this is really my 1st time trying Chick-Fil-A, legitimately. My verdict is torn, but that sauce is BOMB!\n\nChallenge\n+ Chick-Fil-A Deluxe\n+ Spicy Deluxe\n+..."), - )), - Video(VideoItem( - id: "CwRvM2TfYbs", - name: "Gentle healing music of health and to calm the nervous system, deep relaxation! Say Life Yes", - duration: None, - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/CwRvM2TfYbs/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCj3HTq1K0KCuiuZdyh_by4VUZWeA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/CwRvM2TfYbs/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA-rjU_R19afFlCk22vmfHEtfFKcA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UC6jH5GNi0iOR17opA1Vowhw", - name: "Lucid Dream", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/QlTKeA9Cx-4qajm4VaLGGGH0cCVe8Fda_c6SScCLPy8fsu0ZQkDhtBB3qcZastIZPQNew5vi-LM=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: None, - view_count: Some(1416), - is_live: true, - is_short: false, - is_upcoming: false, - short_description: Some("🌿 Music for relaxation, meditation, study, reading, massage, spa or sleep. This music is ideal for dealing with anxiety, stress or insomnia as it promotes relaxation and helps eliminate..."), - )), - Video(VideoItem( - id: "7jz0pXSe_kI", - name: "Craziest \"Fine...I\'ll Do it Myself\" Moments in Sports History (PART 2)", - duration: Some(1822), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/7jz0pXSe_kI/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDEUQzJHcD0s2BgP1znPupwsxf48w", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/7jz0pXSe_kI/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB1yzi-24jCXlAki1xIq0aDMqQY3A", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCd5hdemikI6GxwGKhJCwzww", - name: "Highlight Reel", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/NETjJS3cNlblrg70CD4LH_Mma5lYmZSO3NlUnzi5Vd_cRD3XkVyaO1UCFTq6acK52g9XDly9-A=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("10 months ago"), - view_count: Some(11601863), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("(PART 2) of 👉🏼 Craziest \"Fine...I\'ll Do It Myself\" Moments in Sports History \n\nBIBLE VERSE OF THE DAY: Luke 12:40"), - )), - ], - ctoken: Some("4qmFsgKxAxIPRkV3aGF0X3RvX3dhdGNoGoADQ0RCNmxnSkhUWFpRYzJOVU1UUm1iME5OWjNOSmQzWjZOM0JPWlZWMldqZDFRVlp3ZEVOdGMwdEhXR3d3V0ROQ2FGb3lWbVpqTWpWb1kwaE9iMkl6VW1aamJWWnVZVmM1ZFZsWGQxTklNVlUwVDFSU1dXUXhUbXhXTTBaeVdsaGtSRkpGYkZCWk0yaDZWbXMxTlV4VmVGbE1XRnBSVlcxallVeFJRVUZhVnpSQlFWWldWRUZCUmtWU1VVRkNRVVZhUm1ReWFHaGtSamt3WWpFNU0xbFlVbXBoUVVGQ1FVRkZRa0ZCUVVKQlFVVkJRVUZGUWtGSFNrSkRRVUZUUlROQ2FGb3lWbVpqTWpWb1kwaE9iMkl6VW1aa1J6bHlXbGMwWVVWM2Ftb3hPRkJGT1dWSU5rRm9WVlpZWlVGTFNGaHVSMEp2ZDJsRmQycERObkZmUlRsbFNEWkJhRmRIZG1RMFMwaGxaMGhDTlZnMmJrMWxPVU5SU1VsTlVRJTNEJTNEmgIaYnJvd3NlLWZlZWRGRXdoYXRfdG9fd2F0Y2g%3D"), - endpoint: browse, -) diff --git a/src/client/snapshots/rustypipe__client__trends__tests__map_startpage.snap b/src/client/snapshots/rustypipe__client__trends__tests__map_startpage.snap deleted file mode 100644 index a6ec599..0000000 --- a/src/client/snapshots/rustypipe__client__trends__tests__map_startpage.snap +++ /dev/null @@ -1,784 +0,0 @@ ---- -source: src/client/trends.rs -expression: map_res.c ---- -Paginator( - count: None, - items: [ - VideoItem( - id: "_cyJhGsXDDM", - name: "Ultimate Criminal Canal Found Magnet Fishing! Police on the Hunt", - duration: Some(1096), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/_cyJhGsXDDM/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBBz_ErMMfhKLRZRfcAPTlMTujziw", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/_cyJhGsXDDM/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDaUGJ6GyTv5vwllztR6mN43dlmxA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCMLXec9-wpON8tZegnDsYLw", - name: "Bondi Treasure Hunter", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu91VHy_3HvCaMLthYyMSol6zwqxebNQ9GXc7NUB=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("1 day ago"), - view_count: Some(700385), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Subscribe for more Treasure Hunting videos: https://tinyurl.com/yyl3zerk\n\nMy Magnet! (Use Discount code \'BONDI\'): https://magnetarmagnets.com/\nMy Dive System! (Use Bonus code \'BONDI\'): https://lddy..."), - ), - VideoItem( - id: "36YnV9STBqc", - name: "The Good Life Radio\u{a0}•\u{a0}24/7 Live Radio | Best Relax House, Chillout, Study, Running, Gym, Happy Music", - duration: None, - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/36YnV9STBqc/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLASUZkzmRJDiyIJmcsAdcDGan805Q", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/36YnV9STBqc/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBDrl0k5nr9wH-_aosqOimodx0b-w", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UChs0pSaEoNLV4mevBFGaoKA", - name: "The Good Life Radio x Sensual Musique", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu_V9mOdHaorjNFqGXCecFeOBZhDWB8tVYG_I8gJwA=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: None, - view_count: Some(7202), - is_live: true, - is_short: false, - is_upcoming: false, - short_description: Some("The Good Life is live streaming the best of Relaxing & Chill House Music, Deep House, Tropical House, EDM, Dance & Pop as well as Music for Sleep, Focus, Study, Workout, Gym, Running etc. in..."), - ), - VideoItem( - id: "YYD1qgH5qC4", - name: "چند شنبه با سینــا | فصل چهـارم | قسمت 5 | با حضور نازنین انصاری مدیر روزنامه کیهان لندن", - duration: Some(3261), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/YYD1qgH5qC4/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBkvD-kVL12hteMVVLRZvJHOdlPzQ", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/YYD1qgH5qC4/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDpO5WCJiLDPHrXOWH-xk2hTG_S3A", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCzH_7hfL6Jd1H0WpNO_eryQ", - name: "MBC PERSIA", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu9lP4dhb_R_Y7e8Q4sb6dj7ve-YtalnMd2t1qP05A=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("14 hours ago"), - view_count: Some(104344), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("#mbcpersia\n#chandshanbeh\n#چندشنبه\n\nشبكه ام بى سى پرشيا را از حساب هاى مختلف در شبكه هاى اجتماعى دنبال كنيد\n►MBCPERSIA on Facebook:..."), - ), - VideoItem( - id: "BeJqgI6rw9k", - name: "your city is full of fake buildings, here\'s why", - duration: Some(725), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/BeJqgI6rw9k/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAvkJGHa6h2vzXrG1ueGQA8JysqEg", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/BeJqgI6rw9k/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDEJWMD2gUA572p12E7fZ1VX8qJ3A", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCqVEHtQoXHmUCfJ-9smpTSg", - name: "Answer in Progress", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/b4TIQdFmoHYvQmcMt1XGH40m8-P5VdjyaZKb2C6nmkezGVk2Ln1csqe1PWg5aefEyk-NEFWhzg=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("7 days ago"), - view_count: Some(1447008), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Save 33% on your first Native Deodorant Pack - normally $39, you’ll get it for $26! Click here https://bit.ly/nativeanswer1 and use my code ANSWER #AD\n\nSomewhere on your street there may..."), - ), - VideoItem( - id: "ma28eWd1oyA", - name: "Post Malone, Maroon 5, Adele, Taylor Swift, Ed Sheeran, Shawn Mendes, Pop Hits 2020 Part 6", - duration: Some(29989), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/ma28eWd1oyA/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCznoPDMo_F1NCRBWoD4Ps5IjctxQ", - width: 480, - height: 270, - ), - ], - channel: Some(ChannelTag( - id: "UCldQuUMYTUGrjvcU2vaPSFQ", - name: "Music Library", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu-4BJEmOMTfX96bjwu9AQS02gbODk5YQpZWVi5P=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("Streamed 2 years ago"), - view_count: Some(1861814), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Post Malone, Maroon 5, Adele, Taylor Swift, Ed Sheeran, Shawn Mendes, Charlie Puth Pop Hits 2020\nPost Malone, Maroon 5, Adele, Taylor Swift, Ed Sheeran, Shawn Mendes, Charlie Puth Pop Hits..."), - ), - VideoItem( - id: "mL2LBRM5GBI", - name: "Salahs 6-Minuten-Hattrick & Firmino-Gala: Rangers - FC Liverpool 1:7 | UEFA Champions League | DAZN", - duration: Some(355), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/mL2LBRM5GBI/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBhsDaEALJodPurmS3DywUoRRwzwg", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/mL2LBRM5GBI/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDkvWkbocujg95phnyfNzBB9dhEYA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCB-GdMjyokO9lZkKU_oIK6g", - name: "DAZN UEFA Champions League", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu-D8LIEj-klO1gvUWMOA987HqMBBX9nn_WJS9Ka=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("2 days ago"), - view_count: Some(1471667), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("In der Liga läuft es für die Reds weiterhin nicht rund. Am vergangenen Spieltag gab es gegen Arsenal eine 2:3-Niederlage, am Sonntag trifft man auf Man City. Die Champions League soll für..."), - ), - VideoItem( - id: "Ang18qz2IeQ", - name: "Satisfying Videos of Workers Doing Their Job Perfectly", - duration: Some(1186), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/Ang18qz2IeQ/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA3Cd49wYUuSEXz2MwhO2aqCMq5ZA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/Ang18qz2IeQ/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAWQAks0vkJyJXSiQFIs9zhc2qyTg", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCYenDLnIHsoqQ6smwKXQ7Hg", - name: "#Mind Warehouse", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu8zB2zV3yx2fSYn5zDbv47rZCBr90wX3jW8EC6NBw=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("2 days ago"), - view_count: Some(173121), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("TechZone ► https://goo.gl/Gj3wZs \n\n #incrediblemoments #mindwarehouse #IncredibleMoments #CaughtOnCamera #InterestingFacts \n\nYou can endlessly watch how others work, but in this selection,..."), - ), - VideoItem( - id: "fjHN4jsJnEU", - name: "I Made 200 Players Simulate Survival Island in Minecraft...", - duration: Some(2361), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/fjHN4jsJnEU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDwTosIfmAhNHIzU1sSXrTKT8vjNQ", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/fjHN4jsJnEU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA4aFygGqUcm7-Hrkys95U0EAV9xA", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCqt4mmAqLmH-AwXz31URJsw", - name: "Sword4000", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu_q3--WCh9Oc5o4XxAVVxxUz2narAtLR2QKuEw2lQ=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("7 days ago"), - view_count: Some(751909), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("200 Players Simulate Survival Island Civilizations in Minecraft...\n-------------------------------------------------------------------\nI invited 200 Players to a Survival Island and let them..."), - ), - VideoItem( - id: "FI1XrdBJIUI", - name: "Epic Construction Fails | Expensive Fails Compilation | FailArmy", - duration: Some(631), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/FI1XrdBJIUI/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBe2jCnLhTsXmZQefyAe-WqImk6-g", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/FI1XrdBJIUI/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD01TnIh1pH7TObDgKzx0GupXXVzw", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCPDis9pjXuqyI7RYLJ-TTSA", - name: "FailArmy", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/PLsX6LIg5JbMJR9v7eTD7nQOPmZN16_X7h_uACw5qeWLAewiNfasZFsxQ48Dn8wZ_4McKUPZSA=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("2 days ago"), - view_count: Some(2226471), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("I don\'t think so, Tim. ►►► Submit your videos for the chance to be featured 🔗 https://www.failarmy.com/pages/submit-video ▼ Follow us for more fails! https://linktr.ee/failarmy\n#fails..."), - ), - VideoItem( - id: "MXdplejK8vU", - name: "Chilly autumn Jazz ☕ Smooth September Jazz & Bossa Nova for a great relaxing weekend", - duration: Some(86403), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/MXdplejK8vU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAIOe93l-1elIK0DfMLk0f3nDWgSA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/MXdplejK8vU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLByGLefQ3I9p2VQ5oZDmc5G_pCTlQ", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCeGJ6v6KQt0s88hGKMfybuw", - name: "Cozy Jazz Music", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/tU7x6wNqEM_OIeU-jaaPcdhX3adNhnAY7WaGHsjEMfTLSzVHxm8VVBfaXRjDbf3y_LftGNJ83A=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("1 month ago"), - view_count: Some(148743), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Chilly autumn Jazz ☕ Smooth September Jazz & Bossa Nova for a great relaxing weekend\nhttps://youtu.be/MXdplejK8vU\n*******************************************\nSounds available on: Jazz Bossa..."), - ), - VideoItem( - id: "Jri4_9vBFiQ", - name: "Top 100 Best Classic Rock Songs Of All Time 🔥 R.E.M, Queen, Metallica,Guns N’ Roses,Bon Jovi, U2,CCR", - duration: None, - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/Jri4_9vBFiQ/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA1ZqDfSLi3Mf5qvpUFSYyDIODNQw", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/Jri4_9vBFiQ/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDtwgV7RdHmgDlAESZqSYbuZtFrvw", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCiIWdzEVNH8okhlapR9a-xA", - name: "Rock Music", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/QIEcTVdBg9A2kE3un-IfjgTPiglDGMBbh9vMSXo2J5ZRICmunnVQkfpbMWNP8Kueac09DZrn=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: None, - view_count: Some(192), - is_live: true, - is_short: false, - is_upcoming: false, - short_description: Some("Top 100 Best Classic Rock Songs Of All Time 🔥 R.E.M, Queen, Metallica,Guns N’ Roses,Bon Jovi, U2,CCR\nTop 100 Best Classic Rock Songs Of All Time 🔥 R.E.M, Queen, Metallica,Guns N’..."), - ), - VideoItem( - id: "ll4d5Lt-Ie8", - name: "Relaxing Music Healing Stress, Anxiety and Depressive States Heal Mind, Body and Soul | Sleep music", - duration: Some(42896), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/ll4d5Lt-Ie8/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAqdY2bQaQ3JHl5FYoTPuZFxXRKIQ", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/ll4d5Lt-Ie8/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA6xc8r38_2ygARU0vOR4kI6ZNz5w", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCNS3dqFGBPhxHmOigehpBeg", - name: "Love YourSelf", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/fkgfEL2OtY2mhhyCV3xSOc3OsVK5ylQJmBev7XlBGE548dM6dqS2Z66YF-pdnbQOQpCuvZOlAdk=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("Streamed 5 months ago"), - view_count: Some(5363904), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("The study found that listening to relaxing music of the patient\'s choice resulted in \"significant pain relief and increased mobility.\" Researchers believe that music relieves pain because listening..."), - ), - VideoItem( - id: "Dx2wbKLokuQ", - name: "W. Putin: Die Sehnsucht nach dem Imperium | Mit offenen Karten | ARTE", - duration: Some(729), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/Dx2wbKLokuQ/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBHQXnaEYo6frjkJ3FFuAPkAyOCKQ", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/Dx2wbKLokuQ/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDFtWV_wy25ohVyBthH8a5HwSj6Kw", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCLLibJTCy3sXjHLVaDimnpQ", - name: "ARTEde", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu-1i2jxeXFISJhBbpWWv5vVX2xE5yQbjpaZZP3HPg=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("2 weeks ago"), - view_count: Some(539838), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Jede Woche untersucht „Mit offenen Karten“ die politischen Kräfteverhältnisse in der ganzen Welt anhand detaillierter geografischer Karten \n\nIm Februar 2022 rechtfertigte Wladimir Putin..."), - ), - VideoItem( - id: "jfKfPfyJRdk", - name: "lofi hip hop radio - beats to relax/study to", - duration: None, - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/jfKfPfyJRdk/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCR-bHqcvOP14sSUsNt9PTuf3ZI4Q", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/jfKfPfyJRdk/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBBVEQQnwSLJFllntNgv2JAAlvSMQ", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCSJ4gkVC6NrvII8umztf0Ow", - name: "Lofi Girl", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/KNYElmLFGAOSZoBmxYGKKXhGHrT2e7Hmz3WsBerbam5uaDXFADAmT7htj3OcC-uK1O88lC9fQg=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: None, - view_count: Some(21262), - is_live: true, - is_short: false, - is_upcoming: false, - short_description: Some("🤗 Thank you for listening, I hope you will have a good time here\n\n💽 | Get the latest vinyl (limited edition)\n→ https://vinyl-lofirecords.com/\n\n🎼 | Listen on Spotify, Apple music..."), - ), - VideoItem( - id: "qmrzTUmZ4UU", - name: "850€ für den Verrat am System - UCS AT-AT LEGO® Star Wars 75313", - duration: Some(2043), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/qmrzTUmZ4UU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAsI3VS-wxnt1s_zS4M_YbVrV1pAg", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/qmrzTUmZ4UU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBYk7w0qGeW4kZchFr-tbydELUChQ", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UC_EZd3lsmxudu3IQzpTzOgw", - name: "Held der Steine Inh. Thomas Panke", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu8g9hFxZ2HD4P9pDsUxoAvkHwbZoTVNr3yw12i8YA=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("6 days ago"), - view_count: Some(600150), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Star Wars - erschienen 2021 - 6749 Teile\n\nDieses Set bei Amazon*:\nhttps://amzn.to/3yu9dHX\n\nErwähnt im Video*:\nTassen https://bit.ly/HdSBausteinecke\nBig Boy https://bit.ly/BBLokBigBoy\nBurg..."), - ), - VideoItem( - id: "t0Q2otsqC4I", - name: "Tom & Jerry | Tom & Jerry in Full Screen | Classic Cartoon Compilation | WB Kids", - duration: Some(1298), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/t0Q2otsqC4I/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCFcrz2zM6mPUmJiCsC7c7suOzSug", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/t0Q2otsqC4I/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCVANFKKXmrdehkf7aM9issiuph5A", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UC9trsD1jCTXXtN3xIOIU8gg", - name: "WB Kids", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu80jIF6oehgpUILTaUbqSM5xYHWbPoc_Bz7wddxzg=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: Verified, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("10 months ago"), - view_count: Some(252381571), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("Did you know that there are only 25 classic Tom & Jerry episodes that were displayed in a widescreen CinemaScope from the 1950s? Enjoy a compilation filled with some of the best moments from..."), - ), - VideoItem( - id: "zE-a5eqvlv8", - name: "Dua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me", - duration: None, - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/zE-a5eqvlv8/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCDyvujcpz62sEsL9Ke4ADBpXWqOA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/zE-a5eqvlv8/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCyJ-QdgAD1F-DqcLKivIcalBJOEg", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCX-USfenzQlhrEJR1zD5IYw", - name: "Deep Mood.", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/8WO05hff9bGjmlyPFo_PJRMIfHEoUvN_KbTcWRVX2yqeUO3fLgkz0K4MA6W95s3_NKdNUAwjow=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: None, - view_count: Some(955), - is_live: true, - is_short: false, - is_upcoming: false, - short_description: Some("#Summermix #DeepHouse #DeepHouseSummerMix\nDua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me\n\n🎵 All songs in this spotify playlist: https://spoti.fi/2TJ4Dyj\nSubmit..."), - ), - VideoItem( - id: "HxCcKzRAGWk", - name: "(Music for Man ) Relaxing Whiskey Blues Music - Modern Electric Guitar Blues - JAZZ & BLUES", - duration: Some(42899), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/HxCcKzRAGWk/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD5CNX5XaQAKrLpPq0nxmyUjP5yUw", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/HxCcKzRAGWk/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLANuDaGE9jI_-go6cS_nU3qCu6LRg", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCGr-rTYtP1m-r_-ncspdVQQ", - name: "JAZZ & BLUES", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/zqAxVISjt1hyzRzZKxRTvJfgEc5k2Luf-aEE55ohjUvt0QvqIRvmFBNC6UKj2TxlZrzGo8QMNA=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("Streamed 3 months ago"), - view_count: Some(3156236), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("-----------------------------------------------------------------------------------\n✔Thanks for watching! Have a nice day!\n✔Don\'t forget LIKE - SHARE - COMMENT\n#bluesmusic#slowblues#bluesrock..."), - ), - VideoItem( - id: "HlHYOdZePSE", - name: "Healing Music for Anxiety Disorders, Fears, Depression and Eliminate Negative Thoughts", - duration: None, - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/HlHYOdZePSE/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBeqmmnli6rVdK1k7vcHlwE3kiNaw", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/HlHYOdZePSE/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAk9H5lapp7KBhJCER7uRCr0fDRgg", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCqNYK5QArQRZSIR8v6_FCfA", - name: "Tranquil Music", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/YJUUVEayRZKNtFzWEiYgvxp9XOBw9-ioxiYErE0cNDTYNvkxHBCiuUXse4-a_yaYfSS-GfT-MQ=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: None, - view_count: Some(1585), - is_live: true, - is_short: false, - is_upcoming: false, - short_description: Some("Healing Music for Anxiety Disorders, Fears, Depression and Eliminate Negative Thoughts\n#HealingMusic #RelaxingMusic #TranquilMusic\n__________________________________\nMusic for:\nChakra healing...."), - ), - VideoItem( - id: "CJ2AH3LJeic", - name: "Coldplay Greatest Hits Full Album 2022 New Songs of Coldplay 2022", - duration: Some(7781), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/CJ2AH3LJeic/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLC3A9sBlWQZmFUI9BYe5KzvATqiqw", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/CJ2AH3LJeic/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBaKSeSRdcDjEqQxrAfPaQmDJecvg", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCdK2lzwelugXGhR9SCWuEew", - name: "PLAY MUSIC", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu8fIT4MTyobgM_deRkvcWBMIhKpAeIGfgqqob5p=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("7 months ago"), - view_count: Some(5595965), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\nSubscribe channel for more videos:\n🔔Subscribe: https://bit.ly/2UbIZFv\n⚡Facebook: https://bitly.com.vn/gXDsC..."), - ), - VideoItem( - id: "KJwzKxQ81iA", - name: "Handmade Candy Making Collection / 수제 사탕 만들기 모음 / Korean Candy Store", - duration: Some(3152), - thumbnail: [ - Thumbnail( - url: "https://i.ytimg.com/vi/KJwzKxQ81iA/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCtm3YNbp3mK6RjsACZuz7fs-TUYA", - width: 360, - height: 202, - ), - Thumbnail( - url: "https://i.ytimg.com/vi/KJwzKxQ81iA/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAVzCHCFbAyBRebsCKcSDxaWq0x6A", - width: 720, - height: 404, - ), - ], - channel: Some(ChannelTag( - id: "UCdGwDjTgbSwQDZ8dYOdrplg", - name: "Soon Films 순필름", - avatar: [ - Thumbnail( - url: "https://yt3.ggpht.com/ytc/AMLnZu_eXMJm3sINr84rGTr3aiXD-OZ43aqx4yuNq9wjXw=s68-c-k-c0x00ffffff-no-rj", - width: 68, - height: 68, - ), - ], - verification: None, - subscriber_count: None, - )), - publish_date: "[date]", - publish_date_txt: Some("1 month ago"), - view_count: Some(3127238), - is_live: false, - is_short: false, - is_upcoming: false, - short_description: Some("00:00 Handmade Candy Making\n13:43 Delicate Handmade Candy Making\n28:33 Rainbow Lollipop Handmade Candy Making\n39:10 Cute Handmade Candy Making"), - ), - ], - ctoken: Some("4qmFsgKbAxIPRkV3aGF0X3RvX3dhdGNoGuoCQ0JoNmlBSk5aMjlKYjB0NmVtOWlTR3hxVFRSdlYyMHdTMkYzYjFwbFdGSm1ZMGRHYmxwV09YcGliVVozWXpKb2RtUkdPWGxhVjJSd1lqSTFhR0pDU1daWFZFSXhUbFpuZDFSV09YSldNRlp0WkRCT1JWTlZPV3BsU0U1WFZHNWtiRXhWY0ZSa1ZrSlRXbmh2ZEVGQlFteGlaMEZDVmxaTlFVRlZVa1pCUVVWQlVtdFdNMkZIUmpCWU0xSjJXRE5rYUdSSFRtOUJRVVZCUVZGRlFVRkJSVUZCVVVGQlFWRkZRVmxyUlVsQlFrbFVZMGRHYmxwV09YcGliVVozWXpKb2RtUkdPVEJpTW5Sc1ltaHZWRU5MVDNJeFpuSjROR1p2UTBaU1YwSm1RVzlrVkZWSlN6RnBTVlJEUzA5eU1XWnllRFJtYjBOR1VsZENaa0Z2WkZSVlNVc3hkbkZqZURjd1NrRm5aMW8lM0SaAhpicm93c2UtZmVlZEZFd2hhdF90b193YXRjaA%3D%3D"), - visitor_data: Some("CgtjTXNGWnhNcjdORSiq8qmaBg%3D%3D"), - endpoint: browse, -) diff --git a/src/client/trends.rs b/src/client/trends.rs index a445c91..c271b7d 100644 --- a/src/client/trends.rs +++ b/src/client/trends.rs @@ -2,38 +2,13 @@ use std::borrow::Cow; use crate::{ error::{Error, ExtractionError}, - model::{ - paginator::{ContinuationEndpoint, Paginator}, - VideoItem, - }, - param::Language, + model::VideoItem, serializer::MapResult, }; -use super::{ - response, ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, -}; +use super::{response, ClientType, MapRespCtx, MapResponse, QBrowseParams, RustyPipeQuery}; impl RustyPipeQuery { - /// Get the videos from the YouTube startpage - #[tracing::instrument(skip(self), level = "error")] - pub async fn startpage(&self) -> Result, Error> { - let context = self.get_context(ClientType::Desktop, true, None).await; - let request_body = QBrowse { - context, - browse_id: "FEwhat_to_watch", - }; - - self.execute_request::( - ClientType::Desktop, - "startpage", - "", - "browse", - &request_body, - ) - .await - } - /// Get the videos from the YouTube trending page #[tracing::instrument(skip(self), level = "error")] pub async fn trending(&self) -> Result, Error> { @@ -55,33 +30,6 @@ impl RustyPipeQuery { } } -impl MapResponse> for response::Startpage { - fn map_response( - self, - ctx: &MapRespCtx<'_>, - ) -> Result>, ExtractionError> { - let grid = self - .contents - .two_column_browse_results_renderer - .contents - .into_iter() - .next() - .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no contents")))? - .tab_renderer - .content - .section_list_renderer - .contents; - - Ok(map_startpage_videos( - grid, - ctx.lang, - self.response_context - .visitor_data - .or_else(|| ctx.visitor_data.map(str::to_owned)), - )) - } -} - impl MapResponse> for response::Trending { fn map_response( self, @@ -109,26 +57,6 @@ impl MapResponse> for response::Trending { } } -fn map_startpage_videos( - videos: MapResult>, - lang: Language, - visitor_data: Option, -) -> MapResult> { - let mut mapper = response::YouTubeListMapper::::new(lang); - mapper.map_response(videos); - - MapResult { - c: Paginator::new_ext( - None, - mapper.items, - mapper.ctoken, - visitor_data, - ContinuationEndpoint::Browse, - ), - warnings: mapper.warnings, - } -} - #[cfg(test)] mod tests { use std::{fs::File, io::BufReader}; @@ -138,32 +66,11 @@ mod tests { use crate::{ client::{response, MapRespCtx, MapResponse}, - model::{paginator::Paginator, VideoItem}, + model::VideoItem, serializer::MapResult, util::tests::TESTFILES, }; - #[test] - fn map_startpage() { - let json_path = path!(*TESTFILES / "trends" / "startpage.json"); - let json_file = File::open(json_path).unwrap(); - - let startpage: response::Startpage = - serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = - startpage.map_response(&MapRespCtx::test("")).unwrap(); - - assert!( - map_res.warnings.is_empty(), - "deserialization/mapping warnings: {:?}", - map_res.warnings - ); - - insta::assert_ron_snapshot!("map_startpage", map_res.c, { - ".items[].publish_date" => "[date]", - }); - } - #[rstest] #[case::base("videos")] #[case::page_header_renderer("20230501_page_header_renderer")] @@ -171,10 +78,10 @@ mod tests { let json_path = path!(*TESTFILES / "trends" / format!("trending_{name}.json")); let json_file = File::open(json_path).unwrap(); - let startpage: response::Trending = + let trending: response::Trending = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - startpage.map_response(&MapRespCtx::test("")).unwrap(); + trending.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/tests/youtube.rs b/tests/youtube.rs index 4e5f656..ca87752 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -1449,18 +1449,6 @@ async fn resolve_channel_not_found(rp: RustyPipe) { //#TRENDS -#[rstest] -#[tokio::test] -#[ignore] -async fn startpage(rp: RustyPipe) { - let startpage = rp.query().startpage().await.unwrap(); - - // The startpage requires visitor data to fetch continuations - assert!(startpage.visitor_data.is_some()); - - assert_next(startpage, rp.query(), 8, 2, true).await; -} - #[rstest] #[tokio::test] async fn trending(rp: RustyPipe) { From 0bf60774040fea8f45e8b3203f684ea05a26f0cc Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 18 Aug 2024 00:51:41 +0200 Subject: [PATCH 9/9] fix: dont store cache in current dir with --report option --- cli/src/main.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index ae173c7..be5ae6a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -19,6 +19,7 @@ use rustypipe::{ Verification, YouTubeItem, }, param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter}, + report::FileReporter, }; use rustypipe_downloader::{ DownloadError, DownloadQuery, DownloadVideo, Downloader, DownloaderBuilder, @@ -535,16 +536,18 @@ async fn run() -> anyhow::Result<()> { .with_writer(ProgWriter(multi.clone())) .init(); + let mut storage_dir = dirs::data_dir().expect("no data dir"); + storage_dir.push("rustypipe"); + std::fs::create_dir_all(&storage_dir).expect("could not create data dir"); + let mut rp = RustyPipe::builder() + .storage_dir(storage_dir) .visitor_data_opt(cli.vdata) .timeout(Duration::from_secs(15)); if cli.report { - rp = rp.report(); - } else { - let mut storage_dir = dirs::data_dir().expect("no data dir"); - storage_dir.push("rustypipe"); - std::fs::create_dir_all(&storage_dir).expect("could not create data dir"); - rp = rp.storage_dir(storage_dir); + rp = rp + .report() + .reporter(Box::new(FileReporter::new("rustypipe_reports"))); } if let Some(lang) = cli.lang { rp = rp.lang(Language::from_str(&lang.to_ascii_lowercase()).expect("invalid language"));