diff --git a/src/client/channel.rs b/src/client/channel.rs index 9a4ea00..254a39f 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -16,7 +16,9 @@ use crate::{ util::{self, timeago, ProtoBuilder}, }; -use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext}; +use super::{ + response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext, +}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -201,16 +203,13 @@ impl RustyPipeQuery { impl MapResponse>> for response::Channel { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>>, ExtractionError> { - let content = map_channel_content(id, self.contents, self.alerts)?; + let content = map_channel_content(ctx.id, self.contents, self.alerts)?; let visitor_data = self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)); + .or_else(|| ctx.visitor_data.map(str::to_owned)); let channel_data = map_channel( MapChannelData { @@ -221,12 +220,11 @@ impl MapResponse>> for response::Channel { has_shorts: content.has_shorts, has_live: content.has_live, }, - id, - lang, + ctx, )?; let mut mapper = response::YouTubeListMapper::::with_channel( - lang, + ctx.lang, &channel_data.c, channel_data.warnings, ); @@ -249,16 +247,13 @@ impl MapResponse>> for response::Channel { impl MapResponse>> for response::Channel { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>>, ExtractionError> { - let content = map_channel_content(id, self.contents, self.alerts)?; + let content = map_channel_content(ctx.id, self.contents, self.alerts)?; let visitor_data = self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)); + .or_else(|| ctx.visitor_data.map(str::to_owned)); let channel_data = map_channel( MapChannelData { @@ -269,12 +264,11 @@ impl MapResponse>> for response::Channel { has_shorts: content.has_shorts, has_live: content.has_live, }, - id, - lang, + ctx, )?; let mut mapper = response::YouTubeListMapper::::with_channel( - lang, + ctx.lang, &channel_data.c, channel_data.warnings, ); @@ -289,13 +283,7 @@ impl MapResponse>> for response::Channel { } impl MapResponse for response::ChannelAbout { - fn map_response( - self, - id: &str, - _lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _visitor_data: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { // Channel info is always fetched in English. There is no localized data there // and it allows parsing the country name. let lang = Language::En; @@ -309,7 +297,7 @@ impl MapResponse for response::ChannelAbout { .ok_or(ExtractionError::InvalidData("no received endpoint".into()))?, response::ChannelAbout::Content { contents } => { // Handle errors (e.g. age restriction) when regular channel content was returned - map_channel_content(id, contents, None)?; + map_channel_content(ctx.id, contents, None)?; return Err(ExtractionError::InvalidData( "could not extract aboutData".into(), )); @@ -388,36 +376,35 @@ struct MapChannelData { fn map_channel( d: MapChannelData, - id: &str, - lang: Language, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let header = d.header.ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no header".into(), })?; let metadata = d .metadata .ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no metadata".into(), })? .channel_metadata_renderer; let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no microformat".into(), })?; - if metadata.external_id != id { + if metadata.external_id != ctx.id { return Err(ExtractionError::WrongResult(format!( "got wrong channel id {}, expected {}", - metadata.external_id, id + metadata.external_id, ctx.id ))); } let vanity_url = metadata .vanity_channel_url .as_ref() - .and_then(|url| map_vanity_url(url, id)); + .and_then(|url| map_vanity_url(url, ctx.id)); let mut warnings = Vec::new(); Ok(MapResult { @@ -425,9 +412,9 @@ fn map_channel( response::channel::Header::C4TabbedHeaderRenderer(header) => Channel { id: metadata.external_id, name: metadata.title, - subscriber_count: header - .subscriber_count_text - .and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)), + subscriber_count: header.subscriber_count_text.and_then(|txt| { + util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings) + }), avatar: header.avatar.into(), verification: header.badges.into(), description: metadata.description, @@ -458,7 +445,7 @@ fn map_channel( name: metadata.title, subscriber_count: hdata.as_ref().and_then(|hdata| { hdata.0.as_ref().and_then(|txt| { - util::parse_large_numstr_or_warn(txt, lang, &mut warnings) + util::parse_large_numstr_or_warn(txt, ctx.lang, &mut warnings) }) }), avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(), @@ -487,7 +474,7 @@ fn map_channel( md_rows.first().and_then(|md| md.metadata_parts.get(1)) }; let subscriber_count = sub_part.and_then(|t| { - util::parse_large_numstr_or_warn::(&t.text, lang, &mut warnings) + util::parse_large_numstr_or_warn::(&t.text, ctx.lang, &mut warnings) }); Channel { @@ -697,10 +684,10 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, + client::{response, MapRespCtx, MapResponse}, error::{ExtractionError, UnavailabilityReason}, model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem}, - param::{ChannelOrder, ChannelVideoTab, Language}, + param::{ChannelOrder, ChannelVideoTab}, serializer::MapResult, util::tests::TESTFILES, }; @@ -728,7 +715,7 @@ mod tests { let channel: response::Channel = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult>> = - channel.map_response(id, Language::En, None, None).unwrap(); + channel.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -755,7 +742,7 @@ mod tests { let channel: response::Channel = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let res: Result>>, ExtractionError> = - channel.map_response("UCbfnHqxXs_K3kvaH-WlNlig", Language::En, None, None); + channel.map_response(&MapRespCtx::test("UCbfnHqxXs_K3kvaH-WlNlig")); if let Err(ExtractionError::Unavailable { reason, msg }) = res { assert_eq!(reason, UnavailabilityReason::AgeRestricted); assert!(msg.starts_with("Laphroaig Whisky: ")); @@ -772,7 +759,7 @@ mod tests { let channel: response::Channel = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult>> = channel - .map_response("UC2DjFE7Xf11URZqWBigcVOQ", Language::En, None, None) + .map_response(&MapRespCtx::test("UC2DjFE7Xf11URZqWBigcVOQ")) .unwrap(); assert!( @@ -791,7 +778,7 @@ mod tests { let channel: response::ChannelAbout = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = channel - .map_response("UC2DjFE7Xf11U-RZqWBigcVOQ", Language::En, None, None) + .map_response(&MapRespCtx::test("UC2DjFE7Xf11U-RZqWBigcVOQ")) .unwrap(); assert!( diff --git a/src/client/mod.rs b/src/client/mod.rs index c428139..3091db6 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -432,7 +432,7 @@ impl Default for RustyPipeBuilder { } impl RustyPipeBuilder { - /// Return a new `RustyPipeBuilder`. + /// Create a new [`RustyPipeBuilder`]. /// /// This is the same as [`RustyPipe::builder`] #[must_use] @@ -448,12 +448,12 @@ impl RustyPipeBuilder { } } - /// Return a new, configured RustyPipe instance. + /// Create a new, configured [`RustyPipe`] instance. pub fn build(self) -> Result { self.build_with_client(ClientBuilder::new()) } - /// Return a new, configured RustyPipe instance using a Reqwest client builder. + /// Create a new, configured RustyPipe instance using a Reqwest client builder. pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result { client_builder = client_builder .user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned())) @@ -1268,9 +1268,7 @@ impl RustyPipeQuery { async fn yt_request_attempt + Debug, M>( &self, request: &Request, - id: &str, - visitor_data: Option<&str>, - deobf: Option<&DeobfData>, + ctx: &MapRespCtx<'_>, ) -> Result, Error> { let response = self .client @@ -1289,7 +1287,7 @@ impl RustyPipeQuery { Err(match status { StatusCode::NOT_FOUND => Error::Extraction(ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: error_msg.unwrap_or("404".into()), }), StatusCode::BAD_REQUEST => { @@ -1299,12 +1297,7 @@ impl RustyPipeQuery { }) } else { match serde_json::from_str::(&body) { - Ok(deserialized) => match deserialized.map_response( - id, - self.opts.lang, - deobf, - self.opts.visitor_data.as_deref().or(visitor_data), - ) { + Ok(deserialized) => match deserialized.map_response(ctx) { Ok(mapres) => Ok(mapres), Err(e) => Err(e.into()), }, @@ -1320,15 +1313,11 @@ impl RustyPipeQuery { async fn yt_request + Debug, M>( &self, request: &Request, - id: &str, - visitor_data: Option<&str>, - deobf: Option<&DeobfData>, + ctx: &MapRespCtx<'_>, ) -> Result, Error> { let mut last_resp = None; for n in 0..=self.client.inner.n_http_retries { - let resp = self - .yt_request_attempt::(request, id, visitor_data, deobf) - .await?; + let resp = self.yt_request_attempt::(request, ctx).await?; let err = match &resp.res { Ok(_) => return Ok(resp), @@ -1394,9 +1383,15 @@ impl RustyPipeQuery { .json(body) .build()?; - let req_res = self - .yt_request::(&request, id, visitor_data, deobf) - .await?; + let ctx = MapRespCtx { + id, + lang: self.opts.lang, + deobf, + visitor_data, + client_type: ctype, + }; + + let req_res = self.yt_request::(&request, &ctx).await?; // Uncomment to debug response text // println!("{}", &req_res.body); @@ -1553,6 +1548,28 @@ impl AsRef for RustyPipeQuery { } } +struct MapRespCtx<'a> { + id: &'a str, + lang: Language, + deobf: Option<&'a DeobfData>, + visitor_data: Option<&'a str>, + client_type: ClientType, +} + +impl<'a> MapRespCtx<'a> { + /// Create a [`MapRespCtx`] for testing + #[cfg(test)] + fn test(id: &'a str) -> Self { + Self { + id, + lang: Language::En, + deobf: None, + visitor_data: None, + client_type: ClientType::Desktop, + } + } +} + /// Implement this for YouTube API response structs that need to be mapped to /// RustyPipe models. trait MapResponse { @@ -1569,13 +1586,7 @@ trait MapResponse { /// - `lang`: Language of the request. Used for mapping localized information like dates. /// - `deobf`: Deobfuscator (if passed to the `execute_request_deobf` method) /// - `visitor_data`: Visitor data option of the client - fn map_response( - self, - id: &str, - lang: Language, - deobf: Option<&DeobfData>, - visitor_data: Option<&str>, - ) -> Result, ExtractionError>; + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError>; } fn validate_country(country: Country) -> Country { diff --git a/src/client/music_artist.rs b/src/client/music_artist.rs index bd1ca76..9009644 100644 --- a/src/client/music_artist.rs +++ b/src/client/music_artist.rs @@ -14,7 +14,7 @@ use crate::{ use super::{ response::{self, music_item::MusicListMapper, url_endpoint::PageType}, - ClientType, MapResponse, QBrowse, RustyPipeQuery, + ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, }; impl RustyPipeQuery { @@ -92,14 +92,8 @@ impl RustyPipeQuery { } impl MapResponse for response::MusicArtist { - fn map_response( - self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { - let mapped = map_artist_page(self, id, lang, false)?; + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { + let mapped = map_artist_page(self, ctx, false)?; Ok(MapResult { c: mapped.c.0, warnings: mapped.warnings, @@ -110,19 +104,15 @@ impl MapResponse for response::MusicArtist { impl MapResponse<(MusicArtist, bool)> for response::MusicArtist { fn map_response( self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { - map_artist_page(self, id, lang, true) + map_artist_page(self, ctx, true) } } fn map_artist_page( res: response::MusicArtist, - id: &str, - lang: crate::param::Language, + ctx: &MapRespCtx<'_>, skip_extendables: bool, ) -> Result, ExtractionError> { // dbg!(&res); @@ -138,7 +128,7 @@ fn map_artist_page( .and_then(|pb| util::string_from_pb(pb, 3)); if let Some(share_channel_id) = share_channel_id { - if share_channel_id != id { + if share_channel_id != ctx.id { return Err(ExtractionError::Redirect(share_channel_id)); } } @@ -155,9 +145,9 @@ fn map_artist_page( .unwrap_or_default(); let mut mapper = MusicListMapper::with_artist( - lang, + ctx.lang, ArtistId { - id: Some(id.to_owned()), + id: Some(ctx.id.to_owned()), name: header.title.clone(), }, ); @@ -264,7 +254,7 @@ fn map_artist_page( Ok(MapResult { c: ( MusicArtist { - id: id.to_owned(), + id: ctx.id.to_owned(), name: header.title, header_image: header.thumbnail.into(), description: header.description, @@ -272,7 +262,7 @@ fn map_artist_page( subscriber_count: header.subscription_button.and_then(|btn| { util::parse_large_numstr_or_warn( &btn.subscribe_button_renderer.subscriber_count_text, - lang, + ctx.lang, &mut mapped.warnings, ) }), @@ -293,16 +283,13 @@ fn map_artist_page( impl MapResponse> for response::MusicArtistAlbums { fn map_response( self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { // dbg!(&self); let Some(header) = self.header else { return Err(ExtractionError::NotFound { - id: id.into(), + id: ctx.id.into(), msg: "no header".into(), }); }; @@ -320,9 +307,9 @@ impl MapResponse> for response::MusicArtistAlbums { .contents; let mut mapper = MusicListMapper::with_artist( - lang, + ctx.lang, ArtistId { - id: Some(id.to_owned()), + id: Some(ctx.id.to_owned()), name: header.music_header_renderer.title, }, ); @@ -347,7 +334,7 @@ mod tests { use path_macro::path; use rstest::rstest; - use crate::{param::Language, util::tests::TESTFILES}; + use crate::util::tests::TESTFILES; use super::*; @@ -369,7 +356,7 @@ mod tests { let resp: response::MusicArtist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult<(MusicArtist, bool)> = - resp.map_response(id, Language::En, None, None).unwrap(); + resp.map_response(&MapRespCtx::test(id)).unwrap(); let (mut artist, can_fetch_more) = map_res.c; assert!( @@ -384,7 +371,7 @@ mod tests { let resp: response::MusicArtistAlbums = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let mut map_res: MapResult> = - resp.map_response(id, Language::En, None, None).unwrap(); + resp.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -405,7 +392,7 @@ mod tests { let artist: response::MusicArtist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = artist - .map_response("UClmXPfaYhXOYsNn_QUyheWQ", Language::En, None, None) + .map_response(&MapRespCtx::test("UClmXPfaYhXOYsNn_QUyheWQ")) .unwrap(); assert!( @@ -424,7 +411,7 @@ mod tests { let artist: response::MusicArtist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let res: Result, ExtractionError> = - artist.map_response("UCLkAepWjdylmXSltofFvsYQ", Language::En, None, None); + artist.map_response(&MapRespCtx::test("UCLkAepWjdylmXSltofFvsYQ")); let e = res.unwrap_err(); match e { diff --git a/src/client/music_charts.rs b/src/client/music_charts.rs index 27ac005..1075913 100644 --- a/src/client/music_charts.rs +++ b/src/client/music_charts.rs @@ -11,7 +11,7 @@ use crate::{ use super::{ response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType}, - ClientType, MapResponse, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -56,13 +56,7 @@ impl RustyPipeQuery { } impl MapResponse for response::MusicCharts { - fn map_response( - self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, crate::error::ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { let countries = self .framework_updates .map(|fwu| { @@ -77,9 +71,9 @@ impl MapResponse for response::MusicCharts { let mut top_playlist_id = None; let mut trending_playlist_id = None; - let mut mapper_top = MusicListMapper::new(lang); - let mut mapper_trending = MusicListMapper::new(lang); - let mut mapper_other = MusicListMapper::new(lang); + let mut mapper_top = MusicListMapper::new(ctx.lang); + let mut mapper_trending = MusicListMapper::new(ctx.lang); + let mut mapper_other = MusicListMapper::new(ctx.lang); self.contents .single_column_browse_results_renderer @@ -151,7 +145,6 @@ mod tests { use rstest::rstest; use super::*; - use crate::param::Language; #[rstest] #[case::default("global")] @@ -163,8 +156,7 @@ mod tests { let charts: response::MusicCharts = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult = - charts.map_response("", Language::En, None, None).unwrap(); + let map_res: MapResult = charts.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_details.rs b/src/client/music_details.rs index 0459389..919b07f 100644 --- a/src/client/music_details.rs +++ b/src/client/music_details.rs @@ -8,7 +8,6 @@ use crate::{ paginator::{ContinuationEndpoint, Paginator}, ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem, }, - param::Language, serializer::MapResult, }; @@ -17,7 +16,7 @@ use super::{ self, music_item::{map_queue_item, MusicListMapper}, }, - ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -170,10 +169,7 @@ impl RustyPipeQuery { impl MapResponse for response::MusicDetails { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { let tabs = self .contents @@ -211,7 +207,7 @@ impl MapResponse for response::MusicDetails { } let content = content.ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no content".into(), })?; let track_item = content @@ -225,7 +221,7 @@ impl MapResponse for response::MusicDetails { response::music_item::PlaylistPanelVideo::None => None, }) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?; - let mut track = map_queue_item(track_item, lang); + let mut track = map_queue_item(track_item, ctx.lang); let mut warnings = content.contents.warnings; warnings.append(&mut track.warnings); @@ -244,10 +240,7 @@ impl MapResponse for response::MusicDetails { impl MapResponse> for response::MusicDetails { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let tabs = self .contents @@ -260,7 +253,7 @@ impl MapResponse> for response::MusicDetails { .into_iter() .find_map(|t| t.tab_renderer.content) .ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no content".into(), })? .music_queue_renderer @@ -275,7 +268,7 @@ impl MapResponse> for response::MusicDetails { .into_iter() .filter_map(|item| match item { response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) => { - let mut track = map_queue_item(item, lang); + let mut track = map_queue_item(item, ctx.lang); warnings.append(&mut track.warnings); Some(track.c) } @@ -297,18 +290,12 @@ impl MapResponse> for response::MusicDetails { } impl MapResponse for response::MusicLyrics { - fn map_response( - self, - id: &str, - _lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { let lyrics = self .contents .into_res() .map_err(|msg| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: msg.into(), })? .into_iter() @@ -328,16 +315,13 @@ impl MapResponse for response::MusicLyrics { impl MapResponse for response::MusicRelated { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { let contents = self .contents .into_res() .map_err(|msg| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: msg.into(), })?; @@ -362,10 +346,10 @@ impl MapResponse for response::MusicRelated { _ => None, }); - let mut mapper_tracks = MusicListMapper::new(lang); + let mut mapper_tracks = MusicListMapper::new(ctx.lang); let mut mapper = match artist_id { - Some(artist_id) => MusicListMapper::with_artist(lang, artist_id), - None => MusicListMapper::new(lang), + Some(artist_id) => MusicListMapper::with_artist(ctx.lang, artist_id), + None => MusicListMapper::new(ctx.lang), }; let mut sections = contents.into_iter(); @@ -412,7 +396,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{model, param::Language, util::tests::TESTFILES}; + use crate::{model, util::tests::TESTFILES}; #[rstest] #[case::mv("mv", "ZeerrnuLi5E")] @@ -424,7 +408,7 @@ mod tests { let details: response::MusicDetails = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = - details.map_response(id, Language::En, None, None).unwrap(); + details.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -444,7 +428,7 @@ mod tests { let radio: response::MusicDetails = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - radio.map_response(id, Language::En, None, None).unwrap(); + radio.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -461,7 +445,7 @@ mod tests { let lyrics: response::MusicLyrics = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult = lyrics.map_response("", Language::En, None, None).unwrap(); + let map_res: MapResult = lyrics.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -478,8 +462,7 @@ mod tests { let lyrics: response::MusicRelated = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult = - lyrics.map_response("", Language::En, None, None).unwrap(); + let map_res: MapResult = lyrics.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_genres.rs b/src/client/music_genres.rs index 7fc2511..16f3b53 100644 --- a/src/client/music_genres.rs +++ b/src/client/music_genres.rs @@ -8,7 +8,7 @@ use crate::{ use super::{ response::{self, music_item::MusicListMapper, url_endpoint::NavigationEndpoint}, - ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, + ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, }; impl RustyPipeQuery { @@ -59,11 +59,8 @@ impl RustyPipeQuery { impl MapResponse> for response::MusicGenres { fn map_response( self, - _id: &str, - _lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result>, ExtractionError> { + _ctx: &MapRespCtx<'_>, + ) -> Result>, ExtractionError> { let content = self .contents .single_column_browse_results_renderer @@ -111,13 +108,7 @@ impl MapResponse> for response::MusicGenres { } impl MapResponse for response::MusicGenre { - fn map_response( - self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { // dbg!(&self); let content = self @@ -179,7 +170,7 @@ impl MapResponse for response::MusicGenre { _ => return None, }; - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); mapper.map_response(items); let mut mapped = mapper.conv_items(); warnings.append(&mut mapped.warnings); @@ -194,7 +185,7 @@ impl MapResponse for response::MusicGenre { Ok(MapResult { c: MusicGenre { - id: id.to_owned(), + id: ctx.id.to_owned(), name: self.header.music_header_renderer.title, sections, }, @@ -211,7 +202,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{model, param::Language, util::tests::TESTFILES}; + use crate::{model, util::tests::TESTFILES}; #[test] fn map_music_genres() { @@ -221,7 +212,7 @@ mod tests { let playlist: response::MusicGenres = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - playlist.map_response("", Language::En, None, None).unwrap(); + playlist.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -241,7 +232,7 @@ mod tests { let playlist: response::MusicGenre = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = - playlist.map_response(id, Language::En, None, None).unwrap(); + playlist.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_new.rs b/src/client/music_new.rs index c5cfc5f..68251e6 100644 --- a/src/client/music_new.rs +++ b/src/client/music_new.rs @@ -4,9 +4,10 @@ use crate::{ client::response::music_item::MusicListMapper, error::{Error, ExtractionError}, model::{traits::FromYtItem, AlbumItem, TrackItem}, + serializer::MapResult, }; -use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery}; +use super::{response, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery}; impl RustyPipeQuery { /// Get the new albums that were released on YouTube Music @@ -49,13 +50,7 @@ impl RustyPipeQuery { } impl MapResponse> for response::MusicNew { - fn map_response( - self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result>, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result>, ExtractionError> { let items = self .contents .single_column_browse_results_renderer @@ -73,7 +68,7 @@ impl MapResponse> for response::MusicNew { .grid_renderer .items; - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); mapper.map_response(items); Ok(mapper.conv_items()) @@ -88,7 +83,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{param::Language, serializer::MapResult, util::tests::TESTFILES}; + use crate::{serializer::MapResult, util::tests::TESTFILES}; #[rstest] #[case::default("default")] @@ -98,9 +93,8 @@ mod tests { let new_albums: response::MusicNew = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = new_albums - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult> = + new_albums.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -119,9 +113,8 @@ mod tests { let new_videos: response::MusicNew = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = new_videos - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult> = + new_videos.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index 8c2d29f..8261b2b 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -17,7 +17,7 @@ use super::{ self, music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper}, }, - ClientType, MapResponse, QBrowse, RustyPipeQuery, + ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, }; impl RustyPipeQuery { @@ -138,10 +138,7 @@ impl RustyPipeQuery { impl MapResponse for response::MusicPlaylist { fn map_response( self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { // dbg!(&self); @@ -186,14 +183,15 @@ impl MapResponse for response::MusicPlaylist { )))?; if let Some(playlist_id) = shelf.playlist_id { - if playlist_id != id { + if playlist_id != ctx.id { return Err(ExtractionError::WrongResult(format!( - "got wrong playlist id {playlist_id}, expected {id}" + "got wrong playlist id {}, expected {}", + playlist_id, ctx.id ))); } } - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); mapper.map_response(shelf.contents); let map_res = mapper.conv_items(); @@ -273,7 +271,7 @@ impl MapResponse for response::MusicPlaylist { Ok(MapResult { c: MusicPlaylist { - id: id.to_owned(), + id: ctx.id.to_owned(), name, thumbnail, channel, @@ -284,14 +282,14 @@ impl MapResponse for response::MusicPlaylist { track_count, map_res.c, ctoken, - vdata.map(str::to_owned), + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::MusicBrowse, ), related_playlists: Paginator::new_ext( None, Vec::new(), related_ctoken, - vdata.map(str::to_owned), + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::MusicBrowse, ), }, @@ -301,13 +299,7 @@ impl MapResponse for response::MusicPlaylist { } impl MapResponse for response::MusicPlaylist { - fn map_response( - self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { // dbg!(&self); let (header, sections) = match self.contents { @@ -401,7 +393,7 @@ impl MapResponse for response::MusicPlaylist { .map(|part| part.to_string()) .unwrap_or_default(); - let album_type = map_album_type(album_type_txt.as_str(), lang); + let album_type = map_album_type(album_type_txt.as_str(), ctx.lang); let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok()); fn map_playlist_id(ep: &NavigationEndpoint) -> Option { @@ -448,11 +440,11 @@ impl MapResponse for response::MusicPlaylist { let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone())); let mut mapper = MusicListMapper::with_album( - lang, + ctx.lang, artists.clone(), by_va, AlbumId { - id: id.to_owned(), + id: ctx.id.to_owned(), name: header.title.clone(), }, ); @@ -460,7 +452,7 @@ impl MapResponse for response::MusicPlaylist { let tracks_res = mapper.conv_items(); let mut warnings = tracks_res.warnings; - let mut variants_mapper = MusicListMapper::new(lang); + let mut variants_mapper = MusicListMapper::new(ctx.lang); if let Some(res) = album_variants { variants_mapper.map_response(res); } @@ -469,7 +461,7 @@ impl MapResponse for response::MusicPlaylist { Ok(MapResult { c: MusicAlbum { - id: id.to_owned(), + id: ctx.id.to_owned(), playlist_id, name: header.title, cover: header.thumbnail.into(), @@ -497,7 +489,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{model, param::Language, util::tests::TESTFILES}; + use crate::{model, util::tests::TESTFILES}; #[rstest] #[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")] @@ -512,7 +504,7 @@ mod tests { let playlist: response::MusicPlaylist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = - playlist.map_response(id, Language::En, None, None).unwrap(); + playlist.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -539,7 +531,7 @@ mod tests { let playlist: response::MusicPlaylist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = - playlist.map_response(id, Language::En, None, None).unwrap(); + playlist.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_search.rs b/src/client/music_search.rs index 2216171..f443229 100644 --- a/src/client/music_search.rs +++ b/src/client/music_search.rs @@ -15,7 +15,7 @@ use crate::{ serializer::MapResult, }; -use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext}; +use super::{response, ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -152,10 +152,7 @@ impl RustyPipeQuery { impl MapResponse> for response::MusicSearch { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { // dbg!(&self); @@ -171,7 +168,7 @@ impl MapResponse> for response::MusicSearch let mut corrected_query = None; let mut ctoken = None; - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); sections.into_iter().for_each(|section| match section { response::music_search::ItemSection::MusicShelfRenderer(shelf) => { @@ -199,7 +196,7 @@ impl MapResponse> for response::MusicSearch None, map_res.c, ctoken, - vdata.map(str::to_owned), + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::MusicSearch, ), corrected_query, @@ -212,12 +209,9 @@ impl MapResponse> for response::MusicSearch impl MapResponse for response::MusicSearchSuggestion { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { - let mut mapper = MusicListMapper::new_search_suggest(lang); + let mut mapper = MusicListMapper::new_search_suggest(ctx.lang); let mut terms = Vec::new(); for section in self.contents { @@ -256,12 +250,11 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, + client::{response, MapRespCtx, MapResponse}, model::{ AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult, MusicSearchSuggestion, TrackItem, }, - param::Language, serializer::MapResult, util::tests::TESTFILES, }; @@ -278,7 +271,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -301,7 +294,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -320,7 +313,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -339,7 +332,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -360,7 +353,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -380,9 +373,8 @@ mod tests { let suggestion: response::MusicSearchSuggestion = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult = suggestion - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult = + suggestion.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 20ce792..94c44e8 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -10,7 +10,7 @@ use crate::model::{ use crate::serializer::MapResult; use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo}; -use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery}; +use super::{response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery}; impl RustyPipeQuery { /// Get more YouTube items from the given continuation token and endpoint @@ -103,10 +103,7 @@ fn map_ytm_paginator( impl MapResponse> for response::Continuation { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let items = self .on_response_received_actions @@ -126,7 +123,7 @@ impl MapResponse> for response::Continuation { }) .unwrap_or_default(); - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { @@ -139,12 +136,9 @@ impl MapResponse> for response::Continuation { impl MapResponse> for response::MusicContinuation { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); let mut continuations = Vec::new(); match self.continuation_contents { @@ -173,7 +167,7 @@ impl MapResponse> for response::MusicContinuation { mapper.add_warnings(&mut panel.contents.warnings); panel.contents.c.into_iter().for_each(|item| { if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = item { - let mut track = map_queue_item(item, lang); + let mut track = map_queue_item(item, ctx.lang); mapper.add_item(MusicItem::Track(track.c)); mapper.add_warnings(&mut track.warnings); } @@ -356,7 +350,6 @@ mod tests { use super::*; use crate::{ model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem}, - param::Language, util::tests::TESTFILES, }; @@ -371,7 +364,7 @@ mod tests { let items: response::Continuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -393,7 +386,7 @@ mod tests { let items: response::Continuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); let paginator: Paginator = map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse); @@ -416,7 +409,7 @@ mod tests { let items: response::Continuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); let paginator: Paginator = map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse); @@ -439,7 +432,7 @@ mod tests { let items: response::MusicContinuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); let paginator: Paginator = map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse); @@ -460,7 +453,7 @@ mod tests { let items: response::MusicContinuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); let paginator: Paginator = map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse); diff --git a/src/client/player.rs b/src/client/player.rs index 3c7536c..e7a0751 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -16,13 +16,12 @@ use crate::{ traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset, Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream, }, - param::Language, util, }; use super::{ response::{self, player}, - ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -149,12 +148,9 @@ impl RustyPipeQuery { impl MapResponse for response::Player { fn map_response( self, - id: &str, - _lang: Language, - deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { - let deobf = Deobfuscator::new(deobf.unwrap())?; + let deobf = Deobfuscator::new(ctx.deobf.unwrap())?; let mut warnings = vec![]; // Check playability status @@ -235,10 +231,10 @@ impl MapResponse for response::Player { "no video details", )))?; - if video_details.video_id != id { + if video_details.video_id != ctx.id { return Err(ExtractionError::WrongResult(format!( "video id {}, expected {}", - video_details.video_id, id + video_details.video_id, ctx.id ))); } @@ -375,10 +371,11 @@ impl MapResponse for response::Player { hls_manifest_url: streaming_data.hls_manifest_url, dash_manifest_url: streaming_data.dash_manifest_url, preview_frames, + client_type: ctx.client_type, visitor_data: self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)), + .or_else(|| ctx.visitor_data.map(str::to_owned)), }, warnings, }) @@ -657,7 +654,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{deobfuscate::DeobfData, util::tests::TESTFILES}; + use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES}; static DEOBF_DATA: Lazy = Lazy::new(|| { DeobfData { @@ -669,18 +666,27 @@ mod tests { }); #[rstest] - #[case::desktop("desktop")] - #[case::desktop_music("desktopmusic")] - #[case::tv_html5_embed("tvhtml5embed")] - #[case::android("android")] - #[case::ios("ios")] - fn map_player_data(#[case] name: &str) { + #[case::desktop(ClientType::Desktop)] + #[case::desktop_music(ClientType::DesktopMusic)] + #[case::tv_html5_embed(ClientType::TvHtml5Embed)] + #[case::android(ClientType::Android)] + #[case::ios(ClientType::Ios)] + fn map_player_data(#[case] client_type: ClientType) { + let name = serde_plain::to_string(&client_type) + .unwrap() + .replace('_', ""); let json_path = path!(*TESTFILES / "player" / format!("{name}_video.json")); let json_file = File::open(json_path).unwrap(); let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res = resp - .map_response("pPvd8UxmSbQ", Language::En, Some(&DEOBF_DATA), None) + .map_response(&MapRespCtx { + id: "pPvd8UxmSbQ", + lang: Language::En, + deobf: Some(&DEOBF_DATA), + visitor_data: None, + client_type, + }) .unwrap(); assert!( diff --git a/src/client/playlist.rs b/src/client/playlist.rs index ce198f5..ecbf205 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -13,7 +13,7 @@ use crate::{ util::{self, timeago, TryRemove}, }; -use super::{response, ClientType, MapResponse, MapResult, QBrowse, RustyPipeQuery}; +use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, QBrowse, RustyPipeQuery}; impl RustyPipeQuery { /// Get a YouTube playlist @@ -47,15 +47,9 @@ impl RustyPipeQuery { } impl MapResponse for response::Playlist { - fn map_response( - self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { let (Some(contents), Some(header)) = (self.contents, self.header) else { - return Err(response::alerts_to_err(id, self.alerts)); + return Err(response::alerts_to_err(ctx.id, self.alerts)); }; let video_items = contents @@ -85,7 +79,7 @@ impl MapResponse for response::Playlist { .playlist_video_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(video_items); let (description, thumbnails, last_update_txt) = match self.sidebar { @@ -144,9 +138,10 @@ impl MapResponse for response::Playlist { }; let playlist_id = header.playlist_header_renderer.playlist_id; - if playlist_id != id { + if playlist_id != ctx.id { return Err(ExtractionError::WrongResult(format!( - "got wrong playlist id {playlist_id}, expected {id}" + "got wrong playlist id {}, expected {}", + playlist_id, ctx.id ))); } @@ -165,7 +160,7 @@ impl MapResponse for response::Playlist { .and_then(|link| ChannelId::try_from(link).ok()); let last_update = last_update_txt.as_ref().and_then(|txt| { - timeago::parse_textual_date_or_warn(lang, txt, &mut mapper.warnings) + timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut mapper.warnings) .map(OffsetDateTime::date) }); @@ -177,7 +172,7 @@ impl MapResponse for response::Playlist { Some(n_videos), mapper.items, mapper.ctoken, - vdata.map(str::to_owned), + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::Browse, ), video_count: n_videos, @@ -189,7 +184,7 @@ impl MapResponse for response::Playlist { visitor_data: self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)), + .or_else(|| ctx.visitor_data.map(str::to_owned)), }, warnings: mapper.warnings, }) @@ -203,7 +198,7 @@ mod tests { use path_macro::path; use rstest::rstest; - use crate::{param::Language, util::tests::TESTFILES}; + use crate::util::tests::TESTFILES; use super::*; @@ -218,7 +213,7 @@ mod tests { let playlist: response::Playlist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res = playlist.map_response(id, Language::En, None, None).unwrap(); + let map_res = playlist.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/search.rs b/src/client/search.rs index b99066b..03529f4 100644 --- a/src/client/search.rs +++ b/src/client/search.rs @@ -12,7 +12,7 @@ use crate::{ param::search_filter::SearchFilter, }; -use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext}; +use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -103,10 +103,7 @@ impl RustyPipeQuery { impl MapResponse> for response::Search { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let items = self .contents @@ -115,7 +112,7 @@ impl MapResponse> for response::Search { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { @@ -135,7 +132,7 @@ impl MapResponse> for response::Search { visitor_data: self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)), + .or_else(|| ctx.visitor_data.map(str::to_owned)), }, warnings: mapper.warnings, }) @@ -150,9 +147,8 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, + client::{response, MapRespCtx, MapResponse}, model::{SearchResult, YouTubeItem}, - param::Language, serializer::MapResult, util::tests::TESTFILES, }; @@ -168,7 +164,7 @@ mod tests { let search: response::Search = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap index 9dda7eb..02798cc 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap @@ -466,5 +466,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: android, visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap index ffafe4c..ec4ae26 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap @@ -581,5 +581,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: desktop, visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap index 05f522b..8ea0e63 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap @@ -405,5 +405,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: desktop_music, visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap index 416ecca..dcad31b 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap @@ -196,5 +196,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: ios, visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap index 0e48f31..ee018d8 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap @@ -581,5 +581,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: tv_html5_embed, visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"), ) diff --git a/src/client/trends.rs b/src/client/trends.rs index 8b6f3b8..0a46bc5 100644 --- a/src/client/trends.rs +++ b/src/client/trends.rs @@ -10,7 +10,9 @@ use crate::{ serializer::MapResult, }; -use super::{response, ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery}; +use super::{ + response, ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, +}; impl RustyPipeQuery { /// Get the videos from the YouTube startpage @@ -56,10 +58,7 @@ impl RustyPipeQuery { impl MapResponse> for response::Startpage { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let grid = self .contents @@ -75,10 +74,10 @@ impl MapResponse> for response::Startpage { Ok(map_startpage_videos( grid, - lang, + ctx.lang, self.response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)), + .or_else(|| ctx.visitor_data.map(str::to_owned)), )) } } @@ -86,10 +85,7 @@ impl MapResponse> for response::Startpage { impl MapResponse> for response::Trending { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let items = self .contents @@ -103,7 +99,7 @@ impl MapResponse> for response::Trending { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { @@ -141,9 +137,8 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, + client::{response, MapRespCtx, MapResponse}, model::{paginator::Paginator, VideoItem}, - param::Language, serializer::MapResult, util::tests::TESTFILES, }; @@ -155,9 +150,8 @@ mod tests { let startpage: response::Startpage = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = startpage - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult> = + startpage.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -179,9 +173,8 @@ mod tests { let startpage: response::Trending = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = startpage - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult> = + startpage.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/url_resolver.rs b/src/client/url_resolver.rs index b9b0a0b..ab69903 100644 --- a/src/client/url_resolver.rs +++ b/src/client/url_resolver.rs @@ -5,14 +5,13 @@ use serde::Serialize; use crate::{ error::{Error, ExtractionError}, model::UrlTarget, - param::Language, serializer::MapResult, util, }; use super::{ response::{self, url_endpoint::NavigationEndpoint}, - ClientType, MapResponse, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -325,13 +324,7 @@ impl RustyPipeQuery { } impl MapResponse for response::ResolvedUrl { - fn map_response( - self, - _id: &str, - _lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, _ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { let pt = self.endpoint.page_type(); if let NavigationEndpoint::Browse { browse_endpoint, .. diff --git a/src/client/video_details.rs b/src/client/video_details.rs index d08d0a2..1d8a9db 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -15,7 +15,7 @@ use crate::{ use super::{ response::{self, video_details::Payload, IconType}, - ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -89,28 +89,26 @@ impl RustyPipeQuery { impl MapResponse for response::VideoDetails { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { let mut warnings = Vec::new(); let contents = self.contents.ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no content".into(), })?; let current_video_endpoint = self.current_video_endpoint .ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no current_video_endpoint".into(), })?; let video_id = current_video_endpoint.watch_endpoint.video_id; - if id != video_id { + if ctx.id != video_id { return Err(ExtractionError::WrongResult(format!( - "got wrong video id {video_id}, expected {id}" + "got wrong video id {}, expected {}", + video_id, ctx.id ))); } @@ -120,7 +118,7 @@ impl MapResponse for response::VideoDetails { .results .contents .ok_or_else(|| ExtractionError::NotFound { - id: id.into(), + id: ctx.id.into(), msg: "no primary_results".into(), })?; warnings.append(&mut primary_results.warnings); @@ -189,7 +187,7 @@ 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(lang, txt, &mut warnings) + timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut warnings) }), date_text, view_count @@ -207,7 +205,7 @@ impl MapResponse for response::VideoDetails { let comment_count = comment_count_section.and_then(|s| { util::parse_large_numstr_or_warn::( &s.comments_entry_point_header_renderer.comment_count, - lang, + ctx.lang, &mut warnings, ) }); @@ -275,7 +273,7 @@ impl MapResponse for response::VideoDetails { let visitor_data = self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)); + .or_else(|| ctx.visitor_data.map(str::to_owned)); let recommended = contents .two_column_watch_next_results .secondary_results @@ -285,7 +283,7 @@ impl MapResponse for response::VideoDetails { r, sr.secondary_results.continuations, visitor_data.clone(), - lang, + ctx.lang, ); warnings.append(&mut res.warnings); res.c @@ -350,7 +348,7 @@ impl MapResponse for response::VideoDetails { avatar: owner.thumbnail.into(), verification: owner.badges.into(), subscriber_count: owner.subscriber_count_text.and_then(|txt| { - util::parse_large_numstr_or_warn(&txt, lang, &mut warnings) + util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings) }), }, view_count, @@ -385,10 +383,7 @@ impl MapResponse for response::VideoDetails { impl MapResponse> for response::VideoComments { fn map_response( self, - _id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let received_endpoints = self.on_response_received_endpoints; let mut warnings = Vec::new(); @@ -415,7 +410,7 @@ impl MapResponse> for response::VideoComments { comment.comment_renderer, Some(thread.replies), thread.rendering_priority, - lang, + ctx.lang, &mut warnings, )); } else if let Some(vm) = thread.comment_view_model { @@ -424,7 +419,7 @@ impl MapResponse> for response::VideoComments { &mut mutations, Some(thread.replies), thread.rendering_priority, - lang, + ctx.lang, &mut warnings, ) { comments.push(c); @@ -440,7 +435,7 @@ impl MapResponse> for response::VideoComments { comment, None, response::video_details::CommentPriority::RenderingPriorityUnknown, - lang, + ctx.lang, &mut warnings, )); } @@ -450,7 +445,7 @@ impl MapResponse> for response::VideoComments { &mut mutations, None, response::video_details::CommentPriority::RenderingPriorityUnknown, - lang, + ctx.lang, &mut warnings, ) { comments.push(c); @@ -654,8 +649,7 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, - param::Language, + client::{response, MapRespCtx, MapResponse}, util::tests::TESTFILES, }; @@ -676,7 +670,7 @@ mod tests { let details: response::VideoDetails = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res = details.map_response(id, Language::En, None, None).unwrap(); + let map_res = details.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -696,9 +690,7 @@ mod tests { let details: response::VideoDetails = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let err = details - .map_response("", Language::En, None, None) - .unwrap_err(); + let err = details.map_response(&MapRespCtx::test("")).unwrap_err(); assert!(matches!( err, crate::error::ExtractionError::NotFound { .. } @@ -716,7 +708,7 @@ mod tests { let comments: response::VideoComments = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res = comments.map_response("", Language::En, None, None).unwrap(); + let map_res = comments.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/model/mod.rs b/src/model/mod.rs index e79728d..99744ec 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use time::{Date, OffsetDateTime}; use self::{paginator::Paginator, richtext::RichText}; -use crate::{error::Error, param::Country, validate}; +use crate::{client::ClientType, error::Error, param::Country, validate}; /* #COMMON @@ -143,6 +143,8 @@ pub struct VideoPlayer { pub dash_manifest_url: Option, /// Video frames for seek preview pub preview_frames: Vec, + /// Client type with which the player was fetched + pub client_type: ClientType, /// YouTube visitor data cookie pub visitor_data: Option, } @@ -823,7 +825,7 @@ pub enum YouTubeItem { Channel(ChannelItem), } -/// YouTube video list item +/// YouTube video list item (from search results, recommendations, playlists) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct VideoItem { @@ -862,7 +864,7 @@ pub struct VideoItem { pub short_description: Option, } -/// YouTube channel list item +/// YouTube channel list item (from search results) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct ChannelItem { @@ -884,7 +886,7 @@ pub struct ChannelItem { pub short_description: String, } -/// YouTube playlist list item +/// YouTube playlist list item (from search results) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct PlaylistItem { diff --git a/testfiles/player_model/hdr.json b/testfiles/player_model/hdr.json index d9feb68..61c3192 100644 --- a/testfiles/player_model/hdr.json +++ b/testfiles/player_model/hdr.json @@ -1125,5 +1125,6 @@ "expires_in_seconds": 21540, "hls_manifest_url": null, "dash_manifest_url": null, - "preview_frames": [] + "preview_frames": [], + "client_type": "desktop" } diff --git a/testfiles/player_model/multilanguage.json b/testfiles/player_model/multilanguage.json index 7f6e1de..d36f102 100644 --- a/testfiles/player_model/multilanguage.json +++ b/testfiles/player_model/multilanguage.json @@ -2120,5 +2120,6 @@ "hls_manifest_url": null, "dash_manifest_url": null, "preview_frames": [], - "visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D" + "visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D", + "client_type": "desktop" }