feat: add client_type to VideoPlayer, simplify MapResponse trait

The MapResponse trait needed too many arguments, so I added the MapRespCtx object.
Also added the client_type to the context, so it can be added to the extracted player data.
This is necessary to be able to download videos with the correct user agent
This commit is contained in:
ThetaDev 2024-07-27 02:30:24 +02:00
parent 36d6061a27
commit 1934592ab8
24 changed files with 273 additions and 368 deletions

View file

@ -16,7 +16,9 @@ use crate::{
util::{self, timeago, ProtoBuilder}, util::{self, timeago, ProtoBuilder},
}; };
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext}; use super::{
response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext,
};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -201,16 +203,13 @@ impl RustyPipeQuery {
impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel { impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> { ) -> Result<MapResult<Channel<Paginator<VideoItem>>>, 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 let visitor_data = self
.response_context .response_context
.visitor_data .visitor_data
.or_else(|| vdata.map(str::to_owned)); .or_else(|| ctx.visitor_data.map(str::to_owned));
let channel_data = map_channel( let channel_data = map_channel(
MapChannelData { MapChannelData {
@ -221,12 +220,11 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
has_shorts: content.has_shorts, has_shorts: content.has_shorts,
has_live: content.has_live, has_live: content.has_live,
}, },
id, ctx,
lang,
)?; )?;
let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel( let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel(
lang, ctx.lang,
&channel_data.c, &channel_data.c,
channel_data.warnings, channel_data.warnings,
); );
@ -249,16 +247,13 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel { impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> { ) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, 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 let visitor_data = self
.response_context .response_context
.visitor_data .visitor_data
.or_else(|| vdata.map(str::to_owned)); .or_else(|| ctx.visitor_data.map(str::to_owned));
let channel_data = map_channel( let channel_data = map_channel(
MapChannelData { MapChannelData {
@ -269,12 +264,11 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
has_shorts: content.has_shorts, has_shorts: content.has_shorts,
has_live: content.has_live, has_live: content.has_live,
}, },
id, ctx,
lang,
)?; )?;
let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel( let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel(
lang, ctx.lang,
&channel_data.c, &channel_data.c,
channel_data.warnings, channel_data.warnings,
); );
@ -289,13 +283,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
} }
impl MapResponse<ChannelInfo> for response::ChannelAbout { impl MapResponse<ChannelInfo> for response::ChannelAbout {
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<ChannelInfo>, ExtractionError> {
self,
id: &str,
_lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_visitor_data: Option<&str>,
) -> Result<MapResult<ChannelInfo>, 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 there
// and it allows parsing the country name. // and it allows parsing the country name.
let lang = Language::En; let lang = Language::En;
@ -309,7 +297,7 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?, .ok_or(ExtractionError::InvalidData("no received endpoint".into()))?,
response::ChannelAbout::Content { contents } => { response::ChannelAbout::Content { contents } => {
// Handle errors (e.g. age restriction) when regular channel content was returned // 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( return Err(ExtractionError::InvalidData(
"could not extract aboutData".into(), "could not extract aboutData".into(),
)); ));
@ -388,36 +376,35 @@ struct MapChannelData {
fn map_channel( fn map_channel(
d: MapChannelData, d: MapChannelData,
id: &str, ctx: &MapRespCtx<'_>,
lang: Language,
) -> Result<MapResult<Channel<()>>, ExtractionError> { ) -> Result<MapResult<Channel<()>>, ExtractionError> {
let header = d.header.ok_or_else(|| ExtractionError::NotFound { let header = d.header.ok_or_else(|| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: "no header".into(), msg: "no header".into(),
})?; })?;
let metadata = d let metadata = d
.metadata .metadata
.ok_or_else(|| ExtractionError::NotFound { .ok_or_else(|| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: "no metadata".into(), msg: "no metadata".into(),
})? })?
.channel_metadata_renderer; .channel_metadata_renderer;
let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound { let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: "no microformat".into(), msg: "no microformat".into(),
})?; })?;
if metadata.external_id != id { if metadata.external_id != ctx.id {
return Err(ExtractionError::WrongResult(format!( return Err(ExtractionError::WrongResult(format!(
"got wrong channel id {}, expected {}", "got wrong channel id {}, expected {}",
metadata.external_id, id metadata.external_id, ctx.id
))); )));
} }
let vanity_url = metadata let vanity_url = metadata
.vanity_channel_url .vanity_channel_url
.as_ref() .as_ref()
.and_then(|url| map_vanity_url(url, id)); .and_then(|url| map_vanity_url(url, ctx.id));
let mut warnings = Vec::new(); let mut warnings = Vec::new();
Ok(MapResult { Ok(MapResult {
@ -425,9 +412,9 @@ fn map_channel(
response::channel::Header::C4TabbedHeaderRenderer(header) => Channel { response::channel::Header::C4TabbedHeaderRenderer(header) => Channel {
id: metadata.external_id, id: metadata.external_id,
name: metadata.title, name: metadata.title,
subscriber_count: header subscriber_count: header.subscriber_count_text.and_then(|txt| {
.subscriber_count_text util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings)
.and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)), }),
avatar: header.avatar.into(), avatar: header.avatar.into(),
verification: header.badges.into(), verification: header.badges.into(),
description: metadata.description, description: metadata.description,
@ -458,7 +445,7 @@ fn map_channel(
name: metadata.title, name: metadata.title,
subscriber_count: hdata.as_ref().and_then(|hdata| { subscriber_count: hdata.as_ref().and_then(|hdata| {
hdata.0.as_ref().and_then(|txt| { 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(), 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)) md_rows.first().and_then(|md| md.metadata_parts.get(1))
}; };
let subscriber_count = sub_part.and_then(|t| { let subscriber_count = sub_part.and_then(|t| {
util::parse_large_numstr_or_warn::<u64>(&t.text, lang, &mut warnings) util::parse_large_numstr_or_warn::<u64>(&t.text, ctx.lang, &mut warnings)
}); });
Channel { Channel {
@ -697,10 +684,10 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use crate::{ use crate::{
client::{response, MapResponse}, client::{response, MapRespCtx, MapResponse},
error::{ExtractionError, UnavailabilityReason}, error::{ExtractionError, UnavailabilityReason},
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem}, model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
param::{ChannelOrder, ChannelVideoTab, Language}, param::{ChannelOrder, ChannelVideoTab},
serializer::MapResult, serializer::MapResult,
util::tests::TESTFILES, util::tests::TESTFILES,
}; };
@ -728,7 +715,7 @@ mod tests {
let channel: response::Channel = let channel: response::Channel =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Channel<Paginator<VideoItem>>> = let map_res: MapResult<Channel<Paginator<VideoItem>>> =
channel.map_response(id, Language::En, None, None).unwrap(); channel.map_response(&MapRespCtx::test(id)).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -755,7 +742,7 @@ mod tests {
let channel: response::Channel = let channel: response::Channel =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let res: Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> = let res: Result<MapResult<Channel<Paginator<VideoItem>>>, 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 { if let Err(ExtractionError::Unavailable { reason, msg }) = res {
assert_eq!(reason, UnavailabilityReason::AgeRestricted); assert_eq!(reason, UnavailabilityReason::AgeRestricted);
assert!(msg.starts_with("Laphroaig Whisky: ")); assert!(msg.starts_with("Laphroaig Whisky: "));
@ -772,7 +759,7 @@ mod tests {
let channel: response::Channel = let channel: response::Channel =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Channel<Paginator<PlaylistItem>>> = channel let map_res: MapResult<Channel<Paginator<PlaylistItem>>> = channel
.map_response("UC2DjFE7Xf11URZqWBigcVOQ", Language::En, None, None) .map_response(&MapRespCtx::test("UC2DjFE7Xf11URZqWBigcVOQ"))
.unwrap(); .unwrap();
assert!( assert!(
@ -791,7 +778,7 @@ mod tests {
let channel: response::ChannelAbout = let channel: response::ChannelAbout =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<ChannelInfo> = channel let map_res: MapResult<ChannelInfo> = channel
.map_response("UC2DjFE7Xf11U-RZqWBigcVOQ", Language::En, None, None) .map_response(&MapRespCtx::test("UC2DjFE7Xf11U-RZqWBigcVOQ"))
.unwrap(); .unwrap();
assert!( assert!(

View file

@ -432,7 +432,7 @@ impl Default for RustyPipeBuilder {
} }
impl RustyPipeBuilder { impl RustyPipeBuilder {
/// Return a new `RustyPipeBuilder`. /// Create a new [`RustyPipeBuilder`].
/// ///
/// This is the same as [`RustyPipe::builder`] /// This is the same as [`RustyPipe::builder`]
#[must_use] #[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<RustyPipe, Error> { pub fn build(self) -> Result<RustyPipe, Error> {
self.build_with_client(ClientBuilder::new()) 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<RustyPipe, Error> { pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result<RustyPipe, Error> {
client_builder = client_builder client_builder = client_builder
.user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned())) .user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned()))
@ -1268,9 +1268,7 @@ impl RustyPipeQuery {
async fn yt_request_attempt<R: DeserializeOwned + MapResponse<M> + Debug, M>( async fn yt_request_attempt<R: DeserializeOwned + MapResponse<M> + Debug, M>(
&self, &self,
request: &Request, request: &Request,
id: &str, ctx: &MapRespCtx<'_>,
visitor_data: Option<&str>,
deobf: Option<&DeobfData>,
) -> Result<RequestResult<M>, Error> { ) -> Result<RequestResult<M>, Error> {
let response = self let response = self
.client .client
@ -1289,7 +1287,7 @@ impl RustyPipeQuery {
Err(match status { Err(match status {
StatusCode::NOT_FOUND => Error::Extraction(ExtractionError::NotFound { StatusCode::NOT_FOUND => Error::Extraction(ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: error_msg.unwrap_or("404".into()), msg: error_msg.unwrap_or("404".into()),
}), }),
StatusCode::BAD_REQUEST => { StatusCode::BAD_REQUEST => {
@ -1299,12 +1297,7 @@ impl RustyPipeQuery {
}) })
} else { } else {
match serde_json::from_str::<R>(&body) { match serde_json::from_str::<R>(&body) {
Ok(deserialized) => match deserialized.map_response( Ok(deserialized) => match deserialized.map_response(ctx) {
id,
self.opts.lang,
deobf,
self.opts.visitor_data.as_deref().or(visitor_data),
) {
Ok(mapres) => Ok(mapres), Ok(mapres) => Ok(mapres),
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
}, },
@ -1320,15 +1313,11 @@ impl RustyPipeQuery {
async fn yt_request<R: DeserializeOwned + MapResponse<M> + Debug, M>( async fn yt_request<R: DeserializeOwned + MapResponse<M> + Debug, M>(
&self, &self,
request: &Request, request: &Request,
id: &str, ctx: &MapRespCtx<'_>,
visitor_data: Option<&str>,
deobf: Option<&DeobfData>,
) -> Result<RequestResult<M>, Error> { ) -> Result<RequestResult<M>, Error> {
let mut last_resp = None; let mut last_resp = None;
for n in 0..=self.client.inner.n_http_retries { for n in 0..=self.client.inner.n_http_retries {
let resp = self let resp = self.yt_request_attempt::<R, M>(request, ctx).await?;
.yt_request_attempt::<R, M>(request, id, visitor_data, deobf)
.await?;
let err = match &resp.res { let err = match &resp.res {
Ok(_) => return Ok(resp), Ok(_) => return Ok(resp),
@ -1394,9 +1383,15 @@ impl RustyPipeQuery {
.json(body) .json(body)
.build()?; .build()?;
let req_res = self let ctx = MapRespCtx {
.yt_request::<R, M>(&request, id, visitor_data, deobf) id,
.await?; lang: self.opts.lang,
deobf,
visitor_data,
client_type: ctype,
};
let req_res = self.yt_request::<R, M>(&request, &ctx).await?;
// Uncomment to debug response text // Uncomment to debug response text
// println!("{}", &req_res.body); // println!("{}", &req_res.body);
@ -1553,6 +1548,28 @@ impl AsRef<RustyPipeQuery> 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 /// Implement this for YouTube API response structs that need to be mapped to
/// RustyPipe models. /// RustyPipe models.
trait MapResponse<T> { trait MapResponse<T> {
@ -1569,13 +1586,7 @@ trait MapResponse<T> {
/// - `lang`: Language of the request. Used for mapping localized information like dates. /// - `lang`: Language of the request. Used for mapping localized information like dates.
/// - `deobf`: Deobfuscator (if passed to the `execute_request_deobf` method) /// - `deobf`: Deobfuscator (if passed to the `execute_request_deobf` method)
/// - `visitor_data`: Visitor data option of the client /// - `visitor_data`: Visitor data option of the client
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<T>, ExtractionError>;
self,
id: &str,
lang: Language,
deobf: Option<&DeobfData>,
visitor_data: Option<&str>,
) -> Result<MapResult<T>, ExtractionError>;
} }
fn validate_country(country: Country) -> Country { fn validate_country(country: Country) -> Country {

View file

@ -14,7 +14,7 @@ use crate::{
use super::{ use super::{
response::{self, music_item::MusicListMapper, url_endpoint::PageType}, response::{self, music_item::MusicListMapper, url_endpoint::PageType},
ClientType, MapResponse, QBrowse, RustyPipeQuery, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
}; };
impl RustyPipeQuery { impl RustyPipeQuery {
@ -92,14 +92,8 @@ impl RustyPipeQuery {
} }
impl MapResponse<MusicArtist> for response::MusicArtist { impl MapResponse<MusicArtist> for response::MusicArtist {
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicArtist>, ExtractionError> {
self, let mapped = map_artist_page(self, ctx, false)?;
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<MusicArtist>, ExtractionError> {
let mapped = map_artist_page(self, id, lang, false)?;
Ok(MapResult { Ok(MapResult {
c: mapped.c.0, c: mapped.c.0,
warnings: mapped.warnings, warnings: mapped.warnings,
@ -110,19 +104,15 @@ impl MapResponse<MusicArtist> for response::MusicArtist {
impl MapResponse<(MusicArtist, bool)> for response::MusicArtist { impl MapResponse<(MusicArtist, bool)> for response::MusicArtist {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> { ) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
map_artist_page(self, id, lang, true) map_artist_page(self, ctx, true)
} }
} }
fn map_artist_page( fn map_artist_page(
res: response::MusicArtist, res: response::MusicArtist,
id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
skip_extendables: bool, skip_extendables: bool,
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> { ) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
// dbg!(&res); // dbg!(&res);
@ -138,7 +128,7 @@ fn map_artist_page(
.and_then(|pb| util::string_from_pb(pb, 3)); .and_then(|pb| util::string_from_pb(pb, 3));
if let Some(share_channel_id) = share_channel_id { 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)); return Err(ExtractionError::Redirect(share_channel_id));
} }
} }
@ -155,9 +145,9 @@ fn map_artist_page(
.unwrap_or_default(); .unwrap_or_default();
let mut mapper = MusicListMapper::with_artist( let mut mapper = MusicListMapper::with_artist(
lang, ctx.lang,
ArtistId { ArtistId {
id: Some(id.to_owned()), id: Some(ctx.id.to_owned()),
name: header.title.clone(), name: header.title.clone(),
}, },
); );
@ -264,7 +254,7 @@ fn map_artist_page(
Ok(MapResult { Ok(MapResult {
c: ( c: (
MusicArtist { MusicArtist {
id: id.to_owned(), id: ctx.id.to_owned(),
name: header.title, name: header.title,
header_image: header.thumbnail.into(), header_image: header.thumbnail.into(),
description: header.description, description: header.description,
@ -272,7 +262,7 @@ fn map_artist_page(
subscriber_count: header.subscription_button.and_then(|btn| { subscriber_count: header.subscription_button.and_then(|btn| {
util::parse_large_numstr_or_warn( util::parse_large_numstr_or_warn(
&btn.subscribe_button_renderer.subscriber_count_text, &btn.subscribe_button_renderer.subscriber_count_text,
lang, ctx.lang,
&mut mapped.warnings, &mut mapped.warnings,
) )
}), }),
@ -293,16 +283,13 @@ fn map_artist_page(
impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums { impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> { ) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
// dbg!(&self); // dbg!(&self);
let Some(header) = self.header else { let Some(header) = self.header else {
return Err(ExtractionError::NotFound { return Err(ExtractionError::NotFound {
id: id.into(), id: ctx.id.into(),
msg: "no header".into(), msg: "no header".into(),
}); });
}; };
@ -320,9 +307,9 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
.contents; .contents;
let mut mapper = MusicListMapper::with_artist( let mut mapper = MusicListMapper::with_artist(
lang, ctx.lang,
ArtistId { ArtistId {
id: Some(id.to_owned()), id: Some(ctx.id.to_owned()),
name: header.music_header_renderer.title, name: header.music_header_renderer.title,
}, },
); );
@ -347,7 +334,7 @@ mod tests {
use path_macro::path; use path_macro::path;
use rstest::rstest; use rstest::rstest;
use crate::{param::Language, util::tests::TESTFILES}; use crate::util::tests::TESTFILES;
use super::*; use super::*;
@ -369,7 +356,7 @@ mod tests {
let resp: response::MusicArtist = let resp: response::MusicArtist =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<(MusicArtist, bool)> = 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; let (mut artist, can_fetch_more) = map_res.c;
assert!( assert!(
@ -384,7 +371,7 @@ mod tests {
let resp: response::MusicArtistAlbums = let resp: response::MusicArtistAlbums =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let mut map_res: MapResult<Vec<AlbumItem>> = let mut map_res: MapResult<Vec<AlbumItem>> =
resp.map_response(id, Language::En, None, None).unwrap(); resp.map_response(&MapRespCtx::test(id)).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -405,7 +392,7 @@ mod tests {
let artist: response::MusicArtist = let artist: response::MusicArtist =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicArtist> = artist let map_res: MapResult<MusicArtist> = artist
.map_response("UClmXPfaYhXOYsNn_QUyheWQ", Language::En, None, None) .map_response(&MapRespCtx::test("UClmXPfaYhXOYsNn_QUyheWQ"))
.unwrap(); .unwrap();
assert!( assert!(
@ -424,7 +411,7 @@ mod tests {
let artist: response::MusicArtist = let artist: response::MusicArtist =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let res: Result<MapResult<MusicArtist>, ExtractionError> = let res: Result<MapResult<MusicArtist>, ExtractionError> =
artist.map_response("UCLkAepWjdylmXSltofFvsYQ", Language::En, None, None); artist.map_response(&MapRespCtx::test("UCLkAepWjdylmXSltofFvsYQ"));
let e = res.unwrap_err(); let e = res.unwrap_err();
match e { match e {

View file

@ -11,7 +11,7 @@ use crate::{
use super::{ use super::{
response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType}, response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType},
ClientType, MapResponse, RustyPipeQuery, YTContext, ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext,
}; };
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -56,13 +56,7 @@ impl RustyPipeQuery {
} }
impl MapResponse<MusicCharts> for response::MusicCharts { impl MapResponse<MusicCharts> for response::MusicCharts {
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicCharts>, ExtractionError> {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<crate::serializer::MapResult<MusicCharts>, crate::error::ExtractionError> {
let countries = self let countries = self
.framework_updates .framework_updates
.map(|fwu| { .map(|fwu| {
@ -77,9 +71,9 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
let mut top_playlist_id = None; let mut top_playlist_id = None;
let mut trending_playlist_id = None; let mut trending_playlist_id = None;
let mut mapper_top = MusicListMapper::new(lang); let mut mapper_top = MusicListMapper::new(ctx.lang);
let mut mapper_trending = MusicListMapper::new(lang); let mut mapper_trending = MusicListMapper::new(ctx.lang);
let mut mapper_other = MusicListMapper::new(lang); let mut mapper_other = MusicListMapper::new(ctx.lang);
self.contents self.contents
.single_column_browse_results_renderer .single_column_browse_results_renderer
@ -151,7 +145,6 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use super::*; use super::*;
use crate::param::Language;
#[rstest] #[rstest]
#[case::default("global")] #[case::default("global")]
@ -163,8 +156,7 @@ mod tests {
let charts: response::MusicCharts = let charts: response::MusicCharts =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicCharts> = let map_res: MapResult<MusicCharts> = charts.map_response(&MapRespCtx::test("")).unwrap();
charts.map_response("", Language::En, None, None).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -8,7 +8,6 @@ use crate::{
paginator::{ContinuationEndpoint, Paginator}, paginator::{ContinuationEndpoint, Paginator},
ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem, ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem,
}, },
param::Language,
serializer::MapResult, serializer::MapResult,
}; };
@ -17,7 +16,7 @@ use super::{
self, self,
music_item::{map_queue_item, MusicListMapper}, music_item::{map_queue_item, MusicListMapper},
}, },
ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, YTContext,
}; };
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -170,10 +169,7 @@ impl RustyPipeQuery {
impl MapResponse<TrackDetails> for response::MusicDetails { impl MapResponse<TrackDetails> for response::MusicDetails {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<TrackDetails>, ExtractionError> { ) -> Result<MapResult<TrackDetails>, ExtractionError> {
let tabs = self let tabs = self
.contents .contents
@ -211,7 +207,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
} }
let content = content.ok_or_else(|| ExtractionError::NotFound { let content = content.ok_or_else(|| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: "no content".into(), msg: "no content".into(),
})?; })?;
let track_item = content let track_item = content
@ -225,7 +221,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
response::music_item::PlaylistPanelVideo::None => None, response::music_item::PlaylistPanelVideo::None => None,
}) })
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?; .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; let mut warnings = content.contents.warnings;
warnings.append(&mut track.warnings); warnings.append(&mut track.warnings);
@ -244,10 +240,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
impl MapResponse<Paginator<TrackItem>> for response::MusicDetails { impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> { ) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
let tabs = self let tabs = self
.contents .contents
@ -260,7 +253,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
.into_iter() .into_iter()
.find_map(|t| t.tab_renderer.content) .find_map(|t| t.tab_renderer.content)
.ok_or_else(|| ExtractionError::NotFound { .ok_or_else(|| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: "no content".into(), msg: "no content".into(),
})? })?
.music_queue_renderer .music_queue_renderer
@ -275,7 +268,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
.into_iter() .into_iter()
.filter_map(|item| match item { .filter_map(|item| match item {
response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(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); warnings.append(&mut track.warnings);
Some(track.c) Some(track.c)
} }
@ -297,18 +290,12 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
} }
impl MapResponse<Lyrics> for response::MusicLyrics { impl MapResponse<Lyrics> for response::MusicLyrics {
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Lyrics>, ExtractionError> {
self,
id: &str,
_lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Lyrics>, ExtractionError> {
let lyrics = self let lyrics = self
.contents .contents
.into_res() .into_res()
.map_err(|msg| ExtractionError::NotFound { .map_err(|msg| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: msg.into(), msg: msg.into(),
})? })?
.into_iter() .into_iter()
@ -328,16 +315,13 @@ impl MapResponse<Lyrics> for response::MusicLyrics {
impl MapResponse<MusicRelated> for response::MusicRelated { impl MapResponse<MusicRelated> for response::MusicRelated {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<MusicRelated>, ExtractionError> { ) -> Result<MapResult<MusicRelated>, ExtractionError> {
let contents = self let contents = self
.contents .contents
.into_res() .into_res()
.map_err(|msg| ExtractionError::NotFound { .map_err(|msg| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: msg.into(), msg: msg.into(),
})?; })?;
@ -362,10 +346,10 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
_ => None, _ => None,
}); });
let mut mapper_tracks = MusicListMapper::new(lang); let mut mapper_tracks = MusicListMapper::new(ctx.lang);
let mut mapper = match artist_id { let mut mapper = match artist_id {
Some(artist_id) => MusicListMapper::with_artist(lang, artist_id), Some(artist_id) => MusicListMapper::with_artist(ctx.lang, artist_id),
None => MusicListMapper::new(lang), None => MusicListMapper::new(ctx.lang),
}; };
let mut sections = contents.into_iter(); let mut sections = contents.into_iter();
@ -412,7 +396,7 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use super::*; use super::*;
use crate::{model, param::Language, util::tests::TESTFILES}; use crate::{model, util::tests::TESTFILES};
#[rstest] #[rstest]
#[case::mv("mv", "ZeerrnuLi5E")] #[case::mv("mv", "ZeerrnuLi5E")]
@ -424,7 +408,7 @@ mod tests {
let details: response::MusicDetails = let details: response::MusicDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::TrackDetails> = let map_res: MapResult<model::TrackDetails> =
details.map_response(id, Language::En, None, None).unwrap(); details.map_response(&MapRespCtx::test(id)).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -444,7 +428,7 @@ mod tests {
let radio: response::MusicDetails = let radio: response::MusicDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<TrackItem>> = let map_res: MapResult<Paginator<TrackItem>> =
radio.map_response(id, Language::En, None, None).unwrap(); radio.map_response(&MapRespCtx::test(id)).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -461,7 +445,7 @@ mod tests {
let lyrics: response::MusicLyrics = let lyrics: response::MusicLyrics =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Lyrics> = lyrics.map_response("", Language::En, None, None).unwrap(); let map_res: MapResult<Lyrics> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -478,8 +462,7 @@ mod tests {
let lyrics: response::MusicRelated = let lyrics: response::MusicRelated =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicRelated> = let map_res: MapResult<MusicRelated> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
lyrics.map_response("", Language::En, None, None).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -8,7 +8,7 @@ use crate::{
use super::{ use super::{
response::{self, music_item::MusicListMapper, url_endpoint::NavigationEndpoint}, response::{self, music_item::MusicListMapper, url_endpoint::NavigationEndpoint},
ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
}; };
impl RustyPipeQuery { impl RustyPipeQuery {
@ -59,11 +59,8 @@ impl RustyPipeQuery {
impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres { impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
fn map_response( fn map_response(
self, self,
_id: &str, _ctx: &MapRespCtx<'_>,
_lang: crate::param::Language, ) -> Result<MapResult<Vec<MusicGenreItem>>, ExtractionError> {
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
let content = self let content = self
.contents .contents
.single_column_browse_results_renderer .single_column_browse_results_renderer
@ -111,13 +108,7 @@ impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
} }
impl MapResponse<MusicGenre> for response::MusicGenre { impl MapResponse<MusicGenre> for response::MusicGenre {
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicGenre>, ExtractionError> {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
// dbg!(&self); // dbg!(&self);
let content = self let content = self
@ -179,7 +170,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
_ => return None, _ => return None,
}; };
let mut mapper = MusicListMapper::new(lang); let mut mapper = MusicListMapper::new(ctx.lang);
mapper.map_response(items); mapper.map_response(items);
let mut mapped = mapper.conv_items(); let mut mapped = mapper.conv_items();
warnings.append(&mut mapped.warnings); warnings.append(&mut mapped.warnings);
@ -194,7 +185,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
Ok(MapResult { Ok(MapResult {
c: MusicGenre { c: MusicGenre {
id: id.to_owned(), id: ctx.id.to_owned(),
name: self.header.music_header_renderer.title, name: self.header.music_header_renderer.title,
sections, sections,
}, },
@ -211,7 +202,7 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use super::*; use super::*;
use crate::{model, param::Language, util::tests::TESTFILES}; use crate::{model, util::tests::TESTFILES};
#[test] #[test]
fn map_music_genres() { fn map_music_genres() {
@ -221,7 +212,7 @@ mod tests {
let playlist: response::MusicGenres = let playlist: response::MusicGenres =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<model::MusicGenreItem>> = let map_res: MapResult<Vec<model::MusicGenreItem>> =
playlist.map_response("", Language::En, None, None).unwrap(); playlist.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -241,7 +232,7 @@ mod tests {
let playlist: response::MusicGenre = let playlist: response::MusicGenre =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::MusicGenre> = let map_res: MapResult<model::MusicGenre> =
playlist.map_response(id, Language::En, None, None).unwrap(); playlist.map_response(&MapRespCtx::test(id)).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -4,9 +4,10 @@ use crate::{
client::response::music_item::MusicListMapper, client::response::music_item::MusicListMapper,
error::{Error, ExtractionError}, error::{Error, ExtractionError},
model::{traits::FromYtItem, AlbumItem, TrackItem}, model::{traits::FromYtItem, AlbumItem, TrackItem},
serializer::MapResult,
}; };
use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery}; use super::{response, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery};
impl RustyPipeQuery { impl RustyPipeQuery {
/// Get the new albums that were released on YouTube Music /// Get the new albums that were released on YouTube Music
@ -49,13 +50,7 @@ impl RustyPipeQuery {
} }
impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew { impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Vec<T>>, ExtractionError> {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<crate::serializer::MapResult<Vec<T>>, ExtractionError> {
let items = self let items = self
.contents .contents
.single_column_browse_results_renderer .single_column_browse_results_renderer
@ -73,7 +68,7 @@ impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
.grid_renderer .grid_renderer
.items; .items;
let mut mapper = MusicListMapper::new(lang); let mut mapper = MusicListMapper::new(ctx.lang);
mapper.map_response(items); mapper.map_response(items);
Ok(mapper.conv_items()) Ok(mapper.conv_items())
@ -88,7 +83,7 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use super::*; use super::*;
use crate::{param::Language, serializer::MapResult, util::tests::TESTFILES}; use crate::{serializer::MapResult, util::tests::TESTFILES};
#[rstest] #[rstest]
#[case::default("default")] #[case::default("default")]
@ -98,9 +93,8 @@ mod tests {
let new_albums: response::MusicNew = let new_albums: response::MusicNew =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<AlbumItem>> = new_albums let map_res: MapResult<Vec<AlbumItem>> =
.map_response("", Language::En, None, None) new_albums.map_response(&MapRespCtx::test("")).unwrap();
.unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -119,9 +113,8 @@ mod tests {
let new_videos: response::MusicNew = let new_videos: response::MusicNew =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<TrackItem>> = new_videos let map_res: MapResult<Vec<TrackItem>> =
.map_response("", Language::En, None, None) new_videos.map_response(&MapRespCtx::test("")).unwrap();
.unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -17,7 +17,7 @@ use super::{
self, self,
music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper}, music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper},
}, },
ClientType, MapResponse, QBrowse, RustyPipeQuery, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
}; };
impl RustyPipeQuery { impl RustyPipeQuery {
@ -138,10 +138,7 @@ impl RustyPipeQuery {
impl MapResponse<MusicPlaylist> for response::MusicPlaylist { impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<MusicPlaylist>, ExtractionError> { ) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
// dbg!(&self); // dbg!(&self);
@ -186,14 +183,15 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
)))?; )))?;
if let Some(playlist_id) = shelf.playlist_id { if let Some(playlist_id) = shelf.playlist_id {
if playlist_id != id { if playlist_id != ctx.id {
return Err(ExtractionError::WrongResult(format!( 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); mapper.map_response(shelf.contents);
let map_res = mapper.conv_items(); let map_res = mapper.conv_items();
@ -273,7 +271,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
Ok(MapResult { Ok(MapResult {
c: MusicPlaylist { c: MusicPlaylist {
id: id.to_owned(), id: ctx.id.to_owned(),
name, name,
thumbnail, thumbnail,
channel, channel,
@ -284,14 +282,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
track_count, track_count,
map_res.c, map_res.c,
ctoken, ctoken,
vdata.map(str::to_owned), ctx.visitor_data.map(str::to_owned),
ContinuationEndpoint::MusicBrowse, ContinuationEndpoint::MusicBrowse,
), ),
related_playlists: Paginator::new_ext( related_playlists: Paginator::new_ext(
None, None,
Vec::new(), Vec::new(),
related_ctoken, related_ctoken,
vdata.map(str::to_owned), ctx.visitor_data.map(str::to_owned),
ContinuationEndpoint::MusicBrowse, ContinuationEndpoint::MusicBrowse,
), ),
}, },
@ -301,13 +299,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
} }
impl MapResponse<MusicAlbum> for response::MusicPlaylist { impl MapResponse<MusicAlbum> for response::MusicPlaylist {
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicAlbum>, ExtractionError> {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
// dbg!(&self); // dbg!(&self);
let (header, sections) = match self.contents { let (header, sections) = match self.contents {
@ -401,7 +393,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
.map(|part| part.to_string()) .map(|part| part.to_string())
.unwrap_or_default(); .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()); let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());
fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> { fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> {
@ -448,11 +440,11 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone())); let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone()));
let mut mapper = MusicListMapper::with_album( let mut mapper = MusicListMapper::with_album(
lang, ctx.lang,
artists.clone(), artists.clone(),
by_va, by_va,
AlbumId { AlbumId {
id: id.to_owned(), id: ctx.id.to_owned(),
name: header.title.clone(), name: header.title.clone(),
}, },
); );
@ -460,7 +452,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
let tracks_res = mapper.conv_items(); let tracks_res = mapper.conv_items();
let mut warnings = tracks_res.warnings; 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 { if let Some(res) = album_variants {
variants_mapper.map_response(res); variants_mapper.map_response(res);
} }
@ -469,7 +461,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
Ok(MapResult { Ok(MapResult {
c: MusicAlbum { c: MusicAlbum {
id: id.to_owned(), id: ctx.id.to_owned(),
playlist_id, playlist_id,
name: header.title, name: header.title,
cover: header.thumbnail.into(), cover: header.thumbnail.into(),
@ -497,7 +489,7 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use super::*; use super::*;
use crate::{model, param::Language, util::tests::TESTFILES}; use crate::{model, util::tests::TESTFILES};
#[rstest] #[rstest]
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")] #[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
@ -512,7 +504,7 @@ mod tests {
let playlist: response::MusicPlaylist = let playlist: response::MusicPlaylist =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::MusicPlaylist> = let map_res: MapResult<model::MusicPlaylist> =
playlist.map_response(id, Language::En, None, None).unwrap(); playlist.map_response(&MapRespCtx::test(id)).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -539,7 +531,7 @@ mod tests {
let playlist: response::MusicPlaylist = let playlist: response::MusicPlaylist =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::MusicAlbum> = let map_res: MapResult<model::MusicAlbum> =
playlist.map_response(id, Language::En, None, None).unwrap(); playlist.map_response(&MapRespCtx::test(id)).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -15,7 +15,7 @@ use crate::{
serializer::MapResult, serializer::MapResult,
}; };
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext}; use super::{response, ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -152,10 +152,7 @@ impl RustyPipeQuery {
impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch { impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch {
fn map_response( fn map_response(
self, self,
_id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<MusicSearchResult<T>>, ExtractionError> { ) -> Result<MapResult<MusicSearchResult<T>>, ExtractionError> {
// dbg!(&self); // dbg!(&self);
@ -171,7 +168,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
let mut corrected_query = None; let mut corrected_query = None;
let mut ctoken = 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 { sections.into_iter().for_each(|section| match section {
response::music_search::ItemSection::MusicShelfRenderer(shelf) => { response::music_search::ItemSection::MusicShelfRenderer(shelf) => {
@ -199,7 +196,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
None, None,
map_res.c, map_res.c,
ctoken, ctoken,
vdata.map(str::to_owned), ctx.visitor_data.map(str::to_owned),
ContinuationEndpoint::MusicSearch, ContinuationEndpoint::MusicSearch,
), ),
corrected_query, corrected_query,
@ -212,12 +209,9 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion { impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
fn map_response( fn map_response(
self, self,
_id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> { ) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
let mut mapper = MusicListMapper::new_search_suggest(lang); let mut mapper = MusicListMapper::new_search_suggest(ctx.lang);
let mut terms = Vec::new(); let mut terms = Vec::new();
for section in self.contents { for section in self.contents {
@ -256,12 +250,11 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use crate::{ use crate::{
client::{response, MapResponse}, client::{response, MapRespCtx, MapResponse},
model::{ model::{
AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult, AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult,
MusicSearchSuggestion, TrackItem, MusicSearchSuggestion, TrackItem,
}, },
param::Language,
serializer::MapResult, serializer::MapResult,
util::tests::TESTFILES, util::tests::TESTFILES,
}; };
@ -278,7 +271,7 @@ mod tests {
let search: response::MusicSearch = let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<MusicItem>> = let map_res: MapResult<MusicSearchResult<MusicItem>> =
search.map_response("", Language::En, None, None).unwrap(); search.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -301,7 +294,7 @@ mod tests {
let search: response::MusicSearch = let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<TrackItem>> = let map_res: MapResult<MusicSearchResult<TrackItem>> =
search.map_response("", Language::En, None, None).unwrap(); search.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -320,7 +313,7 @@ mod tests {
let search: response::MusicSearch = let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<AlbumItem>> = let map_res: MapResult<MusicSearchResult<AlbumItem>> =
search.map_response("", Language::En, None, None).unwrap(); search.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -339,7 +332,7 @@ mod tests {
let search: response::MusicSearch = let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<ArtistItem>> = let map_res: MapResult<MusicSearchResult<ArtistItem>> =
search.map_response("", Language::En, None, None).unwrap(); search.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -360,7 +353,7 @@ mod tests {
let search: response::MusicSearch = let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<MusicPlaylistItem>> = let map_res: MapResult<MusicSearchResult<MusicPlaylistItem>> =
search.map_response("", Language::En, None, None).unwrap(); search.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -380,9 +373,8 @@ mod tests {
let suggestion: response::MusicSearchSuggestion = let suggestion: response::MusicSearchSuggestion =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchSuggestion> = suggestion let map_res: MapResult<MusicSearchSuggestion> =
.map_response("", Language::En, None, None) suggestion.map_response(&MapRespCtx::test("")).unwrap();
.unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -10,7 +10,7 @@ use crate::model::{
use crate::serializer::MapResult; use crate::serializer::MapResult;
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo}; 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 { impl RustyPipeQuery {
/// Get more YouTube items from the given continuation token and endpoint /// Get more YouTube items from the given continuation token and endpoint
@ -103,10 +103,7 @@ fn map_ytm_paginator<T: FromYtItem>(
impl MapResponse<Paginator<YouTubeItem>> for response::Continuation { impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
fn map_response( fn map_response(
self, self,
_id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> { ) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
let items = self let items = self
.on_response_received_actions .on_response_received_actions
@ -126,7 +123,7 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
}) })
.unwrap_or_default(); .unwrap_or_default();
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang); let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
mapper.map_response(items); mapper.map_response(items);
Ok(MapResult { Ok(MapResult {
@ -139,12 +136,9 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation { impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
fn map_response( fn map_response(
self, self,
_id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> { ) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
let mut mapper = MusicListMapper::new(lang); let mut mapper = MusicListMapper::new(ctx.lang);
let mut continuations = Vec::new(); let mut continuations = Vec::new();
match self.continuation_contents { match self.continuation_contents {
@ -173,7 +167,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
mapper.add_warnings(&mut panel.contents.warnings); mapper.add_warnings(&mut panel.contents.warnings);
panel.contents.c.into_iter().for_each(|item| { panel.contents.c.into_iter().for_each(|item| {
if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = 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_item(MusicItem::Track(track.c));
mapper.add_warnings(&mut track.warnings); mapper.add_warnings(&mut track.warnings);
} }
@ -356,7 +350,6 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem}, model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem},
param::Language,
util::tests::TESTFILES, util::tests::TESTFILES,
}; };
@ -371,7 +364,7 @@ mod tests {
let items: response::Continuation = let items: response::Continuation =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<YouTubeItem>> = let map_res: MapResult<Paginator<YouTubeItem>> =
items.map_response("", Language::En, None, None).unwrap(); items.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -393,7 +386,7 @@ mod tests {
let items: response::Continuation = let items: response::Continuation =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<YouTubeItem>> = let map_res: MapResult<Paginator<YouTubeItem>> =
items.map_response("", Language::En, None, None).unwrap(); items.map_response(&MapRespCtx::test("")).unwrap();
let paginator: Paginator<VideoItem> = let paginator: Paginator<VideoItem> =
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse); map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
@ -416,7 +409,7 @@ mod tests {
let items: response::Continuation = let items: response::Continuation =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<YouTubeItem>> = let map_res: MapResult<Paginator<YouTubeItem>> =
items.map_response("", Language::En, None, None).unwrap(); items.map_response(&MapRespCtx::test("")).unwrap();
let paginator: Paginator<PlaylistItem> = let paginator: Paginator<PlaylistItem> =
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse); map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
@ -439,7 +432,7 @@ mod tests {
let items: response::MusicContinuation = let items: response::MusicContinuation =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<MusicItem>> = let map_res: MapResult<Paginator<MusicItem>> =
items.map_response("", Language::En, None, None).unwrap(); items.map_response(&MapRespCtx::test("")).unwrap();
let paginator: Paginator<TrackItem> = let paginator: Paginator<TrackItem> =
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse); map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
@ -460,7 +453,7 @@ mod tests {
let items: response::MusicContinuation = let items: response::MusicContinuation =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<MusicItem>> = let map_res: MapResult<Paginator<MusicItem>> =
items.map_response("", Language::En, None, None).unwrap(); items.map_response(&MapRespCtx::test("")).unwrap();
let paginator: Paginator<MusicPlaylistItem> = let paginator: Paginator<MusicPlaylistItem> =
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse); map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);

View file

@ -16,13 +16,12 @@ use crate::{
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset, traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset,
Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream, Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
}, },
param::Language,
util, util,
}; };
use super::{ use super::{
response::{self, player}, response::{self, player},
ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext,
}; };
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -149,12 +148,9 @@ impl RustyPipeQuery {
impl MapResponse<VideoPlayer> for response::Player { impl MapResponse<VideoPlayer> for response::Player {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
_lang: Language,
deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<super::MapResult<VideoPlayer>, ExtractionError> { ) -> Result<super::MapResult<VideoPlayer>, ExtractionError> {
let deobf = Deobfuscator::new(deobf.unwrap())?; let deobf = Deobfuscator::new(ctx.deobf.unwrap())?;
let mut warnings = vec![]; let mut warnings = vec![];
// Check playability status // Check playability status
@ -235,10 +231,10 @@ impl MapResponse<VideoPlayer> for response::Player {
"no video details", "no video details",
)))?; )))?;
if video_details.video_id != id { if video_details.video_id != ctx.id {
return Err(ExtractionError::WrongResult(format!( return Err(ExtractionError::WrongResult(format!(
"video id {}, expected {}", "video id {}, expected {}",
video_details.video_id, id video_details.video_id, ctx.id
))); )));
} }
@ -375,10 +371,11 @@ impl MapResponse<VideoPlayer> for response::Player {
hls_manifest_url: streaming_data.hls_manifest_url, hls_manifest_url: streaming_data.hls_manifest_url,
dash_manifest_url: streaming_data.dash_manifest_url, dash_manifest_url: streaming_data.dash_manifest_url,
preview_frames, preview_frames,
client_type: ctx.client_type,
visitor_data: self visitor_data: self
.response_context .response_context
.visitor_data .visitor_data
.or_else(|| vdata.map(str::to_owned)), .or_else(|| ctx.visitor_data.map(str::to_owned)),
}, },
warnings, warnings,
}) })
@ -657,7 +654,7 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use super::*; use super::*;
use crate::{deobfuscate::DeobfData, util::tests::TESTFILES}; use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES};
static DEOBF_DATA: Lazy<DeobfData> = Lazy::new(|| { static DEOBF_DATA: Lazy<DeobfData> = Lazy::new(|| {
DeobfData { DeobfData {
@ -669,18 +666,27 @@ mod tests {
}); });
#[rstest] #[rstest]
#[case::desktop("desktop")] #[case::desktop(ClientType::Desktop)]
#[case::desktop_music("desktopmusic")] #[case::desktop_music(ClientType::DesktopMusic)]
#[case::tv_html5_embed("tvhtml5embed")] #[case::tv_html5_embed(ClientType::TvHtml5Embed)]
#[case::android("android")] #[case::android(ClientType::Android)]
#[case::ios("ios")] #[case::ios(ClientType::Ios)]
fn map_player_data(#[case] name: &str) { 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_path = path!(*TESTFILES / "player" / format!("{name}_video.json"));
let json_file = File::open(json_path).unwrap(); let json_file = File::open(json_path).unwrap();
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = resp 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(); .unwrap();
assert!( assert!(

View file

@ -13,7 +13,7 @@ use crate::{
util::{self, timeago, TryRemove}, util::{self, timeago, TryRemove},
}; };
use super::{response, ClientType, MapResponse, MapResult, QBrowse, RustyPipeQuery}; use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, QBrowse, RustyPipeQuery};
impl RustyPipeQuery { impl RustyPipeQuery {
/// Get a YouTube playlist /// Get a YouTube playlist
@ -47,15 +47,9 @@ impl RustyPipeQuery {
} }
impl MapResponse<Playlist> for response::Playlist { impl MapResponse<Playlist> for response::Playlist {
fn map_response( fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Playlist>, ExtractionError> {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<Playlist>, ExtractionError> {
let (Some(contents), Some(header)) = (self.contents, self.header) else { 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 let video_items = contents
@ -85,7 +79,7 @@ impl MapResponse<Playlist> for response::Playlist {
.playlist_video_list_renderer .playlist_video_list_renderer
.contents; .contents;
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang); let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
mapper.map_response(video_items); mapper.map_response(video_items);
let (description, thumbnails, last_update_txt) = match self.sidebar { let (description, thumbnails, last_update_txt) = match self.sidebar {
@ -144,9 +138,10 @@ impl MapResponse<Playlist> for response::Playlist {
}; };
let playlist_id = header.playlist_header_renderer.playlist_id; let playlist_id = header.playlist_header_renderer.playlist_id;
if playlist_id != id { if playlist_id != ctx.id {
return Err(ExtractionError::WrongResult(format!( 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<Playlist> for response::Playlist {
.and_then(|link| ChannelId::try_from(link).ok()); .and_then(|link| ChannelId::try_from(link).ok());
let last_update = last_update_txt.as_ref().and_then(|txt| { 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) .map(OffsetDateTime::date)
}); });
@ -177,7 +172,7 @@ impl MapResponse<Playlist> for response::Playlist {
Some(n_videos), Some(n_videos),
mapper.items, mapper.items,
mapper.ctoken, mapper.ctoken,
vdata.map(str::to_owned), ctx.visitor_data.map(str::to_owned),
ContinuationEndpoint::Browse, ContinuationEndpoint::Browse,
), ),
video_count: n_videos, video_count: n_videos,
@ -189,7 +184,7 @@ impl MapResponse<Playlist> for response::Playlist {
visitor_data: self visitor_data: self
.response_context .response_context
.visitor_data .visitor_data
.or_else(|| vdata.map(str::to_owned)), .or_else(|| ctx.visitor_data.map(str::to_owned)),
}, },
warnings: mapper.warnings, warnings: mapper.warnings,
}) })
@ -203,7 +198,7 @@ mod tests {
use path_macro::path; use path_macro::path;
use rstest::rstest; use rstest::rstest;
use crate::{param::Language, util::tests::TESTFILES}; use crate::util::tests::TESTFILES;
use super::*; use super::*;
@ -218,7 +213,7 @@ mod tests {
let playlist: response::Playlist = let playlist: response::Playlist =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); 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!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -12,7 +12,7 @@ use crate::{
param::search_filter::SearchFilter, param::search_filter::SearchFilter,
}; };
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext}; use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -103,10 +103,7 @@ impl RustyPipeQuery {
impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search { impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
fn map_response( fn map_response(
self, self,
_id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<SearchResult<T>>, ExtractionError> { ) -> Result<MapResult<SearchResult<T>>, ExtractionError> {
let items = self let items = self
.contents .contents
@ -115,7 +112,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
.section_list_renderer .section_list_renderer
.contents; .contents;
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang); let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
mapper.map_response(items); mapper.map_response(items);
Ok(MapResult { Ok(MapResult {
@ -135,7 +132,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
visitor_data: self visitor_data: self
.response_context .response_context
.visitor_data .visitor_data
.or_else(|| vdata.map(str::to_owned)), .or_else(|| ctx.visitor_data.map(str::to_owned)),
}, },
warnings: mapper.warnings, warnings: mapper.warnings,
}) })
@ -150,9 +147,8 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use crate::{ use crate::{
client::{response, MapResponse}, client::{response, MapRespCtx, MapResponse},
model::{SearchResult, YouTubeItem}, model::{SearchResult, YouTubeItem},
param::Language,
serializer::MapResult, serializer::MapResult,
util::tests::TESTFILES, util::tests::TESTFILES,
}; };
@ -168,7 +164,7 @@ mod tests {
let search: response::Search = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let search: response::Search = serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<SearchResult<YouTubeItem>> = let map_res: MapResult<SearchResult<YouTubeItem>> =
search.map_response("", Language::En, None, None).unwrap(); search.map_response(&MapRespCtx::test("")).unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -466,5 +466,6 @@ VideoPlayer(
frames_per_page_y: 5, frames_per_page_y: 5,
), ),
], ],
client_type: android,
visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"), visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"),
) )

View file

@ -581,5 +581,6 @@ VideoPlayer(
frames_per_page_y: 5, frames_per_page_y: 5,
), ),
], ],
client_type: desktop,
visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"), visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"),
) )

View file

@ -405,5 +405,6 @@ VideoPlayer(
frames_per_page_y: 5, frames_per_page_y: 5,
), ),
], ],
client_type: desktop_music,
visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"), visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"),
) )

View file

@ -196,5 +196,6 @@ VideoPlayer(
frames_per_page_y: 5, frames_per_page_y: 5,
), ),
], ],
client_type: ios,
visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"), visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"),
) )

View file

@ -581,5 +581,6 @@ VideoPlayer(
frames_per_page_y: 5, frames_per_page_y: 5,
), ),
], ],
client_type: tv_html5_embed,
visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"), visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"),
) )

View file

@ -10,7 +10,9 @@ use crate::{
serializer::MapResult, serializer::MapResult,
}; };
use super::{response, ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery}; use super::{
response, ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
};
impl RustyPipeQuery { impl RustyPipeQuery {
/// Get the videos from the YouTube startpage /// Get the videos from the YouTube startpage
@ -56,10 +58,7 @@ impl RustyPipeQuery {
impl MapResponse<Paginator<VideoItem>> for response::Startpage { impl MapResponse<Paginator<VideoItem>> for response::Startpage {
fn map_response( fn map_response(
self, self,
_id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> { ) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> {
let grid = self let grid = self
.contents .contents
@ -75,10 +74,10 @@ impl MapResponse<Paginator<VideoItem>> for response::Startpage {
Ok(map_startpage_videos( Ok(map_startpage_videos(
grid, grid,
lang, ctx.lang,
self.response_context self.response_context
.visitor_data .visitor_data
.or_else(|| vdata.map(str::to_owned)), .or_else(|| ctx.visitor_data.map(str::to_owned)),
)) ))
} }
} }
@ -86,10 +85,7 @@ impl MapResponse<Paginator<VideoItem>> for response::Startpage {
impl MapResponse<Vec<VideoItem>> for response::Trending { impl MapResponse<Vec<VideoItem>> for response::Trending {
fn map_response( fn map_response(
self, self,
_id: &str, ctx: &MapRespCtx<'_>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> { ) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> {
let items = self let items = self
.contents .contents
@ -103,7 +99,7 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
.section_list_renderer .section_list_renderer
.contents; .contents;
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang); let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
mapper.map_response(items); mapper.map_response(items);
Ok(MapResult { Ok(MapResult {
@ -141,9 +137,8 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use crate::{ use crate::{
client::{response, MapResponse}, client::{response, MapRespCtx, MapResponse},
model::{paginator::Paginator, VideoItem}, model::{paginator::Paginator, VideoItem},
param::Language,
serializer::MapResult, serializer::MapResult,
util::tests::TESTFILES, util::tests::TESTFILES,
}; };
@ -155,9 +150,8 @@ mod tests {
let startpage: response::Startpage = let startpage: response::Startpage =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<VideoItem>> = startpage let map_res: MapResult<Paginator<VideoItem>> =
.map_response("", Language::En, None, None) startpage.map_response(&MapRespCtx::test("")).unwrap();
.unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -179,9 +173,8 @@ mod tests {
let startpage: response::Trending = let startpage: response::Trending =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<VideoItem>> = startpage let map_res: MapResult<Vec<VideoItem>> =
.map_response("", Language::En, None, None) startpage.map_response(&MapRespCtx::test("")).unwrap();
.unwrap();
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -5,14 +5,13 @@ use serde::Serialize;
use crate::{ use crate::{
error::{Error, ExtractionError}, error::{Error, ExtractionError},
model::UrlTarget, model::UrlTarget,
param::Language,
serializer::MapResult, serializer::MapResult,
util, util,
}; };
use super::{ use super::{
response::{self, url_endpoint::NavigationEndpoint}, response::{self, url_endpoint::NavigationEndpoint},
ClientType, MapResponse, RustyPipeQuery, YTContext, ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext,
}; };
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -325,13 +324,7 @@ impl RustyPipeQuery {
} }
impl MapResponse<UrlTarget> for response::ResolvedUrl { impl MapResponse<UrlTarget> for response::ResolvedUrl {
fn map_response( fn map_response(self, _ctx: &MapRespCtx<'_>) -> Result<MapResult<UrlTarget>, ExtractionError> {
self,
_id: &str,
_lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<UrlTarget>, ExtractionError> {
let pt = self.endpoint.page_type(); let pt = self.endpoint.page_type();
if let NavigationEndpoint::Browse { if let NavigationEndpoint::Browse {
browse_endpoint, .. browse_endpoint, ..

View file

@ -15,7 +15,7 @@ use crate::{
use super::{ use super::{
response::{self, video_details::Payload, IconType}, response::{self, video_details::Payload, IconType},
ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext,
}; };
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -89,28 +89,26 @@ impl RustyPipeQuery {
impl MapResponse<VideoDetails> for response::VideoDetails { impl MapResponse<VideoDetails> for response::VideoDetails {
fn map_response( fn map_response(
self, self,
id: &str, ctx: &MapRespCtx<'_>,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<VideoDetails>, ExtractionError> { ) -> Result<MapResult<VideoDetails>, ExtractionError> {
let mut warnings = Vec::new(); let mut warnings = Vec::new();
let contents = self.contents.ok_or_else(|| ExtractionError::NotFound { let contents = self.contents.ok_or_else(|| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: "no content".into(), msg: "no content".into(),
})?; })?;
let current_video_endpoint = let current_video_endpoint =
self.current_video_endpoint self.current_video_endpoint
.ok_or_else(|| ExtractionError::NotFound { .ok_or_else(|| ExtractionError::NotFound {
id: id.to_owned(), id: ctx.id.to_owned(),
msg: "no current_video_endpoint".into(), msg: "no current_video_endpoint".into(),
})?; })?;
let video_id = current_video_endpoint.watch_endpoint.video_id; let video_id = current_video_endpoint.watch_endpoint.video_id;
if id != video_id { if ctx.id != video_id {
return Err(ExtractionError::WrongResult(format!( 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<VideoDetails> for response::VideoDetails {
.results .results
.contents .contents
.ok_or_else(|| ExtractionError::NotFound { .ok_or_else(|| ExtractionError::NotFound {
id: id.into(), id: ctx.id.into(),
msg: "no primary_results".into(), msg: "no primary_results".into(),
})?; })?;
warnings.append(&mut primary_results.warnings); warnings.append(&mut primary_results.warnings);
@ -189,7 +187,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
// so we ignore parse errors here for now // so we ignore parse errors here for now
like_text.and_then(|txt| util::parse_numeric(&txt).ok()), like_text.and_then(|txt| util::parse_numeric(&txt).ok()),
date_text.as_deref().and_then(|txt| { 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, date_text,
view_count view_count
@ -207,7 +205,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
let comment_count = comment_count_section.and_then(|s| { let comment_count = comment_count_section.and_then(|s| {
util::parse_large_numstr_or_warn::<u64>( util::parse_large_numstr_or_warn::<u64>(
&s.comments_entry_point_header_renderer.comment_count, &s.comments_entry_point_header_renderer.comment_count,
lang, ctx.lang,
&mut warnings, &mut warnings,
) )
}); });
@ -275,7 +273,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
let visitor_data = self let visitor_data = self
.response_context .response_context
.visitor_data .visitor_data
.or_else(|| vdata.map(str::to_owned)); .or_else(|| ctx.visitor_data.map(str::to_owned));
let recommended = contents let recommended = contents
.two_column_watch_next_results .two_column_watch_next_results
.secondary_results .secondary_results
@ -285,7 +283,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
r, r,
sr.secondary_results.continuations, sr.secondary_results.continuations,
visitor_data.clone(), visitor_data.clone(),
lang, ctx.lang,
); );
warnings.append(&mut res.warnings); warnings.append(&mut res.warnings);
res.c res.c
@ -350,7 +348,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
avatar: owner.thumbnail.into(), avatar: owner.thumbnail.into(),
verification: owner.badges.into(), verification: owner.badges.into(),
subscriber_count: owner.subscriber_count_text.and_then(|txt| { 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, view_count,
@ -385,10 +383,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
impl MapResponse<Paginator<Comment>> for response::VideoComments { impl MapResponse<Paginator<Comment>> for response::VideoComments {
fn map_response( fn map_response(
self, self,
_id: &str, ctx: &MapRespCtx<'_>,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> { ) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
let received_endpoints = self.on_response_received_endpoints; let received_endpoints = self.on_response_received_endpoints;
let mut warnings = Vec::new(); let mut warnings = Vec::new();
@ -415,7 +410,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
comment.comment_renderer, comment.comment_renderer,
Some(thread.replies), Some(thread.replies),
thread.rendering_priority, thread.rendering_priority,
lang, ctx.lang,
&mut warnings, &mut warnings,
)); ));
} else if let Some(vm) = thread.comment_view_model { } else if let Some(vm) = thread.comment_view_model {
@ -424,7 +419,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
&mut mutations, &mut mutations,
Some(thread.replies), Some(thread.replies),
thread.rendering_priority, thread.rendering_priority,
lang, ctx.lang,
&mut warnings, &mut warnings,
) { ) {
comments.push(c); comments.push(c);
@ -440,7 +435,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
comment, comment,
None, None,
response::video_details::CommentPriority::RenderingPriorityUnknown, response::video_details::CommentPriority::RenderingPriorityUnknown,
lang, ctx.lang,
&mut warnings, &mut warnings,
)); ));
} }
@ -450,7 +445,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
&mut mutations, &mut mutations,
None, None,
response::video_details::CommentPriority::RenderingPriorityUnknown, response::video_details::CommentPriority::RenderingPriorityUnknown,
lang, ctx.lang,
&mut warnings, &mut warnings,
) { ) {
comments.push(c); comments.push(c);
@ -654,8 +649,7 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use crate::{ use crate::{
client::{response, MapResponse}, client::{response, MapRespCtx, MapResponse},
param::Language,
util::tests::TESTFILES, util::tests::TESTFILES,
}; };
@ -676,7 +670,7 @@ mod tests {
let details: response::VideoDetails = let details: response::VideoDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); 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!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
@ -696,9 +690,7 @@ mod tests {
let details: response::VideoDetails = let details: response::VideoDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let err = details let err = details.map_response(&MapRespCtx::test("")).unwrap_err();
.map_response("", Language::En, None, None)
.unwrap_err();
assert!(matches!( assert!(matches!(
err, err,
crate::error::ExtractionError::NotFound { .. } crate::error::ExtractionError::NotFound { .. }
@ -716,7 +708,7 @@ mod tests {
let comments: response::VideoComments = let comments: response::VideoComments =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); 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!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),

View file

@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use time::{Date, OffsetDateTime}; use time::{Date, OffsetDateTime};
use self::{paginator::Paginator, richtext::RichText}; use self::{paginator::Paginator, richtext::RichText};
use crate::{error::Error, param::Country, validate}; use crate::{client::ClientType, error::Error, param::Country, validate};
/* /*
#COMMON #COMMON
@ -143,6 +143,8 @@ pub struct VideoPlayer {
pub dash_manifest_url: Option<String>, pub dash_manifest_url: Option<String>,
/// Video frames for seek preview /// Video frames for seek preview
pub preview_frames: Vec<Frameset>, pub preview_frames: Vec<Frameset>,
/// Client type with which the player was fetched
pub client_type: ClientType,
/// YouTube visitor data cookie /// YouTube visitor data cookie
pub visitor_data: Option<String>, pub visitor_data: Option<String>,
} }
@ -823,7 +825,7 @@ pub enum YouTubeItem {
Channel(ChannelItem), Channel(ChannelItem),
} }
/// YouTube video list item /// YouTube video list item (from search results, recommendations, playlists)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]
pub struct VideoItem { pub struct VideoItem {
@ -862,7 +864,7 @@ pub struct VideoItem {
pub short_description: Option<String>, pub short_description: Option<String>,
} }
/// YouTube channel list item /// YouTube channel list item (from search results)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]
pub struct ChannelItem { pub struct ChannelItem {
@ -884,7 +886,7 @@ pub struct ChannelItem {
pub short_description: String, pub short_description: String,
} }
/// YouTube playlist list item /// YouTube playlist list item (from search results)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]
pub struct PlaylistItem { pub struct PlaylistItem {

View file

@ -1125,5 +1125,6 @@
"expires_in_seconds": 21540, "expires_in_seconds": 21540,
"hls_manifest_url": null, "hls_manifest_url": null,
"dash_manifest_url": null, "dash_manifest_url": null,
"preview_frames": [] "preview_frames": [],
"client_type": "desktop"
} }

View file

@ -2120,5 +2120,6 @@
"hls_manifest_url": null, "hls_manifest_url": null,
"dash_manifest_url": null, "dash_manifest_url": null,
"preview_frames": [], "preview_frames": [],
"visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D" "visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D",
"client_type": "desktop"
} }