fix: send visitor data for YTM playlists
This commit is contained in:
parent
127596687b
commit
b25e9ebbb7
11 changed files with 156 additions and 54 deletions
|
|
@ -238,7 +238,7 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||||
mapper.items,
|
mapper.items,
|
||||||
mapper.ctoken,
|
mapper.ctoken,
|
||||||
visitor_data,
|
visitor_data,
|
||||||
crate::model::paginator::ContinuationEndpoint::Browse,
|
ContinuationEndpoint::Browse,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
|
|
||||||
|
|
@ -1185,8 +1185,14 @@ impl RustyPipeQuery {
|
||||||
/// - `ctype`: Client type (`Desktop`, `DesktopMusic`, `Android`, ...)
|
/// - `ctype`: Client type (`Desktop`, `DesktopMusic`, `Android`, ...)
|
||||||
/// - `method`: HTTP method
|
/// - `method`: HTTP method
|
||||||
/// - `endpoint`: YouTube API endpoint (`https://www.youtube.com/youtubei/v1/<XYZ>?key=...`)
|
/// - `endpoint`: YouTube API endpoint (`https://www.youtube.com/youtubei/v1/<XYZ>?key=...`)
|
||||||
async fn request_builder(&self, ctype: ClientType, endpoint: &str) -> RequestBuilder {
|
/// - `visitor_data`: YouTube visitor data cookie
|
||||||
match ctype {
|
async fn request_builder(
|
||||||
|
&self,
|
||||||
|
ctype: ClientType,
|
||||||
|
endpoint: &str,
|
||||||
|
visitor_data: Option<&str>,
|
||||||
|
) -> RequestBuilder {
|
||||||
|
let mut r = match ctype {
|
||||||
ClientType::Desktop => self
|
ClientType::Desktop => self
|
||||||
.client
|
.client
|
||||||
.inner
|
.inner
|
||||||
|
|
@ -1215,7 +1221,7 @@ impl RustyPipeQuery {
|
||||||
.header("X-YouTube-Client-Name", "67")
|
.header("X-YouTube-Client-Name", "67")
|
||||||
.header(
|
.header(
|
||||||
"X-YouTube-Client-Version",
|
"X-YouTube-Client-Version",
|
||||||
self.client.get_music_client_version().await,
|
self.client.get_music_client_version().await
|
||||||
),
|
),
|
||||||
ClientType::TvHtml5Embed => self
|
ClientType::TvHtml5Embed => self
|
||||||
.client
|
.client
|
||||||
|
|
@ -1258,7 +1264,11 @@ impl RustyPipeQuery {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.header("X-Goog-Api-Format-Version", "2"),
|
.header("X-Goog-Api-Format-Version", "2"),
|
||||||
|
};
|
||||||
|
if let Some(vdata) = self.opts.visitor_data.as_deref().or(visitor_data) {
|
||||||
|
r = r.header("X-Goog-EOM-Visitor-Id", vdata);
|
||||||
}
|
}
|
||||||
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a YouTube visitor data cookie, which is necessary for certain requests
|
/// Get a YouTube visitor data cookie, which is necessary for certain requests
|
||||||
|
|
@ -1273,6 +1283,7 @@ impl RustyPipeQuery {
|
||||||
&self,
|
&self,
|
||||||
request: &Request,
|
request: &Request,
|
||||||
id: &str,
|
id: &str,
|
||||||
|
visitor_data: Option<&str>,
|
||||||
deobf: Option<&DeobfData>,
|
deobf: Option<&DeobfData>,
|
||||||
) -> Result<RequestResult<M>, Error> {
|
) -> Result<RequestResult<M>, Error> {
|
||||||
let response = self
|
let response = self
|
||||||
|
|
@ -1306,7 +1317,7 @@ impl RustyPipeQuery {
|
||||||
id,
|
id,
|
||||||
self.opts.lang,
|
self.opts.lang,
|
||||||
deobf,
|
deobf,
|
||||||
self.opts.visitor_data.as_deref(),
|
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()),
|
||||||
|
|
@ -1324,11 +1335,14 @@ impl RustyPipeQuery {
|
||||||
&self,
|
&self,
|
||||||
request: &Request,
|
request: &Request,
|
||||||
id: &str,
|
id: &str,
|
||||||
|
visitor_data: Option<&str>,
|
||||||
deobf: Option<&DeobfData>,
|
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.yt_request_attempt::<R, M>(request, id, deobf).await?;
|
let resp = self
|
||||||
|
.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),
|
||||||
|
|
@ -1369,7 +1383,9 @@ impl RustyPipeQuery {
|
||||||
/// - `method`: HTTP method
|
/// - `method`: HTTP method
|
||||||
/// - `endpoint`: YouTube API endpoint (`https://www.youtube.com/youtubei/v1/<XYZ>?key=...`)
|
/// - `endpoint`: YouTube API endpoint (`https://www.youtube.com/youtubei/v1/<XYZ>?key=...`)
|
||||||
/// - `body`: Serializable request body to be sent in json format
|
/// - `body`: Serializable request body to be sent in json format
|
||||||
|
/// - `visitor_data`: YouTube visitor data cookie
|
||||||
/// - `deobf`: Deobfuscator (is passed to the mapper to deobfuscate stream URLs).
|
/// - `deobf`: Deobfuscator (is passed to the mapper to deobfuscate stream URLs).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn execute_request_deobf<
|
async fn execute_request_deobf<
|
||||||
R: DeserializeOwned + MapResponse<M> + Debug,
|
R: DeserializeOwned + MapResponse<M> + Debug,
|
||||||
M,
|
M,
|
||||||
|
|
@ -1381,17 +1397,20 @@ impl RustyPipeQuery {
|
||||||
id: &str,
|
id: &str,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &B,
|
body: &B,
|
||||||
|
visitor_data: Option<&str>,
|
||||||
deobf: Option<&DeobfData>,
|
deobf: Option<&DeobfData>,
|
||||||
) -> Result<M, Error> {
|
) -> Result<M, Error> {
|
||||||
tracing::debug!("getting {}({})", operation, id);
|
tracing::debug!("getting {}({})", operation, id);
|
||||||
|
|
||||||
let request = self
|
let request = self
|
||||||
.request_builder(ctype, endpoint)
|
.request_builder(ctype, endpoint, visitor_data)
|
||||||
.await
|
.await
|
||||||
.json(body)
|
.json(body)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let req_res = self.yt_request::<R, M>(&request, id, deobf).await?;
|
let req_res = self
|
||||||
|
.yt_request::<R, M>(&request, id, visitor_data, deobf)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Uncomment to debug response text
|
// Uncomment to debug response text
|
||||||
// println!("{}", &req_res.body);
|
// println!("{}", &req_res.body);
|
||||||
|
|
@ -1477,11 +1496,55 @@ impl RustyPipeQuery {
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &B,
|
body: &B,
|
||||||
) -> Result<M, Error> {
|
) -> Result<M, Error> {
|
||||||
self.execute_request_deobf::<R, M, B>(ctype, operation, id, endpoint, body, None)
|
self.execute_request_deobf::<R, M, B>(ctype, operation, id, endpoint, body, None, None)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute a request to the YouTube API, then map the response.
|
||||||
|
///
|
||||||
|
/// Creates a report in case of failure for easy debugging.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `ctype`: Client type (`Desktop`, `DesktopMusic`, `Android`, ...)
|
||||||
|
/// - `operation`: Name of the RustyPipe operation (only for reporting, e.g. `get_player`)
|
||||||
|
/// - `id`: ID of the requested entity (Video ID, Channel ID, ...).
|
||||||
|
/// The ID is included in reports and is also passed to the mapper for validating the response.
|
||||||
|
/// Set it to an empty string if you are not requesting an entity with an ID.
|
||||||
|
/// - `method`: HTTP method
|
||||||
|
/// - `endpoint`: YouTube API endpoint (`https://www.youtube.com/youtubei/v1/<XYZ>?key=...`)
|
||||||
|
/// - `body`: Serializable request body to be sent in json format
|
||||||
|
/// - `visitor_data`: YouTube visitor data cookie
|
||||||
|
async fn execute_request_vdata<
|
||||||
|
R: DeserializeOwned + MapResponse<M> + Debug,
|
||||||
|
M,
|
||||||
|
B: Serialize + ?Sized,
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
ctype: ClientType,
|
||||||
|
operation: &str,
|
||||||
|
id: &str,
|
||||||
|
endpoint: &str,
|
||||||
|
body: &B,
|
||||||
|
visitor_data: Option<&str>,
|
||||||
|
) -> Result<M, Error> {
|
||||||
|
self.execute_request_deobf::<R, M, B>(
|
||||||
|
ctype,
|
||||||
|
operation,
|
||||||
|
id,
|
||||||
|
endpoint,
|
||||||
|
body,
|
||||||
|
visitor_data,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute a request to the YouTube API and return the response string
|
/// Execute a request to the YouTube API and return the response string
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `ctype`: Client type (`Desktop`, `DesktopMusic`, `Android`, ...)
|
||||||
|
/// - `endpoint`: YouTube API endpoint (`https://www.youtube.com/youtubei/v1/<XYZ>?key=...`)
|
||||||
|
/// - `body`: Serializable request body to be sent in json format
|
||||||
pub async fn raw<B: Serialize + ?Sized>(
|
pub async fn raw<B: Serialize + ?Sized>(
|
||||||
&self,
|
&self,
|
||||||
ctype: ClientType,
|
ctype: ClientType,
|
||||||
|
|
@ -1489,7 +1552,7 @@ impl RustyPipeQuery {
|
||||||
body: &B,
|
body: &B,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let request = self
|
let request = self
|
||||||
.request_builder(ctype, endpoint)
|
.request_builder(ctype, endpoint, None)
|
||||||
.await
|
.await
|
||||||
.json(body)
|
.json(body)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
@ -1519,13 +1582,13 @@ trait MapResponse<T> {
|
||||||
/// that the returned entity matches this ID and return an error instead.
|
/// that the returned entity matches this ID and return an error instead.
|
||||||
/// - `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)
|
||||||
/// - `vdata`: Visitor data option of the client
|
/// - `visitor_data`: Visitor data option of the client
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
deobf: Option<&DeobfData>,
|
deobf: Option<&DeobfData>,
|
||||||
vdata: Option<&str>,
|
visitor_data: Option<&str>,
|
||||||
) -> Result<MapResult<T>, ExtractionError>;
|
) -> Result<MapResult<T>, ExtractionError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{paginator::Paginator, ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem},
|
model::{
|
||||||
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem,
|
||||||
|
},
|
||||||
param::Language,
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
};
|
};
|
||||||
|
|
@ -132,12 +135,13 @@ impl RustyPipeQuery {
|
||||||
tuner_setting_value: "AUTOMIX_SETTING_NORMAL",
|
tuner_setting_value: "AUTOMIX_SETTING_NORMAL",
|
||||||
};
|
};
|
||||||
|
|
||||||
self.execute_request::<response::MusicDetails, _, _>(
|
self.execute_request_vdata::<response::MusicDetails, _, _>(
|
||||||
ClientType::DesktopMusic,
|
ClientType::DesktopMusic,
|
||||||
"music_radio",
|
"music_radio",
|
||||||
radio_id,
|
radio_id,
|
||||||
"next",
|
"next",
|
||||||
&request_body,
|
&request_body,
|
||||||
|
Some(&visitor_data),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -293,13 +297,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
||||||
.map(|c| c.next_continuation_data.continuation);
|
.map(|c| c.next_continuation_data.continuation);
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: Paginator::new_ext(
|
c: Paginator::new_ext(None, tracks, ctoken, None, ContinuationEndpoint::MusicNext),
|
||||||
None,
|
|
||||||
tracks,
|
|
||||||
ctoken,
|
|
||||||
None,
|
|
||||||
crate::model::paginator::ContinuationEndpoint::MusicNext,
|
|
||||||
),
|
|
||||||
warnings,
|
warnings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ use std::{borrow::Cow, fmt::Debug};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{paginator::Paginator, AlbumId, ChannelId, MusicAlbum, MusicPlaylist, TrackItem},
|
model::{
|
||||||
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
AlbumId, ChannelId, MusicAlbum, MusicPlaylist, TrackItem,
|
||||||
|
},
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
util::{self, TryRemove, DOT_SEPARATOR},
|
util::{self, TryRemove, DOT_SEPARATOR},
|
||||||
};
|
};
|
||||||
|
|
@ -23,18 +26,27 @@ impl RustyPipeQuery {
|
||||||
playlist_id: S,
|
playlist_id: S,
|
||||||
) -> Result<MusicPlaylist, Error> {
|
) -> Result<MusicPlaylist, Error> {
|
||||||
let playlist_id = playlist_id.as_ref();
|
let playlist_id = playlist_id.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
// YTM playlists require visitor data for continuations to work
|
||||||
|
let visitor_data = if playlist_id.starts_with("RD") {
|
||||||
|
Some(self.get_visitor_data().await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let context = self
|
||||||
|
.get_context(ClientType::DesktopMusic, true, visitor_data.as_deref())
|
||||||
|
.await;
|
||||||
let request_body = QBrowse {
|
let request_body = QBrowse {
|
||||||
context,
|
context,
|
||||||
browse_id: &format!("VL{playlist_id}"),
|
browse_id: &format!("VL{playlist_id}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.execute_request::<response::MusicPlaylist, _, _>(
|
self.execute_request_vdata::<response::MusicPlaylist, _, _>(
|
||||||
ClientType::DesktopMusic,
|
ClientType::DesktopMusic,
|
||||||
"music_playlist",
|
"music_playlist",
|
||||||
playlist_id,
|
playlist_id,
|
||||||
"browse",
|
"browse",
|
||||||
&request_body,
|
&request_body,
|
||||||
|
visitor_data.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +139,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
_vdata: Option<&str>,
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
|
@ -251,15 +263,15 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
track_count,
|
track_count,
|
||||||
map_res.c,
|
map_res.c,
|
||||||
ctoken,
|
ctoken,
|
||||||
None,
|
vdata.map(str::to_owned),
|
||||||
crate::model::paginator::ContinuationEndpoint::MusicBrowse,
|
ContinuationEndpoint::MusicBrowse,
|
||||||
),
|
),
|
||||||
related_playlists: Paginator::new_ext(
|
related_playlists: Paginator::new_ext(
|
||||||
None,
|
None,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
related_ctoken,
|
related_ctoken,
|
||||||
None,
|
vdata.map(str::to_owned),
|
||||||
crate::model::paginator::ContinuationEndpoint::MusicBrowse,
|
ContinuationEndpoint::MusicBrowse,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
warnings: map_res.warnings,
|
warnings: map_res.warnings,
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ use crate::{
|
||||||
client::response::music_item::MusicListMapper,
|
client::response::music_item::MusicListMapper,
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{
|
model::{
|
||||||
paginator::Paginator, traits::FromYtItem, AlbumItem, ArtistItem, MusicPlaylistItem,
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
MusicSearchFiltered, MusicSearchResult, MusicSearchSuggestion, TrackItem,
|
traits::FromYtItem,
|
||||||
|
AlbumItem, ArtistItem, MusicPlaylistItem, MusicSearchFiltered, MusicSearchResult,
|
||||||
|
MusicSearchSuggestion, TrackItem,
|
||||||
},
|
},
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
};
|
};
|
||||||
|
|
@ -287,7 +289,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> for response::MusicSearc
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
_vdata: Option<&str>,
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<MusicSearchFiltered<T>>, ExtractionError> {
|
) -> Result<MapResult<MusicSearchFiltered<T>>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
|
@ -332,8 +334,8 @@ impl<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> for response::MusicSearc
|
||||||
None,
|
None,
|
||||||
map_res.c,
|
map_res.c,
|
||||||
ctoken,
|
ctoken,
|
||||||
None,
|
vdata.map(str::to_owned),
|
||||||
crate::model::paginator::ContinuationEndpoint::MusicSearch,
|
ContinuationEndpoint::MusicSearch,
|
||||||
),
|
),
|
||||||
corrected_query,
|
corrected_query,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use crate::error::{Error, ExtractionError};
|
use crate::error::{Error, ExtractionError};
|
||||||
|
|
@ -22,8 +23,13 @@ impl RustyPipeQuery {
|
||||||
) -> Result<Paginator<T>, Error> {
|
) -> Result<Paginator<T>, Error> {
|
||||||
let ctoken = ctoken.as_ref();
|
let ctoken = ctoken.as_ref();
|
||||||
if endpoint.is_music() {
|
if endpoint.is_music() {
|
||||||
|
// Visitor data is required for YTM continuations
|
||||||
|
let visitor_data = match visitor_data {
|
||||||
|
Some(vd) => Cow::Borrowed(vd),
|
||||||
|
None => Cow::Owned(self.get_visitor_data().await?),
|
||||||
|
};
|
||||||
let context = self
|
let context = self
|
||||||
.get_context(ClientType::DesktopMusic, true, visitor_data)
|
.get_context(ClientType::DesktopMusic, true, Some(&visitor_data))
|
||||||
.await;
|
.await;
|
||||||
let request_body = QContinuation {
|
let request_body = QContinuation {
|
||||||
context,
|
context,
|
||||||
|
|
@ -31,16 +37,17 @@ impl RustyPipeQuery {
|
||||||
};
|
};
|
||||||
|
|
||||||
let p = self
|
let p = self
|
||||||
.execute_request::<response::MusicContinuation, Paginator<MusicItem>, _>(
|
.execute_request_vdata::<response::MusicContinuation, Paginator<MusicItem>, _>(
|
||||||
ClientType::DesktopMusic,
|
ClientType::DesktopMusic,
|
||||||
"music_continuation",
|
"music_continuation",
|
||||||
ctoken,
|
ctoken,
|
||||||
endpoint.as_str(),
|
endpoint.as_str(),
|
||||||
&request_body,
|
&request_body,
|
||||||
|
Some(&visitor_data),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(map_ytm_paginator(p, visitor_data, endpoint))
|
Ok(map_ytm_paginator(p, Some(&visitor_data), endpoint))
|
||||||
} else {
|
} else {
|
||||||
let context = self
|
let context = self
|
||||||
.get_context(ClientType::Desktop, true, visitor_data)
|
.get_context(ClientType::Desktop, true, visitor_data)
|
||||||
|
|
@ -211,6 +218,9 @@ impl<T: FromYtItem> Paginator<T> {
|
||||||
let mut items = paginator.items;
|
let mut items = paginator.items;
|
||||||
self.items.append(&mut items);
|
self.items.append(&mut items);
|
||||||
self.ctoken = paginator.ctoken;
|
self.ctoken = paginator.ctoken;
|
||||||
|
if paginator.visitor_data.is_some() {
|
||||||
|
self.visitor_data = paginator.visitor_data;
|
||||||
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok(None) => Ok(false),
|
Ok(None) => Ok(false),
|
||||||
|
|
@ -285,6 +295,9 @@ macro_rules! paginator {
|
||||||
let mut items = paginator.items;
|
let mut items = paginator.items;
|
||||||
self.items.append(&mut items);
|
self.items.append(&mut items);
|
||||||
self.ctoken = paginator.ctoken;
|
self.ctoken = paginator.ctoken;
|
||||||
|
if paginator.visitor_data.is_some() {
|
||||||
|
self.visitor_data = paginator.visitor_data;
|
||||||
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok(None) => Ok(false),
|
Ok(None) => Ok(false),
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ impl RustyPipeQuery {
|
||||||
video_id,
|
video_id,
|
||||||
"player",
|
"player",
|
||||||
&request_body,
|
&request_body,
|
||||||
|
None,
|
||||||
Some(&deobf),
|
Some(&deobf),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{paginator::Paginator, SearchResult, YouTubeItem},
|
model::{
|
||||||
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
SearchResult, YouTubeItem,
|
||||||
|
},
|
||||||
param::search_filter::SearchFilter,
|
param::search_filter::SearchFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -119,7 +122,7 @@ impl MapResponse<SearchResult> for response::Search {
|
||||||
mapper.items,
|
mapper.items,
|
||||||
mapper.ctoken,
|
mapper.ctoken,
|
||||||
None,
|
None,
|
||||||
crate::model::paginator::ContinuationEndpoint::Search,
|
ContinuationEndpoint::Search,
|
||||||
),
|
),
|
||||||
corrected_query: mapper.corrected_query,
|
corrected_query: mapper.corrected_query,
|
||||||
visitor_data: self
|
visitor_data: self
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{paginator::Paginator, VideoItem},
|
model::{
|
||||||
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
VideoItem,
|
||||||
|
},
|
||||||
param::Language,
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
};
|
};
|
||||||
|
|
@ -124,7 +127,7 @@ fn map_startpage_videos(
|
||||||
mapper.items,
|
mapper.items,
|
||||||
mapper.ctoken,
|
mapper.ctoken,
|
||||||
visitor_data,
|
visitor_data,
|
||||||
crate::model::paginator::ContinuationEndpoint::Browse,
|
ContinuationEndpoint::Browse,
|
||||||
),
|
),
|
||||||
warnings: mapper.warnings,
|
warnings: mapper.warnings,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{paginator::Paginator, ChannelTag, Chapter, Comment, VideoDetails, VideoItem},
|
model::{
|
||||||
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
ChannelTag, Chapter, Comment, VideoDetails, VideoItem,
|
||||||
|
},
|
||||||
param::Language,
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
util::{self, timeago, TryRemove},
|
util::{self, timeago, TryRemove},
|
||||||
|
|
@ -360,14 +363,14 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
comment_ctoken,
|
comment_ctoken,
|
||||||
visitor_data.clone(),
|
visitor_data.clone(),
|
||||||
crate::model::paginator::ContinuationEndpoint::Next,
|
ContinuationEndpoint::Next,
|
||||||
),
|
),
|
||||||
latest_comments: Paginator::new_ext(
|
latest_comments: Paginator::new_ext(
|
||||||
comment_count,
|
comment_count,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
latest_comments_ctoken,
|
latest_comments_ctoken,
|
||||||
visitor_data.clone(),
|
visitor_data.clone(),
|
||||||
crate::model::paginator::ContinuationEndpoint::Next,
|
ContinuationEndpoint::Next,
|
||||||
),
|
),
|
||||||
visitor_data,
|
visitor_data,
|
||||||
},
|
},
|
||||||
|
|
@ -459,7 +462,7 @@ fn map_recommendations(
|
||||||
mapper.items,
|
mapper.items,
|
||||||
mapper.ctoken,
|
mapper.ctoken,
|
||||||
visitor_data,
|
visitor_data,
|
||||||
crate::model::paginator::ContinuationEndpoint::Next,
|
ContinuationEndpoint::Next,
|
||||||
),
|
),
|
||||||
warnings: mapper.warnings,
|
warnings: mapper.warnings,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1368,17 +1368,21 @@ fn music_playlist(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn music_playlist_cont(rp: RustyPipe) {
|
#[case::user("PLMC9KNkIncKtPzgY-5rmhvj7fax8fdxoj")]
|
||||||
let mut playlist = tokio_test::block_on(
|
#[case::ytm("RDCLAK5uy_kb7EBi6y3GrtJri4_ZH56Ms786DFEimbM")]
|
||||||
rp.query()
|
fn music_playlist_cont(#[case] id: &str, rp: RustyPipe) {
|
||||||
.music_playlist("PLMC9KNkIncKtPzgY-5rmhvj7fax8fdxoj"),
|
let mut playlist = tokio_test::block_on(rp.query().music_playlist(id)).unwrap();
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
tokio_test::block_on(playlist.tracks.extend_pages(rp.query(), usize::MAX)).unwrap();
|
tokio_test::block_on(playlist.tracks.extend_pages(rp.query(), 5)).unwrap();
|
||||||
|
|
||||||
assert_gte(playlist.tracks.items.len(), 100, "tracks");
|
let track_count = playlist.track_count.unwrap();
|
||||||
assert_gte(playlist.tracks.count.unwrap(), 100, "track count");
|
assert_gte(track_count, 100, "tracks");
|
||||||
|
|
||||||
|
assert_eq!(track_count, playlist.tracks.count.unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
usize::try_from(track_count).unwrap(),
|
||||||
|
playlist.tracks.items.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
|
||||||
Reference in a new issue