feat: add history item dates, extend timeago parser
This commit is contained in:
parent
65ada37214
commit
320a8c2c24
28 changed files with 6507 additions and 2160 deletions
|
|
@ -7,11 +7,15 @@ use crate::{
|
|||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
ChannelItem, VideoItem,
|
||||
ChannelItem, HistoryItem, VideoItem,
|
||||
},
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
||||
use self::response::YouTubeListMapper;
|
||||
|
||||
use super::{MapRespOptions, QContinuation};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QHistorySearch<'a> {
|
||||
|
|
@ -24,7 +28,7 @@ impl RustyPipeQuery {
|
|||
///
|
||||
/// Requires authentication cookies.
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn history(&self) -> Result<Paginator<VideoItem>, Error> {
|
||||
pub async fn history(&self) -> Result<Paginator<HistoryItem<VideoItem>>, Error> {
|
||||
let request_body = QBrowse {
|
||||
browse_id: "FEhistory",
|
||||
};
|
||||
|
|
@ -41,6 +45,34 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get more YouTube history items from the given continuation token
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn history_continuation<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
ctoken: S,
|
||||
visitor_data: Option<&str>,
|
||||
) -> Result<Paginator<HistoryItem<VideoItem>>, Error> {
|
||||
let ctoken = ctoken.as_ref();
|
||||
let request_body = QContinuation {
|
||||
continuation: ctoken,
|
||||
};
|
||||
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.execute_request_ctx::<response::Continuation, _, _>(
|
||||
ClientType::Desktop,
|
||||
"history_continuation",
|
||||
ctoken,
|
||||
"browse",
|
||||
&request_body,
|
||||
MapRespOptions {
|
||||
visitor_data,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Search the YouTube playback history of the current user
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
|
|
@ -48,7 +80,7 @@ impl RustyPipeQuery {
|
|||
pub async fn history_search<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
query: S,
|
||||
) -> Result<Paginator<VideoItem>, Error> {
|
||||
) -> Result<Paginator<HistoryItem<VideoItem>>, Error> {
|
||||
let query = query.as_ref();
|
||||
let request_body = QHistorySearch {
|
||||
browse_id: "FEhistory",
|
||||
|
|
@ -104,6 +136,65 @@ impl RustyPipeQuery {
|
|||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::History {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<HistoryItem<VideoItem>>>, ExtractionError> {
|
||||
let items = self
|
||||
.contents
|
||||
.two_column_browse_results_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData(
|
||||
"twoColumnBrowseResultsRenderer empty".into(),
|
||||
))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut map_res = MapResult {
|
||||
warnings: items.warnings,
|
||||
..Default::default()
|
||||
};
|
||||
let mut ctoken = None;
|
||||
for item in items.c {
|
||||
match item {
|
||||
response::YouTubeListItem::ItemSectionRenderer { header, contents } => {
|
||||
let mut mapper = YouTubeListMapper::<VideoItem>::new(ctx.lang);
|
||||
mapper.map_response(contents);
|
||||
mapper.conv_history_items(
|
||||
header.map(|h| h.item_section_header_renderer.title),
|
||||
&mut map_res,
|
||||
);
|
||||
}
|
||||
response::YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
if ctoken.is_none() {
|
||||
ctoken = Some(continuation_endpoint.continuation_command.token);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
crate::model::paginator::ContinuationEndpoint::Browse,
|
||||
true,
|
||||
),
|
||||
warnings: map_res.warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<VideoItem>> for response::History {
|
||||
fn map_response(
|
||||
self,
|
||||
|
|
@ -131,7 +222,7 @@ impl MapResponse<Paginator<VideoItem>> for response::History {
|
|||
None,
|
||||
mapper.items,
|
||||
mapper.ctoken,
|
||||
None,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
crate::model::paginator::ContinuationEndpoint::Browse,
|
||||
true,
|
||||
),
|
||||
|
|
@ -145,29 +236,47 @@ mod tests {
|
|||
use std::{fs::File, io::BufReader};
|
||||
|
||||
use path_macro::path;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::util::tests::TESTFILES;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::history("history")]
|
||||
#[case::subscription_feed("subscription_feed")]
|
||||
fn map_history(#[case] name: &str) {
|
||||
let json_path = path!(*TESTFILES / "history" / format!("{name}.json"));
|
||||
#[test]
|
||||
fn map_history() {
|
||||
let json_path = path!(*TESTFILES / "history" / "history.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let history: response::History =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res = history.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let map_res: MapResult<Paginator<HistoryItem<VideoItem>>> =
|
||||
history.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(format!("map_{name}"), map_res.c, {
|
||||
insta::assert_ron_snapshot!(map_res.c, {
|
||||
".items[].playback_date" => "[date]",
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_subscription_feed() {
|
||||
let json_path = path!(*TESTFILES / "history" / "subscription_feed.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let history: response::History =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<VideoItem>> =
|
||||
history.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(map_res.c, {
|
||||
".items[].publish_date" => "[date]",
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -811,6 +811,16 @@ impl RustyPipeBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Enable authentication for all requests
|
||||
///
|
||||
/// Depending on the client type RustyPipe uses either the authentication cookie or the
|
||||
/// OAuth token to authenticate requests.
|
||||
#[must_use]
|
||||
pub fn authenticated(mut self) -> Self {
|
||||
self.default_opts.auth = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable authentication for all requests
|
||||
#[must_use]
|
||||
pub fn unauthenticated(mut self) -> Self {
|
||||
|
|
|
|||
|
|
@ -113,12 +113,12 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
|
|||
});
|
||||
|
||||
let mapped_top = mapper_top.conv_items::<TrackItem>();
|
||||
let mut mapped_trending = mapper_trending.conv_items::<TrackItem>();
|
||||
let mut mapped_other = mapper_other.group_items();
|
||||
let mapped_trending = mapper_trending.conv_items::<TrackItem>();
|
||||
let mapped_other = mapper_other.group_items();
|
||||
|
||||
let mut warnings = mapped_top.warnings;
|
||||
warnings.append(&mut mapped_trending.warnings);
|
||||
warnings.append(&mut mapped_other.warnings);
|
||||
warnings.extend(mapped_trending.warnings);
|
||||
warnings.extend(mapped_other.warnings);
|
||||
|
||||
Ok(MapResult {
|
||||
c: MusicCharts {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
response::{self, music_item::MusicListMapper},
|
||||
|
|
@ -6,19 +8,19 @@ use crate::{
|
|||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
AlbumItem, ArtistItem, MusicPlaylistItem, TrackItem,
|
||||
AlbumItem, ArtistItem, HistoryItem, MusicPlaylistItem, TrackItem,
|
||||
},
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
||||
use super::MapRespCtx;
|
||||
use super::{MapRespCtx, MapRespOptions, QContinuation};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a list of tracks from YouTube Music which the current user recently played
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_history(&self) -> Result<Paginator<TrackItem>, Error> {
|
||||
pub async fn music_history(&self) -> Result<Paginator<HistoryItem<TrackItem>>, Error> {
|
||||
let request_body = QBrowseParams {
|
||||
browse_id: "FEmusic_history",
|
||||
params: "oggECgIIAQ%3D%3D",
|
||||
|
|
@ -36,6 +38,34 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get more YouTube Music history items from the given continuation token
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_history_continuation<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
ctoken: S,
|
||||
visitor_data: Option<&str>,
|
||||
) -> Result<Paginator<HistoryItem<TrackItem>>, Error> {
|
||||
let ctoken = ctoken.as_ref();
|
||||
let request_body = QContinuation {
|
||||
continuation: ctoken,
|
||||
};
|
||||
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.execute_request_ctx::<response::MusicContinuation, _, _>(
|
||||
ClientType::Desktop,
|
||||
"history_continuation",
|
||||
ctoken,
|
||||
"browse",
|
||||
&request_body,
|
||||
MapRespOptions {
|
||||
visitor_data,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get a list of YouTube Music artists which the current user subscribed to
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
|
|
@ -99,11 +129,11 @@ impl RustyPipeQuery {
|
|||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<TrackItem>> for response::MusicHistory {
|
||||
impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicHistory {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
|
||||
) -> Result<MapResult<Paginator<HistoryItem<TrackItem>>>, ExtractionError> {
|
||||
let contents = match self.contents {
|
||||
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => {
|
||||
c.contents
|
||||
|
|
@ -120,7 +150,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicHistory {
|
|||
} => secondary_contents.section_list_renderer,
|
||||
};
|
||||
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
let mut map_res = MapResult::default();
|
||||
|
||||
for shelf in contents.contents {
|
||||
let shelf = if let response::music_item::ItemSection::MusicShelfRenderer(s) = shelf {
|
||||
|
|
@ -128,11 +158,11 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicHistory {
|
|||
} else {
|
||||
continue;
|
||||
};
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
mapper.map_response(shelf.contents);
|
||||
mapper.conv_history_items(shelf.title, &mut map_res);
|
||||
}
|
||||
|
||||
let map_res = mapper.conv_items();
|
||||
|
||||
let ctoken = contents
|
||||
.continuations
|
||||
.into_iter()
|
||||
|
|
@ -144,7 +174,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicHistory {
|
|||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
None,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
true,
|
||||
),
|
||||
|
|
@ -177,6 +207,8 @@ mod tests {
|
|||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(map_res.c);
|
||||
insta::assert_ron_snapshot!(map_res.c, {
|
||||
".items[].playback_date" => "[date]",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ use crate::model::{
|
|||
traits::FromYtItem,
|
||||
Comment, MusicItem, YouTubeItem,
|
||||
};
|
||||
use crate::model::{HistoryItem, TrackItem, VideoItem};
|
||||
use crate::serializer::MapResult;
|
||||
|
||||
use self::response::YouTubeListItem;
|
||||
|
||||
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo};
|
||||
use super::{
|
||||
response, ClientType, MapRespCtx, MapRespOptions, MapResponse, QContinuation, RustyPipeQuery,
|
||||
|
|
@ -95,38 +98,44 @@ fn map_ytm_paginator<T: FromYtItem>(
|
|||
}
|
||||
}
|
||||
|
||||
fn continuation_items(response: response::Continuation) -> MapResult<Vec<YouTubeListItem>> {
|
||||
response
|
||||
.on_response_received_actions
|
||||
.and_then(|actions| {
|
||||
actions
|
||||
.into_iter()
|
||||
.map(|action| action.append_continuation_items_action.continuation_items)
|
||||
.reduce(|mut acc, mut items| {
|
||||
acc.c.append(&mut items.c);
|
||||
acc.warnings.append(&mut items.warnings);
|
||||
acc
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
response
|
||||
.continuation_contents
|
||||
.map(|contents| contents.rich_grid_continuation.contents)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
|
||||
let items = self
|
||||
.on_response_received_actions
|
||||
.and_then(|actions| {
|
||||
actions
|
||||
.into_iter()
|
||||
.map(|action| action.append_continuation_items_action.continuation_items)
|
||||
.reduce(|mut acc, mut items| {
|
||||
acc.c.append(&mut items.c);
|
||||
acc.warnings.append(&mut items.warnings);
|
||||
acc
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
self.continuation_contents
|
||||
.map(|contents| contents.rich_grid_continuation.contents)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let estimated_results = self.estimated_results;
|
||||
let items = continuation_items(self);
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
self.estimated_results,
|
||||
estimated_results,
|
||||
mapper.items,
|
||||
mapper.ctoken,
|
||||
None,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::Browse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
|
|
@ -201,7 +210,99 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
|||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
warnings: map_res.warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::Continuation {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<HistoryItem<VideoItem>>>, ExtractionError> {
|
||||
let mut map_res = MapResult::default();
|
||||
let mut ctoken = None;
|
||||
|
||||
let items = continuation_items(self);
|
||||
for item in items.c {
|
||||
match item {
|
||||
response::YouTubeListItem::ItemSectionRenderer { header, contents } => {
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
|
||||
mapper.map_response(contents);
|
||||
mapper.conv_history_items(
|
||||
header.map(|h| h.item_section_header_renderer.title),
|
||||
&mut map_res,
|
||||
);
|
||||
}
|
||||
response::YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
if ctoken.is_none() {
|
||||
ctoken = Some(continuation_endpoint.continuation_command.token);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::Browse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
warnings: map_res.warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicContinuation {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<HistoryItem<TrackItem>>>, ExtractionError> {
|
||||
let mut map_res = MapResult::default();
|
||||
let mut continuations = Vec::new();
|
||||
|
||||
let mut map_shelf = |shelf: response::music_item::MusicShelf| {
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
mapper.map_response(shelf.contents);
|
||||
mapper.conv_history_items(shelf.title, &mut map_res);
|
||||
continuations.extend(shelf.continuations);
|
||||
};
|
||||
|
||||
match self.continuation_contents {
|
||||
Some(response::music_item::ContinuationContents::MusicShelfContinuation(shelf)) => {
|
||||
map_shelf(shelf);
|
||||
}
|
||||
Some(response::music_item::ContinuationContents::SectionListContinuation(contents)) => {
|
||||
for c in contents.contents {
|
||||
if let response::music_item::ItemSection::MusicShelfRenderer(shelf) = c {
|
||||
map_shelf(shelf);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let ctoken = continuations
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|cont| cont.next_continuation_data.continuation);
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
|
|
@ -213,11 +314,6 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
|||
impl<T: FromYtItem> Paginator<T> {
|
||||
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
|
||||
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
|
||||
// let mut q = query.as_ref().clone();
|
||||
// if self.authenticated {
|
||||
// q = q.authenticated();
|
||||
// }
|
||||
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => {
|
||||
let q = if self.authenticated {
|
||||
|
|
@ -319,6 +415,36 @@ impl Paginator<Comment> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Paginator<HistoryItem<VideoItem>> {
|
||||
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
|
||||
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => Some(
|
||||
query
|
||||
.as_ref()
|
||||
.history_continuation(ctoken, self.visitor_data.as_deref())
|
||||
.await?,
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Paginator<HistoryItem<TrackItem>> {
|
||||
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
|
||||
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => Some(
|
||||
query
|
||||
.as_ref()
|
||||
.music_history_continuation(ctoken, self.visitor_data.as_deref())
|
||||
.await?,
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! paginator {
|
||||
($entity_type:ty) => {
|
||||
impl Paginator<$entity_type> {
|
||||
|
|
@ -400,6 +526,8 @@ macro_rules! paginator {
|
|||
}
|
||||
|
||||
paginator!(Comment);
|
||||
paginator!(HistoryItem<VideoItem>);
|
||||
paginator!(HistoryItem<TrackItem>);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
|
|
|||
|
|
@ -209,6 +209,14 @@ pub(crate) struct TextBox {
|
|||
pub text: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SimpleHeaderRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkip
|
|||
use crate::{
|
||||
model::{
|
||||
self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId,
|
||||
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem,
|
||||
HistoryItem, MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem,
|
||||
},
|
||||
param::Language,
|
||||
serializer::{
|
||||
|
|
@ -18,7 +18,7 @@ use super::{
|
|||
url_endpoint::{
|
||||
BrowseEndpointWrap, MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, PageType,
|
||||
},
|
||||
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
|
||||
ContentsRenderer, MusicContinuationData, SimpleHeaderRenderer, Thumbnails, ThumbnailsWrap,
|
||||
};
|
||||
|
||||
#[serde_as]
|
||||
|
|
@ -39,6 +39,8 @@ pub(crate) enum ItemSection {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicShelf {
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub title: Option<String>,
|
||||
/// Playlist ID (only for playlists)
|
||||
pub playlist_id: Option<String>,
|
||||
pub contents: MapResult<Vec<MusicResponseItem>>,
|
||||
|
|
@ -396,15 +398,7 @@ pub(crate) struct GridRenderer {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct GridHeader {
|
||||
pub grid_header_renderer: GridHeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct GridHeaderRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
pub grid_header_renderer: SimpleHeaderRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -419,14 +413,6 @@ pub(crate) struct SimpleHeader {
|
|||
pub music_header_renderer: SimpleHeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SimpleHeaderRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum TrackBadge {
|
||||
|
|
@ -1257,6 +1243,26 @@ impl MusicListMapper {
|
|||
warnings: self.warnings,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn conv_history_items(
|
||||
self,
|
||||
date_txt: Option<String>,
|
||||
res: &mut MapResult<Vec<HistoryItem<TrackItem>>>,
|
||||
) {
|
||||
res.warnings.extend(self.warnings);
|
||||
res.c.extend(
|
||||
self.items
|
||||
.into_iter()
|
||||
.filter_map(TrackItem::from_ytm_item)
|
||||
.map(|item| HistoryItem {
|
||||
item,
|
||||
playback_date: date_txt.as_deref().and_then(|s| {
|
||||
timeago::parse_textual_date_to_d(self.lang, s, &mut res.warnings)
|
||||
}),
|
||||
playback_date_txt: date_txt.clone(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Map TextComponents containing artist names to a list of artists and a 'Various Artists' flag
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ use serde_with::{
|
|||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::{ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, Thumbnails};
|
||||
use super::{
|
||||
ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, SimpleHeaderRenderer,
|
||||
Thumbnails,
|
||||
};
|
||||
use crate::{
|
||||
model::{Channel, ChannelItem, ChannelTag, PlaylistItem, VideoItem, YouTubeItem},
|
||||
model::{Channel, ChannelItem, ChannelTag, HistoryItem, PlaylistItem, VideoItem, YouTubeItem},
|
||||
param::Language,
|
||||
serializer::{
|
||||
text::{AttributedText, Text, TextComponent},
|
||||
|
|
@ -63,6 +66,7 @@ pub(crate) enum YouTubeListItem {
|
|||
/// GridRenderer: contains videos on channel page
|
||||
#[serde(alias = "expandedShelfContentsRenderer", alias = "gridRenderer")]
|
||||
ItemSectionRenderer {
|
||||
header: Option<ItemSectionHeader>,
|
||||
#[serde(alias = "items")]
|
||||
contents: MapResult<Vec<YouTubeListItem>>,
|
||||
},
|
||||
|
|
@ -294,6 +298,12 @@ pub(crate) struct YouTubeListRenderer {
|
|||
pub contents: MapResult<Vec<YouTubeListItem>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ItemSectionHeader {
|
||||
pub item_section_header_renderer: SimpleHeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -833,7 +843,7 @@ impl YouTubeListMapper<YouTubeItem> {
|
|||
YouTubeListItem::RichItemRenderer { content } => {
|
||||
self.map_item(*content);
|
||||
}
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents } => {
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents, .. } => {
|
||||
self.warnings.append(&mut contents.warnings);
|
||||
contents.c.into_iter().for_each(|it| self.map_item(it));
|
||||
}
|
||||
|
|
@ -881,7 +891,7 @@ impl YouTubeListMapper<VideoItem> {
|
|||
YouTubeListItem::RichItemRenderer { content } => {
|
||||
self.map_item(*content);
|
||||
}
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents } => {
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents, .. } => {
|
||||
self.warnings.append(&mut contents.warnings);
|
||||
contents.c.into_iter().for_each(|it| self.map_item(it));
|
||||
}
|
||||
|
|
@ -893,6 +903,23 @@ impl YouTubeListMapper<VideoItem> {
|
|||
self.warnings.append(&mut res.warnings);
|
||||
res.c.into_iter().for_each(|item| self.map_item(item));
|
||||
}
|
||||
|
||||
pub(crate) fn conv_history_items(
|
||||
self,
|
||||
date_txt: Option<String>,
|
||||
res: &mut MapResult<Vec<HistoryItem<VideoItem>>>,
|
||||
) {
|
||||
res.warnings.extend(self.warnings);
|
||||
res.c.extend(self.items.into_iter().map(|item| {
|
||||
HistoryItem {
|
||||
item,
|
||||
playback_date: date_txt.as_deref().and_then(|s| {
|
||||
timeago::parse_textual_date_to_d(self.lang, s, &mut res.warnings)
|
||||
}),
|
||||
playback_date_txt: date_txt.clone(),
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl YouTubeListMapper<PlaylistItem> {
|
||||
|
|
@ -916,7 +943,7 @@ impl YouTubeListMapper<PlaylistItem> {
|
|||
YouTubeListItem::RichItemRenderer { content } => {
|
||||
self.map_item(*content);
|
||||
}
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents } => {
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents, .. } => {
|
||||
self.warnings.append(&mut contents.warnings);
|
||||
contents.c.into_iter().for_each(|it| self.map_item(it));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,240 +5,260 @@ expression: map_res.c
|
|||
Paginator(
|
||||
count: None,
|
||||
items: [
|
||||
VideoItem(
|
||||
id: "mPshy_DWxfo",
|
||||
name: "trying TWEENING everything! (FAILED) PLEASE GIVE ME SOME ADVICEEE",
|
||||
duration: Some(6),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mPshy_DWxfo/hqdefault.jpg?sqp=-oaymwFACKgBEF5IWvKriqkDMwgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAHwAQH4Af4JgALOBYoCDAgAEAEYfyAyKEAwDw==&rs=AOn4CLBfBVk2IGdGGGmpqOir2RbC8cY1xw",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mPshy_DWxfo/hqdefault.jpg?sqp=-oaymwFACMQBEG5IWvKriqkDMwgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAHwAQH4Af4JgALOBYoCDAgAEAEYfyAyKEAwDw==&rs=AOn4CLDnRYKBX4qMlA54i-q3W7w1WvGApg",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mPshy_DWxfo/hqdefault.jpg?sqp=-oaymwFBCPYBEIoBSFryq4qpAzMIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB8AEB-AH-CYACzgWKAgwIABABGH8gMihAMA8=&rs=AOn4CLDza_6r3345q6SBZvGm292mOobNPg",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mPshy_DWxfo/hqdefault.jpg?sqp=-oaymwFBCNACELwBSFryq4qpAzMIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB8AEB-AH-CYACzgWKAgwIABABGH8gMihAMA8=&rs=AOn4CLDySwxxAy2hfw2YcAKs6ERLhzPTkQ",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCM7OXM6t80a3e3tzQDWxwEA",
|
||||
name: "Ari",
|
||||
avatar: [
|
||||
HistoryItem(
|
||||
item: VideoItem(
|
||||
id: "mPshy_DWxfo",
|
||||
name: "trying TWEENING everything! (FAILED) PLEASE GIVE ME SOME ADVICEEE",
|
||||
duration: Some(6),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/TpeTKFR6QWu4Cjam4PcpQwCPMnammWnSg93CdBvgFFLhkGm4nbQkUFKaAIYJ1ChUy9IgmJIQMRg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
url: "https://i.ytimg.com/vi/mPshy_DWxfo/hqdefault.jpg?sqp=-oaymwFACKgBEF5IWvKriqkDMwgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAHwAQH4Af4JgALOBYoCDAgAEAEYfyAyKEAwDw==&rs=AOn4CLBfBVk2IGdGGGmpqOir2RbC8cY1xw",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mPshy_DWxfo/hqdefault.jpg?sqp=-oaymwFACMQBEG5IWvKriqkDMwgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAHwAQH4Af4JgALOBYoCDAgAEAEYfyAyKEAwDw==&rs=AOn4CLDnRYKBX4qMlA54i-q3W7w1WvGApg",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mPshy_DWxfo/hqdefault.jpg?sqp=-oaymwFBCPYBEIoBSFryq4qpAzMIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB8AEB-AH-CYACzgWKAgwIABABGH8gMihAMA8=&rs=AOn4CLDza_6r3345q6SBZvGm292mOobNPg",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mPshy_DWxfo/hqdefault.jpg?sqp=-oaymwFBCNACELwBSFryq4qpAzMIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB8AEB-AH-CYACzgWKAgwIABABGH8gMihAMA8=&rs=AOn4CLDySwxxAy2hfw2YcAKs6ERLhzPTkQ",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
verification: none,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(15),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCM7OXM6t80a3e3tzQDWxwEA",
|
||||
name: "Ari",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/TpeTKFR6QWu4Cjam4PcpQwCPMnammWnSg93CdBvgFFLhkGm4nbQkUFKaAIYJ1ChUy9IgmJIQMRg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: none,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: None,
|
||||
publish_date_txt: None,
|
||||
view_count: Some(15),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
playback_date: "[date]",
|
||||
playback_date_txt: Some("Yesterday"),
|
||||
),
|
||||
VideoItem(
|
||||
id: "SRWatgS077k",
|
||||
name: "My Time at \"Camp Operetta\"",
|
||||
duration: Some(578),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SRWatgS077k/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAN8mzi3fbrJgqJiEeqpMZXRa7AuQ",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SRWatgS077k/hqdefault.jpg?sqp=-oaymwEmCMQBEG5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLBgxonLRY-4QQ1-jR3Xen-fAZcHHQ",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SRWatgS077k/hqdefault.jpg?sqp=-oaymwEnCPYBEIoBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLBOk1abznwO5Bm0_m5YXMFkU0JSog",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SRWatgS077k/hqdefault.jpg?sqp=-oaymwEnCNACELwBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLCQt7cAJuE-W8t1TnQnSe5EVbsw8A",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCGwu0nbY2wSkW8N-cghnLpA",
|
||||
name: "JaidenAnimations",
|
||||
avatar: [
|
||||
HistoryItem(
|
||||
item: VideoItem(
|
||||
id: "SRWatgS077k",
|
||||
name: "My Time at \"Camp Operetta\"",
|
||||
duration: Some(578),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/gopbHeiDtEB932rIFqLlR4D_hFtd-BcdGrQgGeyDpkD3guskkbT74DsJYPGo3x7MqkyqtgL-=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
url: "https://i.ytimg.com/vi/SRWatgS077k/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAN8mzi3fbrJgqJiEeqpMZXRa7AuQ",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SRWatgS077k/hqdefault.jpg?sqp=-oaymwEmCMQBEG5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLBgxonLRY-4QQ1-jR3Xen-fAZcHHQ",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SRWatgS077k/hqdefault.jpg?sqp=-oaymwEnCPYBEIoBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLBOk1abznwO5Bm0_m5YXMFkU0JSog",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SRWatgS077k/hqdefault.jpg?sqp=-oaymwEnCNACELwBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLCQt7cAJuE-W8t1TnQnSe5EVbsw8A",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(23907328),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("What can I say other than that was one heck of a time\n\nScribble Showdown Tickets: https://www.scribbleshowdown.com/\n\n\n♥ The Team ♥\nDenny: https://www.instagram.com/90percentknuckles/\nAtrox:..."),
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCGwu0nbY2wSkW8N-cghnLpA",
|
||||
name: "JaidenAnimations",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/gopbHeiDtEB932rIFqLlR4D_hFtd-BcdGrQgGeyDpkD3guskkbT74DsJYPGo3x7MqkyqtgL-=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: None,
|
||||
publish_date_txt: None,
|
||||
view_count: Some(23907328),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("What can I say other than that was one heck of a time\n\nScribble Showdown Tickets: https://www.scribbleshowdown.com/\n\n\n♥ The Team ♥\nDenny: https://www.instagram.com/90percentknuckles/\nAtrox:..."),
|
||||
),
|
||||
playback_date: "[date]",
|
||||
playback_date_txt: Some("Yesterday"),
|
||||
),
|
||||
VideoItem(
|
||||
id: "kTxlkDoqArA",
|
||||
name: "Wie Cartoons Früher gemacht wurden!",
|
||||
duration: Some(283),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/kTxlkDoqArA/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLBYdDA06ekKDlhB0PSlTwf6Ih1cMg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/kTxlkDoqArA/hqdefault.jpg?sqp=-oaymwEmCMQBEG5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAgu_Ad1pFCsa3jINV1ocaVOQWOXg",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/kTxlkDoqArA/hqdefault.jpg?sqp=-oaymwEnCPYBEIoBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLDkOVQbyZlrZ_jbdkSzUd5RiobObA",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/kTxlkDoqArA/hqdefault.jpg?sqp=-oaymwEnCNACELwBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLA5cnUH03I2lg1-FOJ01njh8UOJEw",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCxTHCMaxURhapisCMBv8y0A",
|
||||
name: "Plankton",
|
||||
avatar: [
|
||||
HistoryItem(
|
||||
item: VideoItem(
|
||||
id: "kTxlkDoqArA",
|
||||
name: "Wie Cartoons Früher gemacht wurden!",
|
||||
duration: Some(283),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Cdlsy3IXgis5hNYRwvohPB9AIxH8tNdEo9CwxXK1i3QEUO7YN3p4YJ_cd5ruGsmNhvoX7803=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
url: "https://i.ytimg.com/vi/kTxlkDoqArA/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLBYdDA06ekKDlhB0PSlTwf6Ih1cMg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/kTxlkDoqArA/hqdefault.jpg?sqp=-oaymwEmCMQBEG5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAgu_Ad1pFCsa3jINV1ocaVOQWOXg",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/kTxlkDoqArA/hqdefault.jpg?sqp=-oaymwEnCPYBEIoBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLDkOVQbyZlrZ_jbdkSzUd5RiobObA",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/kTxlkDoqArA/hqdefault.jpg?sqp=-oaymwEnCNACELwBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLA5cnUH03I2lg1-FOJ01njh8UOJEw",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(390010),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Folgt mir auf Instagram!\nhttps://instagram.com/plankton.gif \n\nÜBER DEN KANAL:\nRede viel wenn der Tag lang ist"),
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCxTHCMaxURhapisCMBv8y0A",
|
||||
name: "Plankton",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Cdlsy3IXgis5hNYRwvohPB9AIxH8tNdEo9CwxXK1i3QEUO7YN3p4YJ_cd5ruGsmNhvoX7803=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: None,
|
||||
publish_date_txt: None,
|
||||
view_count: Some(390010),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Folgt mir auf Instagram!\nhttps://instagram.com/plankton.gif \n\nÜBER DEN KANAL:\nRede viel wenn der Tag lang ist"),
|
||||
),
|
||||
playback_date: "[date]",
|
||||
playback_date_txt: Some("Yesterday"),
|
||||
),
|
||||
VideoItem(
|
||||
id: "oIVSKQ8NMqk",
|
||||
name: "What I learned on highschool swim",
|
||||
duration: Some(620),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/oIVSKQ8NMqk/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAeTbOz6FlrH1x3jA4AwYcTGmUwxg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/oIVSKQ8NMqk/hqdefault.jpg?sqp=-oaymwEmCMQBEG5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAEzpr1xBI-8jJwZz72NHj9VKyefA",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/oIVSKQ8NMqk/hqdefault.jpg?sqp=-oaymwEnCPYBEIoBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLBNzC8nvKtO7fmqzavWemou7QOLOg",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/oIVSKQ8NMqk/hqdefault.jpg?sqp=-oaymwEnCNACELwBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLA1_VgkVeq4ELmrQ8a4vhtJhg6TMA",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCsKVP_4zQ877TEiH_Ih5yDQ",
|
||||
name: "illymation",
|
||||
avatar: [
|
||||
HistoryItem(
|
||||
item: VideoItem(
|
||||
id: "oIVSKQ8NMqk",
|
||||
name: "What I learned on highschool swim",
|
||||
duration: Some(620),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AIdro_n3doafn2qRRawkYet_KQdH2Jl1ugSQnjnd0Ham12C9MYI=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
url: "https://i.ytimg.com/vi/oIVSKQ8NMqk/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAeTbOz6FlrH1x3jA4AwYcTGmUwxg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/oIVSKQ8NMqk/hqdefault.jpg?sqp=-oaymwEmCMQBEG5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAEzpr1xBI-8jJwZz72NHj9VKyefA",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/oIVSKQ8NMqk/hqdefault.jpg?sqp=-oaymwEnCPYBEIoBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLBNzC8nvKtO7fmqzavWemou7QOLOg",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/oIVSKQ8NMqk/hqdefault.jpg?sqp=-oaymwEnCNACELwBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLA1_VgkVeq4ELmrQ8a4vhtJhg6TMA",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(6491367),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("okay, so I wasn\'t the BEST ... but I tried my best!\n▶ Black Friday Merch sale: https://www.hereforthechaos.com\n▶ SNEAK PEEKS ON PATREON! http://patreon.com/illymation\n\n▶ BG ARTIST: Ingrid..."),
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCsKVP_4zQ877TEiH_Ih5yDQ",
|
||||
name: "illymation",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AIdro_n3doafn2qRRawkYet_KQdH2Jl1ugSQnjnd0Ham12C9MYI=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: None,
|
||||
publish_date_txt: None,
|
||||
view_count: Some(6491367),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("okay, so I wasn\'t the BEST ... but I tried my best!\n▶ Black Friday Merch sale: https://www.hereforthechaos.com\n▶ SNEAK PEEKS ON PATREON! http://patreon.com/illymation\n\n▶ BG ARTIST: Ingrid..."),
|
||||
),
|
||||
playback_date: "[date]",
|
||||
playback_date_txt: Some("Yesterday"),
|
||||
),
|
||||
VideoItem(
|
||||
id: "X30eFeqrHJo",
|
||||
name: "My Last Week of University!",
|
||||
duration: Some(659),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/X30eFeqrHJo/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAc6-vKGjlwODu5rDSHK2tz4sRzYQ",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/X30eFeqrHJo/hqdefault.jpg?sqp=-oaymwEmCMQBEG5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAE7jwC0MudkKK-I1nyGvCheljvtQ",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/X30eFeqrHJo/hqdefault.jpg?sqp=-oaymwEnCPYBEIoBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLAfIZ4htcNHP3RWahpR4XM7U-UTFQ",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/X30eFeqrHJo/hqdefault.jpg?sqp=-oaymwEnCNACELwBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLDYMLObTeyxHK_PewK4Rwk3V-2KGw",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC6a8lp6vaCMhUVXPyynhjUA",
|
||||
name: "Ruby Granger",
|
||||
avatar: [
|
||||
HistoryItem(
|
||||
item: VideoItem(
|
||||
id: "X30eFeqrHJo",
|
||||
name: "My Last Week of University!",
|
||||
duration: Some(659),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/u9qrR3ceVkt7yen48Rd1WWV_w-OdE5iejCNI2y-PyG0tpd7xlqWFDahsaZa02cMk7O-0WkCL=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
url: "https://i.ytimg.com/vi/X30eFeqrHJo/hqdefault.jpg?sqp=-oaymwEmCKgBEF5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAc6-vKGjlwODu5rDSHK2tz4sRzYQ",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/X30eFeqrHJo/hqdefault.jpg?sqp=-oaymwEmCMQBEG5IWvKriqkDGQgBFQAAiEIYAdgBAeIBCggYEAIYBjgBQAE=&rs=AOn4CLAE7jwC0MudkKK-I1nyGvCheljvtQ",
|
||||
width: 196,
|
||||
height: 110,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/X30eFeqrHJo/hqdefault.jpg?sqp=-oaymwEnCPYBEIoBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLAfIZ4htcNHP3RWahpR4XM7U-UTFQ",
|
||||
width: 246,
|
||||
height: 138,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/X30eFeqrHJo/hqdefault.jpg?sqp=-oaymwEnCNACELwBSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLDYMLObTeyxHK_PewK4Rwk3V-2KGw",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(132844),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("This first year has been somewhat hectic, and certainly was tricky in the first semester; however, I have enjoyed it immensely and, as I said, am sad that this term has now ended. I am returning..."),
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC6a8lp6vaCMhUVXPyynhjUA",
|
||||
name: "Ruby Granger",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/u9qrR3ceVkt7yen48Rd1WWV_w-OdE5iejCNI2y-PyG0tpd7xlqWFDahsaZa02cMk7O-0WkCL=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: None,
|
||||
publish_date_txt: None,
|
||||
view_count: Some(132844),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("This first year has been somewhat hectic, and certainly was tricky in the first semester; however, I have enjoyed it immensely and, as I said, am sad that this term has now ended. I am returning..."),
|
||||
),
|
||||
playback_date: "[date]",
|
||||
playback_date_txt: Some("Yesterday"),
|
||||
),
|
||||
],
|
||||
ctoken: Some("4qmFsgJMEglGRWhpc3RvcnkaKENBSjZHbmx5WVRWb2QyOU9RMmR6U1RSaGNXMTFkMWxSTms4M05WcFKaAhRicm93c2UtZmVlZEZFaGlzdG9yeQ%3D%3D"),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Reference in a new issue