diff --git a/Cargo.toml b/Cargo.toml index 7da8952..9c05861 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", "CHANGELOG.md", "LICENSE", "!snapshots"] +include = ["/src", "README.md", "LICENSE", "!snapshots"] [workspace] members = [".", "codegen", "downloader", "cli"] diff --git a/Justfile b/Justfile index e4d0194..5fd6b75 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,6 @@ test: # cargo test --features=rss - cargo nextest run --workspace --features=rss --no-fail-fast --failure-output final --retries 1 + cargo nextest run --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/README.md b/README.md index 9b5646c..15f6876 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/rustypipe.svg)](https://crates.io/crates/rustypipe) +[![Current crates.io version](https://img.shields.io/crates/v/smartcrop2.svg)](https://crates.io/crates/smartcrop2) [![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) -RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music API -(Innertube), inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). +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 4cb7d8a..e88277e 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,94 +1 @@ -# ![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. +# RustyPipe CLI diff --git a/cli/src/main.rs b/cli/src/main.rs index be5ae6a..96e3925 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -19,7 +19,6 @@ use rustypipe::{ Verification, YouTubeItem, }, param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter}, - report::FileReporter, }; use rustypipe_downloader::{ DownloadError, DownloadQuery, DownloadVideo, Downloader, DownloaderBuilder, @@ -50,13 +49,10 @@ 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, } @@ -97,7 +93,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 and write track information + /// Download only the audio track #[clap(short, long)] audio: bool, /// Number of videos downloaded in parallel @@ -121,21 +117,24 @@ enum Commands { /// ID or URL id: String, /// Output format - #[clap(short, long, value_parser)] - format: Option, + #[clap(long, value_parser, default_value = "json")] + format: Format, /// 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(short, long, default_value = "videos")] + #[clap(long, default_value = "videos")] tab: ChannelTab, /// Use YouTube Music #[clap(short, long)] music: bool, - /// Fetch the RSS feed of a channel + /// Use the RSS feed of a channel #[clap(long)] rss: bool, /// Get comments @@ -149,18 +148,21 @@ 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(short, long, value_parser)] - format: Option, + #[clap(long, value_parser, default_value = "json")] + format: Format, /// 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, @@ -179,7 +181,7 @@ enum Commands { /// Channel ID for searching channel videos #[clap(long)] channel: Option, - /// Search YouTube Music in the given category + /// YouTube Music search filter #[clap(short, long)] music: Option, }, @@ -187,9 +189,8 @@ enum Commands { Vdata, } -#[derive(Default, Copy, Clone, ValueEnum)] +#[derive(Copy, Clone, ValueEnum)] enum Format { - #[default] Json, Yaml, } @@ -387,17 +388,17 @@ fn print_duration(duration: Option) { fn print_music_search( data: &MusicSearchResult, - format: Option, + format: Format, pretty: bool, + txt: bool, ) { - 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); + if txt { + if let Some(corr) = &data.corrected_query { + anstream::println!("Did you mean `{}`?", corr.magenta()); } + print_entities(&data.items.items); + } else { + print_data(data, format, pretty) } } @@ -405,7 +406,7 @@ fn print_description(desc: Option) { if let Some(desc) = desc { if !desc.is_empty() { print_h2("Description"); - println!("{}", desc.trim()); + println!("{}", desc); } } } @@ -536,18 +537,16 @@ 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() - .reporter(Box::new(FileReporter::new("rustypipe_reports"))); + 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); } if let Some(lang) = cli.lang { rp = rp.lang(Language::from_str(&lang.to_ascii_lowercase()).expect("invalid language")); @@ -652,6 +651,7 @@ async fn run() -> anyhow::Result<()> { Commands::Get { id, format, + txt, pretty, limit, tab, @@ -671,60 +671,56 @@ async fn run() -> anyhow::Result<()> { match details.lyrics_id { Some(lyrics_id) => { let lyrics = rp.query().music_lyrics(lyrics_id).await?; - match format { - Some(format) => print_data(&lyrics, format, pretty), - None => println!("{}\n\n{}", lyrics.body, lyrics.footer.blue()), + if txt { + println!("{}\n\n{}", lyrics.body, lyrics.footer.blue()); + } else { + print_data(&lyrics, format, pretty); } } None => eprintln!("no lyrics found"), } } else if music { let details = rp.query().music_details(&id).await?; - 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); - } + if txt { + 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); + } + } else { + print_data(&details, format, pretty); } } else if player { - 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 + let player = if let Some(client_type) = client_type { + rp.query().player_from_client(&id, client_type.into()).await } else { rp.query().player(&id).await }?; - print_data(&player, format.unwrap_or_default(), pretty); + print_data(&player, format, pretty); } else { let mut details = rp.query().video_details(&id).await?; @@ -741,160 +737,153 @@ async fn run() -> anyhow::Result<()> { None => {} } - 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 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()); } + 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?; - 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!(")"); + 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}"); } - } - 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); + 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); + } + } else { + print_data(&artist, format, pretty); } } else if rss { let rss = rp.query().channel_rss(&id).await?; - match format { - Some(format) => print_data(&rss, format, pretty), - None => { + if txt { + anstream::println!( + "{}\n{} [{}]\n{} {}", + "[Channel RSS]".on_green().black(), + rss.name.green().bold(), + rss.id, + "Created on:".blue(), + rss.create_date, + ); + if let Some(v) = rss.videos.first() { anstream::println!( - "{}\n{} [{}]\n{} {}", - "[Channel RSS]".on_green().black(), - rss.name.green().bold(), - rss.id, - "Created on:".blue(), - rss.create_date, + "{} {} [{}]", + "Latest video:".blue(), + v.publish_date, + v.id ); - 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 { @@ -910,105 +899,75 @@ async fn run() -> anyhow::Result<()> { channel.content.extend_limit(rp.query(), limit).await?; - match format { - Some(format) => print_data(&channel, format, pretty), - None => { - anstream::print!( - "{}\n{} {} [{}]", - format!("[Channel {tab:?}]").on_green().black(), - channel.name.green().bold(), - channel.handle.unwrap_or_default(), - channel.id - ); - print_verification(channel.verification); - println!(); - if let Some(subs) = channel.subscriber_count { - anstream::println!( - "{} {}", - "Subscribers:".blue(), - subs - ); - } - if let Some(vids) = channel.video_count { - anstream::println!("{} {}", "Videos:".blue(), vids); - } - print_description(Some(channel.description)); - println!(); - print_entities(&channel.content.items); + 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); } + 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?; - match format { - Some(format) => print_data(&channel, format, pretty), - None => { - anstream::println!( - "{}\n{} {} [{}]", - format!("[Channel {tab:?}]").on_green().black(), - channel.name.green().bold(), - channel.handle.unwrap_or_default(), - channel.id - ); - print_description(Some(channel.description)); - if let Some(subs) = channel.subscriber_count { - anstream::println!( - "{} {}", - "Subscribers:".blue(), - subs - ); - } - if let Some(vids) = channel.video_count { - anstream::println!("{} {}", "Videos:".blue(), vids); - } - println!(); - print_entities(&channel.content.items); + 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); } + println!(); + print_entities(&channel.content.items); + } else { + print_data(&channel, format, pretty); } } ChannelTab::Info => { let info = rp.query().channel_info(&id).await?; - 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 let Some(country) = info.country { - anstream::println!("{} {}", "Country:".blue(), country); - } - if !info.links.is_empty() { - print_h2("Links"); - for (name, url) in &info.links { - anstream::println!("{} {}", name.blue(), url); - } + 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); } } + } else { + print_data(&info, format, pretty); } } } @@ -1018,86 +977,83 @@ async fn run() -> anyhow::Result<()> { if music { let mut playlist = rp.query().music_playlist(&id).await?; playlist.tracks.extend_limit(rp.query(), limit).await?; - 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!(); + 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}]"); } - 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?; - 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 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}]"); } - 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?; - 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); + 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}]"); + } + } + print_description(album.description.map(|d| d.to_plaintext())); + println!(); + print_tracks(&album.tracks); + } else { + print_data(&album, format, pretty); } } } @@ -1106,6 +1062,7 @@ async fn run() -> anyhow::Result<()> { query, format, pretty, + txt, limit, item_type, length, @@ -1115,29 +1072,10 @@ async fn run() -> anyhow::Result<()> { music, } => match music { None => match channel { - 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); - } - } + Some(channel) => { + rustypipe::validate::channel_id(&channel)?; + let res = rp.query().channel_search(&channel, &query).await?; + print_data(&res, format, pretty); } None => { let filter = search_filter::SearchFilter::new() @@ -1151,40 +1089,39 @@ async fn run() -> anyhow::Result<()> { .await?; res.items.extend_limit(rp.query(), limit).await?; - 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); + if txt { + if let Some(corr) = res.corrected_query { + anstream::println!("Did you mean `{}`?", corr.magenta()); } + 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); + print_music_search(&res, format, pretty, txt); } 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); + print_music_search(&res, format, pretty, txt); } 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); + print_music_search(&res, format, pretty, txt); } 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); + print_music_search(&res, format, pretty, txt); } 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); + print_music_search(&res, format, pretty, txt); } Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => { let mut res = rp @@ -1195,7 +1132,7 @@ async fn run() -> anyhow::Result<()> { ) .await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty); + print_music_search(&res, format, pretty, txt); } }, Commands::Vdata => { diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index 201563a..f1d8cbb 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.handle.is_some()) + .map(|sc| sc > 100 && channel.video_count.is_none()) .unwrap_or_default(), _ => false, })) @@ -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.video_count.is_some()) + Ok(channel.mobile_banner.is_empty() && channel.tv_banner.is_empty()) } pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result { diff --git a/codegen/src/download_testfiles.rs b/codegen/src/download_testfiles.rs index 1a580d2..d5fd42e 100644 --- a/codegen/src/download_testfiles.rs +++ b/codegen/src/download_testfiles.rs @@ -38,6 +38,8 @@ pub async fn download_testfiles() { search_cont().await; search_playlists().await; search_empty().await; + startpage().await; + startpage_cont().await; trending().await; music_playlist().await; @@ -446,6 +448,29 @@ 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/downloader/Cargo.toml b/downloader/Cargo.toml index 7582dfa..b04d087 100644 --- a/downloader/Cargo.toml +++ b/downloader/Cargo.toml @@ -48,9 +48,3 @@ 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/README.md b/downloader/README.md index 05ec1a8..24a554a 100644 --- a/downloader/README.md +++ b/downloader/README.md @@ -1,8 +1,4 @@ -# ![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) +# RustyPipe downloader The downloader is a companion crate for RustyPipe that allows for easy and fast downloading of video and audio files. @@ -39,8 +35,8 @@ let dl = DownloaderBuilder::new() .build(); let filter_audio = StreamFilter::new().no_video(); -dl.id("eRsGyueVLvQ").stream_filter(filter_audio).to_file("audio.opus").download().await; +dl.id("ZeerrnuLi5E").stream_filter(filter_audio).to_file("audio.opus").download().await; let filter_video = StreamFilter::new().video_max_res(720); -dl.id("eRsGyueVLvQ").stream_filter(filter_video).to_file("video.mp4").download().await; +dl.id("ZeerrnuLi5E").stream_filter(filter_video).to_file("video.mp4").download().await; ``` diff --git a/downloader/src/error.rs b/downloader/src/error.rs deleted file mode 100644 index 8970dd4..0000000 --- a/downloader/src/error.rs +++ /dev/null @@ -1,54 +0,0 @@ -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 a1483d4..53c301c 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -1,7 +1,6 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs, clippy::todo, clippy::dbg_macro)] -mod error; mod util; use std::{ @@ -43,7 +42,7 @@ use rustypipe::model::{richtext::ToPlaintext, VideoDetails, VideoPlayerDetails}; #[cfg(feature = "audiotag")] use time::{Date, OffsetDateTime}; -pub use error::DownloadError; +pub use util::DownloadError; type Result = core::result::Result; @@ -159,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).trim().to_owned()), + .map(|n| n.strip_suffix(" - Topic").unwrap_or(n).to_owned()), album_id: None, album_name: None, track_nr: None, diff --git a/downloader/src/util.rs b/downloader/src/util.rs index 5f87339..5069c96 100644 --- a/downloader/src/util.rs +++ b/downloader/src/util.rs @@ -1,8 +1,58 @@ -use std::collections::BTreeMap; +use std::{borrow::Cow, collections::BTreeMap, path::PathBuf}; use reqwest::Url; +use rustypipe::client::ClientType; -use crate::DownloadError; +/// 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()) + } +} /// Split an URL into its base string and parameter map /// diff --git a/downloader/tests/tests.rs b/downloader/tests/tests.rs deleted file mode 100644 index 3fc035d..0000000 --- a/downloader/tests/tests.rs +++ /dev/null @@ -1,113 +0,0 @@ -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/src/client/channel.rs b/src/client/channel.rs index 854c728..a688ee3 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -353,6 +353,18 @@ 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, @@ -389,16 +401,10 @@ fn map_channel( ))); } - let handle = metadata + let vanity_url = metadata .vanity_channel_url .as_ref() - .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) - }); + .and_then(|url| map_vanity_url(url, ctx.id)); let mut warnings = Vec::new(); Ok(MapResult { @@ -406,16 +412,17 @@ 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, @@ -436,20 +443,21 @@ 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, @@ -460,33 +468,19 @@ 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, vc_part) = if md_rows.len() > 1 { - let mp = &md_rows[1].metadata_parts; - (mp.first(), mp.get(1)) + let sub_part = if md_rows.len() > 1 { + md_rows.get(1).and_then(|md| md.metadata_parts.first()) } else { - ( - md_rows.first().and_then(|md| md.metadata_parts.get(1)), - None, - ) + md_rows.first().and_then(|md| md.metadata_parts.get(1)) }; 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 @@ -497,7 +491,10 @@ 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, @@ -607,14 +604,15 @@ 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/mod.rs b/src/client/mod.rs index 7546d8b..3bbb724 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 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"; +const TV_CLIENT_VERSION: &str = "7.20240724.13.00"; +const DESKTOP_MUSIC_CLIENT_VERSION: &str = "1.20230123.01.01"; // Mobile client const MOBILE_CLIENT_VERSION: &str = "18.03.33"; @@ -220,8 +220,12 @@ 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::Android, ClientType::Ios]; +pub const DEFAULT_PLAYER_CLIENT_ORDER: &[ClientType] = &[ + ClientType::Tv, + ClientType::TvHtml5Embed, + ClientType::Android, + ClientType::Ios, +]; /// The RustyPipe client used to access YouTube's API /// @@ -372,20 +376,14 @@ 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, } @@ -436,10 +434,6 @@ impl CacheEntry { CacheEntry::None => None, } } - - fn is_none(&self) -> bool { - matches!(self, Self::None) - } } impl From for CacheEntry { @@ -531,7 +525,6 @@ 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, @@ -826,12 +819,6 @@ 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>, @@ -950,37 +937,6 @@ 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 @@ -1022,7 +978,6 @@ 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(), }; @@ -1246,7 +1201,7 @@ impl RustyPipeQuery { ClientType::Tv => YTContext { client: ClientInfo { client_name: "TVHTML5", - client_version: Cow::Owned(self.client.get_tv_client_version().await), + client_version: Cow::Borrowed(TV_CLIENT_VERSION), client_screen: Some("WATCH"), platform: "TV", device_model: Some("SmartTV"), @@ -1738,28 +1693,21 @@ mod tests { } #[tokio::test] - async fn extract_desktop_client_version() { + async fn t_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 extract_music_client_version() { + async fn t_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 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() { + async fn t_get_visitor_data() { let rp = RustyPipe::new(); let visitor_data = rp.get_visitor_data().await.unwrap(); diff --git a/src/client/pagination.rs b/src/client/pagination.rs index e8f53dc..1251574 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -355,6 +355,7 @@ 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/player.rs b/src/client/player.rs index 76b16a5..430d33c 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -74,9 +74,6 @@ 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, @@ -84,6 +81,9 @@ 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,17 +96,11 @@ impl RustyPipeQuery { msg, } = &e { - if let Ok(res) = self - .player_from_client(video_id, ClientType::TvHtml5Embed) - .await - { - return Ok(res); - } else { - return Err(Error::Extraction(ExtractionError::Unavailable { + age_restricted_e = + Some(Error::Extraction(ExtractionError::Unavailable { reason: UnavailabilityReason::AgeRestricted, msg: msg.to_owned(), })); - } } last_e = Error::Extraction(e); } else { @@ -116,7 +110,7 @@ impl RustyPipeQuery { Err(e) => return Err(e), } } - Err(last_e) + Err(age_restricted_e.unwrap_or(last_e)) } /// Get YouTube player data (video/audio streams + basic metadata) using the specified client diff --git a/src/client/response/channel.rs b/src/client/response/channel.rs index c9b0440..4bff8f2 100644 --- a/src/client/response/channel.rs +++ b/src/client/response/channel.rs @@ -95,6 +95,11 @@ 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] @@ -120,35 +125,29 @@ 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(Default, Debug, Deserialize)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PhTitleView { pub dynamic_text_view_model: PhTitleView2, } -#[derive(Default, Debug, Deserialize)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PhTitleView2 { pub text: PhTitleView3, } #[serde_as] -#[derive(Default, Debug, Deserialize)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PhTitleView3 { #[serde_as(as = "VecSkipError<_>")] @@ -243,7 +242,7 @@ pub(crate) struct PhMetadataRow { pub metadata_parts: Vec, } -#[derive(Default, Debug, Deserialize)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PhBannerView { pub image_banner_view_model: ImageView, diff --git a/src/client/response/mod.rs b/src/client/response/mod.rs index e8b7b52..85392cd 100644 --- a/src/client/response/mod.rs +++ b/src/client/response/mod.rs @@ -34,6 +34,7 @@ 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 07f4b9a..f35472d 100644 --- a/src/client/response/trends.rs +++ b/src/client/response/trends.rs @@ -1,6 +1,13 @@ use serde::Deserialize; -use super::{video_item::YouTubeListRendererWrap, Tab, TwoColumnBrowseResults}; +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, +} #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index 9b9a97a..3d7a116 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -610,26 +610,28 @@ impl YouTubeListMapper { fn map_channel(&mut self, channel: ChannelRenderer) -> ChannelItem { // channel handle instead of subscriber count (A/B test 3) - let (handle, sc_txt) = if channel + let (sc_txt, vc_text) = if channel .subscriber_count_text .as_ref() .map(|txt| txt.starts_with('@')) .unwrap_or_default() { - (channel.subscriber_count_text, channel.video_count_text) + (channel.video_count_text, None) } else { - (None, channel.subscriber_count_text) + (channel.subscriber_count_text, channel.video_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__channel__tests__map_channel_livestreams.snap b/src/client/snapshots/rustypipe__client__channel__tests__map_channel_livestreams.snap index a2c19fa..830b5fd 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,9 +5,7 @@ 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", @@ -57,6 +55,7 @@ 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", @@ -89,6 +88,60 @@ 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 7558c9f..1ccf5b6 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,9 +5,7 @@ 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", @@ -57,6 +55,7 @@ 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", @@ -89,6 +88,60 @@ 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 46c6acf..e9e98bf 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,9 +5,7 @@ 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", @@ -28,6 +26,7 @@ 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", @@ -60,6 +59,60 @@ 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 81a993a..47cf981 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,9 +5,7 @@ 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", @@ -28,6 +26,7 @@ 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", @@ -60,6 +59,8 @@ 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 7fd71af..6f0e23e 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,9 +5,7 @@ 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", @@ -28,6 +26,7 @@ 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", @@ -60,6 +59,60 @@ 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 09b224f..e4efa91 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,9 +5,7 @@ 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", @@ -57,6 +55,7 @@ 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", @@ -89,6 +88,60 @@ 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 7279d2e..a365792 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,9 +5,7 @@ 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", @@ -33,7 +31,10 @@ 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 96428f6..42d984e 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,9 +5,7 @@ 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", @@ -57,6 +55,7 @@ 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", @@ -89,6 +88,8 @@ 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 53e191e..cbf872c 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,9 +5,7 @@ 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", @@ -57,6 +55,7 @@ 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", @@ -89,6 +88,60 @@ 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 8a490a0..780e6a6 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,9 +5,7 @@ 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", @@ -28,7 +26,10 @@ 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 fdafcc3..4fa906c 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,9 +5,7 @@ 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", @@ -41,6 +39,7 @@ 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", @@ -73,6 +72,60 @@ 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 0d4a4d1..4d7ed05 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,9 +5,7 @@ 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", @@ -28,6 +26,7 @@ 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", @@ -60,6 +59,60 @@ 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 c655530..46369d0 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,9 +5,7 @@ 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", @@ -28,6 +26,7 @@ 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", @@ -60,6 +59,60 @@ 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 2b5a675..97348c9 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,9 +5,7 @@ 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", @@ -45,6 +43,7 @@ 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", @@ -77,6 +76,60 @@ 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/client/snapshots/rustypipe__client__pagination__tests__map_startpage.snap b/src/client/snapshots/rustypipe__client__pagination__tests__map_startpage.snap new file mode 100644 index 0000000..171c57f --- /dev/null +++ b/src/client/snapshots/rustypipe__client__pagination__tests__map_startpage.snap @@ -0,0 +1,884 @@ +--- +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__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 ec1fe51..361af9c 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,7 +9,6 @@ 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", @@ -24,12 +23,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", @@ -44,12 +43,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", @@ -64,12 +63,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", @@ -84,12 +83,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", @@ -104,12 +103,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", @@ -124,12 +123,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", @@ -144,12 +143,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", @@ -164,12 +163,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", @@ -184,12 +183,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", @@ -204,12 +203,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", @@ -224,12 +223,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", @@ -244,12 +243,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", @@ -264,12 +263,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", @@ -284,12 +283,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", @@ -304,12 +303,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", @@ -324,12 +323,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", @@ -344,12 +343,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", @@ -364,12 +363,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", @@ -384,12 +383,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", @@ -404,6 +403,7 @@ 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 3198cc7..e0ac9fd 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,7 +9,6 @@ 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", @@ -24,6 +23,7 @@ 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/client/snapshots/rustypipe__client__trends__tests__map_startpage.snap b/src/client/snapshots/rustypipe__client__trends__tests__map_startpage.snap new file mode 100644 index 0000000..a6ec599 --- /dev/null +++ b/src/client/snapshots/rustypipe__client__trends__tests__map_startpage.snap @@ -0,0 +1,784 @@ +--- +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 c271b7d..a445c91 100644 --- a/src/client/trends.rs +++ b/src/client/trends.rs @@ -2,13 +2,38 @@ use std::borrow::Cow; use crate::{ error::{Error, ExtractionError}, - model::VideoItem, + model::{ + paginator::{ContinuationEndpoint, Paginator}, + VideoItem, + }, + param::Language, serializer::MapResult, }; -use super::{response, ClientType, MapRespCtx, MapResponse, QBrowseParams, RustyPipeQuery}; +use super::{ + response, ClientType, MapRespCtx, MapResponse, QBrowse, 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> { @@ -30,6 +55,33 @@ 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, @@ -57,6 +109,26 @@ 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}; @@ -66,11 +138,32 @@ mod tests { use crate::{ client::{response, MapRespCtx, MapResponse}, - model::VideoItem, + model::{paginator::Paginator, 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")] @@ -78,10 +171,10 @@ mod tests { let json_path = path!(*TESTFILES / "trends" / format!("trending_{name}.json")); let json_file = File::open(json_path).unwrap(); - let trending: response::Trending = + let startpage: response::Trending = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - trending.map_response(&MapRespCtx::test("")).unwrap(); + startpage.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/model/mod.rs b/src/model/mod.rs index 7adaeac..6ec4d2f 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -700,15 +700,11 @@ 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 @@ -717,8 +713,15 @@ 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? @@ -870,8 +873,6 @@ 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 @@ -880,6 +881,8 @@ 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, } diff --git a/src/util/mod.rs b/src/util/mod.rs index 121c1b0..013ccdd 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -32,10 +32,9 @@ 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})|(?:@[\w\-\.·]{1,30})$").unwrap()); -pub static CHANNEL_HANDLE_REGEX: Lazy = - Lazy::new(|| Regex::new(r#"^@[\w\-\.·]{1,30}$"#).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() +}); /// Separator string for YouTube Music subtitles pub const DOT_SEPARATOR: &str = " • "; diff --git a/src/validate.rs b/src/validate.rs index 7c43a61..ccaa58a 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -11,10 +11,7 @@ //! - 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::{self, CHANNEL_HANDLE_REGEX}, -}; +use crate::{error::Error, util}; use once_cell::sync::Lazy; use regex::Regex; @@ -205,32 +202,6 @@ 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 ca87752..b6a0577 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -885,13 +885,16 @@ 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!( @@ -991,12 +994,15 @@ 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"); } @@ -1449,6 +1455,18 @@ 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) { @@ -2685,7 +2703,6 @@ 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()