Compare commits

..

No commits in common. "0bf60774040fea8f45e8b3203f684ea05a26f0cc" and "abb783219aba4b492c1dff03c2148acf1f51a55d" have entirely different histories.

44 changed files with 2875 additions and 899 deletions

View file

@ -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"]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<PathBuf>,
/// Download to the given file
#[clap(long)]
output_file: Option<PathBuf>,
/// Download to a path determined by a template
#[clap(long)]
template: Option<String>,
}
@ -97,7 +93,7 @@ enum Commands {
/// Video resolution (e.g. 720, 1080). Set to 0 for audio-only.
#[clap(short, long)]
resolution: Option<u32>,
/// 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<Format>,
#[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<Vec<ClientTypeArg>>,
client_type: Option<ClientTypeArg>,
},
/// Search YouTube
Search {
/// Search query
query: String,
/// Output format
#[clap(short, long, value_parser)]
format: Option<Format>,
#[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<String>,
/// Search YouTube Music in the given category
/// YouTube Music search filter
#[clap(short, long)]
music: Option<MusicSearchCategory>,
},
@ -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<u32>) {
fn print_music_search<T: Serialize + YtEntity>(
data: &MusicSearchResult<T>,
format: Option<Format>,
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<String>) {
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::<Vec<_>>();
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!(
"{}\n<b>ID:</b>{}",
"[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!(
"{}\n<b>ID:</b>{}",
"[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 => {

View file

@ -179,7 +179,7 @@ pub async fn channel_handles_in_search_results(rp: &RustyPipeQuery) -> Result<bo
Ok(search.items.items.iter().any(|itm| match itm {
YouTubeItem::Channel(channel) => 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<bool> {
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<bool> {

View file

@ -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() {

View file

@ -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"

View file

@ -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;
```

View file

@ -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<lofty::error::LoftyError> for DownloadError {
fn from(value: lofty::error::LoftyError) -> Self {
Self::AudioTag(value.to_string().into())
}
}
#[cfg(feature = "audiotag")]
impl From<image::ImageError> for DownloadError {
fn from(value: image::ImageError) -> Self {
Self::AudioTag(value.to_string().into())
}
}

View file

@ -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<T> = core::result::Result<T, DownloadError>;
@ -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,

View file

@ -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<lofty::error::LoftyError> for DownloadError {
fn from(value: lofty::error::LoftyError) -> Self {
Self::AudioTag(value.to_string().into())
}
}
#[cfg(feature = "audiotag")]
impl From<image::ImageError> for DownloadError {
fn from(value: image::ImageError) -> Self {
Self::AudioTag(value.to_string().into())
}
}
/// Split an URL into its base string and parameter map
///

View file

@ -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<T: PartialOrd + std::fmt::Display>(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::<serde_json::Value>(&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));
}

View file

@ -353,6 +353,18 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
}
}
fn map_vanity_url(url: &str, id: &str) -> Option<String> {
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<response::channel::Header>,
metadata: Option<response::channel::Metadata>,
@ -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::<u64>(&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<T>(channel_data: Channel<()>, content: T) -> Channel<T>
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,

View file

@ -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<Regex> =
///
/// 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<CacheEntry<ClientData>>,
music_client: RwLock<CacheEntry<ClientData>>,
tv_client: RwLock<CacheEntry<ClientData>>,
deobf: RwLock<CacheEntry<DeobfData>>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
struct CacheData {
#[serde(skip_serializing_if = "CacheEntry::is_none")]
desktop_client: CacheEntry<ClientData>,
#[serde(skip_serializing_if = "CacheEntry::is_none")]
music_client: CacheEntry<ClientData>,
#[serde(skip_serializing_if = "CacheEntry::is_none")]
tv_client: CacheEntry<ClientData>,
#[serde(skip_serializing_if = "CacheEntry::is_none")]
deobf: CacheEntry<DeobfData>,
}
@ -436,10 +434,6 @@ impl<T> CacheEntry<T> {
CacheEntry::None => None,
}
}
fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
impl<T> From<T> for CacheEntry<T> {
@ -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<String, Error> {
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<DeobfData, Error> {
// 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();

View file

@ -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);

View file

@ -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<S: AsRef<str> + Debug>(
&self,
video_id: S,
@ -84,6 +81,9 @@ impl RustyPipeQuery {
) -> Result<VideoPlayer, Error> {
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

View file

@ -95,6 +95,11 @@ pub(crate) struct HeaderRenderer {
pub badges: Vec<ChannelBadge>,
#[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<TextWrap>,
}
#[derive(Default, Debug, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhBannerView {
pub image_banner_view_model: ImageView,

View file

@ -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;

View file

@ -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")]

View file

@ -610,26 +610,28 @@ impl<T> YouTubeListMapper<T> {
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,
}
}

View file

@ -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,

View file

@ -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"),

View file

@ -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, Im 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 its because youre 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"),

View file

@ -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, Im 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 its because youre 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,

View file

@ -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, Im 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 its because youre 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"),

View file

@ -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"),

View file

@ -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"),

View file

@ -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,

View file

@ -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"),

View file

@ -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"),

View file

@ -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"),

View file

@ -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"),

View file

@ -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, Im 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 its because youre 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"),

View file

@ -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"),

View file

@ -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,
)

View file

@ -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)",
)),
],

View file

@ -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(

View file

@ -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, youll 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,
)

View file

@ -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<Paginator<VideoItem>, Error> {
let context = self.get_context(ClientType::Desktop, true, None).await;
let request_body = QBrowse {
context,
browse_id: "FEwhat_to_watch",
};
self.execute_request::<response::Startpage, _, _>(
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<Vec<VideoItem>, Error> {
@ -30,6 +55,33 @@ impl RustyPipeQuery {
}
}
impl MapResponse<Paginator<VideoItem>> for response::Startpage {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
) -> Result<MapResult<Paginator<VideoItem>>, 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<Vec<VideoItem>> for response::Trending {
fn map_response(
self,
@ -57,6 +109,26 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
}
}
fn map_startpage_videos(
videos: MapResult<Vec<response::YouTubeListItem>>,
lang: Language,
visitor_data: Option<String>,
) -> MapResult<Paginator<VideoItem>> {
let mut mapper = response::YouTubeListMapper::<VideoItem>::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<Paginator<VideoItem>> =
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<Vec<VideoItem>> =
trending.map_response(&MapRespCtx::test("")).unwrap();
startpage.map_response(&MapRespCtx::test("")).unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -700,15 +700,11 @@ pub struct Channel<T> {
pub id: String,
/// Channel name
pub name: String,
/// YouTube channel handle (e.g. `@EEVblog`)
pub handle: Option<String>,
/// Channel subscriber count
///
/// [`None`] if the subscriber count was hidden by the owner
/// or could not be parsed.
pub subscriber_count: Option<u64>,
/// Number of videos
pub video_count: Option<u64>,
/// Channel avatar / profile picture
pub avatar: Vec<Thumbnail>,
/// Channel verification mark
@ -717,8 +713,15 @@ pub struct Channel<T> {
pub description: String,
/// List of words to describe the topic of the channel
pub tags: Vec<String>,
/// Custom URL set by the channel owner
/// (e.g. <https://www.youtube.com/c/EevblogDave>)
pub vanity_url: Option<String>,
/// Banner image shown above the channel
pub banner: Vec<Thumbnail>,
/// Banner image shown above the channel (small format for mobile)
pub mobile_banner: Vec<Thumbnail>,
/// Banner image shown above the channel (16:9 fullscreen format for TV)
pub tv_banner: Vec<Thumbnail>,
/// 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<String>,
/// Channel avatar/profile picture
pub avatar: Vec<Thumbnail>,
/// Channel verification mark
@ -880,6 +881,8 @@ pub struct ChannelItem {
///
/// [`None`] if hidden by the owner or not present.
pub subscriber_count: Option<u64>,
/// Number of videos from the channel
pub video_count: Option<u64>,
/// Abbreviated channel description
pub short_description: String,
}

View file

@ -32,10 +32,9 @@ pub static PLAYLIST_ID_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?:PL|RD|OLAK|UU)[A-Za-z0-9_-]{5,50}$").unwrap());
pub static ALBUM_ID_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^MPREb_[A-Za-z0-9_-]{11}$").unwrap());
pub static VANITY_PATH_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^/?(?:(?:c/|user/)?[A-z0-9]{1,100})|(?:@[\w\-\.·]{1,30})$").unwrap());
pub static CHANNEL_HANDLE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^@[\w\-\.·]{1,30}$"#).unwrap());
pub static VANITY_PATH_REGEX: Lazy<Regex> = 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 = "";

View file

@ -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<S: AsRef<str>>(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: <https://support.google.com/youtube/answer/11585688>
///
/// ```
/// # 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<S: AsRef<str>>(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(())

View file

@ -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<T>(channel: &Channel<T>) {
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()