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