From ccb1178b95101e0d95ce2e6c0f7a828c0eb46889 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 6 Feb 2025 00:59:38 +0100 Subject: [PATCH 01/64] fix iOS client doc --- src/client/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 485f2a2..7102f47 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -83,7 +83,6 @@ pub enum ClientType { /// Client used by the iOS app /// /// - no obfuscated stream URLs - /// - does not include opus audio streams Ios, } From 3a2370b97ca3d0f40d72d66a23295557317d29fb Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 18 Jan 2025 07:03:36 +0100 Subject: [PATCH 02/64] feat: add timezone query option --- Cargo.toml | 1 + cli/Cargo.toml | 3 ++ cli/src/main.rs | 18 +++++++++++ src/client/channel.rs | 6 ++-- src/client/history.rs | 4 +-- src/client/mod.rs | 50 ++++++++++++++++++++++++++++++- src/client/pagination.rs | 5 ++-- src/client/player.rs | 2 ++ src/client/playlist.rs | 11 +++++-- src/client/response/video_item.rs | 21 ++++++++++--- src/client/search.rs | 2 +- src/client/trends.rs | 2 +- src/client/video_details.rs | 9 ++++-- src/util/timeago.rs | 23 +++++++++----- 14 files changed, 132 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index efc8ce3..7d4dd18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ time = { version = "0.3.37", features = [ "serde-human-readable", "serde-well-known", ] } +time-tz = { version = "2.0.0" } futures-util = "0.3.31" ress = "0.11.0" phf = "0.11.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 519d210..a1c2257 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -12,6 +12,7 @@ description = "CLI for RustyPipe - download videos and extract data from YouTube [features] default = ["native-tls"] +timezone = ["dep:time", "dep:time-tz"] # Reqwest TLS options native-tls = [ @@ -49,6 +50,8 @@ futures-util.workspace = true serde.workspace = true serde_json.workspace = true quick-xml.workspace = true +time = { workspace = true, optional = true } +time-tz = { workspace = true, optional = true } indicatif.workspace = true anyhow.workspace = true diff --git a/cli/src/main.rs b/cli/src/main.rs index 7f49119..68ff919 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -55,6 +55,10 @@ struct Cli { /// YouTube content country #[clap(long, global = true)] country: Option, + /// UTC offset in minutes + #[cfg(feature = "timezone")] + #[clap(long, global = true)] + timezone: Option, /// Use authentication #[clap(long, global = true)] auth: bool, @@ -913,6 +917,20 @@ async fn run() -> anyhow::Result<()> { if let Some(botguard_bin) = cli.botguard_bin { rp = rp.botguard_bin(botguard_bin); } + + #[cfg(feature = "timezone")] + if let Some(timezone) = cli.timezone { + use time::OffsetDateTime; + use time_tz::{Offset, TimeZone}; + + let tz = time_tz::timezones::get_by_name(&timezone).expect("invalid timezone"); + let offset = tz + .get_offset_utc(&OffsetDateTime::now_utc()) + .to_utc() + .whole_minutes(); + rp = rp.timezone(tz.name(), offset); + } + if cli.no_botguard { rp = rp.no_botguard(); } diff --git a/src/client/channel.rs b/src/client/channel.rs index 533e0a3..7945719 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -220,6 +220,7 @@ impl MapResponse>> for response::Channel { let mut mapper = response::YouTubeListMapper::::with_channel( ctx.lang, + ctx.utc_offset, &channel_data.c, channel_data.warnings, ); @@ -265,6 +266,7 @@ impl MapResponse>> for response::Channel { let mut mapper = response::YouTubeListMapper::::with_channel( ctx.lang, + ctx.utc_offset, &channel_data.c, channel_data.warnings, ); @@ -280,7 +282,7 @@ impl MapResponse>> for response::Channel { impl MapResponse for response::ChannelAbout { fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { - // Channel info is always fetched in English. There is no localized data there + // Channel info is always fetched in English. There is no localized data // and it allows parsing the country name. let lang = Language::En; @@ -335,7 +337,7 @@ impl MapResponse for response::ChannelAbout { .video_count_text .and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)), create_date: about.joined_date_text.and_then(|txt| { - timeago::parse_textual_date_or_warn(lang, &txt, &mut warnings) + timeago::parse_textual_date_or_warn(lang, ctx.utc_offset, &txt, &mut warnings) .map(OffsetDateTime::date) }), view_count: about diff --git a/src/client/history.rs b/src/client/history.rs index 6852b60..d0272b2 100644 --- a/src/client/history.rs +++ b/src/client/history.rs @@ -177,7 +177,7 @@ impl MapResponse>> for response::History { for item in items.c { match item { response::YouTubeListItem::ItemSectionRenderer { header, contents } => { - let mut mapper = YouTubeListMapper::::new(ctx.lang); + let mut mapper = YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); mapper.map_response(contents); mapper.conv_history_items( header.map(|h| h.item_section_header_renderer.title), @@ -228,7 +228,7 @@ impl MapResponse> for response::History { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(ctx.lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); mapper.map_response(items); Ok(MapResult { diff --git a/src/client/mod.rs b/src/client/mod.rs index 7102f47..a44f346 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -35,7 +35,7 @@ use regex::Regex; use reqwest::{header, Client, ClientBuilder, Request, RequestBuilder, Response, StatusCode}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use sha1::{Digest, Sha1}; -use time::OffsetDateTime; +use time::{OffsetDateTime, UtcOffset}; use tokio::sync::RwLock as AsyncRwLock; use crate::error::AuthError; @@ -386,6 +386,8 @@ struct RustyPipeRef { struct RustyPipeOpts { lang: Language, country: Country, + timezone: Option, + utc_offset_minutes: i16, report: bool, strict: bool, auth: Option, @@ -525,6 +527,8 @@ impl Default for RustyPipeOpts { Self { lang: Language::En, country: Country::Us, + timezone: None, + utc_offset_minutes: 0, report: false, strict: false, auth: None, @@ -890,6 +894,21 @@ impl RustyPipeBuilder { self } + /// Set the timezone and its associated UTC offset in minutes used + /// when accessing the YouTube API. + /// + /// This will also change the UTC offset of the returned dates. + /// + /// **Default value**: `0` (UTC) + /// + /// **Info**: you can set this option for individual queries, too + #[must_use] + pub fn timezone>(mut self, timezone: S, utc_offset_minutes: i16) -> Self { + self.default_opts.timezone = Some(timezone.into()); + self.default_opts.utc_offset_minutes = utc_offset_minutes; + self + } + /// Generate a report on every operation. /// /// This should only be used for debugging. @@ -1668,6 +1687,17 @@ impl RustyPipeQuery { self } + /// Set the timezone and its associated UTC offset in minutes used + /// when accessing the YouTube API. + /// + /// This will also change the UTC offset of the returned dates. + #[must_use] + pub fn timezone>(mut self, timezone: S, utc_offset_minutes: i16) -> Self { + self.opts.timezone = Some(timezone.into()); + self.opts.utc_offset_minutes = utc_offset_minutes; + self + } + /// Generate a report on every operation. /// /// This should only be used for debugging. @@ -1822,6 +1852,8 @@ impl RustyPipeQuery { } else { (Language::En, Country::Us) }; + let utc_offset_minutes = self.opts.utc_offset_minutes; + let time_zone = self.opts.timezone.as_deref().unwrap_or("UTC"); match ctype { ClientType::Desktop => YTContext { @@ -1833,6 +1865,8 @@ impl RustyPipeQuery { visitor_data, hl, gl, + time_zone, + utc_offset_minutes, ..Default::default() }, request: Some(RequestYT::default()), @@ -1848,6 +1882,8 @@ impl RustyPipeQuery { visitor_data, hl, gl, + time_zone, + utc_offset_minutes, ..Default::default() }, request: Some(RequestYT::default()), @@ -1863,6 +1899,8 @@ impl RustyPipeQuery { visitor_data, hl, gl, + time_zone, + utc_offset_minutes, ..Default::default() }, request: Some(RequestYT::default()), @@ -1879,6 +1917,8 @@ impl RustyPipeQuery { visitor_data, hl, gl, + time_zone, + utc_offset_minutes, ..Default::default() }, request: Some(RequestYT::default()), @@ -1898,6 +1938,8 @@ impl RustyPipeQuery { visitor_data, hl, gl, + time_zone, + utc_offset_minutes, ..Default::default() }, request: None, @@ -1915,6 +1957,8 @@ impl RustyPipeQuery { visitor_data, hl, gl, + time_zone, + utc_offset_minutes, ..Default::default() }, request: None, @@ -2212,6 +2256,8 @@ impl RustyPipeQuery { let ctx = MapRespCtx { id, lang: self.opts.lang, + utc_offset: UtcOffset::from_whole_seconds(i32::from(self.opts.utc_offset_minutes) * 60) + .map_err(|_| Error::Other("utc_offset overflow".into()))?, deobf: ctx_src.deobf, visitor_data: Some(&visitor_data), client_type: ctype, @@ -2498,6 +2544,7 @@ impl AsRef for RustyPipeQuery { struct MapRespCtx<'a> { id: &'a str, lang: Language, + utc_offset: UtcOffset, deobf: Option<&'a DeobfData>, visitor_data: Option<&'a str>, client_type: ClientType, @@ -2525,6 +2572,7 @@ impl<'a> MapRespCtx<'a> { Self { id, lang: Language::En, + utc_offset: UtcOffset::UTC, deobf: None, visitor_data: None, client_type: ClientType::Desktop, diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 3fad657..2948d06 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -127,7 +127,7 @@ impl MapResponse> for response::Continuation { let estimated_results = self.estimated_results; let items = continuation_items(self); - let mut mapper = response::YouTubeListMapper::::new(ctx.lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); mapper.map_response(items); Ok(MapResult { @@ -237,7 +237,8 @@ impl MapResponse>> for response::Continuation { for item in items.c { match item { response::YouTubeListItem::ItemSectionRenderer { header, contents } => { - let mut mapper = response::YouTubeListMapper::::new(ctx.lang); + let mut mapper = + response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); mapper.map_response(contents); mapper.conv_history_items( header.map(|h| h.item_section_header_renderer.title), diff --git a/src/client/player.rs b/src/client/player.rs index d85394c..f5e0c53 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -937,6 +937,7 @@ mod tests { use path_macro::path; use rstest::rstest; + use time::UtcOffset; use super::*; use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES}; @@ -968,6 +969,7 @@ mod tests { .map_response(&MapRespCtx { id: "pPvd8UxmSbQ", lang: Language::En, + utc_offset: UtcOffset::UTC, deobf: Some(&DEOBF_DATA), visitor_data: None, client_type, diff --git a/src/client/playlist.rs b/src/client/playlist.rs index c2872e6..bf6913a 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -90,7 +90,7 @@ impl MapResponse for response::Playlist { .playlist_video_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(ctx.lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); mapper.map_response(video_items); let (description, thumbnails, last_update_txt) = match self.sidebar { @@ -225,8 +225,13 @@ impl MapResponse for response::Playlist { .as_deref() .or(last_update_txt2.as_deref()) .and_then(|txt| { - timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut mapper.warnings) - .map(OffsetDateTime::date) + timeago::parse_textual_date_or_warn( + ctx.lang, + ctx.utc_offset, + txt, + &mut mapper.warnings, + ) + .map(OffsetDateTime::date) }); Ok(MapResult { diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index 1541728..001d8d1 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -2,7 +2,7 @@ use serde::Deserialize; use serde_with::{ rust::deserialize_ignore_any, serde_as, DefaultOnError, DisplayFromStr, VecSkipError, }; -use time::OffsetDateTime; +use time::{OffsetDateTime, UtcOffset}; use super::{ ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, SimpleHeaderRenderer, @@ -461,6 +461,7 @@ impl IsShort for Vec { #[derive(Debug)] pub(crate) struct YouTubeListMapper { lang: Language, + utc_offset: UtcOffset, channel: Option, pub items: Vec, @@ -470,9 +471,10 @@ pub(crate) struct YouTubeListMapper { } impl YouTubeListMapper { - pub fn new(lang: Language) -> Self { + pub fn new(lang: Language, utc_offset: UtcOffset) -> Self { Self { lang, + utc_offset, channel: None, items: Vec::new(), warnings: Vec::new(), @@ -481,9 +483,15 @@ impl YouTubeListMapper { } } - pub fn with_channel(lang: Language, channel: &Channel, warnings: Vec) -> Self { + pub fn with_channel( + lang: Language, + utc_offset: UtcOffset, + channel: &Channel, + warnings: Vec, + ) -> Self { Self { lang, + utc_offset, channel: Some(ChannelTag { id: channel.id.clone(), name: channel.name.clone(), @@ -786,7 +794,12 @@ impl YouTubeListMapper { thumbnail: tn.image.into(), channel, publish_date: publish_date_txt.as_deref().and_then(|t| { - timeago::parse_textual_date_or_warn(self.lang, t, &mut self.warnings) + timeago::parse_textual_date_or_warn( + self.lang, + self.utc_offset, + t, + &mut self.warnings, + ) }), publish_date_txt, view_count, diff --git a/src/client/search.rs b/src/client/search.rs index b4ba544..5ab5793 100644 --- a/src/client/search.rs +++ b/src/client/search.rs @@ -107,7 +107,7 @@ impl MapResponse> for response::Search { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(ctx.lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); mapper.map_response(items); Ok(MapResult { diff --git a/src/client/trends.rs b/src/client/trends.rs index fa3510c..10e8fe9 100644 --- a/src/client/trends.rs +++ b/src/client/trends.rs @@ -45,7 +45,7 @@ impl MapResponse> for response::Trending { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(ctx.lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); mapper.map_response(items); Ok(MapResult { diff --git a/src/client/video_details.rs b/src/client/video_details.rs index 9cc1ffc..f027c12 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -180,7 +180,12 @@ impl MapResponse for response::VideoDetails { // so we ignore parse errors here for now like_text.and_then(|txt| util::parse_numeric(&txt).ok()), date_text.as_deref().and_then(|txt| { - timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut warnings) + timeago::parse_textual_date_or_warn( + ctx.lang, + ctx.utc_offset, + txt, + &mut warnings, + ) }), date_text, view_count @@ -470,7 +475,7 @@ fn map_recommendations( visitor_data: Option, ctx: &MapRespCtx<'_>, ) -> MapResult> { - let mut mapper = response::YouTubeListMapper::::new(ctx.lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); mapper.map_response(r); mapper.ctoken = mapper.ctoken.or_else(|| { diff --git a/src/util/timeago.rs b/src/util/timeago.rs index 274c653..65b85a7 100644 --- a/src/util/timeago.rs +++ b/src/util/timeago.rs @@ -13,7 +13,7 @@ use std::ops::Mul; use serde::{Deserialize, Serialize}; -use time::{Date, Duration, Month, OffsetDateTime}; +use time::{Date, Duration, Month, OffsetDateTime, UtcOffset}; use crate::{ param::Language, @@ -333,8 +333,13 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option Option { - parse_textual_date(lang, textual_date).map(OffsetDateTime::from) +pub fn parse_textual_date_to_dt( + lang: Language, + utc_offset: UtcOffset, + textual_date: &str, +) -> Option { + parse_textual_date(lang, textual_date) + .map(|parsed| OffsetDateTime::from(parsed).replace_offset(utc_offset)) } /// Parse a textual date (e.g. "29 minutes ago" "Jul 2, 2014") into a Date object. @@ -345,15 +350,17 @@ pub fn parse_textual_date_to_d( textual_date: &str, warnings: &mut Vec, ) -> Option { - parse_textual_date_or_warn(lang, textual_date, warnings).map(OffsetDateTime::date) + parse_textual_date_or_warn(lang, UtcOffset::UTC, textual_date, warnings) + .map(OffsetDateTime::date) } pub fn parse_textual_date_or_warn( lang: Language, + utc_offset: UtcOffset, textual_date: &str, warnings: &mut Vec, ) -> Option { - let res = parse_textual_date_to_dt(lang, textual_date); + let res = parse_textual_date_to_dt(lang, utc_offset, textual_date); if res.is_none() { warnings.push(format!("could not parse textual date `{textual_date}`")); } @@ -1101,11 +1108,13 @@ mod tests { #[test] fn t_to_datetime() { // Absolute date - let date = parse_textual_date_to_dt(Language::En, "Last updated on Jan 3, 2020").unwrap(); + let date = + parse_textual_date_to_dt(Language::En, UtcOffset::UTC, "Last updated on Jan 3, 2020") + .unwrap(); assert_eq!(date, datetime!(2020-1-3 0:00 +0)); // Relative date - let date = parse_textual_date_to_dt(Language::En, "1 year ago").unwrap(); + let date = parse_textual_date_to_dt(Language::En, UtcOffset::UTC, "1 year ago").unwrap(); let now = OffsetDateTime::now_utc(); assert_eq!(date.year(), now.year() - 1); } From a5a7be5b4e0a0b73d7e1dc802ebd7bd48dafc76d Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 25 Jan 2025 01:13:38 +0100 Subject: [PATCH 03/64] fix: correct timezone offset for parsed dates, add timezone_local option --- Cargo.toml | 8 +++ src/client/channel.rs | 2 - src/client/history.rs | 5 +- src/client/mod.rs | 34 +++++++-- src/client/music_history.rs | 2 +- src/client/pagination.rs | 8 +-- src/client/playlist.rs | 2 +- src/client/response/music_item.rs | 9 ++- src/client/response/video_item.rs | 34 +++------ src/client/search.rs | 2 +- src/client/trends.rs | 2 +- src/client/video_details.rs | 2 +- src/util/date.rs | 77 ++++++++++++++++++++- src/util/mod.rs | 2 +- src/util/timeago.rs | 111 +++++++++++++++--------------- 15 files changed, 202 insertions(+), 98 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d4dd18..1ea418a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ time = { version = "0.3.37", features = [ "macros", "serde-human-readable", "serde-well-known", + "local-offset", ] } time-tz = { version = "2.0.0" } futures-util = "0.3.31" @@ -55,6 +56,10 @@ data-encoding = "2.0.0" urlencoding = "2.1.0" quick-xml = { version = "0.37.0", features = ["serialize"] } tracing = { version = "0.1.0", features = ["log"] } +windows-sys = { version = "0.59.0", features = [ + "Win32_System_Time", + "Win32_Foundation", +] } # CLI indicatif = "0.17.0" @@ -115,6 +120,9 @@ urlencoding.workspace = true tracing.workspace = true quick-xml = { workspace = true, optional = true } +[target.'cfg(windows)'.dependencies] +windows-sys.workspace = true + [dev-dependencies] rstest.workspace = true tokio-test.workspace = true diff --git a/src/client/channel.rs b/src/client/channel.rs index 7945719..8dcb827 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -220,7 +220,6 @@ impl MapResponse>> for response::Channel { let mut mapper = response::YouTubeListMapper::::with_channel( ctx.lang, - ctx.utc_offset, &channel_data.c, channel_data.warnings, ); @@ -266,7 +265,6 @@ impl MapResponse>> for response::Channel { let mut mapper = response::YouTubeListMapper::::with_channel( ctx.lang, - ctx.utc_offset, &channel_data.c, channel_data.warnings, ); diff --git a/src/client/history.rs b/src/client/history.rs index d0272b2..427715c 100644 --- a/src/client/history.rs +++ b/src/client/history.rs @@ -177,10 +177,11 @@ impl MapResponse>> for response::History { for item in items.c { match item { response::YouTubeListItem::ItemSectionRenderer { header, contents } => { - let mut mapper = YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); + let mut mapper = YouTubeListMapper::::new(ctx.lang); mapper.map_response(contents); mapper.conv_history_items( header.map(|h| h.item_section_header_renderer.title), + ctx.utc_offset, &mut map_res, ); } @@ -228,7 +229,7 @@ impl MapResponse> for response::History { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { diff --git a/src/client/mod.rs b/src/client/mod.rs index a44f346..e21b752 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -897,8 +897,6 @@ impl RustyPipeBuilder { /// Set the timezone and its associated UTC offset in minutes used /// when accessing the YouTube API. /// - /// This will also change the UTC offset of the returned dates. - /// /// **Default value**: `0` (UTC) /// /// **Info**: you can set this option for individual queries, too @@ -909,6 +907,16 @@ impl RustyPipeBuilder { self } + /// Access the YouTube API using the local system timezone + /// + /// If the local timezone could not be determined, an error is logged and RustyPipe falls + /// back to UTC. + #[must_use] + pub fn timezone_local(self) -> Self { + let (timezone, utc_offset_minutes) = local_tz_offset(); + self.timezone(timezone, utc_offset_minutes) + } + /// Generate a report on every operation. /// /// This should only be used for debugging. @@ -1689,8 +1697,6 @@ impl RustyPipeQuery { /// Set the timezone and its associated UTC offset in minutes used /// when accessing the YouTube API. - /// - /// This will also change the UTC offset of the returned dates. #[must_use] pub fn timezone>(mut self, timezone: S, utc_offset_minutes: i16) -> Self { self.opts.timezone = Some(timezone.into()); @@ -1698,6 +1704,13 @@ impl RustyPipeQuery { self } + /// Access the YouTube API using the local system timezone + #[must_use] + pub fn timezone_local(self) -> Self { + let (timezone, utc_offset_minutes) = local_tz_offset(); + self.timezone(timezone, utc_offset_minutes) + } + /// Generate a report on every operation. /// /// This should only be used for debugging. @@ -2611,6 +2624,19 @@ fn validate_country(country: Country) -> Country { } } +fn local_tz_offset() -> (String, i16) { + match ( + util::local_timezone_name(), + UtcOffset::current_local_offset().map_err(|_| Error::Other("indeterminate offset".into())), + ) { + (Ok(timezone), Ok(offset)) => (timezone, offset.whole_minutes()), + (Err(e), _) | (_, Err(e)) => { + tracing::error!("{e}"); + ("UTC".to_owned(), 0) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/client/music_history.rs b/src/client/music_history.rs index 0ebfcf0..50c5844 100644 --- a/src/client/music_history.rs +++ b/src/client/music_history.rs @@ -160,7 +160,7 @@ impl MapResponse>> for response::MusicHistory { }; let mut mapper = MusicListMapper::new(ctx.lang); mapper.map_response(shelf.contents); - mapper.conv_history_items(shelf.title, &mut map_res); + mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res); } let ctoken = contents diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 2948d06..1440ebe 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -127,7 +127,7 @@ impl MapResponse> for response::Continuation { let estimated_results = self.estimated_results; let items = continuation_items(self); - let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { @@ -237,11 +237,11 @@ impl MapResponse>> for response::Continuation { for item in items.c { match item { response::YouTubeListItem::ItemSectionRenderer { header, contents } => { - let mut mapper = - response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(contents); mapper.conv_history_items( header.map(|h| h.item_section_header_renderer.title), + ctx.utc_offset, &mut map_res, ); } @@ -281,7 +281,7 @@ impl MapResponse>> for response::MusicContinuat 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); + mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res); continuations.extend(shelf.continuations); }; diff --git a/src/client/playlist.rs b/src/client/playlist.rs index bf6913a..79e2329 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -90,7 +90,7 @@ impl MapResponse for response::Playlist { .playlist_video_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(video_items); let (description, thumbnails, last_update_txt) = match self.sidebar { diff --git a/src/client/response/music_item.rs b/src/client/response/music_item.rs index f5e0fac..d2bf983 100644 --- a/src/client/response/music_item.rs +++ b/src/client/response/music_item.rs @@ -1,5 +1,6 @@ use serde::Deserialize; use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError}; +use time::UtcOffset; use crate::{ model::{ @@ -1272,6 +1273,7 @@ impl MusicListMapper { pub fn conv_history_items( self, date_txt: Option, + utc_offset: UtcOffset, res: &mut MapResult>>, ) { res.warnings.extend(self.warnings); @@ -1282,7 +1284,12 @@ impl MusicListMapper { .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) + timeago::parse_textual_date_to_d( + self.lang, + utc_offset, + s, + &mut res.warnings, + ) }), playback_date_txt: date_txt.clone(), }), diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index 001d8d1..f855f3f 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -461,7 +461,6 @@ impl IsShort for Vec { #[derive(Debug)] pub(crate) struct YouTubeListMapper { lang: Language, - utc_offset: UtcOffset, channel: Option, pub items: Vec, @@ -471,10 +470,9 @@ pub(crate) struct YouTubeListMapper { } impl YouTubeListMapper { - pub fn new(lang: Language, utc_offset: UtcOffset) -> Self { + pub fn new(lang: Language) -> Self { Self { lang, - utc_offset, channel: None, items: Vec::new(), warnings: Vec::new(), @@ -483,15 +481,9 @@ impl YouTubeListMapper { } } - pub fn with_channel( - lang: Language, - utc_offset: UtcOffset, - channel: &Channel, - warnings: Vec, - ) -> Self { + pub fn with_channel(lang: Language, channel: &Channel, warnings: Vec) -> Self { Self { lang, - utc_offset, channel: Some(ChannelTag { id: channel.id.clone(), name: channel.name.clone(), @@ -794,12 +786,7 @@ impl YouTubeListMapper { thumbnail: tn.image.into(), channel, publish_date: publish_date_txt.as_deref().and_then(|t| { - timeago::parse_textual_date_or_warn( - self.lang, - self.utc_offset, - t, - &mut self.warnings, - ) + timeago::parse_timeago_dt_or_warn(self.lang, t, &mut self.warnings) }), publish_date_txt, view_count, @@ -920,17 +907,16 @@ impl YouTubeListMapper { pub(crate) fn conv_history_items( self, date_txt: Option, + utc_offset: UtcOffset, res: &mut MapResult>>, ) { 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(), - } + 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, utc_offset, s, &mut res.warnings) + }), + playback_date_txt: date_txt.clone(), })); } } diff --git a/src/client/search.rs b/src/client/search.rs index 5ab5793..b4ba544 100644 --- a/src/client/search.rs +++ b/src/client/search.rs @@ -107,7 +107,7 @@ impl MapResponse> for response::Search { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { diff --git a/src/client/trends.rs b/src/client/trends.rs index 10e8fe9..fa3510c 100644 --- a/src/client/trends.rs +++ b/src/client/trends.rs @@ -45,7 +45,7 @@ impl MapResponse> for response::Trending { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { diff --git a/src/client/video_details.rs b/src/client/video_details.rs index f027c12..f53201b 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -475,7 +475,7 @@ fn map_recommendations( visitor_data: Option, ctx: &MapRespCtx<'_>, ) -> MapResult> { - let mut mapper = response::YouTubeListMapper::::new(ctx.lang, ctx.utc_offset); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(r); mapper.ctoken = mapper.ctoken.or_else(|| { diff --git a/src/util/date.rs b/src/util/date.rs index dde5d88..e80f516 100644 --- a/src/util/date.rs +++ b/src/util/date.rs @@ -1,5 +1,7 @@ use time::{Date, Duration, Month, OffsetDateTime}; +use crate::error::Error; + /// Shift a date by the given number of months. /// Ambiguous month-ends are shifted backwards as necessary. pub fn shift_months(date: Date, months: i32) -> Date { @@ -25,7 +27,8 @@ pub fn shift_years(date: Date, years: i32) -> Date { shift_months(date, years * 12) } -pub fn shift_weeks_mo(date: Date, weeks: i32) -> Date { +/// Shift a date to the monday of its week, plus/minus the given amount of weeks +pub fn shift_weeks_monday(date: Date, weeks: i32) -> Date { let d = date + Duration::weeks(weeks.into()); Date::from_iso_week_date(d.year(), d.iso_week(), time::Weekday::Monday).unwrap() } @@ -40,3 +43,75 @@ pub fn now_sec() -> OffsetDateTime { .replace_nanosecond(0) .unwrap() } + +/// Gets the current timezone from the system. +/// +/// Currently only supported for Windows, Unix, and WASM targets. +/// +/// # Errors +/// Returns an [Error](enum@Error) if the timezone cannot be determined. +pub fn local_timezone_name() -> Result { + #[cfg(unix)] + { + use std::path::Path; + let path = Path::new("/etc/localtime"); + let realpath = std::fs::read_link(path) + .map_err(|_| Error::Other("could not read localtime".into()))?; + // The part of the path we're interested in cannot contain non unicode characters. + return realpath + .to_str() + .and_then(|s| s.split("/zoneinfo/").last()) + .map(str::to_owned) + .ok_or_else(|| { + Error::Other(format!("could not parse zoneinfo path: {realpath:?}").into()) + }); + } + + #[cfg(windows)] + #[allow(unsafe_code)] + { + unsafe { + use windows_sys::Win32::System::Time::GetDynamicTimeZoneInformation; + use windows_sys::Win32::System::Time::DYNAMIC_TIME_ZONE_INFORMATION; + let mut data: DYNAMIC_TIME_ZONE_INFORMATION = std::mem::zeroed(); + let res = GetDynamicTimeZoneInformation(&mut data as _); + if res > 2 { + return Err(Error::Other("local timezone could not be read".into())); + } else { + let win_name_utf16 = &data.TimeZoneKeyName; + let mut len: usize = 0; + while win_name_utf16[len] != 0x0 { + len += 1; + } + if len == 0 { + return Err(Error::Other("local timezone could not be read".into())); + } + return String::from_utf16(&win_name_utf16[..len]) + .map_err(|_| "local timezone is invalid UTF16".into())?; + } + } + } + + #[allow(unreachable_code)] + Err(Error::Other("local timezone unsupported".into())) +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + use time::{macros::date, Date}; + + #[rstest] + #[case::this_week(date!(2025-01-17), 0, date!(2025-01-13))] + #[case::last_week(date!(2025-01-17), -1, date!(2025-01-06))] + #[case::last_month(date!(2025-01-17), -4, date!(2024-12-16))] + fn shift_weeks_monday(#[case] date: Date, #[case] weeks: i32, #[case] expect: Date) { + let res = super::shift_weeks_monday(date, weeks); + assert_eq!(res, expect); + } + + #[test] + fn local_timezone_name() { + super::local_timezone_name().unwrap(); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 9a49571..c42eb4b 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -5,7 +5,7 @@ mod visitor_data; pub mod dictionary; pub mod timeago; -pub use date::{now_sec, shift_months, shift_weeks_mo, shift_years}; +pub use date::{local_timezone_name, now_sec, shift_months, shift_weeks_monday, shift_years}; pub use protobuf::{string_from_pb, ProtoBuilder}; pub use visitor_data::VisitorDataCache; diff --git a/src/util/timeago.rs b/src/util/timeago.rs index 65b85a7..011b3ed 100644 --- a/src/util/timeago.rs +++ b/src/util/timeago.rs @@ -97,6 +97,26 @@ impl TimeAgo { fn secs(self) -> u32 { u32::from(self.n) * self.unit.secs() } + + fn into_datetime(self, utc_offset: UtcOffset) -> OffsetDateTime { + let ts = util::now_sec().to_offset(utc_offset); + match self.unit { + TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -i32::from(self.n))), + TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -i32::from(self.n))), + TimeUnit::LastWeek => { + ts.replace_date(util::shift_weeks_monday(ts.date(), -i32::from(self.n))) + } + TimeUnit::LastWeekday => ts.replace_date( + Date::from_iso_week_date( + ts.year(), + ts.iso_week(), + time::Weekday::Monday.nth_next(self.n), + ) + .unwrap(), + ), + _ => ts - Duration::from(self), + } + } } impl Mul for TimeAgo { @@ -116,33 +136,11 @@ impl From for Duration { } } -impl From for OffsetDateTime { - fn from(ta: TimeAgo) -> Self { - let ts = util::now_sec(); - match ta.unit { - TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -i32::from(ta.n))), - TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -i32::from(ta.n))), - TimeUnit::LastWeek => { - ts.replace_date(util::shift_weeks_mo(ts.date(), -i32::from(ta.n))) - } - TimeUnit::LastWeekday => ts.replace_date( - Date::from_iso_week_date( - ts.year(), - ts.iso_week(), - time::Weekday::Monday.nth_next(ta.n), - ) - .unwrap(), - ), - _ => ts - Duration::from(ta), - } - } -} - -impl From for OffsetDateTime { - fn from(date: ParsedDate) -> Self { - match date { - ParsedDate::Absolute(date) => date.with_hms(0, 0, 0).unwrap().assume_utc(), - ParsedDate::Relative(timeago) => timeago.into(), +impl ParsedDate { + fn into_datetime(self, utc_offset: UtcOffset) -> OffsetDateTime { + match self { + ParsedDate::Absolute(date) => date.with_hms(0, 0, 0).unwrap().assume_offset(utc_offset), + ParsedDate::Relative(timeago) => timeago.into_datetime(utc_offset), } } } @@ -247,7 +245,7 @@ pub fn parse_timeago(lang: Language, textual_date: &str) -> Option { /// /// Returns [`None`] if the date could not be parsed. pub fn parse_timeago_dt(lang: Language, textual_date: &str) -> Option { - parse_timeago(lang, textual_date).map(OffsetDateTime::from) + parse_timeago(lang, textual_date).map(|t| t.into_datetime(UtcOffset::UTC)) } pub fn parse_timeago_dt_or_warn( @@ -265,7 +263,11 @@ pub fn parse_timeago_dt_or_warn( /// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a ParsedDate object. /// /// Returns [`None`] if the date could not be parsed. -pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option { +pub fn parse_textual_date( + lang: Language, + utc_offset: UtcOffset, + textual_date: &str, +) -> Option { let entry = dictionary::entry(lang); let by_char = util::lang_by_char(lang); let filtered_str = filter_datestr(textual_date); @@ -317,8 +319,9 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option Option { - parse_textual_date(lang, textual_date) - .map(|parsed| OffsetDateTime::from(parsed).replace_offset(utc_offset)) + parse_textual_date(lang, utc_offset, textual_date).map(|t| t.into_datetime(utc_offset)) } /// Parse a textual date (e.g. "29 minutes ago" "Jul 2, 2014") into a Date object. @@ -347,11 +349,12 @@ pub fn parse_textual_date_to_dt( /// Returns None if the date could not be parsed. pub fn parse_textual_date_to_d( lang: Language, + utc_offset: UtcOffset, textual_date: &str, warnings: &mut Vec, ) -> Option { - parse_textual_date_or_warn(lang, UtcOffset::UTC, textual_date, warnings) - .map(OffsetDateTime::date) + parse_textual_date_or_warn(lang, utc_offset, textual_date, warnings) + .map(|d| d.to_offset(utc_offset).date()) } pub fn parse_textual_date_or_warn( @@ -871,7 +874,7 @@ mod tests { for (t, entry) in entries { entry.cases.iter().for_each(|(txt, n)| { let timeago = parse_timeago(*lang, txt); - let textual_date = parse_textual_date(*lang, txt); + let textual_date = parse_textual_date(*lang, UtcOffset::UTC, txt); assert_eq!( timeago, Some(TimeAgo { n: *n, unit: *t }), @@ -913,7 +916,7 @@ mod tests { #[case] textual_date: &str, #[case] expect: Option, ) { - let parsed_date = parse_textual_date(lang, textual_date); + let parsed_date = parse_textual_date(lang, UtcOffset::UTC, textual_date); assert_eq!(parsed_date, expect); } @@ -924,7 +927,7 @@ mod tests { #[case] textual_date: &str, #[case] expect: Date, ) { - let parsed_date = parse_textual_date(lang, textual_date); + let parsed_date = parse_textual_date(lang, UtcOffset::UTC, textual_date); let expected_date = expect .replace_year(OffsetDateTime::now_utc().year()) .unwrap(); @@ -940,7 +943,7 @@ mod tests { for (lang, samples) in &date_samples { assert_eq!( - parse_textual_date(*lang, samples.get("Today").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Today").unwrap()), Some(ParsedDate::Relative(TimeAgo { n: 0, unit: TimeUnit::Day @@ -948,7 +951,7 @@ mod tests { "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Yesterday").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Yesterday").unwrap()), Some(ParsedDate::Relative(TimeAgo { n: 1, unit: TimeUnit::Day @@ -956,7 +959,7 @@ mod tests { "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Ago").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Ago").unwrap()), Some(ParsedDate::Relative(TimeAgo { n: 5, unit: TimeUnit::Day @@ -964,62 +967,62 @@ mod tests { "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Jan").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jan").unwrap()), Some(ParsedDate::Absolute(date!(2020 - 1 - 3))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Feb").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Feb").unwrap()), Some(ParsedDate::Absolute(date!(2016 - 2 - 7))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Mar").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Mar").unwrap()), Some(ParsedDate::Absolute(date!(2015 - 3 - 9))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Apr").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Apr").unwrap()), Some(ParsedDate::Absolute(date!(2017 - 4 - 2))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("May").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("May").unwrap()), Some(ParsedDate::Absolute(date!(2014 - 5 - 22))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Jun").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jun").unwrap()), Some(ParsedDate::Absolute(date!(2014 - 6 - 28))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Jul").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jul").unwrap()), Some(ParsedDate::Absolute(date!(2014 - 7 - 2))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Aug").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Aug").unwrap()), Some(ParsedDate::Absolute(date!(2015 - 8 - 23))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Sep").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Sep").unwrap()), Some(ParsedDate::Absolute(date!(2018 - 9 - 16))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Oct").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Oct").unwrap()), Some(ParsedDate::Absolute(date!(2014 - 10 - 31))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Nov").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Nov").unwrap()), Some(ParsedDate::Absolute(date!(2016 - 11 - 3))), "lang: {lang}" ); assert_eq!( - parse_textual_date(*lang, samples.get("Dec").unwrap()), + parse_textual_date(*lang, UtcOffset::UTC, samples.get("Dec").unwrap()), Some(ParsedDate::Absolute(date!(2021 - 12 - 24))), "lang: {lang}" ); @@ -1065,7 +1068,7 @@ mod tests { } }; assert_eq!( - parse_textual_date(lang, &v), + parse_textual_date(lang, UtcOffset::UTC, &v), Some(expected), "lang={lang}; {k}" ); From 4f2bb47ab42ae0c68a64f3b3c2831fa7850b6f56 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 25 Jan 2025 01:17:06 +0100 Subject: [PATCH 04/64] feat: add --timezone-local CLI option --- cli/src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index 68ff919..1940ed0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -59,6 +59,9 @@ struct Cli { #[cfg(feature = "timezone")] #[clap(long, global = true)] timezone: Option, + /// Use local timezone + #[clap(long, global = true)] + timezone_local: bool, /// Use authentication #[clap(long, global = true)] auth: bool, @@ -917,6 +920,9 @@ async fn run() -> anyhow::Result<()> { if let Some(botguard_bin) = cli.botguard_bin { rp = rp.botguard_bin(botguard_bin); } + if cli.timezone_local { + rp = rp.timezone_local(); + } #[cfg(feature = "timezone")] if let Some(timezone) = cli.timezone { From 34f8e9b5510f0530e3afb040a640a61ce8aa21c0 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 25 Jan 2025 03:24:35 +0100 Subject: [PATCH 05/64] fix: compile error on windows --- src/lib.rs | 1 - src/util/date.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 012211e..88ef929 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_cfg))] -#![forbid(unsafe_code)] #![warn(missing_docs, clippy::todo, clippy::dbg_macro)] //! ## Go to diff --git a/src/util/date.rs b/src/util/date.rs index e80f516..d9ed6f9 100644 --- a/src/util/date.rs +++ b/src/util/date.rs @@ -68,7 +68,6 @@ pub fn local_timezone_name() -> Result { } #[cfg(windows)] - #[allow(unsafe_code)] { unsafe { use windows_sys::Win32::System::Time::GetDynamicTimeZoneInformation; @@ -87,7 +86,7 @@ pub fn local_timezone_name() -> Result { return Err(Error::Other("local timezone could not be read".into())); } return String::from_utf16(&win_name_utf16[..len]) - .map_err(|_| "local timezone is invalid UTF16".into())?; + .map_err(|_| Error::Other("local timezone is invalid UTF16".into())); } } } From 5acbf0e456b1f10707e0a56125d993a8129eee3a Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 25 Jan 2025 21:12:46 +0100 Subject: [PATCH 06/64] fix: use localzone crate to get local tz --- Cargo.toml | 9 ++------ src/client/mod.rs | 2 +- src/lib.rs | 1 + src/util/date.rs | 58 ----------------------------------------------- src/util/mod.rs | 2 +- 5 files changed, 5 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1ea418a..26b1c61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,10 +56,7 @@ data-encoding = "2.0.0" urlencoding = "2.1.0" quick-xml = { version = "0.37.0", features = ["serialize"] } tracing = { version = "0.1.0", features = ["log"] } -windows-sys = { version = "0.59.0", features = [ - "Win32_System_Time", - "Win32_Foundation", -] } +localzone = "0.3.1" # CLI indicatif = "0.17.0" @@ -118,11 +115,9 @@ phf.workspace = true data-encoding.workspace = true urlencoding.workspace = true tracing.workspace = true +localzone.workspace = true quick-xml = { workspace = true, optional = true } -[target.'cfg(windows)'.dependencies] -windows-sys.workspace = true - [dev-dependencies] rstest.workspace = true tokio-test.workspace = true diff --git a/src/client/mod.rs b/src/client/mod.rs index e21b752..61e358e 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -2626,7 +2626,7 @@ fn validate_country(country: Country) -> Country { fn local_tz_offset() -> (String, i16) { match ( - util::local_timezone_name(), + localzone::get_local_zone().ok_or(Error::Other("could not get local timezone".into())), UtcOffset::current_local_offset().map_err(|_| Error::Other("indeterminate offset".into())), ) { (Ok(timezone), Ok(offset)) => (timezone, offset.whole_minutes()), diff --git a/src/lib.rs b/src/lib.rs index 88ef929..012211e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_cfg))] +#![forbid(unsafe_code)] #![warn(missing_docs, clippy::todo, clippy::dbg_macro)] //! ## Go to diff --git a/src/util/date.rs b/src/util/date.rs index d9ed6f9..b21fed3 100644 --- a/src/util/date.rs +++ b/src/util/date.rs @@ -1,7 +1,5 @@ use time::{Date, Duration, Month, OffsetDateTime}; -use crate::error::Error; - /// Shift a date by the given number of months. /// Ambiguous month-ends are shifted backwards as necessary. pub fn shift_months(date: Date, months: i32) -> Date { @@ -44,57 +42,6 @@ pub fn now_sec() -> OffsetDateTime { .unwrap() } -/// Gets the current timezone from the system. -/// -/// Currently only supported for Windows, Unix, and WASM targets. -/// -/// # Errors -/// Returns an [Error](enum@Error) if the timezone cannot be determined. -pub fn local_timezone_name() -> Result { - #[cfg(unix)] - { - use std::path::Path; - let path = Path::new("/etc/localtime"); - let realpath = std::fs::read_link(path) - .map_err(|_| Error::Other("could not read localtime".into()))?; - // The part of the path we're interested in cannot contain non unicode characters. - return realpath - .to_str() - .and_then(|s| s.split("/zoneinfo/").last()) - .map(str::to_owned) - .ok_or_else(|| { - Error::Other(format!("could not parse zoneinfo path: {realpath:?}").into()) - }); - } - - #[cfg(windows)] - { - unsafe { - use windows_sys::Win32::System::Time::GetDynamicTimeZoneInformation; - use windows_sys::Win32::System::Time::DYNAMIC_TIME_ZONE_INFORMATION; - let mut data: DYNAMIC_TIME_ZONE_INFORMATION = std::mem::zeroed(); - let res = GetDynamicTimeZoneInformation(&mut data as _); - if res > 2 { - return Err(Error::Other("local timezone could not be read".into())); - } else { - let win_name_utf16 = &data.TimeZoneKeyName; - let mut len: usize = 0; - while win_name_utf16[len] != 0x0 { - len += 1; - } - if len == 0 { - return Err(Error::Other("local timezone could not be read".into())); - } - return String::from_utf16(&win_name_utf16[..len]) - .map_err(|_| Error::Other("local timezone is invalid UTF16".into())); - } - } - } - - #[allow(unreachable_code)] - Err(Error::Other("local timezone unsupported".into())) -} - #[cfg(test)] mod tests { use rstest::rstest; @@ -108,9 +55,4 @@ mod tests { let res = super::shift_weeks_monday(date, weeks); assert_eq!(res, expect); } - - #[test] - fn local_timezone_name() { - super::local_timezone_name().unwrap(); - } } diff --git a/src/util/mod.rs b/src/util/mod.rs index c42eb4b..0f4499f 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -5,7 +5,7 @@ mod visitor_data; pub mod dictionary; pub mod timeago; -pub use date::{local_timezone_name, now_sec, shift_months, shift_weeks_monday, shift_years}; +pub use date::{now_sec, shift_months, shift_weeks_monday, shift_years}; pub use protobuf::{string_from_pb, ProtoBuilder}; pub use visitor_data::VisitorDataCache; From 9890538c0ee940ea80fd00cdfc85e99a6cb39f92 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 6 Feb 2025 14:21:43 +0100 Subject: [PATCH 07/64] reorganize time-tz dependency --- Cargo.toml | 1 - cli/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 26b1c61..4831324 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,6 @@ time = { version = "0.3.37", features = [ "serde-well-known", "local-offset", ] } -time-tz = { version = "2.0.0" } futures-util = "0.3.31" ress = "0.11.0" phf = "0.11.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a1c2257..c4cd005 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -51,7 +51,7 @@ serde.workspace = true serde_json.workspace = true quick-xml.workspace = true time = { workspace = true, optional = true } -time-tz = { workspace = true, optional = true } +time-tz = { version = "2.0.0", optional = true } indicatif.workspace = true anyhow.workspace = true From c87bac18563683b74e441bc25ae027cc5b3b991b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 6 Feb 2025 14:28:37 +0100 Subject: [PATCH 08/64] shorten CLI timezone flags, add docs --- cli/README.md | 6 ++++++ cli/src/main.rs | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cli/README.md b/cli/README.md index b4056c4..28c34a4 100644 --- a/cli/README.md +++ b/cli/README.md @@ -135,6 +135,12 @@ Fetch a list of all the items saved in your YouTube/YouTube Music profile. `--auth` flag you can use authentication for any request. - `--lang` Change the YouTube content language - `--country` Change the YouTube content country +- `--tz` Use a specific + [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g. + Europe/Berlin, Australia/Sydney) + + **Note:** this requires building rustypipe-cli with the `timezone` feature +- `--local-tz` Use the local timezone instead of UTC - `--report` Generate a report on every request and store it in a `rustypipe_reports` folder in the current directory - `--cache-file` Change the RustyPipe cache file location (Default: diff --git a/cli/src/main.rs b/cli/src/main.rs index 1940ed0..f192dd8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -55,13 +55,13 @@ struct Cli { /// YouTube content country #[clap(long, global = true)] country: Option, - /// UTC offset in minutes + /// Use a specific timezone (e.g. Europe/Berlin, Australia/Sydney) #[cfg(feature = "timezone")] #[clap(long, global = true)] - timezone: Option, + tz: Option, /// Use local timezone #[clap(long, global = true)] - timezone_local: bool, + tz_local: bool, /// Use authentication #[clap(long, global = true)] auth: bool, @@ -920,12 +920,12 @@ async fn run() -> anyhow::Result<()> { if let Some(botguard_bin) = cli.botguard_bin { rp = rp.botguard_bin(botguard_bin); } - if cli.timezone_local { + if cli.tz_local { rp = rp.timezone_local(); } #[cfg(feature = "timezone")] - if let Some(timezone) = cli.timezone { + if let Some(timezone) = cli.tz { use time::OffsetDateTime; use time_tz::{Offset, TimeZone}; From 65cb4244c6ab547f53d0cb12af802c4189188c86 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 7 Feb 2025 04:43:35 +0100 Subject: [PATCH 09/64] feat!: add userdata feature for all personal data queries (playback history, subscriptions) --- .forgejo/workflows/ci.yaml | 9 +- .pre-commit-config.yaml | 6 +- .woodpecker.yml | 10 --- Cargo.toml | 5 +- Justfile | 16 ++-- cli/Cargo.toml | 2 +- codegen/Cargo.toml | 2 +- codegen/src/download_testfiles.rs | 25 +++--- src/client/mod.rs | 9 +- src/client/music_playlist.rs | 14 --- .../{music_history.rs => music_userdata.rs} | 18 +++- src/client/pagination.rs | 29 ++++-- src/client/playlist.rs | 22 ----- src/client/response/mod.rs | 13 ++- src/client/response/music_item.rs | 10 ++- src/client/response/video_item.rs | 17 ++-- ...__music_userdata__tests__map_history.snap} | 0 ...client__userdata__tests__map_history.snap} | 0 ...erdata__tests__map_subscription_feed.snap} | 0 src/client/{history.rs => userdata.rs} | 28 +++++- src/util/mod.rs | 7 +- src/util/timeago.rs | 1 + .../music_history.json | 0 .../saved_albums.json | 0 .../saved_artists.json | 0 .../saved_playlists.json | 0 .../saved_tracks.json | 0 testfiles/{history => userdata}/history.json | 0 .../subscription_feed.json | 0 .../{history => userdata}/subscriptions.json | 0 tests/youtube.rs | 89 ++++++++++--------- 31 files changed, 189 insertions(+), 143 deletions(-) delete mode 100644 .woodpecker.yml rename src/client/{music_history.rs => music_userdata.rs} (90%) rename src/client/snapshots/{rustypipe__client__music_history__tests__map_history.snap => rustypipe__client__music_userdata__tests__map_history.snap} (100%) rename src/client/snapshots/{rustypipe__client__history__tests__map_history.snap => rustypipe__client__userdata__tests__map_history.snap} (100%) rename src/client/snapshots/{rustypipe__client__history__tests__map_subscription_feed.snap => rustypipe__client__userdata__tests__map_subscription_feed.snap} (100%) rename src/client/{history.rs => userdata.rs} (90%) rename testfiles/{music_history => music_userdata}/music_history.json (100%) rename testfiles/{music_history => music_userdata}/saved_albums.json (100%) rename testfiles/{music_history => music_userdata}/saved_artists.json (100%) rename testfiles/{music_history => music_userdata}/saved_playlists.json (100%) rename testfiles/{music_history => music_userdata}/saved_tracks.json (100%) rename testfiles/{history => userdata}/history.json (100%) rename testfiles/{history => userdata}/subscription_feed.json (100%) rename testfiles/{history => userdata}/subscriptions.json (100%) diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index e7610ed..49e7479 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -35,10 +35,15 @@ jobs: rustypipe-botguard --version - name: 📎 Clippy - run: cargo clippy --all --tests --features=rss,indicatif,audiotag -- -D warnings + run: | + cargo clippy --all --tests --features=rss,userdata,indicatif,audiotag -- -D warnings + cargo clippy --package=rustypipe --tests -- -D warnings + cargo clippy --package=rustypipe-downloader -- -D warnings + cargo clippy --package=rustypipe-cli -- -D warnings + cargo clippy --package=rustypipe-cli --features=timezone -- -D warnings - name: 🧪 Test - run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss --workspace -- --skip 'cookie_auth::' + run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss,userdata --workspace -- --skip 'user_data::' env: ALL_PROXY: "http://warpproxy:8124" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d48fd4e..9a0cbb3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,4 +10,8 @@ repos: hooks: - id: cargo-fmt - id: cargo-clippy - args: ["--all", "--tests", "--features=rss,indicatif,audiotag", "--", "-D", "warnings"] + name: cargo-clippy rustypipe + args: ["--package=rustypipe", "--tests", "--", "-D", "warnings"] + - id: cargo-clippy + name: cargo-clippy workspace + args: ["--all", "--tests", "--features=rss,userdata,indicatif,audiotag", "--", "-D", "warnings"] diff --git a/.woodpecker.yml b/.woodpecker.yml deleted file mode 100644 index c76d6d0..0000000 --- a/.woodpecker.yml +++ /dev/null @@ -1,10 +0,0 @@ -steps: - test: - image: rust:latest - environment: - - CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse - commands: - - rustup component add rustfmt clippy - - cargo fmt --all --check - - cargo clippy --all --features=rss -- -D warnings - - cargo test --features=rss --workspace diff --git a/Cargo.toml b/Cargo.toml index 4831324..b9a7dc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ rustypipe-downloader = { path = "./downloader", version = "0.2.1", default-featu default = ["default-tls"] rss = ["dep:quick-xml"] +userdata = [] # Reqwest TLS options default-tls = ["reqwest/default-tls"] @@ -126,6 +127,6 @@ tracing-test.workspace = true [package.metadata.docs.rs] # To build locally: -# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features rss --no-deps --open -features = ["rss"] +# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features rss,userdata --no-deps --open +features = ["rss", "userdata"] rustdoc-args = ["--cfg", "docsrs"] diff --git a/Justfile b/Justfile index 76e8102..d8bd7aa 100644 --- a/Justfile +++ b/Justfile @@ -1,19 +1,19 @@ test: - # cargo test --features=rss - cargo nextest run --workspace --features=rss --no-fail-fast --retries 1 -- --skip 'cookie_auth::' + # cargo test --features=rss,userdata + cargo nextest run --workspace --features=rss,userdata --no-fail-fast --retries 1 -- --skip 'user_data::' unittest: - cargo nextest run --features=rss --no-fail-fast --lib + cargo nextest run --features=rss,userdata --no-fail-fast --lib testyt: - cargo nextest run --features=rss --no-fail-fast --retries 1 --test youtube -- --skip 'cookie_auth::' + cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube -- --skip 'user_data::' testyt-cookie: - cargo nextest run --features=rss --no-fail-fast --retries 1 --test youtube + cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube testyt-localized: - YT_LANG=th cargo nextest run --features=rss --no-fail-fast --retries 1 --test youtube -- \ - --skip 'cookie_auth::' --skip 'search_suggestion' --skip 'isrc_search_languages' + YT_LANG=th cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube -- \ + --skip 'user_data::' --skip 'search_suggestion' --skip 'isrc_search_languages' testintl: #!/usr/bin/env bash @@ -33,7 +33,7 @@ testintl: echo "---TESTS FOR $YT_LANG ---" if YT_LANG="$YT_LANG" cargo nextest run --no-fail-fast --retries 1 --test-threads 4 --test youtube -- \ - --skip 'cookie_auth::' --skip 'search_suggestion' --skip 'isrc_search_languages' --skip 'resolve_'; then + --skip 'user_data::' --skip 'search_suggestion' --skip 'isrc_search_languages' --skip 'resolve_'; then echo "--- $YT_LANG COMPLETED ---" else echo "--- $YT_LANG FAILED ---" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c4cd005..d6954b5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -42,7 +42,7 @@ rustls-tls-native-roots = [ ] [dependencies] -rustypipe = { workspace = true, features = ["rss"] } +rustypipe = { workspace = true, features = ["rss", "userdata"] } rustypipe-downloader.workspace = true reqwest.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 0ccb5ac..4b602d8 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true publish = false [dependencies] -rustypipe = { path = "../" } +rustypipe = { path = "../", features = ["userdata"] } reqwest.workspace = true tokio = { workspace = true, features = ["rt-multi-thread"] } futures-util.workspace = true diff --git a/codegen/src/download_testfiles.rs b/codegen/src/download_testfiles.rs index 9e6f2bf..cd85654 100644 --- a/codegen/src/download_testfiles.rs +++ b/codegen/src/download_testfiles.rs @@ -39,9 +39,6 @@ pub async fn download_testfiles() { search_playlists().await; search_empty().await; trending().await; - history().await; - subscriptions().await; - subscription_feed().await; music_playlist().await; music_playlist_cont().await; @@ -65,6 +62,12 @@ pub async fn download_testfiles() { music_charts().await; music_genres().await; music_genre().await; + + // User data + history().await; + subscriptions().await; + subscription_feed().await; + music_history().await; music_saved_artists().await; music_saved_albums().await; @@ -464,7 +467,7 @@ async fn trending() { } async fn history() { - let json_path = path!(*TESTFILES_DIR / "history" / "history.json"); + let json_path = path!(*TESTFILES_DIR / "userdata" / "history.json"); if json_path.exists() { return; } @@ -474,7 +477,7 @@ async fn history() { } async fn subscriptions() { - let json_path = path!(*TESTFILES_DIR / "history" / "subscriptions.json"); + let json_path = path!(*TESTFILES_DIR / "userdata" / "subscriptions.json"); if json_path.exists() { return; } @@ -484,7 +487,7 @@ async fn subscriptions() { } async fn subscription_feed() { - let json_path = path!(*TESTFILES_DIR / "history" / "subscription_feed.json"); + let json_path = path!(*TESTFILES_DIR / "userdata" / "subscription_feed.json"); if json_path.exists() { return; } @@ -816,7 +819,7 @@ async fn music_genre() { } async fn music_history() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "music_history.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "music_history.json"); if json_path.exists() { return; } @@ -826,7 +829,7 @@ async fn music_history() { } async fn music_saved_artists() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "saved_artists.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_artists.json"); if json_path.exists() { return; } @@ -836,7 +839,7 @@ async fn music_saved_artists() { } async fn music_saved_albums() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "saved_albums.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_albums.json"); if json_path.exists() { return; } @@ -846,7 +849,7 @@ async fn music_saved_albums() { } async fn music_saved_tracks() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "saved_tracks.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_tracks.json"); if json_path.exists() { return; } @@ -856,7 +859,7 @@ async fn music_saved_tracks() { } async fn music_saved_playlists() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "saved_playlists.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_playlists.json"); if json_path.exists() { return; } diff --git a/src/client/mod.rs b/src/client/mod.rs index 61e358e..cbdd5b4 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,12 +3,10 @@ pub(crate) mod response; mod channel; -mod history; mod music_artist; mod music_charts; mod music_details; mod music_genres; -mod music_history; mod music_new; mod music_playlist; mod music_search; @@ -20,6 +18,13 @@ mod trends; mod url_resolver; mod video_details; +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] +mod music_userdata; +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] +mod userdata; + #[cfg(feature = "rss")] #[cfg_attr(docsrs, doc(cfg(feature = "rss")))] mod channel_rss; diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index b09656a..002d7c1 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -122,20 +122,6 @@ impl RustyPipeQuery { } Ok(album) } - - /// Get all liked YouTube Music tracks of the logged-in user - /// - /// The difference to [`RustyPipeQuery::music_saved_tracks`] is that this function only returns - /// tracks that were explicitly liked by the user. - /// - /// Requires authentication cookies. - pub async fn music_liked_tracks(&self) -> Result { - self.clone() - .authenticated() - .music_playlist("LM") - .await - .map_err(util::map_internal_playlist_err) - } } impl MapResponse for response::MusicPlaylist { diff --git a/src/client/music_history.rs b/src/client/music_userdata.rs similarity index 90% rename from src/client/music_history.rs rename to src/client/music_userdata.rs index 50c5844..8c256cb 100644 --- a/src/client/music_history.rs +++ b/src/client/music_userdata.rs @@ -8,7 +8,7 @@ use crate::{ error::{Error, ExtractionError}, model::{ paginator::{ContinuationEndpoint, Paginator}, - AlbumItem, ArtistItem, HistoryItem, MusicPlaylistItem, TrackItem, + AlbumItem, ArtistItem, HistoryItem, MusicPlaylist, MusicPlaylistItem, TrackItem, }, serializer::MapResult, }; @@ -127,6 +127,20 @@ impl RustyPipeQuery { ) .await } + + /// Get all liked YouTube Music tracks of the logged-in user + /// + /// The difference to [`RustyPipeQuery::music_saved_tracks`] is that this function only returns + /// tracks that were explicitly liked by the user. + /// + /// Requires authentication cookies. + pub async fn music_liked_tracks(&self) -> Result { + self.clone() + .authenticated() + .music_playlist("LM") + .await + .map_err(crate::util::map_internal_playlist_err) + } } impl MapResponse>> for response::MusicHistory { @@ -195,7 +209,7 @@ mod tests { #[test] fn map_history() { - let json_path = path!(*TESTFILES / "music_history" / "music_history.json"); + let json_path = path!(*TESTFILES / "music_userdata" / "music_history.json"); let json_file = File::open(json_path).unwrap(); let history: response::MusicHistory = diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 1440ebe..49093eb 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -6,12 +6,15 @@ use crate::model::{ traits::FromYtItem, Comment, MusicItem, YouTubeItem, }; -use crate::model::{HistoryItem, TrackItem, VideoItem}; use crate::serializer::MapResult; -use self::response::YouTubeListItem; +#[cfg(feature = "userdata")] +use crate::model::{HistoryItem, TrackItem, VideoItem}; -use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo}; +use super::response::{ + music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo}, + YouTubeListItem, +}; use super::{ response, ClientType, MapRespCtx, MapRespOptions, MapResponse, QContinuation, RustyPipeQuery, }; @@ -225,6 +228,7 @@ impl MapResponse> for response::MusicContinuation { } } +#[cfg(feature = "userdata")] impl MapResponse>> for response::Continuation { fn map_response( self, @@ -270,6 +274,7 @@ impl MapResponse>> for response::Continuation { } } +#[cfg(feature = "userdata")] impl MapResponse>> for response::MusicContinuation { fn map_response( self, @@ -422,6 +427,8 @@ impl Paginator { } } +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] impl Paginator> { /// Get the next page from the paginator (or `None` if the paginator is exhausted) pub async fn next>(&self, query: Q) -> Result, Error> { @@ -437,6 +444,8 @@ impl Paginator> { } } +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] impl Paginator> { /// Get the next page from the paginator (or `None` if the paginator is exhausted) pub async fn next>(&self, query: Q) -> Result, Error> { @@ -533,7 +542,11 @@ macro_rules! paginator { } paginator!(Comment); +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] paginator!(HistoryItem); +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] paginator!(HistoryItem); #[cfg(test)] @@ -620,7 +633,7 @@ mod tests { } #[rstest] - #[case::subscriptions("subscriptions", path!("history" / "subscriptions.json"))] + #[case::subscriptions("subscriptions", path!("userdata" / "subscriptions.json"))] fn map_continuation_channels(#[case] name: &str, #[case] path: PathBuf) { let json_path = path!(*TESTFILES / path); let json_file = File::open(json_path).unwrap(); @@ -644,7 +657,7 @@ mod tests { #[case::playlist_tracks("playlist_tracks", path!("music_playlist" / "playlist_cont.json"))] #[case::search_tracks("search_tracks", path!("music_search" / "tracks_cont.json"))] #[case::radio_tracks("radio_tracks", path!("music_details" / "radio_cont.json"))] - #[case::saved_tracks("saved_tracks", path!("music_history" / "saved_tracks.json"))] + #[case::saved_tracks("saved_tracks", path!("music_userdata" / "saved_tracks.json"))] fn map_continuation_tracks(#[case] name: &str, #[case] path: PathBuf) { let json_path = path!(*TESTFILES / path); let json_file = File::open(json_path).unwrap(); @@ -665,7 +678,7 @@ mod tests { } #[rstest] - #[case::saved_artists("saved_artists", path!("music_history" / "saved_artists.json"))] + #[case::saved_artists("saved_artists", path!("music_userdata" / "saved_artists.json"))] fn map_continuation_artists(#[case] name: &str, #[case] path: PathBuf) { let json_path = path!(*TESTFILES / path); let json_file = File::open(json_path).unwrap(); @@ -686,7 +699,7 @@ mod tests { } #[rstest] - #[case::saved_albums("saved_albums", path!("music_history" / "saved_albums.json"))] + #[case::saved_albums("saved_albums", path!("music_userdata" / "saved_albums.json"))] fn map_continuation_albums(#[case] name: &str, #[case] path: PathBuf) { let json_path = path!(*TESTFILES / path); let json_file = File::open(json_path).unwrap(); @@ -708,7 +721,7 @@ mod tests { #[rstest] #[case::playlist_related("playlist_related", path!("music_playlist" / "playlist_related.json"))] - #[case::saved_playlists("saved_playlists", path!("music_history" / "saved_playlists.json"))] + #[case::saved_playlists("saved_playlists", path!("music_userdata" / "saved_playlists.json"))] fn map_continuation_music_playlists(#[case] name: &str, #[case] path: PathBuf) { let json_path = path!(*TESTFILES / path); let json_file = File::open(json_path).unwrap(); diff --git a/src/client/playlist.rs b/src/client/playlist.rs index 79e2329..b911b99 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -33,28 +33,6 @@ impl RustyPipeQuery { ) .await } - - /// Get all liked videos of the logged-in user - /// - /// Requires authentication cookies. - pub async fn liked_videos(&self) -> Result { - self.clone() - .authenticated() - .playlist("LL") - .await - .map_err(util::map_internal_playlist_err) - } - - /// Get the "Watch later" playlist of the logged-in user - /// - /// Requires authentication cookies. - pub async fn watch_later(&self) -> Result { - self.clone() - .authenticated() - .playlist("WL") - .await - .map_err(util::map_internal_playlist_err) - } } impl MapResponse for response::Playlist { diff --git a/src/client/response/mod.rs b/src/client/response/mod.rs index c23c449..0dc466f 100644 --- a/src/client/response/mod.rs +++ b/src/client/response/mod.rs @@ -1,10 +1,8 @@ pub(crate) mod channel; -pub(crate) mod history; pub(crate) mod music_artist; pub(crate) mod music_charts; pub(crate) mod music_details; pub(crate) mod music_genres; -pub(crate) mod music_history; pub(crate) mod music_item; pub(crate) mod music_new; pub(crate) mod music_playlist; @@ -19,7 +17,6 @@ pub(crate) mod video_item; pub(crate) use channel::Channel; pub(crate) use channel::ChannelAbout; -pub(crate) use history::History; pub(crate) use music_artist::MusicArtist; pub(crate) use music_artist::MusicArtistAlbums; pub(crate) use music_charts::MusicCharts; @@ -28,7 +25,6 @@ pub(crate) use music_details::MusicLyrics; pub(crate) use music_details::MusicRelated; pub(crate) use music_genres::MusicGenre; pub(crate) use music_genres::MusicGenres; -pub(crate) use music_history::MusicHistory; pub(crate) use music_item::MusicContinuation; pub(crate) use music_new::MusicNew; pub(crate) use music_playlist::MusicPlaylist; @@ -51,6 +47,15 @@ pub(crate) mod channel_rss; #[cfg(feature = "rss")] pub(crate) use channel_rss::ChannelRss; +#[cfg(feature = "userdata")] +pub(crate) mod history; +#[cfg(feature = "userdata")] +pub(crate) use history::History; +#[cfg(feature = "userdata")] +pub(crate) mod music_history; +#[cfg(feature = "userdata")] +pub(crate) use music_history::MusicHistory; + use std::borrow::Cow; use std::collections::HashMap; use std::marker::PhantomData; diff --git a/src/client/response/music_item.rs b/src/client/response/music_item.rs index d2bf983..f7cadf6 100644 --- a/src/client/response/music_item.rs +++ b/src/client/response/music_item.rs @@ -1,11 +1,10 @@ use serde::Deserialize; use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError}; -use time::UtcOffset; use crate::{ model::{ self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId, - HistoryItem, MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem, + MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem, }, param::Language, serializer::{ @@ -23,6 +22,11 @@ use super::{ SimpleHeaderRenderer, Thumbnails, ThumbnailsWrap, }; +#[cfg(feature = "userdata")] +use crate::model::HistoryItem; +#[cfg(feature = "userdata")] +use time::UtcOffset; + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) enum ItemSection { @@ -40,6 +44,7 @@ pub(crate) enum ItemSection { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicShelf { + #[cfg(feature = "userdata")] #[serde_as(as = "Option")] pub title: Option, /// Playlist ID (only for playlists) @@ -1270,6 +1275,7 @@ impl MusicListMapper { } } + #[cfg(feature = "userdata")] pub fn conv_history_items( self, date_txt: Option, diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index f855f3f..2a48bc6 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -2,14 +2,11 @@ use serde::Deserialize; use serde_with::{ rust::deserialize_ignore_any, serde_as, DefaultOnError, DisplayFromStr, VecSkipError, }; -use time::{OffsetDateTime, UtcOffset}; +use time::OffsetDateTime; -use super::{ - ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, SimpleHeaderRenderer, - Thumbnails, -}; +use super::{ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, Thumbnails}; use crate::{ - model::{Channel, ChannelItem, ChannelTag, HistoryItem, PlaylistItem, VideoItem, YouTubeItem}, + model::{Channel, ChannelItem, ChannelTag, PlaylistItem, VideoItem, YouTubeItem}, param::Language, serializer::{ text::{AttributedText, Text, TextComponent}, @@ -18,6 +15,11 @@ use crate::{ util::{self, timeago, TryRemove}, }; +#[cfg(feature = "userdata")] +use crate::{client::response::SimpleHeaderRenderer, model::HistoryItem}; +#[cfg(feature = "userdata")] +use time::UtcOffset; + #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -66,6 +68,7 @@ pub(crate) enum YouTubeListItem { /// GridRenderer: contains videos on channel page #[serde(alias = "expandedShelfContentsRenderer", alias = "gridRenderer")] ItemSectionRenderer { + #[cfg(feature = "userdata")] header: Option, #[serde(alias = "items")] contents: MapResult>, @@ -298,6 +301,7 @@ pub(crate) struct YouTubeListRenderer { pub contents: MapResult>, } +#[cfg(feature = "userdata")] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ItemSectionHeader { @@ -904,6 +908,7 @@ impl YouTubeListMapper { res.c.into_iter().for_each(|item| self.map_item(item)); } + #[cfg(feature = "userdata")] pub(crate) fn conv_history_items( self, date_txt: Option, diff --git a/src/client/snapshots/rustypipe__client__music_history__tests__map_history.snap b/src/client/snapshots/rustypipe__client__music_userdata__tests__map_history.snap similarity index 100% rename from src/client/snapshots/rustypipe__client__music_history__tests__map_history.snap rename to src/client/snapshots/rustypipe__client__music_userdata__tests__map_history.snap diff --git a/src/client/snapshots/rustypipe__client__history__tests__map_history.snap b/src/client/snapshots/rustypipe__client__userdata__tests__map_history.snap similarity index 100% rename from src/client/snapshots/rustypipe__client__history__tests__map_history.snap rename to src/client/snapshots/rustypipe__client__userdata__tests__map_history.snap diff --git a/src/client/snapshots/rustypipe__client__history__tests__map_subscription_feed.snap b/src/client/snapshots/rustypipe__client__userdata__tests__map_subscription_feed.snap similarity index 100% rename from src/client/snapshots/rustypipe__client__history__tests__map_subscription_feed.snap rename to src/client/snapshots/rustypipe__client__userdata__tests__map_subscription_feed.snap diff --git a/src/client/history.rs b/src/client/userdata.rs similarity index 90% rename from src/client/history.rs rename to src/client/userdata.rs index 427715c..c2e4815 100644 --- a/src/client/history.rs +++ b/src/client/userdata.rs @@ -7,7 +7,7 @@ use crate::{ error::{Error, ExtractionError}, model::{ paginator::{ContinuationEndpoint, Paginator}, - ChannelItem, HistoryItem, PlaylistItem, VideoItem, + ChannelItem, HistoryItem, Playlist, PlaylistItem, VideoItem, }, serializer::MapResult, }; @@ -148,6 +148,28 @@ impl RustyPipeQuery { ) .await } + + /// Get all liked videos of the logged-in user + /// + /// Requires authentication cookies. + pub async fn liked_videos(&self) -> Result { + self.clone() + .authenticated() + .playlist("LL") + .await + .map_err(crate::util::map_internal_playlist_err) + } + + /// Get the "Watch later" playlist of the logged-in user + /// + /// Requires authentication cookies. + pub async fn watch_later(&self) -> Result { + self.clone() + .authenticated() + .playlist("WL") + .await + .map_err(crate::util::map_internal_playlist_err) + } } impl MapResponse>> for response::History { @@ -258,7 +280,7 @@ mod tests { #[test] fn map_history() { - let json_path = path!(*TESTFILES / "history" / "history.json"); + let json_path = path!(*TESTFILES / "userdata" / "history.json"); let json_file = File::open(json_path).unwrap(); let history: response::History = @@ -278,7 +300,7 @@ mod tests { #[test] fn map_subscription_feed() { - let json_path = path!(*TESTFILES / "history" / "subscription_feed.json"); + let json_path = path!(*TESTFILES / "userdata" / "subscription_feed.json"); let json_file = File::open(json_path).unwrap(); let history: response::History = diff --git a/src/util/mod.rs b/src/util/mod.rs index 0f4499f..fc6af15 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -21,7 +21,7 @@ use regex::Regex; use url::Url; use crate::{ - error::{AuthError, Error, ExtractionError}, + error::Error, param::{Country, Language, COUNTRIES}, serializer::text::TextComponent, }; @@ -581,9 +581,10 @@ where /// /// If no user is logged in, YouTube returns a "NotFound" error. This has to be corrected /// into a NoLogin error. +#[cfg(feature = "userdata")] pub fn map_internal_playlist_err(e: Error) -> Error { - if let Error::Extraction(ExtractionError::NotFound { .. }) = e { - Error::Auth(AuthError::NoLogin) + if let Error::Extraction(crate::error::ExtractionError::NotFound { .. }) = e { + Error::Auth(crate::error::AuthError::NoLogin) } else { e } diff --git a/src/util/timeago.rs b/src/util/timeago.rs index 011b3ed..8452415 100644 --- a/src/util/timeago.rs +++ b/src/util/timeago.rs @@ -347,6 +347,7 @@ pub fn parse_textual_date_to_dt( /// Parse a textual date (e.g. "29 minutes ago" "Jul 2, 2014") into a Date object. /// /// Returns None if the date could not be parsed. +#[cfg(feature = "userdata")] pub fn parse_textual_date_to_d( lang: Language, utc_offset: UtcOffset, diff --git a/testfiles/music_history/music_history.json b/testfiles/music_userdata/music_history.json similarity index 100% rename from testfiles/music_history/music_history.json rename to testfiles/music_userdata/music_history.json diff --git a/testfiles/music_history/saved_albums.json b/testfiles/music_userdata/saved_albums.json similarity index 100% rename from testfiles/music_history/saved_albums.json rename to testfiles/music_userdata/saved_albums.json diff --git a/testfiles/music_history/saved_artists.json b/testfiles/music_userdata/saved_artists.json similarity index 100% rename from testfiles/music_history/saved_artists.json rename to testfiles/music_userdata/saved_artists.json diff --git a/testfiles/music_history/saved_playlists.json b/testfiles/music_userdata/saved_playlists.json similarity index 100% rename from testfiles/music_history/saved_playlists.json rename to testfiles/music_userdata/saved_playlists.json diff --git a/testfiles/music_history/saved_tracks.json b/testfiles/music_userdata/saved_tracks.json similarity index 100% rename from testfiles/music_history/saved_tracks.json rename to testfiles/music_userdata/saved_tracks.json diff --git a/testfiles/history/history.json b/testfiles/userdata/history.json similarity index 100% rename from testfiles/history/history.json rename to testfiles/userdata/history.json diff --git a/testfiles/history/subscription_feed.json b/testfiles/userdata/subscription_feed.json similarity index 100% rename from testfiles/history/subscription_feed.json rename to testfiles/userdata/subscription_feed.json diff --git a/testfiles/history/subscriptions.json b/testfiles/userdata/subscriptions.json similarity index 100% rename from testfiles/history/subscriptions.json rename to testfiles/userdata/subscriptions.json diff --git a/tests/youtube.rs b/tests/youtube.rs index 140770a..9a5fc35 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -5,7 +5,7 @@ use std::fmt::Display; use std::str::FromStr; use rstest::{fixture, rstest}; -use rustypipe::model::{HistoryItem, TrackItem, TrackType, VideoItem}; +use rustypipe::model::TrackType; use rustypipe::param::{AlbumOrder, LANGUAGES}; use time::{macros::date, OffsetDateTime}; @@ -2728,9 +2728,12 @@ async fn isrc_search_languages(rp: RustyPipe) { } } -mod cookie_auth { +#[cfg(feature = "userdata")] +mod user_data { use super::*; + use rustypipe::model::{HistoryItem, TrackItem, VideoItem}; + #[rstest] #[tokio::test] async fn history(rp: RustyPipe) { @@ -2814,6 +2817,30 @@ mod cookie_auth { let tracks = rp.query().music_liked_tracks().await.unwrap(); assert_next_items(tracks.tracks, rp.query(), 5).await; } + + /// Assert that the history paginator produces at least n items + async fn assert_next_history>( + paginator: Paginator>, + query: Q, + n_items: usize, + ) { + let mut p = paginator; + let query = query.as_ref(); + p.extend_limit(query, n_items).await.unwrap(); + assert_gte(p.items.len(), n_items, "items"); + } + + /// Assert that the music history paginator produces at least n items + async fn assert_next_music_history>( + paginator: Paginator>, + query: Q, + n_items: usize, + ) { + let mut p = paginator; + let query = query.as_ref(); + p.extend_limit(query, n_items).await.unwrap(); + assert_gte(p.items.len(), n_items, "items"); + } } #[rstest] @@ -2940,30 +2967,6 @@ async fn assert_next_items>( assert_gte(p.items.len(), n_items, "items"); } -/// Assert that the history paginator produces at least n items -async fn assert_next_history>( - paginator: Paginator>, - query: Q, - n_items: usize, -) { - let mut p = paginator; - let query = query.as_ref(); - p.extend_limit(query, n_items).await.unwrap(); - assert_gte(p.items.len(), n_items, "items"); -} - -/// Assert that the music history paginator produces at least n items -async fn assert_next_music_history>( - paginator: Paginator>, - query: Q, - n_items: usize, -) { - let mut p = paginator; - let query = query.as_ref(); - p.extend_limit(query, n_items).await.unwrap(); - assert_gte(p.items.len(), n_items, "items"); -} - #[track_caller] fn assert_frameset(frameset: &Frameset) { assert_gte(frameset.frame_height, 20, "frame height"); @@ -3025,10 +3028,6 @@ async fn all_send_and_sync() { rp.query() .drm_license("", rustypipe::model::DrmSystem::Widevine, "", "", &[]), ); - send_and_sync(rp.query().history()); - send_and_sync(rp.query().history_continuation("", None)); - send_and_sync(rp.query().history_search("")); - send_and_sync(rp.query().liked_videos()); send_and_sync(rp.query().music_album("")); send_and_sync(rp.query().music_artist("", false)); send_and_sync(rp.query().music_artist_albums("", None, None)); @@ -3037,9 +3036,6 @@ async fn all_send_and_sync() { send_and_sync(rp.query().music_details("")); send_and_sync(rp.query().music_genre("")); send_and_sync(rp.query().music_genres()); - send_and_sync(rp.query().music_history()); - send_and_sync(rp.query().music_history_continuation("", None)); - send_and_sync(rp.query().music_liked_tracks()); send_and_sync(rp.query().music_lyrics("")); send_and_sync(rp.query().music_new_albums()); send_and_sync(rp.query().music_new_videos()); @@ -3048,10 +3044,6 @@ async fn all_send_and_sync() { send_and_sync(rp.query().music_radio_playlist("")); send_and_sync(rp.query().music_radio_track("")); send_and_sync(rp.query().music_related("")); - send_and_sync(rp.query().music_saved_albums()); - send_and_sync(rp.query().music_saved_artists()); - send_and_sync(rp.query().music_saved_playlists()); - send_and_sync(rp.query().music_saved_tracks()); send_and_sync(rp.query().music_search::("", None)); send_and_sync(rp.query().music_search_albums("")); send_and_sync(rp.query().music_search_artists("")); @@ -3068,17 +3060,32 @@ async fn all_send_and_sync() { send_and_sync(rp.query().raw(ClientType::Desktop, "", "")); send_and_sync(rp.query().resolve_string("", false)); send_and_sync(rp.query().resolve_url("", false)); - send_and_sync(rp.query().saved_playlists()); send_and_sync(rp.query().search::("")); send_and_sync( rp.query() .search_filter::("", &SearchFilter::default()), ); send_and_sync(rp.query().search_suggestion("")); - send_and_sync(rp.query().subscription_feed()); - send_and_sync(rp.query().subscriptions()); send_and_sync(rp.query().trending()); send_and_sync(rp.query().video_comments("", None)); send_and_sync(rp.query().video_details("")); - send_and_sync(rp.query().watch_later()); + + #[cfg(feature = "userdata")] + { + send_and_sync(rp.query().history()); + send_and_sync(rp.query().history_continuation("", None)); + send_and_sync(rp.query().history_search("")); + send_and_sync(rp.query().liked_videos()); + send_and_sync(rp.query().watch_later()); + send_and_sync(rp.query().music_history()); + send_and_sync(rp.query().music_history_continuation("", None)); + send_and_sync(rp.query().music_saved_albums()); + send_and_sync(rp.query().music_saved_artists()); + send_and_sync(rp.query().music_saved_playlists()); + send_and_sync(rp.query().music_saved_tracks()); + send_and_sync(rp.query().saved_playlists()); + send_and_sync(rp.query().subscription_feed()); + send_and_sync(rp.query().subscriptions()); + send_and_sync(rp.query().music_liked_tracks()); + } } From a80f046a198c6d9e2cdad20d74dc6e4436f8d1b1 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 7 Feb 2025 20:46:33 +0100 Subject: [PATCH 10/64] ci: update rustypipe-botguard --- .forgejo/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 49e7479..6c53208 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -28,7 +28,7 @@ jobs: run: | TARGET=$(rustc --version --verbose | grep "host:" | sed -e 's/^host: //') cd ~ - curl -SsL -o rustypipe-botguard.tar.xz "https://codeberg.org/ThetaDev/rustypipe-botguard/releases/download/v0.1.0/rustypipe-botguard-v0.1.0-${TARGET}.tar.xz" + curl -SsL -o rustypipe-botguard.tar.xz "https://codeberg.org/ThetaDev/rustypipe-botguard/releases/download/v0.1.1/rustypipe-botguard-v0.1.1-${TARGET}.tar.xz" cd /usr/local/bin sudo tar -xJf ~/rustypipe-botguard.tar.xz rm ~/rustypipe-botguard.tar.xz From 0c94267d0371b2b26c7b5c9abfa156d5cde2153e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 7 Feb 2025 22:00:48 +0100 Subject: [PATCH 11/64] fix: only use cached potokens with min. 10min lifetime --- src/client/mod.rs | 64 ++++++++++++++++++++++++++++++---------- src/util/visitor_data.rs | 2 +- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index cbdd5b4..3962be8 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -508,6 +508,26 @@ impl DefaultOpt { /// - [`music_new_albums`](RustyPipeQuery::music_new_albums) /// - [`music_new_videos`](RustyPipeQuery::music_new_videos) /// +/// ### User data (🔒 Feature `userdata`) +/// +/// - **Playback history** +/// - [`history`](RustyPipeQuery::history) +/// - [`history_search`](RustyPipeQuery::history_search) +/// - [`music_history`](RustyPipeQuery::music_history) +/// - **YouTube library** +/// - [`liked_videos`](RustyPipeQuery::liked_videos) +/// - [`watch_later`](RustyPipeQuery::watch_later) +/// - [`saved_playlists`](RustyPipeQuery::saved_playlists) +/// - **Music library** +/// - [`music_saved_artists`](RustyPipeQuery::music_saved_artists) +/// - [`music_saved_albums`](RustyPipeQuery::music_saved_albums) +/// - [`music_saved_tracks`](RustyPipeQuery::music_saved_tracks) +/// - [`music_saved_playlists`](RustyPipeQuery::music_saved_playlists) +/// - [`music_liked_tracks`](RustyPipeQuery::music_liked_tracks) +/// - **Subscriptions** +/// - [`subscriptions`](RustyPipeQuery::subscriptions) +/// - [`subscription_feed`](RustyPipeQuery::subscription_feed) +/// /// ## Options /// /// You can set the language, country and visitor data ID for individual requests. @@ -848,9 +868,9 @@ impl RustyPipeBuilder { self } - /// Set the maximum number of attempts for HTTP requests (at least 1). + /// Set the maximum number of attempts for YouTube requests (at least 1). /// - /// If a HTTP requests fails because of a serverside error and retries are enabled, + /// If a request fails because of a serverside error and retries are enabled, /// RustyPipe waits 1 second before the next attempt. /// /// The wait time is doubled for subsequent attempts (including a bit of @@ -2196,26 +2216,38 @@ impl RustyPipeQuery { } let mut valid_until = None; + let mut from_snapshot = false; for word in words { if let Some((k, v)) = word.split_once('=') { - if k == "valid_until" { - valid_until = Some( - v.parse::() - .ok() - .and_then(|x| OffsetDateTime::from_unix_timestamp(x).ok()) - .ok_or(ExtractionError::Botguard( - format!("invalid validity date: {v}").into(), - ))?, - ); + match k { + "valid_until" => { + valid_until = Some( + v.parse::() + .ok() + .and_then(|x| OffsetDateTime::from_unix_timestamp(x).ok()) + .ok_or(ExtractionError::Botguard( + format!("invalid validity date: {v}").into(), + ))?, + ); + } + "from_snapshot" => { + from_snapshot = v.eq_ignore_ascii_case("true") || v == "1"; + } + _ => {} } } } - tracing::debug!("generated PO token (took {:?})", start.elapsed()); - Ok(( - tokens, - valid_until.unwrap_or_else(|| OffsetDateTime::now_utc() + time::Duration::hours(12)), - )) + let valid_until = + valid_until.unwrap_or_else(|| OffsetDateTime::now_utc() + time::Duration::hours(12)); + + tracing::debug!( + "generated PO token (valid_until {}, from_snapshot={}, took {}ms)", + valid_until, + from_snapshot, + start.elapsed().as_millis() + ); + Ok((tokens, valid_until)) } async fn get_session_po_token(&self, visitor_data: &str) -> Result { diff --git a/src/util/visitor_data.rs b/src/util/visitor_data.rs index b9a3d7b..1daf1bd 100644 --- a/src/util/visitor_data.rs +++ b/src/util/visitor_data.rs @@ -181,7 +181,7 @@ impl VisitorDataCache { pub fn get_pot(&self, visitor_data: &str) -> Option { let pots = self.inner.session_potoken.read().unwrap(); if let Some(entry) = pots.get(visitor_data) { - if entry.valid_until > OffsetDateTime::now_utc() { + if entry.valid_until > OffsetDateTime::now_utc() + time::Duration::minutes(10) { return Some(entry.clone()); } } From c1a872e1c14ea0956053bd7c65f6875b1cb3bc55 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 7 Feb 2025 22:50:56 +0100 Subject: [PATCH 12/64] refactor: rename rustypipe-cli binary name to rustypipe --- cli/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d6954b5..67fa3df 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -64,3 +64,7 @@ dirs.workspace = true anstream = "0.6.15" owo-colors = "4.0.0" const_format = "0.2.33" + +[[bin]] +name = "rustypipe" +path = "src/main.rs" From 9957add2b5d6391b2c1869d2019fd7dd91b8cd41 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 7 Feb 2025 23:09:05 +0100 Subject: [PATCH 13/64] doc: add Botguard info to README --- README.md | 28 ++++++++++++++++++++++++++++ cli/README.md | 13 ++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c4df0c2..767680f 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,19 @@ Subscribers: 1780000 ... ``` +## Crate features + +Some features of RustyPipe are gated behind features to avoid compiling unneeded +dependencies. + +- `rss` Fetch a channel's RSS feed, which is faster than fetching the channel page +- `userdata` Add functions to fetch YouTube user data (watch history, subscriptions, + music library) + +You can also choose the TLS library used for making web requests using the same features +as the reqwest crate (`default-tls`, `native-tls`, `native-tls-alpn`, +`native-tls-vendored`, `rustls-tls-webpki-roots`, `rustls-tls-native-roots`). + ## Cache storage The RustyPipe cache holds the current version numbers for all clients, the JavaScript @@ -213,6 +226,21 @@ RustyPipe reports come in 3 severity levels: incomplete) - ERR (entire response could not be deserialized/parsed, RustyPipe returned an error) +## PO tokens + +Since August 2024 YouTube requires PO tokens to access streams from web-based clients +(Desktop, Mobile). Otherwise streams will return a 403 error. + +Generating PO tokens requires a simulated browser environment, which would be too large +to include in RustyPipe directly. + +Therefore, the PO token generation is handled by a seperate CLI application +([rustypipe-botguard](https://codeberg.org/ThetaDev/rustypipe-botguard)) which is called +by the RustyPipe crate. RustyPipe automatically detects the rustypipe-botguard binary if +it is located in PATH or the current working directory. If your rustypipe-botguard +binary is located at a different path, you can specify it with the `.botguard_bin(path)` +option. + ## Authentication RustyPipe supports authenticating with your YouTube account to access diff --git a/cli/README.md b/cli/README.md index 28c34a4..25a9e49 100644 --- a/cli/README.md +++ b/cli/README.md @@ -8,7 +8,17 @@ The RustyPipe CLI is a powerful YouTube client for the command line. It allows y access most of the features of the RustyPipe crate: getting data from YouTube and downloading videos. -The following subcommands are included: +## Installation + +You can download a compiled version of RustyPipe here: + + +Alternatively, you can compile it yourself by installing [Rust](https://rustup.rs/) and +running `cargo install rustypipe-cli`. + +To be able to download streams from web-based clients (Desktop, Mobile) you need to +download [rustypipe-botguard](https://codeberg.org/ThetaDev/rustypipe-botguard/releases) +and place the binary either in the PATH or the current working directory. ## `get`: Fetch information @@ -140,6 +150,7 @@ Fetch a list of all the items saved in your YouTube/YouTube Music profile. Europe/Berlin, Australia/Sydney) **Note:** this requires building rustypipe-cli with the `timezone` feature + - `--local-tz` Use the local timezone instead of UTC - `--report` Generate a report on every request and store it in a `rustypipe_reports` folder in the current directory From 1d755b76bf4569f7d0bb90a65494ac8e7aae499a Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 01:52:09 +0100 Subject: [PATCH 14/64] feat: add RustyPipe::version_botguard fn, detect rustypipe-botguard in current dir, add botguard version to report --- src/client/channel_rss.rs | 4 +- src/client/mod.rs | 152 ++++++++++++++++++++++++++++---------- src/deobfuscate.rs | 2 +- src/report.rs | 7 +- 4 files changed, 120 insertions(+), 45 deletions(-) diff --git a/src/client/channel_rss.rs b/src/client/channel_rss.rs index b28a802..f3f7319 100644 --- a/src/client/channel_rss.rs +++ b/src/client/channel_rss.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use crate::{ error::{Error, ExtractionError}, model::ChannelRss, - report::{Report, RustyPipeInfo}, + report::Report, util, }; @@ -45,7 +45,7 @@ impl RustyPipeQuery { Err(e) => { if let Some(reporter) = &self.client.inner.reporter { let report = Report { - info: RustyPipeInfo::new(Some(self.opts.lang)), + info: self.rp_info(), level: crate::report::Level::ERR, operation: "channel_rss", error: Some(e.to_string()), diff --git a/src/client/mod.rs b/src/client/mod.rs index 3962be8..b3df2b2 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -362,6 +362,8 @@ const OAUTH_CLIENT_ID: &str = const OAUTH_CLIENT_SECRET: &str = "SboVhoG9s0rNafixCSGGKXAT"; const OAUTH_SCOPES: &str = "http://gdata.youtube.com https://www.googleapis.com/auth/youtube"; +const BOTGUARD_API_VERSION: &str = "1"; + static CLIENT_VERSION_REGEX: Lazy = Lazy::new(|| Regex::new(r#""INNERTUBE_CONTEXT_CLIENT_VERSION":"([\w\d\._-]+?)""#).unwrap()); @@ -409,11 +411,13 @@ pub struct RustyPipeBuilder { default_opts: RustyPipeOpts, storage_dir: Option, botguard_bin: DefaultOpt, + snapshot_file: Option, po_token_cache: bool, } struct BotguardCfg { program: OsString, + version: String, snapshot_file: PathBuf, po_token_cache: bool, } @@ -441,13 +445,6 @@ impl DefaultOpt { DefaultOpt::Default => Some(f()), } } - fn or_default_opt Option>(self, f: F) -> Option { - match self { - DefaultOpt::Some(x) => Some(x), - DefaultOpt::None => None, - DefaultOpt::Default => f(), - } - } } /// # RustyPipe query @@ -684,6 +681,7 @@ impl RustyPipeBuilder { user_agent: None, storage_dir: None, botguard_bin: DefaultOpt::Default, + snapshot_file: None, po_token_cache: false, } } @@ -749,27 +747,31 @@ impl RustyPipeBuilder { let visitor_data_cache = VisitorDataCache::new(http.clone(), 50, 20); - let botguard_bin = self.botguard_bin.or_default_opt(|| { - let n = OsString::from("rustypipe-botguard"); - let out = std::process::Command::new(&n) - .arg("--version") - .output() - .ok()?; - if !out.status.success() { - return None; + let botguard = match self.botguard_bin { + DefaultOpt::Some(botguard_bin) => Some(detect_botguard_bin(botguard_bin)?), + DefaultOpt::None => None, + DefaultOpt::Default => detect_botguard_bin("./rustypipe-botguard".into()) + .or_else(|_| detect_botguard_bin("rustypipe-botguard".into())) + .map_err(|e| tracing::debug!("could not detect rustypipe-botguard: {e}")) + .ok(), + } + .map(|(program, version)| { + tracing::debug!( + "rustypipe-botguard: using {} at {}", + version, + program.to_string_lossy() + ); + + BotguardCfg { + program: program.to_owned(), + version, + snapshot_file: self.snapshot_file.unwrap_or_else(|| { + let mut snapshot_file = storage_dir.clone(); + snapshot_file.push("bg_snapshot.bin"); + snapshot_file + }), + po_token_cache: self.po_token_cache, } - let output = String::from_utf8_lossy(&out.stdout); - let pat = "rustypipe-botguard-api "; - let pos = output.find(pat)? + pat.len(); - let pos_end = output[pos..] - .char_indices() - .find(|(_, c)| !c.is_ascii_digit()) - .map(|(p, _)| p + pos) - .unwrap_or(output.len()); - if &output[pos..pos_end] != "1" { - return None; - } - Some(n) }); Ok(RustyPipe { @@ -777,7 +779,7 @@ impl RustyPipeBuilder { http, storage, reporter: self.reporter.or_default(|| { - let mut report_dir = storage_dir.clone(); + let mut report_dir = storage_dir; report_dir.push(DEFAULT_REPORT_DIR); Box::new(FileReporter::new(report_dir)) }), @@ -791,15 +793,7 @@ impl RustyPipeBuilder { default_opts: self.default_opts, user_agent, visitor_data_cache, - botguard: botguard_bin.map(|program| { - let mut snapshot_file = storage_dir; - snapshot_file.push("bg_snapshot.bin"); - BotguardCfg { - program, - snapshot_file, - po_token_cache: self.po_token_cache, - } - }), + botguard, }), }) } @@ -1035,6 +1029,18 @@ impl RustyPipeBuilder { self } + /// Set the path where the rustypipe-botguard snapshot file is stored + /// + /// After solving a Botguard challenge, rustypipe-botguard stores its + /// JavaScript environment in a snapshot file, so it can quickly generate additional tokens. + /// + /// By default the snapshot is stored in the storage_dir (Filename: bg_snapshot.bin). + #[must_use] + pub fn botguard_snapshot_file>(mut self, snapshot_file: P) -> Self { + self.snapshot_file = Some(snapshot_file.into()); + self + } + /// Enable caching for session-bound PO tokens /// /// By default, RustyPipe calls Botguard for every player request to fetch both a @@ -1699,6 +1705,11 @@ impl RustyPipe { ); Ok(()) } + + /// Get the version string (e.g. `rustypipe-botguard 0.1.1`) of the used botguard binary + pub async fn version_botguard(&self) -> Option { + self.inner.botguard.as_ref().map(|bg| bg.version.to_owned()) + } } impl RustyPipeQuery { @@ -2177,7 +2188,7 @@ impl RustyPipeQuery { self.client.inner.visitor_data_cache.remove(visitor_data); } - /// Get PO tokens + /// Generate PO tokens async fn get_po_tokens(&self, idents: &[&str]) -> Result<(Vec, OffsetDateTime), Error> { let bg = self .client @@ -2250,6 +2261,7 @@ impl RustyPipeQuery { Ok((tokens, valid_until)) } + /// Get a session-bound PO token (either from cache or newly generated) async fn get_session_po_token(&self, visitor_data: &str) -> Result { if let Some(po_token) = self.client.inner.visitor_data_cache.get_pot(visitor_data) { return Ok(po_token); @@ -2263,7 +2275,7 @@ impl RustyPipeQuery { Ok(po_token) } - /// Get a Proof-of-origin token + /// Get a PO token (Proof-of-origin token) /// /// PO tokens are used by the web-based YouTube clients for requesting player data and video streams. /// @@ -2277,6 +2289,22 @@ impl RustyPipeQuery { }) } + /// Get a new RustyPipeInfo object for reports + fn rp_info(&self) -> RustyPipeInfo<'_> { + RustyPipeInfo::new( + Some(self.opts.lang), + self.client + .inner + .botguard + .as_ref() + .map(|bg| bg.version.as_str()), + ) + } + + /// Execute a request to the YouTube API, then deobfuscate and map the response. + /// + /// Runs a single attempt, returns Ok with a erroneous RequestResult in case of a + /// HTTP or mapping error so it can be retried/reported. async fn execute_request_attempt< R: DeserializeOwned + MapResponse + Debug, M, @@ -2368,6 +2396,10 @@ impl RustyPipeQuery { }) } + /// Execute a request to the YouTube API, then deobfuscate and map the response. + /// + /// Runs up to n_request_attempts, returns Ok with a erroneous RequestResult in case of a + /// HTTP or mapping error so it can be reported. async fn execute_request_inner< R: DeserializeOwned + MapResponse + Debug, M, @@ -2474,7 +2506,7 @@ impl RustyPipeQuery { if level > Level::DBG || self.opts.report { if let Some(reporter) = &self.client.inner.reporter { let report = Report { - info: RustyPipeInfo::new(Some(self.opts.lang)), + info: self.rp_info(), level, operation: &format!("{operation}({id})"), error, @@ -2674,6 +2706,46 @@ fn local_tz_offset() -> (String, i16) { } } +/// Check if a valid Botguard binary is available at the given location +fn detect_botguard_bin(program: OsString) -> Result<(OsString, String), Error> { + let out = std::process::Command::new(&program) + .arg("--version") + .output() + .map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + Error::Other("rustypipe-botguard binary not found".into()) + } else { + Error::Other(format!("error calling rustypipe-botguard {e}").into()) + } + })?; + if !out.status.success() { + return Err(Error::Extraction(ExtractionError::Botguard( + format!("version check failed with status {}", out.status).into(), + ))); + } + let output = String::from_utf8_lossy(&out.stdout); + let pat = "rustypipe-botguard-api "; + let pos = output.find(pat).ok_or(Error::Other( + "no rustypipe-botguard-api version returned".into(), + ))? + pat.len(); + let pos_end = output[pos..] + .char_indices() + .find(|(_, c)| !c.is_ascii_digit()) + .map(|(p, _)| p + pos) + .unwrap_or(output.len()); + let api_version = &output[pos..pos_end]; + if api_version != BOTGUARD_API_VERSION { + return Err(Error::Other( + format!( + "incompatible rustypipe-botguard-api version {api_version}, expected {BOTGUARD_API_VERSION}" + ) + .into(), + )); + } + let version = output[..pos].lines().next().unwrap_or_default().to_owned(); + Ok((program, version)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index d0adc12..75748b2 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -39,7 +39,7 @@ impl DeobfData { if let Err(e) = &res { if let Some(reporter) = reporter { let report = Report { - info: RustyPipeInfo::new(None), + info: RustyPipeInfo::new(None, None), level: Level::ERR, operation: "extract_deobf", error: Some(e.to_string()), diff --git a/src/report.rs b/src/report.rs index 85477fa..3ff5d4d 100644 --- a/src/report.rs +++ b/src/report.rs @@ -70,6 +70,8 @@ pub struct RustyPipeInfo<'a> { /// YouTube content language #[serde(skip_serializing_if = "Option::is_none")] pub language: Option, + /// RustyPipe Botguard version (`rustypipe-botguard 0.1.1`) + pub botguard_version: Option<&'a str>, } /// Reported HTTP request data @@ -104,13 +106,14 @@ pub enum Level { ERR, } -impl RustyPipeInfo<'_> { - pub(crate) fn new(language: Option) -> Self { +impl<'a> RustyPipeInfo<'a> { + pub(crate) fn new(language: Option, botguard_version: Option<&'a str>) -> Self { Self { package: env!("CARGO_PKG_NAME"), version: crate::VERSION, date: util::now_sec(), language, + botguard_version, } } } From c0770f281cbe807a0075ef417bb9aa86bc0989f0 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 01:53:12 +0100 Subject: [PATCH 15/64] ci: release rustypipe-cli binaries --- .forgejo/workflows/release-cli.yaml | 68 +++++++++++++++++++++++++++++ cli/README.md | 4 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 .forgejo/workflows/release-cli.yaml diff --git a/.forgejo/workflows/release-cli.yaml b/.forgejo/workflows/release-cli.yaml new file mode 100644 index 0000000..0e0ca64 --- /dev/null +++ b/.forgejo/workflows/release-cli.yaml @@ -0,0 +1,68 @@ +name: Release CLI +on: + push: + tags: + - "rustypipe-cli/v*.*.*" + +jobs: + Release: + runs-on: cimaster-latest + steps: + - name: 📦 Checkout repository + uses: actions/checkout@v4 + + - name: Setup cross compilation + run: | + rustup target add x86_64-pc-windows-msvc x86_64-apple-darwin aarch64-apple-darwin + cargo install cargo-xwin + + # https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html/ + sudo apt-get install -y llvm clang cmake + cd ~ + git clone https://github.com/tpoechtrager/osxcross + cd osxcross + wget -nc "https://github.com/joseluisq/macosx-sdks/releases/download/12.3/MacOSX12.3.sdk.tar.xz" + mv MacOSX12.3.sdk.tar.xz tarballs/ + UNATTENDED=yes OSX_VERSION_MIN=12.3 ./build.sh + OSXCROSS_BIN="$(pwd)/target/bin" + + echo "CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER=$(find "$OSXCROSS_BIN" -name "x86_64-apple-darwin*-clang")" >> $GITHUB_ENV + echo "CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS=-Car=$(find "$OSXCROSS_BIN" -name "x86_64-apple-darwin*-ar"),-Clink-arg=-undefined,-Clink-arg=dynamic_lookup" >> $GITHUB_ENV + echo "CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=$(find "$OSXCROSS_BIN" -name "aarch64-apple-darwin*-clang")" >> $GITHUB_ENV + echo "CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS=-Car=$(find "$OSXCROSS_BIN" -name "aarch64-apple-darwin*-ar"),-Clink-arg=-undefined,-Clink-arg=dynamic_lookup" >> $GITHUB_ENV + + - name: ⚒️ Build application + run: | + export PATH="$PATH:$HOME/osxcross/target/bin" + CRATE="rustypipe-cli" + PKG_CONFIG_SYSROOT_DIR=/usr/x86_64-linux-gnu cargo build --release --package=$CRATE --target x86_64-unknown-linux-gnu + PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu cargo build --release --package=$CRATE --target aarch64-unknown-linux-gnu + CC="$CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER" CXX="$CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER++" cargo build --release --package=$CRATE --target x86_64-apple-darwin + CC="$CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER" CXX="$CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER++" cargo build --release --package=$CRATE --target aarch64-apple-darwin + cargo xwin build --release --package=$CRATE --target x86_64-pc-windows-msvc + + - name: Prepare release + run: | + CRATE="rustypipe-cli" + BIN="rustypipe" + echo "CRATE=$CRATE" >> "$GITHUB_ENV" + echo "CRATE_VERSION=$(echo '${{ github.ref_name }}' | awk 'BEGIN{RS="/"} NR==2{print}')" >> "$GITHUB_ENV" + CL_PATH="cli/CHANGELOG.md" + { + echo 'CHANGELOG<> "$GITHUB_ENV" + + mkdir dist + + for arch in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin; do + tar -cJf "dist/${BIN}-${CRATE_VERSION}-${arch}.tar.xz" -C target/${arch}/release "${BIN}" + done + (cd target/x86_64-pc-windows-msvc/release && zip -9 "../../../dist/${BIN}-${CRATE_VERSION}-x86_64-pc-windows-msvc.zip" "${BIN}.exe") + + - name: 🎉 Publish release + uses: https://gitea.com/actions/release-action@main + with: + title: "${{ env.CRATE }} ${{ env.CRATE_VERSION }}" + body: "${{ env.CHANGELOG }}" diff --git a/cli/README.md b/cli/README.md index 25a9e49..2af17e9 100644 --- a/cli/README.md +++ b/cli/README.md @@ -16,10 +16,12 @@ You can download a compiled version of RustyPipe here: Alternatively, you can compile it yourself by installing [Rust](https://rustup.rs/) and running `cargo install rustypipe-cli`. -To be able to download streams from web-based clients (Desktop, Mobile) you need to +To be able to access streams from web-based clients (Desktop, Mobile) you need to download [rustypipe-botguard](https://codeberg.org/ThetaDev/rustypipe-botguard/releases) and place the binary either in the PATH or the current working directory. +For downloading videos you also need to have ffmpeg installed. + ## `get`: Fetch information You can call the get command with any YouTube entity ID or URL and RustyPipe will fetch From 80a358ee54ee8933a5bb0a2827d8fdf523fd7956 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 02:20:55 +0100 Subject: [PATCH 16/64] Revert "refactor!: rename n_http_retries option to n_request_attempts to be less misleading" This reverts commit b8cfe1b034a7470a9c4a587d709de7542d459091. --- src/client/mod.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index b3df2b2..8a2c31b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -381,7 +381,7 @@ struct RustyPipeRef { http: Client, storage: Option>, reporter: Option>, - n_request_attempts: u32, + n_http_retries: u32, cache: CacheHolder, default_opts: RustyPipeOpts, user_agent: Cow<'static, str>, @@ -405,7 +405,7 @@ struct RustyPipeOpts { pub struct RustyPipeBuilder { storage: DefaultOpt>, reporter: DefaultOpt>, - n_request_attempts: u32, + n_http_retries: u32, timeout: DefaultOpt, user_agent: Option, default_opts: RustyPipeOpts, @@ -677,7 +677,7 @@ impl RustyPipeBuilder { storage: DefaultOpt::Default, reporter: DefaultOpt::Default, timeout: DefaultOpt::Default, - n_request_attempts: 2, + n_http_retries: 2, user_agent: None, storage_dir: None, botguard_bin: DefaultOpt::Default, @@ -783,7 +783,7 @@ impl RustyPipeBuilder { report_dir.push(DEFAULT_REPORT_DIR); Box::new(FileReporter::new(report_dir)) }), - n_request_attempts: self.n_request_attempts, + n_http_retries: self.n_http_retries, cache: CacheHolder { clients: cache_clients, deobf: AsyncRwLock::new(cdata.deobf), @@ -862,7 +862,7 @@ impl RustyPipeBuilder { self } - /// Set the maximum number of attempts for YouTube requests (at least 1). + /// Set the maximum number of retries for YouTube requests. /// /// If a request fails because of a serverside error and retries are enabled, /// RustyPipe waits 1 second before the next attempt. @@ -872,8 +872,8 @@ impl RustyPipeBuilder { /// /// **Default value**: 2 #[must_use] - pub fn n_request_attempts(mut self, n_retries: u32) -> Self { - self.n_request_attempts = n_retries.max(1); + pub fn n_http_retries(mut self, n_retries: u32) -> Self { + self.n_http_retries = n_retries.max(1); self } @@ -1091,7 +1091,7 @@ impl RustyPipe { /// Execute the given http request. async fn http_request(&self, request: &Request) -> Result { let mut last_resp = None; - for n in 0..=self.inner.n_request_attempts { + for n in 0..=self.inner.n_http_retries { let resp = self.inner.http.execute(request.try_clone().unwrap()).await; let err = match resp { @@ -1117,7 +1117,7 @@ impl RustyPipe { }; // Retry in case of a recoverable status code (server err, too many requests) - if n != self.inner.n_request_attempts { + if n != self.inner.n_http_retries { let ms = util::retry_delay(n, 1000, 60000, 3); tracing::warn!( "Retry attempt #{}. Error: {}. Waiting {} ms", @@ -2413,7 +2413,7 @@ impl RustyPipeQuery { ctx_src: &MapRespOptions<'_>, ) -> Result, Error> { let mut last_resp = None; - for n in 0..=self.client.inner.n_request_attempts { + for n in 0..=self.client.inner.n_http_retries { let resp = self .execute_request_attempt::(ctype, id, endpoint, body, ctx_src) .await?; @@ -2431,7 +2431,7 @@ impl RustyPipeQuery { // Remove the used visitor data from cache if the request resulted in a recoverable error self.remove_visitor_data(&resp.visitor_data); - if n != self.client.inner.n_request_attempts { + if n != self.client.inner.n_http_retries { let ms = util::retry_delay(n, 1000, 60000, 3); tracing::warn!( "Retry attempt #{}. Error: {}. Waiting {} ms", From fb1b732d5660bf0d374359d6228a9e255e759ad7 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 02:32:44 +0100 Subject: [PATCH 17/64] chore(release): release rustypipe v0.10.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8d160..77eaabf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,44 @@ All notable changes to this project will be documented in this file. +## [v0.10.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.9.0..rustypipe/v0.10.0) - 2025-02-09 + +### 🚀 Features + +- Add visitor data cache, remove random visitor data - ([b12f4c5](https://codeberg.org/ThetaDev/rustypipe/commit/b12f4c5d821a9189d7ed8410ad860824b6d052ef)) +- Add support for rustypipe-botguard to get PO tokens - ([b90a252](https://codeberg.org/ThetaDev/rustypipe/commit/b90a252a5e1bf05a5294168b0ec16a73cbb88f42)) +- Add session po token cache - ([b72b501](https://codeberg.org/ThetaDev/rustypipe/commit/b72b501b6dbcf4333b24cd80e7c8c61b0c21ec91)) +- Check rustypipe-botguard-api version - ([8385b87](https://codeberg.org/ThetaDev/rustypipe/commit/8385b87c63677f32a240679a78702f53072e517a)) +- Rewrite request attempt system, retry with different visitor data - ([dfd03ed](https://codeberg.org/ThetaDev/rustypipe/commit/dfd03edfadff2657e9cfbf04e5d313ba409520ac)) +- Log failed player fetch attempts with player_from_clients - ([8e35358](https://codeberg.org/ThetaDev/rustypipe/commit/8e35358c8941301f6ebf7646a11ab22711082569)) +- Add timezone query option - ([3a2370b](https://codeberg.org/ThetaDev/rustypipe/commit/3a2370b97ca3d0f40d72d66a23295557317d29fb)) +- [**breaking**] Add userdata feature for all personal data queries (playback history, subscriptions) - ([65cb424](https://codeberg.org/ThetaDev/rustypipe/commit/65cb4244c6ab547f53d0cb12af802c4189188c86)) +- Add RustyPipe::version_botguard fn, detect rustypipe-botguard in current dir, add botguard version to report - ([1d755b7](https://codeberg.org/ThetaDev/rustypipe/commit/1d755b76bf4569f7d0bb90a65494ac8e7aae499a)) + +### 🐛 Bug Fixes + +- Parsing history dates - ([af7dc10](https://codeberg.org/ThetaDev/rustypipe/commit/af7dc1016322a87dd8fec0b739939c2b12b6f400)) +- A/V streams incorrectly recognized as video-only - ([2b891ca](https://codeberg.org/ThetaDev/rustypipe/commit/2b891ca0788f91f16dbb9203191cb3d2092ecc74)) +- Update iOS client - ([e915416](https://codeberg.org/ThetaDev/rustypipe/commit/e91541629d6c944c1001f5883e3c1264aeeb3969)) +- A/B test 20: music continuation item renderer - ([9c67f8f](https://codeberg.org/ThetaDev/rustypipe/commit/9c67f8f85bef8214848dc9d17bff6cff252e015e)) +- Include whole request body in report - ([15245c1](https://codeberg.org/ThetaDev/rustypipe/commit/15245c18b584e42523762b94fcc7284d483660a0)) +- Extracting nsig fn when outside variable starts with $ - ([eda16e3](https://codeberg.org/ThetaDev/rustypipe/commit/eda16e378730a3b57c4982a626df1622a93c574a)) +- Retry updating deobf data after a RustyPipe update - ([50ab1f7](https://codeberg.org/ThetaDev/rustypipe/commit/50ab1f7a5d8aeaa3720264b4a4b27805bb0e8121)) +- Allow player data to be fetched without botguard - ([29c854b](https://codeberg.org/ThetaDev/rustypipe/commit/29c854b20d7a6677415b1744e7ba7ecd4f594ea5)) +- Output full request body in reports, clean up `get_player_po_token` - ([a0d850f](https://codeberg.org/ThetaDev/rustypipe/commit/a0d850f8e01428a73bbd66397d0dbf797b45958f)) +- Correct timezone offset for parsed dates, add timezone_local option - ([a5a7be5](https://codeberg.org/ThetaDev/rustypipe/commit/a5a7be5b4e0a0b73d7e1dc802ebd7bd48dafc76d)) +- Use localzone crate to get local tz - ([5acbf0e](https://codeberg.org/ThetaDev/rustypipe/commit/5acbf0e456b1f10707e0a56125d993a8129eee3a)) +- Only use cached potokens with min. 10min lifetime - ([0c94267](https://codeberg.org/ThetaDev/rustypipe/commit/0c94267d0371b2b26c7b5c9abfa156d5cde2153e)) + +### 📚 Documentation + +- Add Botguard info to README - ([9957add](https://codeberg.org/ThetaDev/rustypipe/commit/9957add2b5d6391b2c1869d2019fd7dd91b8cd41)) + +### ⚙️ Miscellaneous Tasks + +- *(deps)* Update rust crate rquickjs to 0.9.0 (#33) - ([2c8ac41](https://codeberg.org/ThetaDev/rustypipe/commit/2c8ac410aa535d83f8bcc7181f81914b13bceb77)) + + ## [v0.9.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.8.0..rustypipe/v0.9.0) - 2025-01-16 ### 🚀 Features diff --git a/Cargo.toml b/Cargo.toml index b9a7dc3..bd15120 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustypipe" -version = "0.9.0" +version = "0.10.0" rust-version = "1.67.1" edition.workspace = true authors.workspace = true @@ -74,7 +74,7 @@ path_macro = "1.0.0" tracing-test = "0.2.5" # Included crates -rustypipe = { path = ".", version = "0.9.0", default-features = false } +rustypipe = { path = ".", version = "0.10.0", default-features = false } rustypipe-downloader = { path = "./downloader", version = "0.2.1", default-features = false, features = [ "indicatif", "audiotag", From 26e0c2cb2b83e54677625508731924215fc6ba1b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 02:53:59 +0100 Subject: [PATCH 18/64] chore(release): release rustypipe-downloader v0.3.0 --- Cargo.toml | 2 +- downloader/CHANGELOG.md | 24 ++++++++++++++++++++++++ downloader/Cargo.toml | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bd15120..473ecb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ tracing-test = "0.2.5" # Included crates rustypipe = { path = ".", version = "0.10.0", default-features = false } -rustypipe-downloader = { path = "./downloader", version = "0.2.1", default-features = false, features = [ +rustypipe-downloader = { path = "./downloader", version = "0.3.0", default-features = false, features = [ "indicatif", "audiotag", ] } diff --git a/downloader/CHANGELOG.md b/downloader/CHANGELOG.md index 25d7681..309381d 100644 --- a/downloader/CHANGELOG.md +++ b/downloader/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. +## [v0.3.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.7..rustypipe-downloader/v0.3.0) - 2025-02-09 + +### 🚀 Features + +- [**breaking**] Add userdata feature for all personal data queries (playback history, subscriptions) - ([65cb424](https://codeberg.org/ThetaDev/rustypipe/commit/65cb4244c6ab547f53d0cb12af802c4189188c86)) + +### 🐛 Bug Fixes + +- Ensure downloader futures are send - ([812ff4c](https://codeberg.org/ThetaDev/rustypipe/commit/812ff4c5bafffc5708a6d5066f1ebadb6d9fc958)) +- Download audio with dolby codec - ([9234005](https://codeberg.org/ThetaDev/rustypipe/commit/92340056f868007beccb64e9e26eb39abc40f7aa)) + +### 🚜 Refactor + +- [**breaking**] Add client_type field to DownloadError, rename cli option po-token-cache to pot-cache - ([594e675](https://codeberg.org/ThetaDev/rustypipe/commit/594e675b39efc5fbcdbd5e920a4d2cdee64f718e)) + +### 📚 Documentation + +- Add Botguard info to README - ([9957add](https://codeberg.org/ThetaDev/rustypipe/commit/9957add2b5d6391b2c1869d2019fd7dd91b8cd41)) + +### ⚙️ Miscellaneous Tasks + +- *(deps)* Update rustypipe to 0.10.0 + + ## [v0.2.7](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.6..rustypipe-downloader/v0.2.7) - 2025-01-16 ### 🚀 Features diff --git a/downloader/Cargo.toml b/downloader/Cargo.toml index a410beb..fb49ec5 100644 --- a/downloader/Cargo.toml +++ b/downloader/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustypipe-downloader" -version = "0.2.7" +version = "0.3.0" rust-version = "1.67.1" edition.workspace = true authors.workspace = true From 629b5905da653c6fe0f3c6b5814dd2f49030e7ed Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 03:09:47 +0100 Subject: [PATCH 19/64] feat: add verbose flag --- cli/README.md | 4 ++-- cli/src/main.rs | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cli/README.md b/cli/README.md index 2af17e9..7474f0a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -139,8 +139,8 @@ Fetch a list of all the items saved in your YouTube/YouTube Music profile. - **Proxy:** RustyPipe respects the environment variables `HTTP_PROXY`, `HTTPS_PROXY` and `ALL_PROXY` -- **Logging:** You can change the log level with the `RUST_LOG` environment variable, it - is set to `info` by default +- **Logging:** Enable debug logging with the `-v` (verbose) flag. If you want more + fine-grained control, use the `RUST_LOG` environment variable. - **Visitor data:** A custom visitor data ID can be used with the `--vdata` flag - **Authentication:** Use the commands `rustypipe login` and `rustypipe login --cookie` to log into your Google account using either OAuth or YouTube cookies. With the diff --git a/cli/src/main.rs b/cli/src/main.rs index f192dd8..b226221 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -80,6 +80,9 @@ struct Cli { /// Enable caching for session-bound PO tokens #[clap(long, global = true)] pot_cache: bool, + /// Enable debug logging + #[clap(short, long, global = true)] + verbose: bool, } #[derive(Parser)] @@ -878,12 +881,15 @@ async fn run() -> anyhow::Result<()> { let cli = Cli::parse(); let multi = MultiProgress::new(); + let mut env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + if cli.verbose { + env_filter = env_filter.add_directive("rustypipe=debug".parse().unwrap()); + } + tracing_subscriber::fmt::SubscriberBuilder::default() - .with_env_filter( - EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env_lossy(), - ) + .with_env_filter(env_filter) .with_writer(ProgWriter(multi.clone())) .init(); From 8933c6fa2a38103ec1ae689a4197c5a417bdced8 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 03:14:30 +0100 Subject: [PATCH 20/64] chore(release): release rustypipe-cli v0.7.0 --- cli/CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ cli/Cargo.toml | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 9ebf81a..8d60ece 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,36 @@ All notable changes to this project will be documented in this file. +## [v0.7.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.6.0..rustypipe-cli/v0.7.0) - 2025-02-09 + +### 🚀 Features + +- Add support for rustypipe-botguard to get PO tokens - ([b90a252](https://codeberg.org/ThetaDev/rustypipe/commit/b90a252a5e1bf05a5294168b0ec16a73cbb88f42)) +- [**breaking**] Remove manual PO token options from downloader/cli, add new rustypipe-botguard options - ([cddb32f](https://codeberg.org/ThetaDev/rustypipe/commit/cddb32f190276265258c6ab45b3d43a8891c4b39)) +- Add session po token cache - ([b72b501](https://codeberg.org/ThetaDev/rustypipe/commit/b72b501b6dbcf4333b24cd80e7c8c61b0c21ec91)) +- Add timezone query option - ([3a2370b](https://codeberg.org/ThetaDev/rustypipe/commit/3a2370b97ca3d0f40d72d66a23295557317d29fb)) +- Add --timezone-local CLI option - ([4f2bb47](https://codeberg.org/ThetaDev/rustypipe/commit/4f2bb47ab42ae0c68a64f3b3c2831fa7850b6f56)) +- Add verbose flag - ([629b590](https://codeberg.org/ThetaDev/rustypipe/commit/629b5905da653c6fe0f3c6b5814dd2f49030e7ed)) + +### 🐛 Bug Fixes + +- Parsing mixed-case language codes like zh-CN - ([9c73ed4](https://codeberg.org/ThetaDev/rustypipe/commit/9c73ed4b3008cb093c0fa7fd94fd9f1ba8cd3627)) + +### 🚜 Refactor + +- [**breaking**] Add client_type field to DownloadError, rename cli option po-token-cache to pot-cache - ([594e675](https://codeberg.org/ThetaDev/rustypipe/commit/594e675b39efc5fbcdbd5e920a4d2cdee64f718e)) +- Rename rustypipe-cli binary to rustypipe - ([c1a872e](https://codeberg.org/ThetaDev/rustypipe/commit/c1a872e1c14ea0956053bd7c65f6875b1cb3bc55)) + +### 📚 Documentation + +- Add Botguard info to README - ([9957add](https://codeberg.org/ThetaDev/rustypipe/commit/9957add2b5d6391b2c1869d2019fd7dd91b8cd41)) + +### ⚙️ Miscellaneous Tasks + +- *(deps)* Update rustypipe to 0.10.0 +- *(deps)* Update rust crate rquickjs to 0.9.0 (#33) - ([2c8ac41](https://codeberg.org/ThetaDev/rustypipe/commit/2c8ac410aa535d83f8bcc7181f81914b13bceb77)) + + ## [v0.6.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.5.0..rustypipe-cli/v0.6.0) - 2025-01-16 ### 🚀 Features diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 67fa3df..b429215 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustypipe-cli" -version = "0.6.0" +version = "0.7.0" rust-version = "1.70.0" edition.workspace = true authors.workspace = true From f8a0a253cc3a7a4cd5b36c126313541e86653c0d Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 03:15:30 +0100 Subject: [PATCH 21/64] change line in downloader changelog --- downloader/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/downloader/CHANGELOG.md b/downloader/CHANGELOG.md index 309381d..3877458 100644 --- a/downloader/CHANGELOG.md +++ b/downloader/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file. ### 🚀 Features -- [**breaking**] Add userdata feature for all personal data queries (playback history, subscriptions) - ([65cb424](https://codeberg.org/ThetaDev/rustypipe/commit/65cb4244c6ab547f53d0cb12af802c4189188c86)) +- [**breaking**] Remove manual PO token options from downloader in favor of rustypipe-botguard - ([cddb32f](https://codeberg.org/ThetaDev/rustypipe/commit/cddb32f190276265258c6ab45b3d43a8891c4b39)) ### 🐛 Bug Fixes From 45d3a9cd3337c7093cf400368060e60c6fc09d1e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 03:55:58 +0100 Subject: [PATCH 22/64] ci: add CLI release files --- .forgejo/workflows/release-cli.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.forgejo/workflows/release-cli.yaml b/.forgejo/workflows/release-cli.yaml index 0e0ca64..88578c1 100644 --- a/.forgejo/workflows/release-cli.yaml +++ b/.forgejo/workflows/release-cli.yaml @@ -3,6 +3,7 @@ on: push: tags: - "rustypipe-cli/v*.*.*" + workflow_dispatch: jobs: Release: @@ -66,3 +67,4 @@ jobs: with: title: "${{ env.CRATE }} ${{ env.CRATE_VERSION }}" body: "${{ env.CHANGELOG }}" + files: dist/* From 4d60e64f2c06d4c1dbcc0b8c70cbeced8f5ecc38 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 9 Feb 2025 04:35:30 +0100 Subject: [PATCH 23/64] ci: remove workflow_dispatch trigger --- .forgejo/workflows/release-cli.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.forgejo/workflows/release-cli.yaml b/.forgejo/workflows/release-cli.yaml index 88578c1..6268ead 100644 --- a/.forgejo/workflows/release-cli.yaml +++ b/.forgejo/workflows/release-cli.yaml @@ -3,7 +3,6 @@ on: push: tags: - "rustypipe-cli/v*.*.*" - workflow_dispatch: jobs: Release: From 739eac4d1fa9414367fda929535b72f344d24ee5 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 18 Feb 2025 00:16:09 +0100 Subject: [PATCH 24/64] test: fix tests --- .../youtube__music_artist_basic_all.snap | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/snapshots/youtube__music_artist_basic_all.snap b/tests/snapshots/youtube__music_artist_basic_all.snap index 2fd25a1..aef63a7 100644 --- a/tests/snapshots/youtube__music_artist_basic_all.snap +++ b/tests/snapshots/youtube__music_artist_basic_all.snap @@ -146,21 +146,6 @@ MusicArtist( year: Some(2015), by_va: false, ), - AlbumItem( - id: "MPREb_ghrNI6BJSM8", - name: "Friends And Family", - cover: "[cover]", - artists: [ - ArtistId( - id: Some("UCFKUUtHjT4iq3p0JJA13SOA"), - name: "Every Time I Die", - ), - ], - artist_id: Some("UCFKUUtHjT4iq3p0JJA13SOA"), - album_type: album, - year: Some(2017), - by_va: false, - ), AlbumItem( id: "MPREb_h0UZr2ALQXf", name: "From Parts Unknown (Deluxe Edition)", From 83f86527766e49dbddf45f1c2999500d58bb8ebd Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 22 Feb 2025 23:02:15 +0000 Subject: [PATCH 25/64] ci: disable renovate --- .forgejo/workflows/{renovate.yaml => renovate.yaml.bak} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .forgejo/workflows/{renovate.yaml => renovate.yaml.bak} (100%) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml.bak similarity index 100% rename from .forgejo/workflows/renovate.yaml rename to .forgejo/workflows/renovate.yaml.bak From 544782f8de728cda0aca9a1cb95837cdfbd001f1 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 25 Feb 2025 22:16:14 +0100 Subject: [PATCH 26/64] feat: add original album track count, fix fetching albums with more than 200 tracks --- src/client/music_playlist.rs | 35 +- ...__map_music_album_20240228_twoColumns.snap | 1 + ...t__tests__map_music_album_description.snap | 1 + ...st__tests__map_music_album_one_artist.snap | 1 + ...aylist__tests__map_music_album_single.snap | 1 + ...t__tests__map_music_album_unavailable.snap | 1 + ...ests__map_music_album_various_artists.snap | 1 + src/model/mod.rs | 2 + src/util/visitor_data.rs | 11 +- .../youtube__music_album_audiobook.snap | 3813 ++++++++++++++++- tests/snapshots/youtube__music_album_ep.snap | 1 + .../youtube__music_album_no_artist.snap | 1 + .../youtube__music_album_no_year.snap | 1 + .../youtube__music_album_one_artist.snap | 1 + .../snapshots/youtube__music_album_show.snap | 1 + .../youtube__music_album_single.snap | 1 + .../youtube__music_album_unavailable.snap | 1 + .../youtube__music_album_various_artists.snap | 1 + 18 files changed, 3870 insertions(+), 5 deletions(-) diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index 002d7c1..a11e6bf 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -95,11 +95,21 @@ impl RustyPipeQuery { }) .collect::>(); - if !to_replace.is_empty() { + let last_tn = album + .tracks + .last() + .and_then(|t| t.track_nr) + .unwrap_or_default(); + if !to_replace.is_empty() || last_tn < album.track_count { + tracing::debug!( + "fetching album playlist ({} tracks, {} to replace)", + album.track_count, + to_replace.len() + ); let mut playlist = self.music_playlist(playlist_id).await?; playlist .tracks - .extend_limit(&self, album.tracks.len()) + .extend_limit(&self, album.track_count.into()) .await?; for (i, title) in to_replace { @@ -118,6 +128,18 @@ impl RustyPipeQuery { album.tracks[i].track_type = TrackType::Track; } } + + // Extend the list of album tracks with the ones from the playlist if the playlist returned more tracks + // This is the case for albums with more than 200 tracks (e.g. audiobooks) + if album.tracks.len() < playlist.tracks.items.len() { + let mut tn = last_tn; + for mut t in playlist.tracks.items.into_iter().skip(album.tracks.len()) { + tn += 1; + t.album = album.tracks.first().and_then(|t| t.album.clone()); + t.track_nr = Some(tn); + album.tracks.push(t); + } + } } } Ok(album) @@ -457,6 +479,14 @@ impl MapResponse for response::MusicPlaylist { .unwrap_or_default(); let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone())); + let second_subtitle_parts = header + .second_subtitle + .split(|p| p == DOT_SEPARATOR) + .collect::>(); + let track_count = second_subtitle_parts + .get(usize::from(second_subtitle_parts.len() > 2)) + .and_then(|txt| util::parse_numeric::(&txt[0]).ok()); + let mut mapper = MusicListMapper::with_album( ctx.lang, artists.clone(), @@ -491,6 +521,7 @@ impl MapResponse for response::MusicPlaylist { album_type, year, by_va, + track_count: track_count.unwrap_or(tracks_res.c.len() as u16), tracks: tracks_res.c, variants: variants_res.c, }, diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_20240228_twoColumns.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_20240228_twoColumns.snap index 84ca230..3a080cd 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_20240228_twoColumns.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_20240228_twoColumns.snap @@ -43,6 +43,7 @@ MusicAlbum( album_type: single, year: Some(2020), by_va: false, + track_count: 1, tracks: [ TrackItem( id: "XX0epju-YvY", diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_description.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_description.snap index 13e1d87..09e8d67 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_description.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_description.snap @@ -43,6 +43,7 @@ MusicAlbum( album_type: album, year: Some(2015), by_va: false, + track_count: 11, tracks: [ TrackItem( id: "YQHsXMglC9A", diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_one_artist.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_one_artist.snap index 486d93f..1d55603 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_one_artist.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_one_artist.snap @@ -39,6 +39,7 @@ MusicAlbum( album_type: album, year: Some(2016), by_va: false, + track_count: 18, tracks: [ TrackItem( id: "g0iRiJ_ck48", diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_single.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_single.snap index ed4ce55..bf5add2 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_single.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_single.snap @@ -43,6 +43,7 @@ MusicAlbum( album_type: single, year: Some(2020), by_va: false, + track_count: 1, tracks: [ TrackItem( id: "XX0epju-YvY", diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_unavailable.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_unavailable.snap index 16fb88f..27d07af 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_unavailable.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_unavailable.snap @@ -34,6 +34,7 @@ MusicAlbum( album_type: album, year: Some(2019), by_va: true, + track_count: 18, tracks: [ TrackItem( id: "JWeJHN5P-E8", diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_various_artists.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_various_artists.snap index 932ff7c..189d625 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_various_artists.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_various_artists.snap @@ -34,6 +34,7 @@ MusicAlbum( album_type: single, year: Some(2022), by_va: true, + track_count: 6, tracks: [ TrackItem( id: "8IqLxg0GqXc", diff --git a/src/model/mod.rs b/src/model/mod.rs index 0912f74..9d53e60 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1234,6 +1234,8 @@ pub struct MusicAlbum { pub year: Option, /// Is the album by 'Various artists'? pub by_va: bool, + /// Number of album tracks + pub track_count: u16, /// Album tracks pub tracks: Vec, /// Album variants diff --git a/src/util/visitor_data.rs b/src/util/visitor_data.rs index 1daf1bd..0087f96 100644 --- a/src/util/visitor_data.rs +++ b/src/util/visitor_data.rs @@ -245,12 +245,21 @@ mod tests { for _ in 0..4 { cache.get().await.unwrap(); } - tokio::time::sleep(Duration::from_millis(1000)).await; + for _ in 0..3 { + tokio::time::sleep(Duration::from_millis(1000)).await; + { + let vd = cache.inner.visitor_data.read().unwrap(); + if !vd.contains(&v1) { + break; + } + } + } { let vd = cache.inner.visitor_data.read().unwrap(); assert!(!vd.contains(&v1), "first token still present"); } + assert_eq!(cache.get_pot(&v1), None); } } diff --git a/tests/snapshots/youtube__music_album_audiobook.snap b/tests/snapshots/youtube__music_album_audiobook.snap index a1f0632..3f3bd96 100644 --- a/tests/snapshots/youtube__music_album_audiobook.snap +++ b/tests/snapshots/youtube__music_album_audiobook.snap @@ -18,6 +18,7 @@ MusicAlbum( album_type: audiobook, year: Some(2022), by_va: false, + track_count: 319, tracks: [ TrackItem( id: "F28BV_Y-970", @@ -1427,6 +1428,22 @@ MusicAlbum( track_nr: Some(88), by_va: false, ), + TrackItem( + id: "2klbe4CmXaQ", + name: "Kapitel 7.14 - 1984", + duration: Some(129), + cover: [], + artists: [], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(89), + by_va: false, + ), TrackItem( id: "qePq7ltD6j4", name: "Kapitel 7.15 & Kapitel 8.1 - 1984", @@ -3207,8 +3224,24 @@ MusicAlbum( id: "svY6h_e3LYI", name: "Kapitel 17.5 - 1984", duration: Some(128), - cover: [], - artists: [], + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), album: Some(AlbumId( id: "MPREb_gaoNzsQHedo", @@ -3219,6 +3252,3782 @@ MusicAlbum( track_nr: Some(201), by_va: false, ), + TrackItem( + id: "Hir7IMW_37k", + name: "Kapitel 17.6 - 1984", + duration: Some(203), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(202), + by_va: false, + ), + TrackItem( + id: "wrp-KxyqmKs", + name: "Kapitel 17.7 - 1984", + duration: Some(185), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(203), + by_va: false, + ), + TrackItem( + id: "tFQF6fw09ec", + name: "Kapitel 17.8 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(204), + by_va: false, + ), + TrackItem( + id: "tzlG81GlTXQ", + name: "Kapitel 17.9 - 1984", + duration: Some(212), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(205), + by_va: false, + ), + TrackItem( + id: "Vno9w1ba-C0", + name: "Kapitel 17.10 - 1984", + duration: Some(152), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(206), + by_va: false, + ), + TrackItem( + id: "Ve19y3AhLBk", + name: "Kapitel 17.11 - 1984", + duration: Some(143), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(207), + by_va: false, + ), + TrackItem( + id: "8oHasQ_tiuI", + name: "Kapitel 17.12 - 1984", + duration: Some(171), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(208), + by_va: false, + ), + TrackItem( + id: "Ap42W06bL2c", + name: "Kapitel 17.13 - 1984", + duration: Some(174), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(209), + by_va: false, + ), + TrackItem( + id: "qmiLwRF0fOs", + name: "Kapitel 17.14 - 1984", + duration: Some(173), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(210), + by_va: false, + ), + TrackItem( + id: "BdiOwfRyToY", + name: "Kapitel 17.15 - 1984", + duration: Some(192), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(211), + by_va: false, + ), + TrackItem( + id: "PlBZ0cGf5DE", + name: "Kapitel 17.16 - 1984", + duration: Some(214), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(212), + by_va: false, + ), + TrackItem( + id: "v9MgxVJWCow", + name: "Kapitel 17.17 - 1984", + duration: Some(367), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(213), + by_va: false, + ), + TrackItem( + id: "5nwe4RMmA8s", + name: "Kapitel 17.18 & Kapitel 18.1 - 1984", + duration: Some(137), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(214), + by_va: false, + ), + TrackItem( + id: "6ZP4rXcPFd0", + name: "Kapitel 18.2 - 1984", + duration: Some(141), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(215), + by_va: false, + ), + TrackItem( + id: "eVhWJKVdo40", + name: "Kapitel 18.3 - 1984", + duration: Some(230), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(216), + by_va: false, + ), + TrackItem( + id: "Vt7keSTK0No", + name: "Kapitel 18.4 - 1984", + duration: Some(180), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(217), + by_va: false, + ), + TrackItem( + id: "Ff3SyoUlcyY", + name: "Kapitel 18.5 - 1984", + duration: Some(234), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(218), + by_va: false, + ), + TrackItem( + id: "gMTJQzds2ac", + name: "Kapitel 18.6 - 1984", + duration: Some(199), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(219), + by_va: false, + ), + TrackItem( + id: "UgAOUifyrqc", + name: "Kapitel 18.7 - 1984", + duration: Some(138), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(220), + by_va: false, + ), + TrackItem( + id: "IGZoSexffaY", + name: "Kapitel 18.8 - 1984", + duration: Some(261), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(221), + by_va: false, + ), + TrackItem( + id: "_yk6rIXnfyw", + name: "Kapitel 18.9 - 1984", + duration: Some(375), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(222), + by_va: false, + ), + TrackItem( + id: "nhznAYDKokI", + name: "Kapitel 18.10 - 1984", + duration: Some(171), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(223), + by_va: false, + ), + TrackItem( + id: "xxcWex5wCpc", + name: "Kapitel 18.11 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(224), + by_va: false, + ), + TrackItem( + id: "bHuVgebOTtY", + name: "Kapitel 18.12 - 1984", + duration: Some(409), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(225), + by_va: false, + ), + TrackItem( + id: "4dSMRBLFXE8", + name: "Kapitel 18.13 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(226), + by_va: false, + ), + TrackItem( + id: "7nrceuJOGrQ", + name: "Kapitel 18.14 & Kapitel 19.1 - 1984", + duration: Some(152), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(227), + by_va: false, + ), + TrackItem( + id: "rs_bvUV0-ZE", + name: "Kapitel 19.2 - 1984", + duration: Some(194), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(228), + by_va: false, + ), + TrackItem( + id: "SjDAX5b1sCA", + name: "Kapitel 19.3 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(229), + by_va: false, + ), + TrackItem( + id: "9LPuIbBxM_4", + name: "Kapitel 19.4 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(230), + by_va: false, + ), + TrackItem( + id: "xoTJCozaeFw", + name: "Kapitel 19.5 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(231), + by_va: false, + ), + TrackItem( + id: "o9R4zEStCg0", + name: "Kapitel 19.6 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(232), + by_va: false, + ), + TrackItem( + id: "v9aTac8EXeU", + name: "Kapitel 19.7 - 1984", + duration: Some(144), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(233), + by_va: false, + ), + TrackItem( + id: "Q6LQBbg9OFg", + name: "Kapitel 19.8 & Kapitel 20.1 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(234), + by_va: false, + ), + TrackItem( + id: "m3uHhmbvtLQ", + name: "Kapitel 20.2 - 1984", + duration: Some(149), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(235), + by_va: false, + ), + TrackItem( + id: "6Q6paXKnxN4", + name: "Kapitel 20.3 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(236), + by_va: false, + ), + TrackItem( + id: "bZmfzI9OhCs", + name: "Kapitel 20.4 - 1984", + duration: Some(173), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(237), + by_va: false, + ), + TrackItem( + id: "LywfUGAGUPc", + name: "Kapitel 20.5 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(238), + by_va: false, + ), + TrackItem( + id: "r3vVFZ3zl94", + name: "Kapitel 20.6 - 1984", + duration: Some(161), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(239), + by_va: false, + ), + TrackItem( + id: "Qenx-MfJ9mw", + name: "Kapitel 20.7 - 1984", + duration: Some(138), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(240), + by_va: false, + ), + TrackItem( + id: "vl_qPscUgdQ", + name: "Kapitel 20.8 - 1984", + duration: Some(137), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(241), + by_va: false, + ), + TrackItem( + id: "FQwclGtvT8A", + name: "Kapitel 20.9 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(242), + by_va: false, + ), + TrackItem( + id: "XfZemPEMHYM", + name: "Kapitel 20.10 - 1984", + duration: Some(137), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(243), + by_va: false, + ), + TrackItem( + id: "tXEWSfM5jBI", + name: "Kapitel 20.11 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(244), + by_va: false, + ), + TrackItem( + id: "CyYeGELAYks", + name: "Kapitel 20.12 - 1984", + duration: Some(132), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(245), + by_va: false, + ), + TrackItem( + id: "V4crS8euIlY", + name: "Kapitel 20.13 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(246), + by_va: false, + ), + TrackItem( + id: "6wkfFwX8hEk", + name: "Kapitel 20.14 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(247), + by_va: false, + ), + TrackItem( + id: "7BrXtew4Xf8", + name: "Kapitel 20.15 - 1984", + duration: Some(125), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(248), + by_va: false, + ), + TrackItem( + id: "zBOsHSF010g", + name: "Kapitel 20.16 - 1984", + duration: Some(129), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(249), + by_va: false, + ), + TrackItem( + id: "5hY-Mmqfg-U", + name: "Kapitel 20.17 - 1984", + duration: Some(137), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(250), + by_va: false, + ), + TrackItem( + id: "GWsWxGtQG2U", + name: "Kapitel 20.18 & Kapitel 21.1 - 1984", + duration: Some(178), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(251), + by_va: false, + ), + TrackItem( + id: "CS61jsoqGxM", + name: "Kapitel 21.2 - 1984", + duration: Some(201), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(252), + by_va: false, + ), + TrackItem( + id: "--MDZ6MoFOk", + name: "Kapitel 21.3 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(253), + by_va: false, + ), + TrackItem( + id: "APxYXEvS1gI", + name: "Kapitel 21.4 - 1984", + duration: Some(147), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(254), + by_va: false, + ), + TrackItem( + id: "hTZCV0xIaNo", + name: "Kapitel 21.5 - 1984", + duration: Some(147), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(255), + by_va: false, + ), + TrackItem( + id: "fUyVsvSLXAA", + name: "Kapitel 21.6 - 1984", + duration: Some(129), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(256), + by_va: false, + ), + TrackItem( + id: "Ex8KXjkzH6U", + name: "Kapitel 21.7 - 1984", + duration: Some(129), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(257), + by_va: false, + ), + TrackItem( + id: "Jq9_Roh-_qQ", + name: "Kapitel 21.8 - 1984", + duration: Some(138), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(258), + by_va: false, + ), + TrackItem( + id: "O-k4WTkVF_Y", + name: "Kapitel 21.9 - 1984", + duration: Some(125), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(259), + by_va: false, + ), + TrackItem( + id: "e0VydtqA7zA", + name: "Kapitel 21.10 - 1984", + duration: Some(129), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(260), + by_va: false, + ), + TrackItem( + id: "tYHQ1jqyY04", + name: "Kapitel 21.11 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(261), + by_va: false, + ), + TrackItem( + id: "uJdxfG7mtxg", + name: "Kapitel 21.12 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(262), + by_va: false, + ), + TrackItem( + id: "y9WgL5asujI", + name: "Kapitel 21.13 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(263), + by_va: false, + ), + TrackItem( + id: "M2_kYfPFD_o", + name: "Kapitel 21.14 - 1984", + duration: Some(148), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(264), + by_va: false, + ), + TrackItem( + id: "mMk8rWLu2kQ", + name: "Kapitel 21.15 - 1984", + duration: Some(144), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(265), + by_va: false, + ), + TrackItem( + id: "CdlO0Z_uhbM", + name: "Kapitel 21.16 - 1984", + duration: Some(129), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(266), + by_va: false, + ), + TrackItem( + id: "5loS8hlMQ7U", + name: "Kapitel 21.17 - 1984", + duration: Some(138), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(267), + by_va: false, + ), + TrackItem( + id: "2db5x8VGr8A", + name: "Kapitel 21.18 - 1984", + duration: Some(128), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(268), + by_va: false, + ), + TrackItem( + id: "cTRGKM8Jsgc", + name: "Kapitel 21.19 - 1984", + duration: Some(160), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(269), + by_va: false, + ), + TrackItem( + id: "nrfztfR0soo", + name: "Kapitel 21.20 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(270), + by_va: false, + ), + TrackItem( + id: "3om6N_pNGZA", + name: "Kapitel 21.21 - 1984", + duration: Some(181), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(271), + by_va: false, + ), + TrackItem( + id: "VN1KvIPeJ00", + name: "Kapitel 21.22 - 1984", + duration: Some(129), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(272), + by_va: false, + ), + TrackItem( + id: "LRuhIRN5d1U", + name: "Kapitel 21.23 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(273), + by_va: false, + ), + TrackItem( + id: "dE8U1hliRr4", + name: "Kapitel 21.24 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(274), + by_va: false, + ), + TrackItem( + id: "vjIb2Klunv0", + name: "Kapitel 21.25 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(275), + by_va: false, + ), + TrackItem( + id: "h8BydzKZOtI", + name: "Kapitel 21.26 & Kapitel 22.1 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(276), + by_va: false, + ), + TrackItem( + id: "ADwyhkSHBVM", + name: "Kapitel 22.2 - 1984", + duration: Some(135), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(277), + by_va: false, + ), + TrackItem( + id: "6cDrKGImmQk", + name: "Kapitel 22.3 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(278), + by_va: false, + ), + TrackItem( + id: "CdrnLzIJGdk", + name: "Kapitel 22.4 - 1984", + duration: Some(162), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(279), + by_va: false, + ), + TrackItem( + id: "N-ZzOlAX43w", + name: "Kapitel 22.5 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(280), + by_va: false, + ), + TrackItem( + id: "7MHmAXMX1F8", + name: "Kapitel 22.6 - 1984", + duration: Some(135), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(281), + by_va: false, + ), + TrackItem( + id: "vSlyuw3eyeU", + name: "Kapitel 22.7 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(282), + by_va: false, + ), + TrackItem( + id: "K8ez5KNbl-A", + name: "Kapitel 22.8 - 1984", + duration: Some(145), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(283), + by_va: false, + ), + TrackItem( + id: "J9y2_6f5Dqc", + name: "Kapitel 22.9 - 1984", + duration: Some(155), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(284), + by_va: false, + ), + TrackItem( + id: "d4Grp66WWsM", + name: "Kapitel 22.10 - 1984", + duration: Some(138), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(285), + by_va: false, + ), + TrackItem( + id: "gYCKh7_DAAw", + name: "Kapitel 22.11 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(286), + by_va: false, + ), + TrackItem( + id: "L59dwOH-nHQ", + name: "Kapitel 22.12 - 1984", + duration: Some(151), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(287), + by_va: false, + ), + TrackItem( + id: "KS4nFrdUDS8", + name: "Kapitel 22.13 - 1984", + duration: Some(132), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(288), + by_va: false, + ), + TrackItem( + id: "4bEhuh2DtBQ", + name: "Kapitel 22.14 - 1984", + duration: Some(128), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(289), + by_va: false, + ), + TrackItem( + id: "76nvjlkt2xY", + name: "Kapitel 22.15 - 1984", + duration: Some(147), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(290), + by_va: false, + ), + TrackItem( + id: "F7mUlqh-y04", + name: "Kapitel 22.16 - 1984", + duration: Some(145), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(291), + by_va: false, + ), + TrackItem( + id: "KivOUvqJ2n4", + name: "Kapitel 22.17 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(292), + by_va: false, + ), + TrackItem( + id: "EHzrgnOh5GA", + name: "Kapitel 22.18 & Kapitel 23.1 - 1984", + duration: Some(146), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(293), + by_va: false, + ), + TrackItem( + id: "10xak85mCso", + name: "Kapitel 23.2 - 1984", + duration: Some(147), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(294), + by_va: false, + ), + TrackItem( + id: "yDnNqeQnn3A", + name: "Kapitel 23.3 - 1984", + duration: Some(137), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(295), + by_va: false, + ), + TrackItem( + id: "l7n3auySj4A", + name: "Kapitel 23.4 - 1984", + duration: Some(141), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(296), + by_va: false, + ), + TrackItem( + id: "Ywtfz72Zywk", + name: "Kapitel 23.5 - 1984", + duration: Some(156), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(297), + by_va: false, + ), + TrackItem( + id: "FZpI_baOd_s", + name: "Kapitel 23.6 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(298), + by_va: false, + ), + TrackItem( + id: "8OXK57SjsL0", + name: "Kapitel 23.7 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(299), + by_va: false, + ), + TrackItem( + id: "5M-AVirsHzY", + name: "Kapitel 23.8 - 1984", + duration: Some(143), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(300), + by_va: false, + ), + TrackItem( + id: "4BQlY5tiXqY", + name: "Kapitel 23.9 & Kapitel 24.1 - 1984", + duration: Some(165), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(301), + by_va: false, + ), + TrackItem( + id: "1n-_LCwDMUg", + name: "Kapitel 24.2 - 1984", + duration: Some(134), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(302), + by_va: false, + ), + TrackItem( + id: "V-DyXyW8UI4", + name: "Kapitel 24.3 - 1984", + duration: Some(145), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(303), + by_va: false, + ), + TrackItem( + id: "GygcLOKL8B4", + name: "Kapitel 24.4 - 1984", + duration: Some(133), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(304), + by_va: false, + ), + TrackItem( + id: "S78K3PuQ-GM", + name: "Kapitel 24.5 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(305), + by_va: false, + ), + TrackItem( + id: "HcbO6kTLRZo", + name: "Kapitel 24.6 - 1984", + duration: Some(132), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(306), + by_va: false, + ), + TrackItem( + id: "-MCqkAaGfl0", + name: "Kapitel 24.7 & Kapitel 25.1 - 1984", + duration: Some(127), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(307), + by_va: false, + ), + TrackItem( + id: "G4edEm1rbeo", + name: "Kapitel 25.2 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(308), + by_va: false, + ), + TrackItem( + id: "KA26JvGwM28", + name: "Kapitel 25.3 - 1984", + duration: Some(133), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(309), + by_va: false, + ), + TrackItem( + id: "9GR32DB5RwY", + name: "Kapitel 25.4 - 1984", + duration: Some(131), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(310), + by_va: false, + ), + TrackItem( + id: "06rRLhqrcTk", + name: "Kapitel 25.5 - 1984", + duration: Some(133), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(311), + by_va: false, + ), + TrackItem( + id: "gw2h1uGGwak", + name: "Kapitel 25.6 - 1984", + duration: Some(137), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(312), + by_va: false, + ), + TrackItem( + id: "U1OnhVQHw4c", + name: "Kapitel 25.7 - 1984", + duration: Some(126), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(313), + by_va: false, + ), + TrackItem( + id: "C8UAKN2G0R8", + name: "Kapitel 25.8 - 1984", + duration: Some(133), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(314), + by_va: false, + ), + TrackItem( + id: "-q-TeXqQ9AA", + name: "Kapitel 25.9 - 1984", + duration: Some(128), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(315), + by_va: false, + ), + TrackItem( + id: "r77eoDVeFh0", + name: "Kapitel 25.10 - 1984", + duration: Some(128), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(316), + by_va: false, + ), + TrackItem( + id: "g8RfSPyjiXc", + name: "Kapitel 25.11 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(317), + by_va: false, + ), + TrackItem( + id: "au27GX2h7Zc", + name: "Kapitel 25.12 - 1984", + duration: Some(130), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(318), + by_va: false, + ), + TrackItem( + id: "hGHRyXDMR0M", + name: "Kapitel 25.13 - 1984", + duration: Some(138), + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w60-h60-l90-rj", + width: 60, + height: 60, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/wKdD-SHqfdz6FzU3vyq-je-yqJ2DrRqbwmLxbK0OjpP55RF0Uh1LvtkLiypabLL5oSUWW7YEWxmpw7s=w120-h120-l90-rj", + width: 120, + height: 120, + ), + ], + artists: [ + ArtistId( + id: None, + name: "George Orwell & Dirk Jacobs", + ), + ], + artist_id: Some("UCTlkMP4GiGuhOiBdcmI65kg"), + album: Some(AlbumId( + id: "MPREb_gaoNzsQHedo", + name: "1984", + )), + view_count: "[view_count]", + track_type: track, + track_nr: Some(319), + by_va: false, + ), ], variants: [], ) diff --git a/tests/snapshots/youtube__music_album_ep.snap b/tests/snapshots/youtube__music_album_ep.snap index 92e2f4d..0920a61 100644 --- a/tests/snapshots/youtube__music_album_ep.snap +++ b/tests/snapshots/youtube__music_album_ep.snap @@ -18,6 +18,7 @@ MusicAlbum( album_type: ep, year: Some(2016), by_va: false, + track_count: 5, tracks: [ TrackItem( id: "aGd3VKSOTxY", diff --git a/tests/snapshots/youtube__music_album_no_artist.snap b/tests/snapshots/youtube__music_album_no_artist.snap index c4831dd..36c65ca 100644 --- a/tests/snapshots/youtube__music_album_no_artist.snap +++ b/tests/snapshots/youtube__music_album_no_artist.snap @@ -13,6 +13,7 @@ MusicAlbum( album_type: album, year: Some(2024), by_va: true, + track_count: 14, tracks: [ TrackItem( id: "ilNEztApdjI", diff --git a/tests/snapshots/youtube__music_album_no_year.snap b/tests/snapshots/youtube__music_album_no_year.snap index 44c8290..dc9a815 100644 --- a/tests/snapshots/youtube__music_album_no_year.snap +++ b/tests/snapshots/youtube__music_album_no_year.snap @@ -26,6 +26,7 @@ MusicAlbum( album_type: single, year: None, by_va: false, + track_count: 1, tracks: [ TrackItem( id: "1Sz3lUVGBSM", diff --git a/tests/snapshots/youtube__music_album_one_artist.snap b/tests/snapshots/youtube__music_album_one_artist.snap index 20e8a9a..31f2166 100644 --- a/tests/snapshots/youtube__music_album_one_artist.snap +++ b/tests/snapshots/youtube__music_album_one_artist.snap @@ -36,6 +36,7 @@ MusicAlbum( album_type: album, year: Some(2011), by_va: false, + track_count: 15, tracks: [ TrackItem( id: "js0moD0CIRQ", diff --git a/tests/snapshots/youtube__music_album_show.snap b/tests/snapshots/youtube__music_album_show.snap index b91af31..87ad246 100644 --- a/tests/snapshots/youtube__music_album_show.snap +++ b/tests/snapshots/youtube__music_album_show.snap @@ -22,6 +22,7 @@ MusicAlbum( album_type: show, year: Some(2015), by_va: false, + track_count: 27, tracks: [ TrackItem( id: "ZIjGPc6vG0Y", diff --git a/tests/snapshots/youtube__music_album_single.snap b/tests/snapshots/youtube__music_album_single.snap index e650e4c..baa3162 100644 --- a/tests/snapshots/youtube__music_album_single.snap +++ b/tests/snapshots/youtube__music_album_single.snap @@ -22,6 +22,7 @@ MusicAlbum( album_type: single, year: Some(2020), by_va: false, + track_count: 1, tracks: [ TrackItem( id: "VU6lEv0PKAo", diff --git a/tests/snapshots/youtube__music_album_unavailable.snap b/tests/snapshots/youtube__music_album_unavailable.snap index 607c137..b3624c5 100644 --- a/tests/snapshots/youtube__music_album_unavailable.snap +++ b/tests/snapshots/youtube__music_album_unavailable.snap @@ -26,6 +26,7 @@ MusicAlbum( album_type: album, year: Some(2019), by_va: false, + track_count: 18, tracks: [ TrackItem( id: "R3VIKRtzAdE", diff --git a/tests/snapshots/youtube__music_album_various_artists.snap b/tests/snapshots/youtube__music_album_various_artists.snap index f3ab8f0..f7eb3d8 100644 --- a/tests/snapshots/youtube__music_album_various_artists.snap +++ b/tests/snapshots/youtube__music_album_various_artists.snap @@ -13,6 +13,7 @@ MusicAlbum( album_type: single, year: Some(2022), by_va: true, + track_count: 6, tracks: [ TrackItem( id: "Tzai7JXo45w", From 6737512f5f67c8cd05d4552dd0e0f24381035b35 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 26 Feb 2025 15:20:25 +0100 Subject: [PATCH 27/64] fix: A/B test 21: music album recommendations --- codegen/src/abtest.rs | 22 +- codegen/src/collect_album_versions_titles.rs | 130 + codegen/src/gen_dictionary.rs | 6 +- codegen/src/main.rs | 31 +- codegen/src/model.rs | 2 + notes/AB_Tests.md | 29 +- notes/_img/ab_21.png | Bin 0 -> 297321 bytes src/client/music_playlist.rs | 17 +- ...__map_music_album_20250225_recommends.snap | 151 + src/util/dictionary.rs | 81 + testfiles/dict/dictionary.json | 250 +- testfiles/dict/other_versions_titles.json | 85 + .../album_20250225_recommends.json | 7989 +++++++++++++++++ 13 files changed, 8680 insertions(+), 113 deletions(-) create mode 100644 codegen/src/collect_album_versions_titles.rs create mode 100644 notes/_img/ab_21.png create mode 100644 src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_20250225_recommends.snap create mode 100644 testfiles/dict/other_versions_titles.json create mode 100644 testfiles/music_playlist/album_20250225_recommends.json diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index 91aaf5e..ac01788 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -40,13 +40,11 @@ pub enum ABTest { MusicPlaylistFacepile = 18, MusicAlbumGroupsReordered = 19, MusicContinuationItemRenderer = 20, + AlbumRecommends = 21, } /// List of active A/B tests that are run when none is manually specified -const TESTS_TO_RUN: &[ABTest] = &[ - ABTest::MusicAlbumGroupsReordered, - ABTest::MusicContinuationItemRenderer, -]; +const TESTS_TO_RUN: &[ABTest] = &[ABTest::MusicAlbumGroupsReordered, ABTest::AlbumRecommends]; #[derive(Debug, Serialize, Deserialize)] pub struct ABTestRes { @@ -121,6 +119,7 @@ pub async fn run_test( ABTest::MusicContinuationItemRenderer => { music_continuation_item_renderer(&query).await } + ABTest::AlbumRecommends => album_recommends(&query).await, } .unwrap(); pb.inc(1); @@ -443,3 +442,18 @@ pub async fn music_continuation_item_renderer(rp: &RustyPipeQuery) -> Result Result { + let id = "MPREb_u1I69lSAe5v"; + let res = rp + .raw( + ClientType::DesktopMusic, + "browse", + &QBrowse { + browse_id: id, + params: None, + }, + ) + .await?; + Ok(res.contains("\"musicCarouselShelfRenderer\"")) +} diff --git a/codegen/src/collect_album_versions_titles.rs b/codegen/src/collect_album_versions_titles.rs new file mode 100644 index 0000000..0cb513c --- /dev/null +++ b/codegen/src/collect_album_versions_titles.rs @@ -0,0 +1,130 @@ +use std::{collections::BTreeMap, fs::File, io::BufReader}; + +use path_macro::path; +use rustypipe::{ + client::{ClientType, RustyPipe}, + param::{Language, LANGUAGES}, +}; +use serde::Deserialize; +use serde_with::rust::deserialize_ignore_any; + +use crate::{ + model::{QBrowse, SectionList, TextRuns}, + util::{self, DICT_DIR}, +}; + +pub async fn collect_album_versions_titles() { + let json_path = path!(*DICT_DIR / "other_versions_titles.json"); + let mut res = BTreeMap::new(); + + let rp = RustyPipe::new(); + + for lang in LANGUAGES { + let query = QBrowse { + browse_id: "MPREb_nlBWQROfvjo", + params: None, + }; + let raw_resp = rp + .query() + .lang(lang) + .raw(ClientType::DesktopMusic, "browse", &query) + .await + .unwrap(); + let data = serde_json::from_str::(&raw_resp).unwrap(); + let title = data + .contents + .two_column_browse_results_renderer + .secondary_contents + .section_list_renderer + .contents + .into_iter() + .find_map(|x| match x { + ItemSection::MusicCarouselShelfRenderer(music_carousel_shelf) => { + Some(music_carousel_shelf) + } + ItemSection::None => None, + }) + .expect("other versions") + .header + .expect("header") + .music_carousel_shelf_basic_header_renderer + .title + .runs + .into_iter() + .next() + .unwrap() + .text; + println!("{lang}: {title}"); + res.insert(lang, title); + } + + let file = File::create(json_path).unwrap(); + serde_json::to_writer_pretty(file, &res).unwrap(); +} + +pub fn write_samples_to_dict() { + let json_path = path!(*DICT_DIR / "other_versions_titles.json"); + let json_file = File::open(json_path).unwrap(); + let collected: BTreeMap = + serde_json::from_reader(BufReader::new(json_file)).unwrap(); + let mut dict = util::read_dict(); + let langs = dict.keys().copied().collect::>(); + + for lang in langs { + let dict_entry = dict.entry(lang).or_default(); + + let e = collected.get(&lang).unwrap(); + assert_eq!(e, e.trim()); + dict_entry.album_versions_title = e.to_owned(); + + for lang in &dict_entry.equivalent { + let ee = collected.get(lang).unwrap(); + if ee != e { + panic!("equivalent lang conflict, lang: {lang}"); + } + } + } + + util::write_dict(dict); +} + +#[derive(Debug, Deserialize)] +struct AlbumData { + contents: AlbumDataContents, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AlbumDataContents { + two_column_browse_results_renderer: X1, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct X1 { + secondary_contents: SectionList, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +enum ItemSection { + MusicCarouselShelfRenderer(MusicCarouselShelf), + #[serde(other, deserialize_with = "deserialize_ignore_any")] + None, +} + +#[derive(Debug, Deserialize)] +struct MusicCarouselShelf { + header: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct MusicCarouselShelfHeader { + music_carousel_shelf_basic_header_renderer: MusicCarouselShelfHeaderRenderer, +} + +#[derive(Debug, Deserialize)] +struct MusicCarouselShelfHeaderRenderer { + title: TextRuns, +} diff --git a/codegen/src/gen_dictionary.rs b/codegen/src/gen_dictionary.rs index e5ce310..549fb83 100644 --- a/codegen/src/gen_dictionary.rs +++ b/codegen/src/gen_dictionary.rs @@ -90,6 +90,8 @@ pub(crate) struct Entry { pub chan_prefix: &'static str, /// Channel name suffix on playlist pages pub chan_suffix: &'static str, + /// "Other versions" title on album pages + pub album_versions_title: &'static str, } "#; @@ -178,8 +180,8 @@ pub(crate) fn entry(lang: Language) -> Entry { .to_string() .replace('\n', "\n "); - write!(code_timeago_tokens, "{} => Entry {{\n timeago_tokens: {},\n month_before_day: {:?},\n months: {},\n timeago_nd_tokens: {},\n comma_decimal: {:?},\n number_tokens: {},\n number_nd_tokens: {},\n album_types: {},\n chan_prefix: {:?},\n chan_suffix: {:?},\n }},\n ", - selector, code_ta_tokens, entry.month_before_day, code_months, code_ta_nd_tokens, entry.comma_decimal, code_number_tokens, code_number_nd_tokens, code_album_types, entry.chan_prefix, entry.chan_suffix).unwrap(); + write!(code_timeago_tokens, "{} => Entry {{\n timeago_tokens: {},\n month_before_day: {:?},\n months: {},\n timeago_nd_tokens: {},\n comma_decimal: {:?},\n number_tokens: {},\n number_nd_tokens: {},\n album_types: {},\n chan_prefix: {:?},\n chan_suffix: {:?},\n album_versions_title: {:?},\n }},\n ", + selector, code_ta_tokens, entry.month_before_day, code_months, code_ta_nd_tokens, entry.comma_decimal, code_number_tokens, code_number_nd_tokens, code_album_types, entry.chan_prefix, entry.chan_suffix, entry.album_versions_title).unwrap(); } code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n"; diff --git a/codegen/src/main.rs b/codegen/src/main.rs index 87f484f..74b5d85 100644 --- a/codegen/src/main.rs +++ b/codegen/src/main.rs @@ -2,6 +2,7 @@ mod abtest; mod collect_album_types; +mod collect_album_versions_titles; mod collect_chan_prefixes; mod collect_history_dates; mod collect_large_numbers; @@ -34,12 +35,14 @@ enum Commands { CollectHistoryDates, CollectMusicHistoryDates, CollectChanPrefixes, + CollectAlbumVersionsTitles, ParsePlaylistDates, ParseHistoryDates, ParseLargeNumbers, ParseAlbumTypes, ParseVideoDurations, ParseChanPrefixes, + ParseAlbumVersionsTitles, GenLocales, GenDict, DownloadTestfiles, @@ -58,28 +61,25 @@ async fn main() { match cli.command { Commands::CollectPlaylistDates => { - collect_playlist_dates::collect_dates(cli.concurrency).await; + collect_playlist_dates::collect_dates(cli.concurrency).await } Commands::CollectLargeNumbers => { - collect_large_numbers::collect_large_numbers(cli.concurrency).await; + collect_large_numbers::collect_large_numbers(cli.concurrency).await } Commands::CollectAlbumTypes => { - collect_album_types::collect_album_types(cli.concurrency).await; + collect_album_types::collect_album_types(cli.concurrency).await } Commands::CollectVideoDurations => { - collect_video_durations::collect_video_durations(cli.concurrency).await; + collect_video_durations::collect_video_durations(cli.concurrency).await } Commands::CollectVideoDates => { - collect_video_dates::collect_video_dates(cli.concurrency).await; + collect_video_dates::collect_video_dates(cli.concurrency).await } - Commands::CollectHistoryDates => { - collect_history_dates::collect_dates().await; - } - Commands::CollectMusicHistoryDates => { - collect_history_dates::collect_dates_music().await; - } - Commands::CollectChanPrefixes => { - collect_chan_prefixes::collect_chan_prefixes().await; + Commands::CollectHistoryDates => collect_history_dates::collect_dates().await, + Commands::CollectMusicHistoryDates => collect_history_dates::collect_dates_music().await, + Commands::CollectChanPrefixes => collect_chan_prefixes::collect_chan_prefixes().await, + Commands::CollectAlbumVersionsTitles => { + collect_album_versions_titles::collect_album_versions_titles().await } Commands::ParsePlaylistDates => collect_playlist_dates::write_samples_to_dict(), Commands::ParseHistoryDates => collect_history_dates::write_samples_to_dict(), @@ -87,9 +87,10 @@ async fn main() { Commands::ParseAlbumTypes => collect_album_types::write_samples_to_dict(), Commands::ParseVideoDurations => collect_video_durations::parse_video_durations(), Commands::ParseChanPrefixes => collect_chan_prefixes::write_samples_to_dict(), - Commands::GenLocales => { - gen_locales::generate_locales().await; + Commands::ParseAlbumVersionsTitles => { + collect_album_versions_titles::write_samples_to_dict() } + Commands::GenLocales => gen_locales::generate_locales().await, Commands::GenDict => gen_dictionary::generate_dictionary(), Commands::DownloadTestfiles => download_testfiles::download_testfiles().await, Commands::AbTest { id, n } => { diff --git a/codegen/src/model.rs b/codegen/src/model.rs index 3002451..2d9929f 100644 --- a/codegen/src/model.rs +++ b/codegen/src/model.rs @@ -61,6 +61,8 @@ pub struct DictEntry { pub chan_prefix: String, /// Channel name suffix on playlist pages pub chan_suffix: String, + /// "Other versions" title on album pages + pub album_versions_title: String, } /// Parsed TimeAgo string, contains amount and time unit. diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index c85156a..1c18a85 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -3,13 +3,13 @@ When YouTube introduces a new feature, it does so gradually. When a user creates a new session, YouTube decided randomly which new features should be enabled. -YouTube sessions are identified by the visitor data ID. This cookie is sent with -every API request using the `context.client.visitor_data` JSON parameter. It is also -returned in the `responseContext.visitorData` response parameter and stored as the -`__SECURE-YEC` cookie. +YouTube sessions are identified by the visitor data ID. This cookie is sent with every +API request using the `context.client.visitor_data` JSON parameter. It is also returned +in the `responseContext.visitorData` response parameter and stored as the `__SECURE-YEC` +cookie. -By sending the same visitor data ID, A/B tests can be reproduced, which is important -for testing alternative YouTube clients. +By sending the same visitor data ID, A/B tests can be reproduced, which is important for +testing alternative YouTube clients. This page lists all A/B tests that were encountered while maintaining the RustyPipe client. @@ -1042,7 +1042,7 @@ omitted for albums in their group, while singles and EPs have a label with their - **Encountered on:** 25.01.2025 - **Impact:** 🟢 Low - **Endpoint:** browse (YTM) -- **Status:** Common (4%) +- **Status:** Stabilized YouTube Music now uses a `continuationItemRenderer` for music playlists instead of putting the continuations in a separate attribute of the MusicShelf. @@ -1052,3 +1052,18 @@ items. YouTube Music now also sends a random 16-character string as a `clientScreenNonce` in the request context. This is not mandatory though. + +## [21] Music album recommendations + +- **Encountered on:** 26.02.2025 +- **Impact:** 🟢 Low +- **Endpoint:** browse (YTM) +- **Status:** Common (15%) + +![A/B test 21 screenshot](./_img/ab_21.png) + +YouTube Music has added "Recommended" and "More from \" carousels to album +pages. The difficulty is distinguishing them reliably for parsing the album variants. + +The current solution is adding the "Other versions" title in all languages to the +dictionary and comparing it. diff --git a/notes/_img/ab_21.png b/notes/_img/ab_21.png new file mode 100644 index 0000000000000000000000000000000000000000..929f5d39d96dd0284fe803e23c14ff50937d4b9d GIT binary patch literal 297321 zcmb@tbySq$*EKvK>L4|QbeD9eG?GIPNTX8HEe#?FQbRg)$AHApgEZ38(hVZgEdtW- z&GW3^^F9B4f4nbiu~-B5TytONK4OuOLiq#)0^zBuD8N7<%(o!W0}C)F z@Ee6Mi9x^*Y*!T{4-g2y`~Djvi4&g+1fmD2D#+>iyxLpxO*K zK&AR2zABYog6)?mr8Xo!Cts`q{(4o1g8W_g(}z#{tQTEn&1JvQQ4&HR@xs_@^r7*g z?}Ab@RNsUW)3B#ED|KC3zj`!{j|Lu!7nkxdhJG+7|71=cgy)QV z|MSzay?Vb8ZWkZ?#sXS&|EE1NT>-ocH!+S>7TmO5k)FL->4FY-;(jZ3fRgECw=k|8sWYYH0U!CxX$@n{OF;k`IjRkAOKsTCMW;m0z~YZ#*fJ$4V-7<+-M!Nvz{tADtV$TbE)&f zb-LW>+4T0-maPik?PD|leRSqYYu}Xp>B7qG+3PbZ;H^u(z+34qzAGnln@mFjR>R1g z+i%W>GemWvo5LCB9R%-PJ3=0wV$Z5uPg=K-JHs;1)-E_eSdEi#Jy?@XoUP4 zkvLpLI~Z8rKfM(S1+X2wm1nTVYj2j{yfgIj=Su^#)1|}r1`blBd5HJ>4zHVkMRD%b z{9I3YE&b;^Q(wBw^(w*LG2NY8x$l@F*^<|SljX4A+snB7{E=~96n%Riflq!HH-|xY z5-S&)fvs2|hHkR9qc9VfRjTQda$9;5uTX}PiBH$2ilX}UE-=ERODE`Bs@CV-Y+}zxK zbV1h);ncsxG=Vg%GVe+kcF1S5S7`>Gq0Dil7F@WpaU=cp-Q|qu&7Jjfi*Fybhq{Rg z6@!(JkIzz*r^8IeOQ`Th>nGsTxy_p1{pyaCgOtB+e}B3&WtT3(I%U-2bF8CGGc`X? z*>{{R<)7_;&`2ZUCB#bc$($N&mFef__bw~v_HS_vMbLId^Firo&~Xo0U(jCOxP((;X^G^f>T%u|s|WVUFEx!n-(M z@?J3<8A#&9WX+h!dY&3g76Vrwc08ujgHAp6_xG3ilMpA=;#yfd^4wuCnftWgA6P&w=W?=` z+%kq2cyg;$cl+ckxn&X2vHb1U+{qs}1(2 zmX~k4iM51^rj1(tJe8G|Z`5|8f9y<^X-OV;=jG*9t7vIy1^#K%6k83gwnCi(MfBph z;kcJ-mv}^cGqbt^*y5by8IZ!X(TAM4UDMOk)pS>vET8$kPKHHip6%_fhSQB~o}5RE5VYwEa}Q(?V~$ zhK9zooPa-upCNy;&s%KD#h*X-TM0am5hhridw)5R4T0_4oX#+X?!YLQ033^90FYdC>R-0? zTYl9ZMP4&LE2EH-Hw=M;L(uaXr`de z2!JwYBBMb3E>HRF(wE;~?E^XQmKB3 zcHBE$jbMuXQd!yB+6q80fxPX(Vxw9L58CM}0l>U%WJ)g)z)kp}$4NthG8?tn%2FVLH%+=!oFj>;;b_OZtL5UbKD2mEm=-1>8@rCO`#VD z0C80J(Jm}p10Y-FzCHjH{l3#HpVhERacQU9o zwWN=N3E-Yt@5WUv`%a$`fM#9?-4F!*>uT)_lV8n`<}q&eataIqKtGtTjjHx)WOiuX z9WUY0ML-)v%^?9rB@ocPvjvxx=9^`vILUEs+rEmrKNr{R#hC)6?EvHTazH?EIQ*U& zEr-3^PO>cfuA27i7f87bUDuNgZnuTvfwE# z@nryVfaGXC|0kQ{6sTk8

j?qF-XPw6qm*Ef@rn zN3ww)yVI!C;c5+nw?nBqL7wga!$!6LjedP?QU0~Dv2m65FcpBg*Y5o+L8nS?9{~;h+8WJXen+@a}4bE_$K< zW^VMo*U82(aI4?^mrt^1IQjt&cc}o9+r4pdW#u)U?@c2I$Q7uQ8PTtfb2WFIXRnfA zrRK@duFqrDqCY-yliijkCnxtpK%m8dli@4<-1eP&=6Su>;Qiq8vbn~1_cGBpceMWN z*K@ew$d{ZHED#M1je(ix6u<`UM`65gT3!d?);q<$6aWF0vvgiB2XISZZAnQ$Wv>pb!1Qy;_>vvg>T=rR3TCRi_R9edf{d^@KF_X^Q3_y&6@SScrdN0aQ9m7SQd;$|U$P@BfQ+(?lR5(`oUBDrw7f4v=Yt@IHKMMi z4fO7v(>m4l2mJ(~8KC|%!`i&__D#nEw#%8Io0&90n|m>Es%3)l6VRHD{{l`*#Wo8H z3M_}~>+1u*UOS~WG#{34i#qPlqXBfh+89dnSn`~iy1TKeouhJTspc8GXCl#q#72vr zs86EjoPcBk=+nU`TLFBrW6KDB@R4i|%4uO#=Dy3gyV%&+sD5*!C3>T6KhVIWXceVz}_(58Od$omA`rHeT~ zJ&hR_T`mL2rwwS1jyEV*nuc2MZ)|-q1!y!5D+p)<|Bfls1-=U`>=+hZF>N2l1350# z?*m9Kb222@#|X|Vh<(2Er66{G;ri@ryHvg3YRtt{<@=X+tXBX)T(z8x_#bsVzPh?9 z*2wVccu1z6DR#V9I||6r6QB<7dT`*rVXyE zGwrvh!Vc2_LCw{hwR00*ufw;h(J24nG{Ma%AZY+L2EGn~k14Bc^K*QE2&^mOID7Q- zBSDd!-@IM1Mz&<*ZP?X8-`_{tP~PUf8oP-iKunhdIR)Twr5*>M6CD3#pR^xW_5dk) z+M1A9g1-F`F8vNrGW%u@4Kjehl?l98GjPoN*l`+ENV<|e21Muyus{%ie{qUCYJXml zuL0e`X~SBqe^#WR%WQ89)l7w1v+Y>^7 zER+(<%*MO-q!cgxFqE>%{T zvJfmlfArl;0RO|*^Ot0pN;>am%)IZ*IOl3{^Odl9rACGb4bT+>E%Qj0_wwA-RFAmJ z(7y#-eEe*HM(^8~!F6{_x^I`KyQ4t+aGx8MGGl*T+^+V&ieAGi{S3S+AH1sv_6RUb z*-BCID=n?jXMmm<4cHLGJHDq+GC=1<^R_h{cB!zh_W*(z0GD>4Q1Q$_e84?Vy)?*j z>!ql?G-<&|dHwz(%_h6?JfoAcsSv>l$SOdXzP|Yc0;Fg=A?1wwx0%Gj_4}M}^w|85 z2T*4>fy3JZ!IHDwc|UuV6c2)6b+oxmyO3KtZjU2zNVx!oqeFs+iz|7r5**sI{(-W% z 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