Compare commits

..

3 commits

32 changed files with 34160 additions and 14005 deletions

View file

@ -15,7 +15,7 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
- [X] **Search** (with filters) - [X] **Search** (with filters)
- [X] **Search suggestions** - [X] **Search suggestions**
- [X] **Trending** - [X] **Trending**
- [ ] **URL resolver** - [X] **URL resolver**
### YouTube Music ### YouTube Music

View file

@ -30,6 +30,7 @@ pub async fn download_testfiles(project_root: &Path) {
search_playlists(&testfiles).await; search_playlists(&testfiles).await;
search_empty(&testfiles).await; search_empty(&testfiles).await;
startpage(&testfiles).await; startpage(&testfiles).await;
startpage_cont(&testfiles).await;
trending(&testfiles).await; trending(&testfiles).await;
} }
@ -381,6 +382,21 @@ async fn startpage(testfiles: &Path) {
rp.query().startpage().await.unwrap(); rp.query().startpage().await.unwrap();
} }
async fn startpage_cont(testfiles: &Path) {
let mut json_path = testfiles.to_path_buf();
json_path.push("trends");
json_path.push("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(testfiles: &Path) { async fn trending(testfiles: &Path) {
let mut json_path = testfiles.to_path_buf(); let mut json_path = testfiles.to_path_buf();
json_path.push("trends"); json_path.push("trends");

View file

@ -237,11 +237,10 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
links: meta links: meta
.primary_links .primary_links
.into_iter() .into_iter()
.map(|l| { .filter_map(|l| {
( l.navigation_endpoint
l.title, .url_endpoint
util::sanitize_yt_url(&l.navigation_endpoint.url_endpoint.url), .map(|url| (l.title, util::sanitize_yt_url(&url.url)))
)
}) })
.collect(), .collect(),
}) })

View file

@ -1,12 +1,14 @@
//! YouTube API Client //! YouTube API Client
pub(crate) mod response;
mod channel; mod channel;
mod pagination; mod pagination;
mod player; mod player;
mod playlist; mod playlist;
mod response;
mod search; mod search;
mod trends; mod trends;
mod url_resolver;
mod video_details; mod video_details;
#[cfg(feature = "rss")] #[cfg(feature = "rss")]
@ -90,6 +92,8 @@ struct ClientInfo {
platform: String, platform: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
original_url: Option<String>, original_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
visitor_data: Option<String>,
hl: Language, hl: Language,
gl: Country, gl: Country,
} }
@ -773,6 +777,7 @@ impl RustyPipeQuery {
device_model: None, device_model: None,
platform: "DESKTOP".to_owned(), platform: "DESKTOP".to_owned(),
original_url: Some("https://www.youtube.com/".to_owned()), original_url: Some("https://www.youtube.com/".to_owned()),
visitor_data: None,
hl, hl,
gl, gl,
}, },
@ -788,6 +793,7 @@ impl RustyPipeQuery {
device_model: None, device_model: None,
platform: "DESKTOP".to_owned(), platform: "DESKTOP".to_owned(),
original_url: Some("https://music.youtube.com/".to_owned()), original_url: Some("https://music.youtube.com/".to_owned()),
visitor_data: None,
hl, hl,
gl, gl,
}, },
@ -803,6 +809,7 @@ impl RustyPipeQuery {
device_model: None, device_model: None,
platform: "TV".to_owned(), platform: "TV".to_owned(),
original_url: None, original_url: None,
visitor_data: None,
hl, hl,
gl, gl,
}, },
@ -820,6 +827,7 @@ impl RustyPipeQuery {
device_model: None, device_model: None,
platform: "MOBILE".to_owned(), platform: "MOBILE".to_owned(),
original_url: None, original_url: None,
visitor_data: None,
hl, hl,
gl, gl,
}, },
@ -835,6 +843,7 @@ impl RustyPipeQuery {
device_model: Some(IOS_DEVICE_MODEL.to_owned()), device_model: Some(IOS_DEVICE_MODEL.to_owned()),
platform: "MOBILE".to_owned(), platform: "MOBILE".to_owned(),
original_url: None, original_url: None,
visitor_data: None,
hl, hl,
gl, gl,
}, },
@ -1070,9 +1079,16 @@ impl RustyPipeQuery {
}; };
if status.is_client_error() || status.is_server_error() { if status.is_client_error() || status.is_server_error() {
let e = Error::HttpStatus(status.into()); let status_code = status.as_u16();
return if status_code == 404 {
Err(Error::Extraction(ExtractionError::ContentUnavailable(
"Not found".into(),
)))
} else {
let e = Error::HttpStatus(status_code);
create_report(Level::ERR, Some(e.to_string()), vec![]); create_report(Level::ERR, Some(e.to_string()), vec![]);
return Err(e); Err(e)
};
} }
match serde_json::from_str::<R>(&resp_str) { match serde_json::from_str::<R>(&resp_str) {

View file

@ -1,6 +1,7 @@
use crate::error::Error; use crate::error::Error;
use crate::model::{ use crate::model::{
ChannelPlaylist, ChannelVideo, Comment, Paginator, PlaylistVideo, RecommendedVideo, SearchItem, ChannelPlaylist, ChannelVideo, Comment, Paginator, PlaylistVideo, RecommendedVideo, SearchItem,
SearchVideo,
}; };
use super::RustyPipeQuery; use super::RustyPipeQuery;
@ -70,3 +71,57 @@ paginator!(
RustyPipeQuery::channel_playlists_continuation RustyPipeQuery::channel_playlists_continuation
); );
paginator!(SearchItem, RustyPipeQuery::search_continuation); paginator!(SearchItem, RustyPipeQuery::search_continuation);
impl Paginator<SearchVideo> {
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>, Error> {
Ok(match (&self.ctoken, &self.visitor_data) {
(Some(ctoken), Some(visitor_data)) => {
Some(query.startpage_continuation(ctoken, visitor_data).await?)
}
_ => None,
})
}
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool, Error> {
match self.next(query).await {
Ok(Some(paginator)) => {
let mut items = paginator.items;
self.items.append(&mut items);
self.ctoken = paginator.ctoken;
Ok(true)
}
Ok(None) => Ok(false),
Err(e) => Err(e),
}
}
pub async fn extend_pages(
&mut self,
query: RustyPipeQuery,
n_pages: usize,
) -> Result<(), Error> {
for _ in 0..n_pages {
match self.extend(query.clone()).await {
Ok(false) => break,
Err(e) => return Err(e),
_ => {}
}
}
Ok(())
}
pub async fn extend_limit(
&mut self,
query: RustyPipeQuery,
n_items: usize,
) -> Result<(), Error> {
while self.items.len() < n_items {
match self.extend(query.clone()).await {
Ok(false) => break,
Err(e) => return Err(e),
_ => {}
}
}
Ok(())
}
}

View file

@ -285,7 +285,7 @@ impl MapResponse<VideoPlayer> for response::Player {
fn cipher_to_url_params( fn cipher_to_url_params(
signature_cipher: &str, signature_cipher: &str,
deobf: &Deobfuscator, deobf: &Deobfuscator,
) -> Result<(String, BTreeMap<String, String>), DeobfError> { ) -> Result<(Url, BTreeMap<String, String>), DeobfError> {
let params: HashMap<Cow<str>, Cow<str>> = let params: HashMap<Cow<str>, Cow<str>> =
url::form_urlencoded::parse(signature_cipher.as_bytes()).collect(); url::form_urlencoded::parse(signature_cipher.as_bytes()).collect();

View file

@ -2,6 +2,7 @@ use serde::Deserialize;
use serde_with::serde_as; use serde_with::serde_as;
use serde_with::{DefaultOnError, VecSkipError}; use serde_with::{DefaultOnError, VecSkipError};
use super::url_endpoint::NavigationEndpoint;
use super::Thumbnails; use super::Thumbnails;
use super::{Alert, ChannelBadge}; use super::{Alert, ChannelBadge};
use super::{ContentRenderer, ContentsRenderer, VideoListItem}; use super::{ContentRenderer, ContentsRenderer, VideoListItem};
@ -205,18 +206,6 @@ pub struct PrimaryLink {
pub navigation_endpoint: NavigationEndpoint, pub navigation_endpoint: NavigationEndpoint,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigationEndpoint {
pub url_endpoint: UrlEndpoint,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UrlEndpoint {
pub url: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OnResponseReceivedAction { pub struct OnResponseReceivedAction {

View file

@ -4,6 +4,7 @@ pub mod playlist;
pub mod playlist_music; pub mod playlist_music;
pub mod search; pub mod search;
pub mod trends; pub mod trends;
pub mod url_endpoint;
pub mod video_details; pub mod video_details;
pub use channel::Channel; pub use channel::Channel;
@ -15,7 +16,9 @@ pub use playlist_music::PlaylistMusic;
pub use search::Search; pub use search::Search;
pub use search::SearchCont; pub use search::SearchCont;
pub use trends::Startpage; pub use trends::Startpage;
pub use trends::StartpageCont;
pub use trends::Trending; pub use trends::Trending;
pub use url_endpoint::ResolvedUrl;
pub use video_details::VideoComments; pub use video_details::VideoComments;
pub use video_details::VideoDetails; pub use video_details::VideoDetails;
pub use video_details::VideoRecommendations; pub use video_details::VideoRecommendations;

View file

@ -9,6 +9,16 @@ use super::{ContentRenderer, ContentsRenderer, VideoListItem, VideoRenderer};
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Startpage { pub struct Startpage {
pub contents: Contents<BrowseResultsStartpage>, pub contents: Contents<BrowseResultsStartpage>,
pub response_context: ResponseContext,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartpageCont {
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub on_response_received_actions: Vec<OnResponseReceivedAction>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -91,6 +101,12 @@ pub struct ShelfContentsRenderer {
pub items: MapResult<Vec<TrendingListItem>>, pub items: MapResult<Vec<TrendingListItem>>,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseContext {
pub visitor_data: Option<String>,
}
#[serde_as] #[serde_as]
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -101,3 +117,17 @@ pub enum TrendingListItem {
#[serde(other, deserialize_with = "ignore_any")] #[serde(other, deserialize_with = "ignore_any")]
None, None,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnResponseReceivedAction {
pub append_continuation_items_action: AppendAction,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppendAction {
#[serde_as(as = "VecLogError<_>")]
pub continuation_items: MapResult<Vec<VideoListItem>>,
}

View file

@ -0,0 +1,100 @@
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError};
use crate::model::UrlTarget;
/// navigation/resolve_url response model
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolvedUrl {
pub endpoint: NavigationEndpoint,
}
#[serde_as]
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct NavigationEndpoint {
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub watch_endpoint: Option<WatchEndpoint>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub browse_endpoint: Option<BrowseEndpoint>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub url_endpoint: Option<UrlEndpoint>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub command_metadata: Option<CommandMetadata>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WatchEndpoint {
pub video_id: String,
#[serde(default)]
pub start_time_seconds: u32,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowseEndpoint {
pub browse_id: String,
pub browse_endpoint_context_supported_configs: Option<BrowseEndpointConfig>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UrlEndpoint {
pub url: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowseEndpointConfig {
pub browse_endpoint_context_music_config: BrowseEndpointMusicConfig,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowseEndpointMusicConfig {
pub page_type: PageType,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandMetadata {
pub web_command_metadata: WebCommandMetadata,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebCommandMetadata {
pub web_page_type: PageType,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
pub enum PageType {
#[serde(rename = "MUSIC_PAGE_TYPE_ARTIST")]
Artist,
#[serde(rename = "MUSIC_PAGE_TYPE_ALBUM")]
Album,
#[serde(
rename = "WEB_PAGE_TYPE_CHANNEL",
alias = "MUSIC_PAGE_TYPE_USER_CHANNEL"
)]
Channel,
#[serde(rename = "MUSIC_PAGE_TYPE_PLAYLIST", alias = "WEB_PAGE_TYPE_PLAYLIST")]
Playlist,
}
impl PageType {
pub fn to_url_target(self, id: String) -> UrlTarget {
match self {
PageType::Artist => UrlTarget::Channel { id },
PageType::Album => UrlTarget::Playlist { id },
PageType::Channel => UrlTarget::Channel { id },
PageType::Playlist => UrlTarget::Playlist { id },
}
}
}

View file

@ -11,8 +11,8 @@ use crate::serializer::{
}; };
use super::{ use super::{
ContinuationEndpoint, ContinuationItemRenderer, Icon, MusicContinuation, Thumbnails, url_endpoint::BrowseEndpoint, ContinuationEndpoint, ContinuationItemRenderer, Icon,
VideoListItem, VideoOwner, MusicContinuation, Thumbnails, VideoListItem, VideoOwner,
}; };
/* /*
@ -561,12 +561,6 @@ pub struct AuthorEndpoint {
pub browse_endpoint: BrowseEndpoint, pub browse_endpoint: BrowseEndpoint,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowseEndpoint {
pub browse_id: String,
}
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)] #[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CommentPriority { pub enum CommentPriority {

View file

@ -0,0 +1,762 @@
---
source: src/client/trends.rs
expression: map_res.c
---
Paginator(
count: None,
items: [
SearchVideo(
id: "_cyJhGsXDDM",
title: "Ultimate Criminal Canal Found Magnet Fishing! Police on the Hunt",
length: 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: 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: 700385,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "36YnV9STBqc",
title: "The Good Life Radio\u{a0}•\u{a0}24/7 Live Radio | Best Relax House, Chillout, Study, Running, Gym, Happy Music",
length: 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: 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: 7202,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "YYD1qgH5qC4",
title: "چند شنبه با سینــا | فصل چهـارم | قسمت 5 | با حضور نازنین انصاری مدیر روزنامه کیهان لندن",
length: 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: 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: 104344,
is_live: false,
is_short: false,
short_description: "#mbcpersia\n#chandshanbeh\n#چندشنبه\n\nشبكه ام بى سى پرشيا را از حساب هاى مختلف در شبكه هاى اجتماعى دنبال كنيد\n►MBCPERSIA on Facebook:...",
),
SearchVideo(
id: "BeJqgI6rw9k",
title: "your city is full of fake buildings, here\'s why",
length: 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: 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: 1447008,
is_live: false,
is_short: false,
short_description: "Save 33% on your first Native Deodorant Pack - normally $39, youll get it for $26! Click here https://bit.ly/nativeanswer1 and use my code ANSWER #AD\n\nSomewhere on your street there may...",
),
SearchVideo(
id: "ma28eWd1oyA",
title: "Post Malone, Maroon 5, Adele, Taylor Swift, Ed Sheeran, Shawn Mendes, Pop Hits 2020 Part 6",
length: Some(29989),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ma28eWd1oyA/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCznoPDMo_F1NCRBWoD4Ps5IjctxQ",
width: 480,
height: 270,
),
],
channel: 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: 1861814,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "mL2LBRM5GBI",
title: "Salahs 6-Minuten-Hattrick & Firmino-Gala: Rangers - FC Liverpool 1:7 | UEFA Champions League | DAZN",
length: 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: 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: 1471667,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "Ang18qz2IeQ",
title: "Satisfying Videos of Workers Doing Their Job Perfectly",
length: 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: 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: 173121,
is_live: false,
is_short: false,
short_description: "TechZone ► https://goo.gl/Gj3wZs \n\n #incrediblemoments #mindwarehouse #IncredibleMoments #CaughtOnCamera #InterestingFacts \n\nYou can endlessly watch how others work, but in this selection,...",
),
SearchVideo(
id: "fjHN4jsJnEU",
title: "I Made 200 Players Simulate Survival Island in Minecraft...",
length: 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: 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: 751909,
is_live: false,
is_short: false,
short_description: "200 Players Simulate Survival Island Civilizations in Minecraft...\n-------------------------------------------------------------------\nI invited 200 Players to a Survival Island and let them...",
),
SearchVideo(
id: "FI1XrdBJIUI",
title: "Epic Construction Fails | Expensive Fails Compilation | FailArmy",
length: 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: 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: 2226471,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "MXdplejK8vU",
title: "Chilly autumn Jazz ☕ Smooth September Jazz & Bossa Nova for a great relaxing weekend",
length: 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: 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: 148743,
is_live: false,
is_short: false,
short_description: "Chilly autumn Jazz ☕ Smooth September Jazz & Bossa Nova for a great relaxing weekend\nhttps://youtu.be/MXdplejK8vU\n*******************************************\nSounds available on: Jazz Bossa...",
),
SearchVideo(
id: "Jri4_9vBFiQ",
title: "Top 100 Best Classic Rock Songs Of All Time 🔥 R.E.M, Queen, Metallica,Guns N Roses,Bon Jovi, U2,CCR",
length: 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: 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: 192,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "ll4d5Lt-Ie8",
title: "Relaxing Music Healing Stress, Anxiety and Depressive States Heal Mind, Body and Soul | Sleep music",
length: 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: 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: 5363904,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "Dx2wbKLokuQ",
title: "W. Putin: Die Sehnsucht nach dem Imperium | Mit offenen Karten | ARTE",
length: 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: 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: 539838,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "jfKfPfyJRdk",
title: "lofi hip hop radio - beats to relax/study to",
length: 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: 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: 21262,
is_live: false,
is_short: false,
short_description: "🤗 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...",
),
SearchVideo(
id: "qmrzTUmZ4UU",
title: "850€ für den Verrat am System - UCS AT-AT LEGO® Star Wars 75313",
length: 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: 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: 600150,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "t0Q2otsqC4I",
title: "Tom & Jerry | Tom & Jerry in Full Screen | Classic Cartoon Compilation | WB Kids",
length: 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: 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: 252381571,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "zE-a5eqvlv8",
title: "Dua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me",
length: 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: 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: 955,
is_live: false,
is_short: false,
short_description: "#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...",
),
SearchVideo(
id: "HxCcKzRAGWk",
title: "(Music for Man ) Relaxing Whiskey Blues Music - Modern Electric Guitar Blues - JAZZ & BLUES",
length: 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: 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: 3156236,
is_live: false,
is_short: false,
short_description: "-----------------------------------------------------------------------------------\n✔Thanks for watching! Have a nice day!\n✔Don\'t forget LIKE - SHARE - COMMENT\n#bluesmusic#slowblues#bluesrock...",
),
SearchVideo(
id: "HlHYOdZePSE",
title: "Healing Music for Anxiety Disorders, Fears, Depression and Eliminate Negative Thoughts",
length: 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: 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: 1585,
is_live: false,
is_short: false,
short_description: "Healing Music for Anxiety Disorders, Fears, Depression and Eliminate Negative Thoughts\n#HealingMusic #RelaxingMusic #TranquilMusic\n__________________________________\nMusic for:\nChakra healing....",
),
SearchVideo(
id: "CJ2AH3LJeic",
title: "Coldplay Greatest Hits Full Album 2022 New Songs of Coldplay 2022",
length: 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: 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: 5595965,
is_live: false,
is_short: false,
short_description: "▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\nSubscribe channel for more videos:\n🔔Subscribe: https://bit.ly/2UbIZFv\n⚡Facebook: https://bitly.com.vn/gXDsC...",
),
SearchVideo(
id: "KJwzKxQ81iA",
title: "Handmade Candy Making Collection / 수제 사탕 만들기 모음 / Korean Candy Store",
length: 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: 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: 3127238,
is_live: false,
is_short: false,
short_description: "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"),
)

View file

@ -0,0 +1,859 @@
---
source: src/client/trends.rs
expression: map_res.c
---
Paginator(
count: None,
items: [
SearchVideo(
id: "mRmlXh7Hams",
title: "Extra 3 vom 12.10.2022 im NDR | extra 3 | NDR",
length: Some(1839),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/mRmlXh7Hams/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAbO4lI0dDo_r85A1fi9XQS0rNiOQ",
width: 480,
height: 270,
),
],
channel: 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: 585257,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "LsXC5r64Pvc",
title: "Most Rarest Plays In Baseball History",
length: Some(1975),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/LsXC5r64Pvc/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB2KXmgKxrJVUy3Naqi_R-R2X92FA",
width: 480,
height: 270,
),
],
channel: 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: 985521,
is_live: false,
is_short: false,
short_description: "#baseball #mlb #mlbb",
),
SearchVideo(
id: "dwPmd1GqQHE",
title: "90S RAP & HIPHOP MIX - Notorious B I G , Dr Dre, 50 Cent, Snoop Dogg, 2Pac, DMX, Lil Jon and more",
length: Some(5457),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/dwPmd1GqQHE/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAAyGcLGzFkfdEmqqohpxZsGOM9Kw",
width: 480,
height: 270,
),
],
channel: 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: 1654055,
is_live: false,
is_short: false,
short_description: "",
),
SearchVideo(
id: "qxI-Ob8lpLE",
title: "Schlatt\'s Chips Tier List",
length: 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: 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: 9029628,
is_live: false,
is_short: false,
short_description: "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:...",
),
SearchVideo(
id: "qmrzTUmZ4UU",
title: "850€ für den Verrat am System - UCS AT-AT LEGO® Star Wars 75313",
length: 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: 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: 600516,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "4q4vpQCIZ6w",
title: "🌉 Manhattan Jazz 💖 l Relaxing Jazz Piano Music l Background Music",
length: 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: 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: 2343407,
is_live: false,
is_short: false,
short_description: "- 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...",
),
SearchVideo(
id: "Z_k31kqZxaE",
title: "1 in 1,000,000 NBA Moments",
length: 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: 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: 4334298,
is_live: false,
is_short: false,
short_description: "• 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...",
),
SearchVideo(
id: "zE-a5eqvlv8",
title: "Dua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me",
length: 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: 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: 889,
is_live: false,
is_short: false,
short_description: "#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...",
),
SearchVideo(
id: "gNlOk0LXi5M",
title: "Soll ich dir 1g GOLD schenken? oder JEMAND anderen DOPPELT?",
length: 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: 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: 463834,
is_live: false,
is_short: false,
short_description: "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",
),
SearchVideo(
id: "dbMvZjs8Yc8",
title: "Brad Pitt- Die Revanche eines Sexsymbols | Doku HD | ARTE",
length: 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: 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: 293878,
is_live: false,
is_short: false,
short_description: "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....",
),
SearchVideo(
id: "mFxi3lOAcFs",
title: "Craziest Soviet Machines You Won\'t Believe Exist - Part 1",
length: 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: 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: 14056843,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "eu7ubm7g59E",
title: "People Hated Me For Using This Slab",
length: 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: 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: 2845035,
is_live: false,
is_short: false,
short_description: "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:...",
),
SearchVideo(
id: "TRGHIN2PGIA",
title: "Christian Bale Breaks Down His Most Iconic Characters | GQ",
length: 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: 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: 8044465,
is_live: false,
is_short: false,
short_description: "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,\'...",
),
SearchVideo(
id: "w3tENzcssDU",
title: "NFL Trick Plays But They Get Increasingly Higher IQ",
length: 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: 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: 1172372,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "gUAd2XXzH7w",
title: "⚓\u{fe0f}Found ABANDONED SHIP!!! Big CRUISE SHIP on a desert island☠\u{fe0f} Where did the people go?!?",
length: 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: 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: 1883533,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "YpGjaJ1ettI",
title: "[Working BGM] Comfortable music that makes you feel positive -- Morning Mood -- Daily Routine",
length: 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: 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: 1465389,
is_live: false,
is_short: false,
short_description: "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 😊😊😊...",
),
SearchVideo(
id: "rPAhFD8hKxQ",
title: "Survival Camping 9ft/3m Under Snow - Giant Winter Bushcraft Shelter and Quinzee",
length: 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: 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: 20488431,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "2rye4u-cCNk",
title: "Pink Panther Fights Off Pests | 54 Minute Compilation | The Pink Panther Show",
length: 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: 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: 27357653,
is_live: false,
is_short: false,
short_description: "(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...",
),
SearchVideo(
id: "O0xAlfSaBNQ",
title: "FC Nantes vs. SC Freiburg Highlights & Tore | UEFA Europa League",
length: 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: 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: 117395,
is_live: false,
is_short: false,
short_description: "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 ...",
),
SearchVideo(
id: "Mhs9Sbnw19o",
title: "Dramatisches Duell: 400 Jahre altes Kästchen erzielt zig-fachen Wunschpreis! | Bares für Rares XXL",
length: 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: 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: 836333,
is_live: false,
is_short: false,
short_description: "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...",
),
SearchVideo(
id: "Bzzp5Cay7DI",
title: "Sweet Jazz - Cool autumn Bossa Nova & October Jazz Positive Mood",
length: 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: 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: 1216,
is_live: false,
is_short: false,
short_description: "Sweet Jazz - Cool autumn Bossa Nova & October Jazz Positive Mood\nhttps://youtu.be/Bzzp5Cay7DI\n********************************************\nSounds available on: Jazz Bossa Nova\nOFFICIAL VIDEO:...",
),
SearchVideo(
id: "SlskTqc9CEc",
title: "The Chick-Fil-A Full Menu Challenge",
length: 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: 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: 39286403,
is_live: false,
is_short: false,
short_description: "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+...",
),
SearchVideo(
id: "CwRvM2TfYbs",
title: "Gentle healing music of health and to calm the nervous system, deep relaxation! Say Life Yes",
length: 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: 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: 1416,
is_live: false,
is_short: false,
short_description: "🌿 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...",
),
SearchVideo(
id: "7jz0pXSe_kI",
title: "Craziest \"Fine...I\'ll Do it Myself\" Moments in Sports History (PART 2)",
length: 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: 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: 11601863,
is_live: false,
is_short: false,
short_description: "(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"),
)

File diff suppressed because it is too large Load diff

View file

@ -12,11 +12,13 @@ VideoDetails(
url: "https://smarturl.it/aespa_BlackMamba", url: "https://smarturl.it/aespa_BlackMamba",
), ),
Text("\n🐍The Debut Stage "), Text("\n🐍The Debut Stage "),
Video( YouTube(
text: "aespa 에스파 \'Black ...", text: "aespa 에스파 \'Black ...",
target: Video(
id: "Ky5RT5oGg0w", id: "Ky5RT5oGg0w",
start_time: 0, start_time: 0,
), ),
),
Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "), Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "),
Web( Web(
text: "https://www.ticketmaster.com/event/0A...", text: "https://www.ticketmaster.com/event/0A...",

View file

@ -12,11 +12,13 @@ VideoDetails(
url: "https://smarturl.it/aespa_BlackMamba", url: "https://smarturl.it/aespa_BlackMamba",
), ),
Text("\n🐍The Debut Stage "), Text("\n🐍The Debut Stage "),
Video( YouTube(
text: "https://youtu.be/Ky5RT5oGg0w", text: "https://youtu.be/Ky5RT5oGg0w",
target: Video(
id: "Ky5RT5oGg0w", id: "Ky5RT5oGg0w",
start_time: 0, start_time: 0,
), ),
),
Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "), Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "),
Web( Web(
text: "https://www.ticketmaster.com/event/0A...", text: "https://www.ticketmaster.com/event/0A...",

View file

@ -96,11 +96,13 @@ VideoDetails(
Text("\n\nMUSIC CREDIT\n"), Text("\n\nMUSIC CREDIT\n"),
Text("-------------------------------------------------"), Text("-------------------------------------------------"),
Text("\nIntro: Laszlo - Supernova\nVideo Link: "), Text("\nIntro: Laszlo - Supernova\nVideo Link: "),
Video( YouTube(
text: "https://www.youtube.com/watch?v=PKfxm...", text: "https://www.youtube.com/watch?v=PKfxm...",
target: Video(
id: "PKfxmFU3lWY", id: "PKfxmFU3lWY",
start_time: 0, start_time: 0,
), ),
),
Text("\niTunes Download Link: "), Text("\niTunes Download Link: "),
Web( Web(
text: "https://itunes.apple.com/us/album/sup...", text: "https://itunes.apple.com/us/album/sup...",
@ -112,11 +114,13 @@ VideoDetails(
url: "https://soundcloud.com/laszlomusic", url: "https://soundcloud.com/laszlomusic",
), ),
Text("\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: "), Text("\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: "),
Video( YouTube(
text: "https://www.youtube.com/watch?v=ngsGB...", text: "https://www.youtube.com/watch?v=ngsGB...",
target: Video(
id: "ngsGBSCDwcI", id: "ngsGBSCDwcI",
start_time: 0, start_time: 0,
), ),
),
Text("\nListen on Spotify: "), Text("\nListen on Spotify: "),
Web( Web(
text: "http://spoti.fi/UxWkUw", text: "http://spoti.fi/UxWkUw",
@ -150,89 +154,117 @@ VideoDetails(
Text("\n\nCHAPTERS\n"), Text("\n\nCHAPTERS\n"),
Text("-------------------------------------------------"), Text("-------------------------------------------------"),
Text("\n"), Text("\n"),
Video( YouTube(
text: "0:00", text: "0:00",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 0, start_time: 0,
), ),
),
Text(" Intro\n"), Text(" Intro\n"),
Video( YouTube(
text: "0:42", text: "0:42",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 42, start_time: 42,
), ),
),
Text(" The PC Built for Super Efficiency\n"), Text(" The PC Built for Super Efficiency\n"),
Video( YouTube(
text: "2:41", text: "2:41",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 161, start_time: 161,
), ),
),
Text(" Our BURIAL ENCLOSURE?!\n"), Text(" Our BURIAL ENCLOSURE?!\n"),
Video( YouTube(
text: "3:31", text: "3:31",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 211, start_time: 211,
), ),
),
Text(" Our Power Solution (Thanks Jackery!)\n"), Text(" Our Power Solution (Thanks Jackery!)\n"),
Video( YouTube(
text: "4:47", text: "4:47",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 287, start_time: 287,
), ),
),
Text(" Diggin\' Holes\n"), Text(" Diggin\' Holes\n"),
Video( YouTube(
text: "5:30", text: "5:30",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 330, start_time: 330,
), ),
),
Text(" Colonoscopy?\n"), Text(" Colonoscopy?\n"),
Video( YouTube(
text: "7:04", text: "7:04",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 424, start_time: 424,
), ),
),
Text(" Diggin\' like a man\n"), Text(" Diggin\' like a man\n"),
Video( YouTube(
text: "8:29", text: "8:29",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 509, start_time: 509,
), ),
),
Text(" The world\'s worst woodsman\n"), Text(" The world\'s worst woodsman\n"),
Video( YouTube(
text: "9:03", text: "9:03",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 543, start_time: 543,
), ),
),
Text(" Backyard cable management\n"), Text(" Backyard cable management\n"),
Video( YouTube(
text: "10:02", text: "10:02",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 602, start_time: 602,
), ),
),
Text(" Time to bury this boy\n"), Text(" Time to bury this boy\n"),
Video( YouTube(
text: "10:46", text: "10:46",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 646, start_time: 646,
), ),
),
Text(" Solar Power Generation\n"), Text(" Solar Power Generation\n"),
Video( YouTube(
text: "11:37", text: "11:37",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 697, start_time: 697,
), ),
),
Text(" Issues\n"), Text(" Issues\n"),
Video( YouTube(
text: "12:08", text: "12:08",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 728, start_time: 728,
), ),
),
Text(" First Play Test\n"), Text(" First Play Test\n"),
Video( YouTube(
text: "13:20", text: "13:20",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 800, start_time: 800,
), ),
),
Text(" Conclusion"), Text(" Conclusion"),
]), ]),
channel: ChannelTag( channel: ChannelTag(

View file

@ -92,11 +92,13 @@ VideoDetails(
url: "https://www.twitch.tv/linustech", url: "https://www.twitch.tv/linustech",
), ),
Text("\n\nMUSIC CREDIT\n---------------------------------------------------\nIntro: Laszlo - Supernova\nVideo Link: "), Text("\n\nMUSIC CREDIT\n---------------------------------------------------\nIntro: Laszlo - Supernova\nVideo Link: "),
Video( YouTube(
text: "https://www.youtube.com/watch?v=PKfxm...", text: "https://www.youtube.com/watch?v=PKfxm...",
target: Video(
id: "PKfxmFU3lWY", id: "PKfxmFU3lWY",
start_time: 0, start_time: 0,
), ),
),
Text("\niTunes Download Link: "), Text("\niTunes Download Link: "),
Web( Web(
text: "https://itunes.apple.com/us/album/sup...", text: "https://itunes.apple.com/us/album/sup...",
@ -108,11 +110,13 @@ VideoDetails(
url: "https://soundcloud.com/laszlomusic", url: "https://soundcloud.com/laszlomusic",
), ),
Text("\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: "), Text("\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: "),
Video( YouTube(
text: "https://www.youtube.com/watch?v=ngsGB...", text: "https://www.youtube.com/watch?v=ngsGB...",
target: Video(
id: "ngsGBSCDwcI", id: "ngsGBSCDwcI",
start_time: 0, start_time: 0,
), ),
),
Text("\nListen on Spotify: "), Text("\nListen on Spotify: "),
Web( Web(
text: "http://spoti.fi/UxWkUw", text: "http://spoti.fi/UxWkUw",
@ -144,89 +148,117 @@ VideoDetails(
url: "https://geni.us/Ps3XfE", url: "https://geni.us/Ps3XfE",
), ),
Text("\n\nCHAPTERS\n---------------------------------------------------\n"), Text("\n\nCHAPTERS\n---------------------------------------------------\n"),
Video( YouTube(
text: "0:00", text: "0:00",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 0, start_time: 0,
), ),
),
Text(" Intro\n"), Text(" Intro\n"),
Video( YouTube(
text: "0:42", text: "0:42",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 42, start_time: 42,
), ),
),
Text(" The PC Built for Super Efficiency\n"), Text(" The PC Built for Super Efficiency\n"),
Video( YouTube(
text: "2:41", text: "2:41",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 161, start_time: 161,
), ),
),
Text(" Our BURIAL ENCLOSURE?!\n"), Text(" Our BURIAL ENCLOSURE?!\n"),
Video( YouTube(
text: "3:31", text: "3:31",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 211, start_time: 211,
), ),
),
Text(" Our Power Solution (Thanks Jackery!)\n"), Text(" Our Power Solution (Thanks Jackery!)\n"),
Video( YouTube(
text: "4:47", text: "4:47",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 287, start_time: 287,
), ),
),
Text(" Diggin\' Holes\n"), Text(" Diggin\' Holes\n"),
Video( YouTube(
text: "5:30", text: "5:30",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 330, start_time: 330,
), ),
),
Text(" Colonoscopy?\n"), Text(" Colonoscopy?\n"),
Video( YouTube(
text: "7:04", text: "7:04",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 424, start_time: 424,
), ),
),
Text(" Diggin\' like a man\n"), Text(" Diggin\' like a man\n"),
Video( YouTube(
text: "8:29", text: "8:29",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 509, start_time: 509,
), ),
),
Text(" The world\'s worst woodsman\n"), Text(" The world\'s worst woodsman\n"),
Video( YouTube(
text: "9:03", text: "9:03",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 543, start_time: 543,
), ),
),
Text(" Backyard cable management\n"), Text(" Backyard cable management\n"),
Video( YouTube(
text: "10:02", text: "10:02",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 602, start_time: 602,
), ),
),
Text(" Time to bury this boy\n"), Text(" Time to bury this boy\n"),
Video( YouTube(
text: "10:46", text: "10:46",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 646, start_time: 646,
), ),
),
Text(" Solar Power Generation\n"), Text(" Solar Power Generation\n"),
Video( YouTube(
text: "11:37", text: "11:37",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 697, start_time: 697,
), ),
),
Text(" Issues\n"), Text(" Issues\n"),
Video( YouTube(
text: "12:08", text: "12:08",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 728, start_time: 728,
), ),
),
Text(" First Play Test\n"), Text(" First Play Test\n"),
Video( YouTube(
text: "13:20", text: "13:20",
target: Video(
id: "nFDBxBUfE74", id: "nFDBxBUfE74",
start_time: 800, start_time: 800,
), ),
),
Text(" Conclusion"), Text(" Conclusion"),
]), ]),
channel: ChannelTag( channel: ChannelTag(

View file

@ -7,11 +7,13 @@ VideoDetails(
title: "🌎 Nasa Live Stream - Earth From Space : Live Views from the ISS", title: "🌎 Nasa Live Stream - Earth From Space : Live Views from the ISS",
description: RichText([ description: RichText([
Text("Live NASA - Views Of Earth from Space\nLive video feed of Earth from the International Space Station (ISS) Cameras\n-----------------------------------------------------------------------------------------------------\nWatch our latest video - The Sun - 4K Video / Solar Flares\n"), Text("Live NASA - Views Of Earth from Space\nLive video feed of Earth from the International Space Station (ISS) Cameras\n-----------------------------------------------------------------------------------------------------\nWatch our latest video - The Sun - 4K Video / Solar Flares\n"),
Video( YouTube(
text: "https://www.youtube.com/watch?v=SEzK4...", text: "https://www.youtube.com/watch?v=SEzK4...",
target: Video(
id: "SEzK4ZfMvUQ", id: "SEzK4ZfMvUQ",
start_time: 0, start_time: 0,
), ),
),
Text("\n-----------------------------------------------------------------------------------------------------\nNasa ISS live stream from aboard the International Space Station as it circles the earth at 240 miles above the planet, on the edge of space in low earth orbit. \n\nThe station is crewed by NASA astronauts as well as Russian Cosmonauts and a mixture of Japanese, Canadian and European astronauts as well.\n\n"), Text("\n-----------------------------------------------------------------------------------------------------\nNasa ISS live stream from aboard the International Space Station as it circles the earth at 240 miles above the planet, on the edge of space in low earth orbit. \n\nThe station is crewed by NASA astronauts as well as Russian Cosmonauts and a mixture of Japanese, Canadian and European astronauts as well.\n\n"),
Text("#nasalive"), Text("#nasalive"),
Text(" "), Text(" "),

View file

@ -12,11 +12,13 @@ VideoDetails(
url: "https://smarturl.it/aespa_BlackMamba", url: "https://smarturl.it/aespa_BlackMamba",
), ),
Text("\n🐍The Debut Stage "), Text("\n🐍The Debut Stage "),
Video( YouTube(
text: "https://youtu.be/Ky5RT5oGg0w", text: "https://youtu.be/Ky5RT5oGg0w",
target: Video(
id: "Ky5RT5oGg0w", id: "Ky5RT5oGg0w",
start_time: 0, start_time: 0,
), ),
),
Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "), Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "),
Web( Web(
text: "https://www.ticketmaster.com/event/0A...", text: "https://www.ticketmaster.com/event/0A...",

View file

@ -1,13 +1,14 @@
use crate::{ use crate::{
error::{Error, ExtractionError}, error::{Error, ExtractionError},
model::{Paginator, SearchVideo}, model::{Paginator, SearchVideo},
param::Language,
serializer::MapResult, serializer::MapResult,
util::TryRemove, util::TryRemove,
}; };
use super::{ use super::{
response::{self, TryFromWLang}, response::{self, TryFromWLang},
ClientType, MapResponse, QBrowse, RustyPipeQuery, ClientType, MapResponse, QBrowse, QContinuation, RustyPipeQuery,
}; };
impl RustyPipeQuery { impl RustyPipeQuery {
@ -28,6 +29,32 @@ impl RustyPipeQuery {
.await .await
} }
pub async fn startpage_continuation(
self,
ctoken: &str,
visitor_data: &str,
) -> Result<Paginator<SearchVideo>, Error> {
let mut context = self.get_context(ClientType::Desktop, true).await;
context.client.visitor_data = Some(visitor_data.to_owned());
let request_body = QContinuation {
context,
continuation: ctoken,
};
self.execute_request::<response::StartpageCont, _, _>(
ClientType::Desktop,
"startpage_continuation",
ctoken,
"browse",
&request_body,
)
.await
.map(|res| Paginator {
visitor_data: Some(visitor_data.to_owned()),
..res
})
}
pub async fn trending(self) -> Result<Vec<SearchVideo>, Error> { pub async fn trending(self) -> Result<Vec<SearchVideo>, Error> {
let context = self.get_context(ClientType::Desktop, true).await; let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QBrowse { let request_body = QBrowse {
@ -62,35 +89,29 @@ impl MapResponse<Paginator<SearchVideo>> for response::Startpage {
.rich_grid_renderer .rich_grid_renderer
.contents; .contents;
let mut warnings = grid.warnings; Ok(map_startpage_videos(
let mut ctoken = None; grid,
let items = grid lang,
.c self.response_context.visitor_data,
.into_iter() ))
.filter_map(|item| match item {
response::VideoListItem::RichItemRenderer {
content: response::RichItem::VideoRenderer(video),
} => match SearchVideo::from_w_lang(video, lang) {
Ok(video) => Some(video),
Err(e) => {
warnings.push(e.to_string());
None
} }
}, }
response::VideoListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
ctoken = Some(continuation_endpoint.continuation_command.token);
None
}
_ => None,
})
.collect();
Ok(MapResult { impl MapResponse<Paginator<SearchVideo>> for response::StartpageCont {
c: Paginator::new(None, items, ctoken), fn map_response(
warnings, self,
}) _id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<Paginator<SearchVideo>>, ExtractionError> {
let mut received_actions = self.on_response_received_actions;
let items = received_actions
.try_swap_remove(0)
.ok_or_else(|| ExtractionError::InvalidData("no contents".into()))?
.append_continuation_items_action
.continuation_items;
Ok(map_startpage_videos(items, lang, None))
} }
} }
@ -146,3 +167,117 @@ impl MapResponse<Vec<SearchVideo>> for response::Trending {
Ok(MapResult { c: items, warnings }) Ok(MapResult { c: items, warnings })
} }
} }
fn map_startpage_videos(
videos: MapResult<Vec<response::VideoListItem>>,
lang: Language,
visitor_data: Option<String>,
) -> MapResult<Paginator<SearchVideo>> {
let mut warnings = videos.warnings;
let mut ctoken = None;
let items = videos
.c
.into_iter()
.filter_map(|item| match item {
response::VideoListItem::RichItemRenderer {
content: response::RichItem::VideoRenderer(video),
} => match SearchVideo::from_w_lang(video, lang) {
Ok(video) => Some(video),
Err(e) => {
warnings.push(e.to_string());
None
}
},
response::VideoListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
ctoken = Some(continuation_endpoint.continuation_command.token);
None
}
_ => None,
})
.collect();
MapResult {
c: Paginator::new_with_vdata(None, items, ctoken, visitor_data),
warnings,
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
use crate::{
client::{response, MapResponse},
model::{Paginator, SearchVideo},
param::Language,
serializer::MapResult,
};
#[test]
fn map_startpage() {
let filename = "testfiles/trends/startpage.json";
let json_path = Path::new(&filename);
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<SearchVideo>> =
startpage.map_response("", Language::En, None).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]",
});
}
#[test]
fn map_startpage_cont() {
let filename = "testfiles/trends/startpage_cont.json";
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let startpage: response::StartpageCont =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<SearchVideo>> =
startpage.map_response("", Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!("map_startpage_cont", map_res.c, {
".items[].publish_date" => "[date]",
});
}
#[test]
fn map_trending() {
let filename = "testfiles/trends/trending.json";
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let startpage: response::Trending =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<SearchVideo>> =
startpage.map_response("", Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!("map_trending", map_res.c, {
"[].publish_date" => "[date]",
});
}
}

208
src/client/url_resolver.rs Normal file
View file

@ -0,0 +1,208 @@
use serde::Serialize;
use crate::{
error::{Error, ExtractionError},
model::UrlTarget,
param::Language,
serializer::MapResult,
util,
};
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QResolveUrl {
context: YTContext,
url: String,
}
impl RustyPipeQuery {
pub async fn resolve_url(self, url: &str) -> Result<UrlTarget, Error> {
let (url, params) = util::url_to_params(url)?;
let mut is_shortlink = url.domain().and_then(|d| match d {
"youtu.be" => Some(true),
"youtube.com" => Some(false),
_ => None,
});
let mut path_split = url
.path_segments()
.ok_or_else(|| Error::Other("invalid url: empty path".into()))?;
let get_start_time = || {
params
.get("t")
.and_then(|t| t.parse::<u32>().ok())
.unwrap_or_default()
};
let target = match path_split.next() {
Some("watch") => {
let id = params
.get("v")
.ok_or_else(|| Error::Other("invalid url: no video id".into()))?
.to_string();
Ok(UrlTarget::Video {
id,
start_time: get_start_time(),
})
}
Some("channel") => match path_split.next() {
Some(id) => Ok(UrlTarget::Channel { id: id.to_owned() }),
None => Err(Error::Other("invalid url: no channel id".into())),
},
Some("playlist") => {
let id = params
.get("list")
.ok_or_else(|| Error::Other("invalid url: no playlist id".into()))?
.to_string();
Ok(UrlTarget::Playlist { id })
}
// Channel vanity URL or youtu.be shortlink
Some(mut id) => {
if id == "c" || id == "user" {
id = path_split.next().unwrap_or(id);
is_shortlink = Some(false);
}
if id.is_empty() || id == "user" {
return Err(Error::Other(
"invalid url: no channel name / video id".into(),
));
}
match is_shortlink {
Some(true) => {
// youtu.be shortlink (e.g. youtu.be/gHzuabZUd6c)
Ok(UrlTarget::Video {
id: id.to_owned(),
start_time: get_start_time(),
})
}
Some(false) => {
// Vanity URL (e.g. youtube.com/LinusTechTips) has to be resolved by the Innertube API
self._navigation_resolve_url(url.path()).await
}
None => {
// We dont have the original YT domain, so this can be both
// If there is a timestamp parameter, it has to be a video
// First check the innertube API if this is a channel vanity url
// If no channel is found and the identifier has the video ID format, assume it is a video
if !params.contains_key("t")
&& util::VANITY_PATH_REGEX
.is_match(url.path())
.unwrap_or_default()
{
match self._navigation_resolve_url(url.path()).await {
Ok(target) => Ok(target),
Err(Error::Extraction(ExtractionError::ContentUnavailable(e))) => {
match util::VIDEO_ID_REGEX.is_match(id).unwrap_or_default() {
true => Ok(UrlTarget::Video {
id: id.to_owned(),
start_time: get_start_time(),
}),
false => Err(Error::Extraction(
ExtractionError::ContentUnavailable(e),
)),
}
}
Err(e) => Err(e),
}
} else if util::VIDEO_ID_REGEX.is_match(id).unwrap_or_default() {
Ok(UrlTarget::Video {
id: id.to_owned(),
start_time: get_start_time(),
})
} else {
Err(Error::Other("invalid video / channel id".into()))
}
}
}
}
None => Err(Error::Other("invalid url: empty path".into())),
}?;
target.validate()?;
Ok(target)
}
pub async fn resolve_string(self, string: &str) -> Result<UrlTarget, Error> {
// URL with protocol
if string.starts_with("http://") || string.starts_with("https://") {
self.resolve_url(string).await
}
// URL without protocol
else if string.contains('/') && string.contains('.') {
self.resolve_url(&format!("https://{}", string)).await
}
// ID only
else if util::VIDEO_ID_REGEX.is_match(string).unwrap_or_default() {
Ok(UrlTarget::Video {
id: string.to_owned(),
start_time: 0,
})
} else if util::CHANNEL_ID_REGEX.is_match(string).unwrap_or_default() {
Ok(UrlTarget::Channel {
id: string.to_owned(),
})
} else if util::PLAYLIST_ID_REGEX.is_match(string).unwrap_or_default() {
Ok(UrlTarget::Playlist {
id: string.to_owned(),
})
}
// Channel name only
else if util::VANITY_PATH_REGEX.is_match(string).unwrap_or_default() {
self._navigation_resolve_url(&format!("/{}", string.trim_start_matches('/')))
.await
} else {
Err(Error::Other("invalid input string".into()))
}
}
async fn _navigation_resolve_url(&self, url_path: &str) -> Result<UrlTarget, Error> {
let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QResolveUrl {
context,
url: format!("https://www.youtube.com{}", url_path),
};
self.execute_request::<response::ResolvedUrl, _, _>(
ClientType::Desktop,
"channel_id",
&request_body.url,
"navigation/resolve_url",
&request_body,
)
.await
}
}
impl MapResponse<UrlTarget> for response::ResolvedUrl {
fn map_response(
self,
_id: &str,
_lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<UrlTarget>, ExtractionError> {
let page_type = self
.endpoint
.command_metadata
.ok_or_else(|| ExtractionError::InvalidData("No command metadata".into()))?
.web_command_metadata
.web_page_type;
let id = self
.endpoint
.browse_endpoint
.ok_or_else(|| ExtractionError::InvalidData("No browse ID".into()))?
.browse_id;
Ok(MapResult {
c: page_type.to_url_target(id),
warnings: Vec::new(),
})
}
}

View file

@ -239,7 +239,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
page_type, page_type,
browse_id, browse_id,
} => match page_type { } => match page_type {
crate::serializer::text::PageType::Channel => (browse_id, text), response::url_endpoint::PageType::Channel => (browse_id, text),
_ => { _ => {
return Err(ExtractionError::InvalidData( return Err(ExtractionError::InvalidData(
"invalid channel link type".into(), "invalid channel link type".into(),

View file

@ -88,7 +88,9 @@ async fn download_single_file<P: Into<PathBuf>>(
// If the url is from googlevideo, extract file size from clen parameter // If the url is from googlevideo, extract file size from clen parameter
let (url_base, url_params) = let (url_base, url_params) =
util::url_to_params(url).map_err(|e| DownloadError::Other(e.to_string().into()))?; util::url_to_params(url).map_err(|e| DownloadError::Other(e.to_string().into()))?;
let is_gvideo = url_base.ends_with(".googlevideo.com/videoplayback"); let is_gvideo = url_base
.as_str()
.ends_with(".googlevideo.com/videoplayback");
if is_gvideo { if is_gvideo {
size = url_params.get("clen").and_then(|s| s.parse::<u64>().ok()); size = url_params.get("clen").and_then(|s| s.parse::<u64>().ok());
} }

View file

@ -11,6 +11,8 @@ use std::ops::Range;
use chrono::{DateTime, Local, Utc}; use chrono::{DateTime, Local, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{error::Error, util};
use self::richtext::RichText; use self::richtext::RichText;
/* /*
@ -26,6 +28,64 @@ pub struct Thumbnail {
pub height: u32, pub height: u32,
} }
/// Entities extracted from a YouTube URL
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum UrlTarget {
Video { id: String, start_time: u32 },
Channel { id: String },
Playlist { id: String },
}
impl ToString for UrlTarget {
fn to_string(&self) -> String {
self.to_url()
}
}
impl UrlTarget {
pub fn to_url(&self) -> String {
self.to_url_yt_host("https://www.youtube.com")
}
pub fn to_url_yt_host(&self, yt_host: &str) -> String {
match self {
UrlTarget::Video { id, start_time, .. } => match start_time {
0 => format!("{}/watch?v={}", yt_host, id),
n => format!("{}/watch?v={}&t={}s", yt_host, id, n),
},
UrlTarget::Channel { id } => {
format!("{}/channel/{}", yt_host, id)
}
UrlTarget::Playlist { id } => {
format!("{}/playlist?list={}", yt_host, id)
}
}
}
pub(crate) fn validate(&self) -> Result<(), Error> {
match self {
UrlTarget::Video { id, .. } => {
match util::VIDEO_ID_REGEX.is_match(id).unwrap_or_default() {
true => Ok(()),
false => Err(Error::Other("invalid video id".into())),
}
}
UrlTarget::Channel { id } => {
match util::CHANNEL_ID_REGEX.is_match(id).unwrap_or_default() {
true => Ok(()),
false => Err(Error::Other("invalid channel id".into())),
}
}
UrlTarget::Playlist { id } => {
match util::PLAYLIST_ID_REGEX.is_match(id).unwrap_or_default() {
true => Ok(()),
false => Err(Error::Other("invalid playlist id".into())),
}
}
}
}
}
/* /*
#PLAYER #PLAYER
*/ */

View file

@ -28,6 +28,9 @@ pub struct Paginator<T> {
/// ///
/// If it is None, it means that no more items can be fetched. /// If it is None, it means that no more items can be fetched.
pub ctoken: Option<String>, pub ctoken: Option<String>,
/// YouTube visitor data. Required for fetching the startpage
#[serde(skip_serializing_if = "Option::is_none")]
pub visitor_data: Option<String>,
} }
impl<T> Default for Paginator<T> { impl<T> Default for Paginator<T> {
@ -36,12 +39,22 @@ impl<T> Default for Paginator<T> {
count: Some(0), count: Some(0),
items: Vec::new(), items: Vec::new(),
ctoken: None, ctoken: None,
visitor_data: None,
} }
} }
} }
impl<T> Paginator<T> { impl<T> Paginator<T> {
pub(crate) fn new(count: Option<u64>, items: Vec<T>, ctoken: Option<String>) -> Self { pub(crate) fn new(count: Option<u64>, items: Vec<T>, ctoken: Option<String>) -> Self {
Self::new_with_vdata(count, items, ctoken, None)
}
pub(crate) fn new_with_vdata(
count: Option<u64>,
items: Vec<T>,
ctoken: Option<String>,
visitor_data: Option<String>,
) -> Self {
Self { Self {
count: match ctoken { count: match ctoken {
Some(_) => count, Some(_) => count,
@ -49,6 +62,7 @@ impl<T> Paginator<T> {
}, },
items, items,
ctoken, ctoken,
visitor_data,
} }
} }

View file

@ -2,6 +2,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::UrlTarget;
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]
pub struct RichText(pub Vec<TextComponent>); pub struct RichText(pub Vec<TextComponent>);
@ -13,20 +15,8 @@ pub enum TextComponent {
Text(String), Text(String),
/// Web link /// Web link
Web { text: String, url: String }, Web { text: String, url: String },
/// Link to a YouTube video /// Link to a YouTube entity
Video { YouTube { text: String, target: UrlTarget },
text: String,
id: String,
start_time: u32,
},
/// Link to a YouTube channel
Channel { text: String, id: String },
/// Link to a YouTube playlist
Playlist { text: String, id: String },
/// Link to a YouTube Music artist
Artist { text: String, id: String },
/// Link to a YouTube Music album
Album { text: String, id: String },
} }
/// Trait for converting rich text to plain text. /// Trait for converting rich text to plain text.
@ -60,11 +50,7 @@ impl TextComponent {
match self { match self {
TextComponent::Text(text) => text, TextComponent::Text(text) => text,
TextComponent::Web { text, .. } => text, TextComponent::Web { text, .. } => text,
TextComponent::Video { text, .. } => text, TextComponent::YouTube { text, .. } => text,
TextComponent::Channel { text, .. } => text,
TextComponent::Playlist { text, .. } => text,
TextComponent::Artist { text, .. } => text,
TextComponent::Album { text, .. } => text,
} }
} }
@ -72,16 +58,7 @@ impl TextComponent {
match self { match self {
TextComponent::Text(_) => "".to_owned(), TextComponent::Text(_) => "".to_owned(),
TextComponent::Web { url, .. } => url.to_owned(), TextComponent::Web { url, .. } => url.to_owned(),
TextComponent::Video { id, start_time, .. } => match start_time { TextComponent::YouTube { target, .. } => target.to_url_yt_host(yt_host),
0 => format!("{}/watch?v={}", yt_host, id),
n => format!("{}/watch?v={}&t={}s", yt_host, id, n),
},
TextComponent::Channel { id, .. } | TextComponent::Artist { id, .. } => {
format!("{}/channel/{}", yt_host, id)
}
TextComponent::Playlist { id, .. } | TextComponent::Album { id, .. } => {
format!("{}/playlist?list={}", yt_host, id)
}
} }
} }
} }

View file

@ -3,9 +3,13 @@ use std::convert::TryFrom;
use fancy_regex::Regex; use fancy_regex::Regex;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use serde_with::{serde_as, DefaultOnError, DeserializeAs}; use serde_with::{serde_as, DeserializeAs};
use crate::util; use crate::{
client::response::url_endpoint::{NavigationEndpoint, PageType},
model::UrlTarget,
util,
};
/// # Text /// # Text
/// ///
@ -146,84 +150,6 @@ struct AttributedTextOnTap {
innertube_command: NavigationEndpoint, innertube_command: NavigationEndpoint,
} }
#[serde_as]
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct NavigationEndpoint {
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
watch_endpoint: Option<WatchEndpoint>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
browse_endpoint: Option<BrowseEndpoint>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
url_endpoint: Option<UrlEndpoint>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
command_metadata: Option<CommandMetadata>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct WatchEndpoint {
video_id: String,
#[serde(default)]
start_time_seconds: u32,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct BrowseEndpoint {
browse_id: String,
browse_endpoint_context_supported_configs: Option<BrowseEndpointConfig>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UrlEndpoint {
url: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct BrowseEndpointConfig {
browse_endpoint_context_music_config: BrowseEndpointMusicConfig,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct BrowseEndpointMusicConfig {
page_type: PageType,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CommandMetadata {
web_command_metadata: WebCommandMetadata,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct WebCommandMetadata {
web_page_type: PageType,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
pub enum PageType {
#[serde(rename = "MUSIC_PAGE_TYPE_ARTIST")]
Artist,
#[serde(rename = "MUSIC_PAGE_TYPE_ALBUM")]
Album,
#[serde(
rename = "MUSIC_PAGE_TYPE_USER_CHANNEL",
alias = "WEB_PAGE_TYPE_CHANNEL"
)]
Channel,
#[serde(rename = "MUSIC_PAGE_TYPE_PLAYLIST", alias = "WEB_PAGE_TYPE_PLAYLIST")]
Playlist,
}
impl From<RichTextRun> for TextComponent { impl From<RichTextRun> for TextComponent {
fn from(run: RichTextRun) -> Self { fn from(run: RichTextRun) -> Self {
map_text_component(run.text, run.navigation_endpoint) map_text_component(run.text, run.navigation_endpoint)
@ -387,32 +313,20 @@ impl From<TextComponent> for crate::model::richtext::TextComponent {
text, text,
video_id, video_id,
start_time, start_time,
} => Self::Video { } => Self::YouTube {
text, text,
target: UrlTarget::Video {
id: video_id, id: video_id,
start_time, start_time,
}, },
},
TextComponent::Browse { TextComponent::Browse {
text, text,
page_type, page_type,
browse_id, browse_id,
} => match page_type { } => Self::YouTube {
PageType::Artist => Self::Artist {
text, text,
id: browse_id, target: page_type.to_url_target(browse_id),
},
PageType::Album => Self::Album {
text,
id: browse_id,
},
PageType::Channel => Self::Channel {
text,
id: browse_id,
},
PageType::Playlist => Self::Playlist {
text,
id: browse_id,
},
}, },
TextComponent::Web { text, url } => Self::Web { TextComponent::Web { text, url } => Self::Web {
text, text,

View file

@ -17,6 +17,14 @@ use url::Url;
use crate::{error::Error, param::Language}; use crate::{error::Error, param::Language};
pub static VIDEO_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[A-Za-z0-9_-]{11}$").unwrap());
pub static CHANNEL_ID_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^UC[A-Za-z0-9_-]{22}$").unwrap());
pub static PLAYLIST_ID_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?:PL|RD)[A-Za-z0-9_-]{30,}$").unwrap());
pub static VANITY_PATH_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^/?(?:(?:c\/|user\/)?[A-z0-9]+)|(?:@[A-z0-9-_.]+)$").unwrap());
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] = const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@ -57,7 +65,7 @@ pub fn generate_content_playback_nonce() -> String {
/// Example: /// Example:
/// ///
/// `example.com/api?k1=v1&k2=v2 => example.com/api; {k1: v1, k2: v2}` /// `example.com/api?k1=v1&k2=v2 => example.com/api; {k1: v1, k2: v2}`
pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>), Error> { pub fn url_to_params(url: &str) -> Result<(Url, BTreeMap<String, String>), Error> {
let mut parsed_url = Url::parse(url) let mut parsed_url = Url::parse(url)
.map_err(|e| Error::Other(format!("could not parse url `{}` err: {}", url, e).into()))?; .map_err(|e| Error::Other(format!("could not parse url `{}` err: {}", url, e).into()))?;
let url_params: BTreeMap<String, String> = parsed_url let url_params: BTreeMap<String, String> = parsed_url
@ -67,7 +75,7 @@ pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>), Er
parsed_url.set_query(None); parsed_url.set_query(None);
Ok((parsed_url.to_string(), url_params)) Ok((parsed_url, url_params))
} }
pub fn urlencode(string: &str) -> String { pub fn urlencode(string: &str) -> String {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@ use rustypipe::client::{ClientType, RustyPipe};
use rustypipe::error::{Error, ExtractionError}; use rustypipe::error::{Error, ExtractionError};
use rustypipe::model::richtext::ToPlaintext; use rustypipe::model::richtext::ToPlaintext;
use rustypipe::model::{ use rustypipe::model::{
AudioCodec, AudioFormat, Channel, SearchItem, Verification, VideoCodec, VideoFormat, AudioCodec, AudioFormat, Channel, SearchItem, UrlTarget, Verification, VideoCodec, VideoFormat,
}; };
use rustypipe::param::{ use rustypipe::param::{
search_filter::{self, SearchFilter}, search_filter::{self, SearchFilter},
@ -1205,6 +1205,66 @@ async fn search_suggestion_empty() {
assert!(result.is_empty()); assert!(result.is_empty());
} }
//#URL RESOLVER
#[rstest]
#[case("https://www.youtube.com/LinusTechTips", UrlTarget::Channel {id: "UCXuqSBlHAE6Xw-yeJA0Tunw".to_owned()})]
#[case("https://www.youtube.com/@AndroidAuthority", UrlTarget::Channel {id: "UCgyqtNWZmIxTx3b6OxTSALw".to_owned()})]
#[case("https://www.youtube.com/channel/UC5I2hjZYiW9gZPVkvzM8_Cw", UrlTarget::Channel {id: "UC5I2hjZYiW9gZPVkvzM8_Cw".to_owned()})]
#[case("https://www.youtube.com/c", UrlTarget::Channel {id: "UCXE6F2oZzy_6xEXiJiUFo2w".to_owned()})]
#[case("https://www.youtube.com/user/MrBeast6000", UrlTarget::Channel {id: "UCX6OQ3DkcsbYNE6H8uQQuVA".to_owned()})]
#[case("https://www.youtube.com/watch?v=dQw4w9WgXcQ", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0})]
#[case("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=60", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 60})]
#[case("https://www.youtube.com/playlist?list=PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI", UrlTarget::Playlist {id: "PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI".to_owned()})]
#[case("https://www.youtube.com/playlist?list=RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk", UrlTarget::Playlist {id: "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk".to_owned()})]
#[case("https://youtu.be/dQw4w9WgXcQ", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0})]
#[case("https://youtu.be/dQw4w9WgXcQ?t=60", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 60})]
#[case("https://youtu.be/dQw4w9WgXcQ", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0})]
#[case("https://youtu.be/dQw4w9WgXcQ?t=60", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 60})]
#[case("https://piped.mha.fi/watch?v=dQw4w9WgXcQ", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0})]
// Both a video ID and a channel name => returns channel
#[case("https://piped.mha.fi/dQw4w9WgXcQ", UrlTarget::Channel {id: "UCoG6BrhgmivrkcbEHcYtK4Q".to_owned()})]
// Both a video ID and a channel name + video time param => returns video
#[case("https://piped.mha.fi/dQw4w9WgXcQ?t=0", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0})]
#[tokio::test]
async fn resolve_url(#[case] url: &str, #[case] expect: UrlTarget) {
let rp = RustyPipe::builder().strict().build();
let target = rp.query().resolve_url(url).await.unwrap();
assert_eq!(target, expect);
}
#[rstest]
#[case("LinusTechTips", UrlTarget::Channel {id: "UCXuqSBlHAE6Xw-yeJA0Tunw".to_owned()})]
#[case("@AndroidAuthority", UrlTarget::Channel {id: "UCgyqtNWZmIxTx3b6OxTSALw".to_owned()})]
#[case("UC5I2hjZYiW9gZPVkvzM8_Cw", UrlTarget::Channel {id: "UC5I2hjZYiW9gZPVkvzM8_Cw".to_owned()})]
#[case("c", UrlTarget::Channel {id: "UCXE6F2oZzy_6xEXiJiUFo2w".to_owned()})]
#[case("user/MrBeast6000", UrlTarget::Channel {id: "UCX6OQ3DkcsbYNE6H8uQQuVA".to_owned()})]
#[case("@AndroidAuthority", UrlTarget::Channel {id: "UCgyqtNWZmIxTx3b6OxTSALw".to_owned()})]
#[case("dQw4w9WgXcQ", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0})]
#[case("PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI", UrlTarget::Playlist {id: "PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI".to_owned()})]
#[case("RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk", UrlTarget::Playlist {id: "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk".to_owned()})]
#[tokio::test]
async fn resolve_string(#[case] string: &str, #[case] expect: UrlTarget) {
let rp = RustyPipe::builder().strict().build();
let target = rp.query().resolve_string(string).await.unwrap();
assert_eq!(target, expect);
}
#[tokio::test]
async fn resolve_channel_not_found() {
let rp = RustyPipe::builder().strict().build();
let err = rp
.query()
.resolve_url("https://www.youtube.com/feeqegnhq3rkwghjq43ruih43io3")
.await
.unwrap_err();
assert!(matches!(
err,
Error::Extraction(ExtractionError::ContentUnavailable(_))
));
}
//#TRENDS //#TRENDS
#[tokio::test] #[tokio::test]
@ -1213,21 +1273,36 @@ async fn startpage() {
let result = rp.query().startpage().await.unwrap(); let result = rp.query().startpage().await.unwrap();
assert!( assert!(
result.items.len() > 20, result.items.len() >= 20,
"expected > 20 items, got {}", "expected >= 20 items, got {}",
result.items.len() result.items.len()
); );
assert!(!result.is_exhausted()); assert!(!result.is_exhausted());
} }
#[tokio::test]
async fn startpage_cont() {
let rp = RustyPipe::builder().strict().build();
let startpage = rp.query().startpage().await.unwrap();
let next = startpage.next(rp.query()).await.unwrap().unwrap();
assert!(
next.items.len() >= 20,
"expected >= 20 items, got {}",
next.items.len()
);
assert!(!next.is_exhausted());
}
#[tokio::test] #[tokio::test]
async fn trending() { async fn trending() {
let rp = RustyPipe::builder().strict().build(); let rp = RustyPipe::builder().strict().build();
let result = rp.query().trending().await.unwrap(); let result = rp.query().trending().await.unwrap();
assert!( assert!(
result.len() > 50, result.len() >= 50,
"expected > 50 items, got {}", "expected >= 50 items, got {}",
result.len() result.len()
); );
} }