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:
parent
dd0565ba98
commit
90540c6aaa
24 changed files with 273 additions and 368 deletions
|
|
@ -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<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> 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
|
||||
.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<Channel<Paginator<VideoItem>>> for response::Channel {
|
|||
has_shorts: content.has_shorts,
|
||||
has_live: content.has_live,
|
||||
},
|
||||
id,
|
||||
lang,
|
||||
ctx,
|
||||
)?;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel(
|
||||
lang,
|
||||
ctx.lang,
|
||||
&channel_data.c,
|
||||
channel_data.warnings,
|
||||
);
|
||||
|
|
@ -249,16 +247,13 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
|||
impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> 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
|
||||
.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<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
|||
has_shorts: content.has_shorts,
|
||||
has_live: content.has_live,
|
||||
},
|
||||
id,
|
||||
lang,
|
||||
ctx,
|
||||
)?;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel(
|
||||
lang,
|
||||
ctx.lang,
|
||||
&channel_data.c,
|
||||
channel_data.warnings,
|
||||
);
|
||||
|
|
@ -289,13 +283,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
|||
}
|
||||
|
||||
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_visitor_data: Option<&str>,
|
||||
) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<ChannelInfo>, 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<ChannelInfo> 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<MapResult<Channel<()>>, 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::<u64>(&t.text, lang, &mut warnings)
|
||||
util::parse_large_numstr_or_warn::<u64>(&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<Paginator<VideoItem>>> =
|
||||
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<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 {
|
||||
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<Paginator<PlaylistItem>>> = 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<ChannelInfo> = channel
|
||||
.map_response("UC2DjFE7Xf11U-RZqWBigcVOQ", Language::En, None, None)
|
||||
.map_response(&MapRespCtx::test("UC2DjFE7Xf11U-RZqWBigcVOQ"))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -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<RustyPipe, Error> {
|
||||
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> {
|
||||
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<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
||||
&self,
|
||||
request: &Request,
|
||||
id: &str,
|
||||
visitor_data: Option<&str>,
|
||||
deobf: Option<&DeobfData>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<RequestResult<M>, 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::<R>(&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<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
||||
&self,
|
||||
request: &Request,
|
||||
id: &str,
|
||||
visitor_data: Option<&str>,
|
||||
deobf: Option<&DeobfData>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<RequestResult<M>, Error> {
|
||||
let mut last_resp = None;
|
||||
for n in 0..=self.client.inner.n_http_retries {
|
||||
let resp = self
|
||||
.yt_request_attempt::<R, M>(request, id, visitor_data, deobf)
|
||||
.await?;
|
||||
let resp = self.yt_request_attempt::<R, M>(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::<R, M>(&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::<R, M>(&request, &ctx).await?;
|
||||
|
||||
// Uncomment to debug response text
|
||||
// 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
|
||||
/// RustyPipe models.
|
||||
trait MapResponse<T> {
|
||||
|
|
@ -1569,13 +1586,7 @@ trait MapResponse<T> {
|
|||
/// - `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<MapResult<T>, ExtractionError>;
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<T>, ExtractionError>;
|
||||
}
|
||||
|
||||
fn validate_country(country: Country) -> Country {
|
||||
|
|
|
|||
|
|
@ -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<MusicArtist> for response::MusicArtist {
|
||||
fn map_response(
|
||||
self,
|
||||
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)?;
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicArtist>, ExtractionError> {
|
||||
let mapped = map_artist_page(self, ctx, false)?;
|
||||
Ok(MapResult {
|
||||
c: mapped.c.0,
|
||||
warnings: mapped.warnings,
|
||||
|
|
@ -110,19 +104,15 @@ impl MapResponse<MusicArtist> 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<MapResult<(MusicArtist, bool)>, 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<MapResult<(MusicArtist, bool)>, 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<Vec<AlbumItem>> for response::MusicArtistAlbums {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Vec<AlbumItem>>, 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<Vec<AlbumItem>> 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<Vec<AlbumItem>> =
|
||||
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<MusicArtist> = 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<MapResult<MusicArtist>, ExtractionError> =
|
||||
artist.map_response("UCLkAepWjdylmXSltofFvsYQ", Language::En, None, None);
|
||||
artist.map_response(&MapRespCtx::test("UCLkAepWjdylmXSltofFvsYQ"));
|
||||
let e = res.unwrap_err();
|
||||
|
||||
match e {
|
||||
|
|
|
|||
|
|
@ -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<MusicCharts> for response::MusicCharts {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<crate::serializer::MapResult<MusicCharts>, crate::error::ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicCharts>, ExtractionError> {
|
||||
let countries = self
|
||||
.framework_updates
|
||||
.map(|fwu| {
|
||||
|
|
@ -77,9 +71,9 @@ impl MapResponse<MusicCharts> 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<MusicCharts> =
|
||||
charts.map_response("", Language::En, None, None).unwrap();
|
||||
let map_res: MapResult<MusicCharts> = charts.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -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<TrackDetails> for response::MusicDetails {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<TrackDetails>, ExtractionError> {
|
||||
let tabs = self
|
||||
.contents
|
||||
|
|
@ -211,7 +207,7 @@ impl MapResponse<TrackDetails> 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<TrackDetails> 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<TrackDetails> for response::MusicDetails {
|
|||
impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
|
||||
let tabs = self
|
||||
.contents
|
||||
|
|
@ -260,7 +253,7 @@ impl MapResponse<Paginator<TrackItem>> 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<Paginator<TrackItem>> 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<Paginator<TrackItem>> for response::MusicDetails {
|
|||
}
|
||||
|
||||
impl MapResponse<Lyrics> for response::MusicLyrics {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<MapResult<Lyrics>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Lyrics>, 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<Lyrics> for response::MusicLyrics {
|
|||
impl MapResponse<MusicRelated> for response::MusicRelated {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicRelated>, 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<MusicRelated> 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<model::TrackDetails> =
|
||||
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<Paginator<TrackItem>> =
|
||||
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> = lyrics.map_response("", Language::En, None, None).unwrap();
|
||||
let map_res: MapResult<Lyrics> = 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<MusicRelated> =
|
||||
lyrics.map_response("", Language::En, None, None).unwrap();
|
||||
let map_res: MapResult<MusicRelated> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -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<Vec<MusicGenreItem>> for response::MusicGenres {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
_lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
||||
_ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
||||
let content = self
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
|
|
@ -111,13 +108,7 @@ impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
|||
}
|
||||
|
||||
impl MapResponse<MusicGenre> for response::MusicGenre {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicGenre>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let content = self
|
||||
|
|
@ -179,7 +170,7 @@ impl MapResponse<MusicGenre> 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<MusicGenre> 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<Vec<model::MusicGenreItem>> =
|
||||
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<model::MusicGenre> =
|
||||
playlist.map_response(id, Language::En, None, None).unwrap();
|
||||
playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -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<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<crate::serializer::MapResult<Vec<T>>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Vec<T>>, ExtractionError> {
|
||||
let items = self
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
|
|
@ -73,7 +68,7 @@ impl<T: FromYtItem> MapResponse<Vec<T>> 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<Vec<AlbumItem>> = new_albums
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<Vec<AlbumItem>> =
|
||||
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<Vec<TrackItem>> = new_videos
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<Vec<TrackItem>> =
|
||||
new_videos.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -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<MusicPlaylist> for response::MusicPlaylist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
|
|
@ -186,14 +183,15 @@ impl MapResponse<MusicPlaylist> 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<MusicPlaylist> 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<MusicPlaylist> 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<MusicPlaylist> for response::MusicPlaylist {
|
|||
}
|
||||
|
||||
impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let (header, sections) = match self.contents {
|
||||
|
|
@ -401,7 +393,7 @@ impl MapResponse<MusicAlbum> 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<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 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<MusicAlbum> 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<MusicAlbum> 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<model::MusicPlaylist> =
|
||||
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<model::MusicAlbum> =
|
||||
playlist.map_response(id, Language::En, None, None).unwrap();
|
||||
playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -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<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicSearchResult<T>>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
|
|
@ -171,7 +168,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> 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<T: FromYtItem> MapResponse<MusicSearchResult<T>> 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<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
|||
impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> 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();
|
||||
|
||||
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<MusicSearchResult<MusicItem>> =
|
||||
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<MusicSearchResult<TrackItem>> =
|
||||
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<MusicSearchResult<AlbumItem>> =
|
||||
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<MusicSearchResult<ArtistItem>> =
|
||||
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<MusicSearchResult<MusicPlaylistItem>> =
|
||||
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<MusicSearchSuggestion> = suggestion
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<MusicSearchSuggestion> =
|
||||
suggestion.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -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<T: FromYtItem>(
|
|||
impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
|
||||
let items = self
|
||||
.on_response_received_actions
|
||||
|
|
@ -126,7 +123,7 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
|
||||
Ok(MapResult {
|
||||
|
|
@ -139,12 +136,9 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
|||
impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<MusicItem>>, 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<Paginator<MusicItem>> 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<Paginator<YouTubeItem>> =
|
||||
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<Paginator<YouTubeItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<VideoItem> =
|
||||
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<Paginator<YouTubeItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<PlaylistItem> =
|
||||
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<Paginator<MusicItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<TrackItem> =
|
||||
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<Paginator<MusicItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<MusicPlaylistItem> =
|
||||
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<VideoPlayer> for response::Player {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_lang: Language,
|
||||
deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<super::MapResult<VideoPlayer>, 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<VideoPlayer> 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<VideoPlayer> 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<DeobfData> = 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!(
|
||||
|
|
|
|||
|
|
@ -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<Playlist> for response::Playlist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
) -> Result<MapResult<Playlist>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Playlist>, 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<Playlist> for response::Playlist {
|
|||
.playlist_video_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
|
||||
mapper.map_response(video_items);
|
||||
|
||||
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;
|
||||
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<Playlist> 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<Playlist> 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<Playlist> 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(),
|
||||
|
|
|
|||
|
|
@ -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<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<SearchResult<T>>, ExtractionError> {
|
||||
let items = self
|
||||
.contents
|
||||
|
|
@ -115,7 +112,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
|||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
|
||||
Ok(MapResult {
|
||||
|
|
@ -135,7 +132,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> 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<SearchResult<YouTubeItem>> =
|
||||
search.map_response("", Language::En, None, None).unwrap();
|
||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -466,5 +466,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: android,
|
||||
visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -581,5 +581,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: desktop,
|
||||
visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -405,5 +405,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: desktop_music,
|
||||
visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -196,5 +196,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: ios,
|
||||
visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -581,5 +581,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: tv_html5_embed,
|
||||
visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Paginator<VideoItem>> for response::Startpage {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> {
|
||||
let grid = self
|
||||
.contents
|
||||
|
|
@ -75,10 +74,10 @@ impl MapResponse<Paginator<VideoItem>> 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<Paginator<VideoItem>> for response::Startpage {
|
|||
impl MapResponse<Vec<VideoItem>> for response::Trending {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> {
|
||||
let items = self
|
||||
.contents
|
||||
|
|
@ -103,7 +99,7 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
|
|||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::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<Paginator<VideoItem>> = startpage
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<Paginator<VideoItem>> =
|
||||
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<Vec<VideoItem>> = startpage
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<Vec<VideoItem>> =
|
||||
startpage.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -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<UrlTarget> for response::ResolvedUrl {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<MapResult<UrlTarget>, ExtractionError> {
|
||||
fn map_response(self, _ctx: &MapRespCtx<'_>) -> Result<MapResult<UrlTarget>, ExtractionError> {
|
||||
let pt = self.endpoint.page_type();
|
||||
if let NavigationEndpoint::Browse {
|
||||
browse_endpoint, ..
|
||||
|
|
|
|||
|
|
@ -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<VideoDetails> for response::VideoDetails {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<VideoDetails>, 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<VideoDetails> 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<VideoDetails> 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<VideoDetails> for response::VideoDetails {
|
|||
let comment_count = comment_count_section.and_then(|s| {
|
||||
util::parse_large_numstr_or_warn::<u64>(
|
||||
&s.comments_entry_point_header_renderer.comment_count,
|
||||
lang,
|
||||
ctx.lang,
|
||||
&mut warnings,
|
||||
)
|
||||
});
|
||||
|
|
@ -275,7 +273,7 @@ impl MapResponse<VideoDetails> 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<VideoDetails> 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<VideoDetails> 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<VideoDetails> for response::VideoDetails {
|
|||
impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
|
||||
let received_endpoints = self.on_response_received_endpoints;
|
||||
let mut warnings = Vec::new();
|
||||
|
|
@ -415,7 +410,7 @@ impl MapResponse<Paginator<Comment>> 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<Paginator<Comment>> 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<Paginator<Comment>> for response::VideoComments {
|
|||
comment,
|
||||
None,
|
||||
response::video_details::CommentPriority::RenderingPriorityUnknown,
|
||||
lang,
|
||||
ctx.lang,
|
||||
&mut warnings,
|
||||
));
|
||||
}
|
||||
|
|
@ -450,7 +445,7 @@ impl MapResponse<Paginator<Comment>> 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(),
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
/// Video frames for seek preview
|
||||
pub preview_frames: Vec<Frameset>,
|
||||
/// Client type with which the player was fetched
|
||||
pub client_type: ClientType,
|
||||
/// YouTube visitor data cookie
|
||||
pub visitor_data: Option<String>,
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
|
|
|||
|
|
@ -1125,5 +1125,6 @@
|
|||
"expires_in_seconds": 21540,
|
||||
"hls_manifest_url": null,
|
||||
"dash_manifest_url": null,
|
||||
"preview_frames": []
|
||||
"preview_frames": [],
|
||||
"client_type": "desktop"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue