feat!: generate random visitorData, remove RustyPipeQuery::get_context and YTContext<'a> from public API
This commit is contained in:
parent
9e835c8f38
commit
7c4f44d09c
22 changed files with 99 additions and 258 deletions
|
|
@ -6,7 +6,7 @@ use indicatif::{ProgressBar, ProgressStyle};
|
|||
use num_enum::TryFromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery, YTContext};
|
||||
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
|
||||
use rustypipe::model::{MusicItem, YouTubeItem};
|
||||
use rustypipe::param::search_filter::{ItemType, SearchFilter};
|
||||
use rustypipe::param::ChannelVideoTab;
|
||||
|
|
@ -57,7 +57,6 @@ pub struct ABTestRes {
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct QVideo<'a> {
|
||||
context: YTContext<'a>,
|
||||
video_id: &'a str,
|
||||
content_check_ok: bool,
|
||||
racy_check_ok: bool,
|
||||
|
|
@ -66,7 +65,6 @@ struct QVideo<'a> {
|
|||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QBrowse<'a> {
|
||||
context: YTContext<'a>,
|
||||
browse_id: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
params: Option<&'a str>,
|
||||
|
|
@ -153,9 +151,7 @@ pub async fn run_all_tests(n: usize, concurrency: usize) -> Vec<ABTestRes> {
|
|||
}
|
||||
|
||||
pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let context = rp.get_context(ClientType::Desktop, true, None).await;
|
||||
let q = QVideo {
|
||||
context,
|
||||
video_id: "ZeerrnuLi5E",
|
||||
content_check_ok: false,
|
||||
racy_check_ok: false,
|
||||
|
|
@ -190,13 +186,11 @@ pub async fn channel_handles_in_search_results(rp: &RustyPipeQuery) -> Result<bo
|
|||
}
|
||||
|
||||
pub async fn trends_video_tab(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let context = rp.get_context(ClientType::Desktop, true, None).await;
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context,
|
||||
browse_id: "FEtrending",
|
||||
params: None,
|
||||
},
|
||||
|
|
@ -207,13 +201,11 @@ pub async fn trends_video_tab(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
}
|
||||
|
||||
pub async fn trends_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let context = rp.get_context(ClientType::Desktop, true, None).await;
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context,
|
||||
browse_id: "FEtrending",
|
||||
params: None,
|
||||
},
|
||||
|
|
@ -237,7 +229,6 @@ pub async fn discography_page(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
ClientType::DesktopMusic,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: rp.get_context(ClientType::DesktopMusic, true, None).await,
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
|
|
@ -301,7 +292,6 @@ pub async fn channel_about_modal(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: rp.get_context(ClientType::Desktop, true, None).await,
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
|
|
@ -317,7 +307,6 @@ pub async fn like_button_viewmodel(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
ClientType::Desktop,
|
||||
"next",
|
||||
&QVideo {
|
||||
context: rp.get_context(ClientType::Desktop, true, None).await,
|
||||
video_id: "ZeerrnuLi5E",
|
||||
content_check_ok: true,
|
||||
racy_check_ok: true,
|
||||
|
|
@ -341,7 +330,6 @@ pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
ClientType::DesktopMusic,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: rp.get_context(ClientType::DesktopMusic, true, None).await,
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
|
|
@ -355,14 +343,7 @@ pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
let continuation =
|
||||
"Eg0SC3dMZHBSN2d1S3k4GAYyJSIRIgt3TGRwUjdndUt5ODAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::Desktop,
|
||||
"next",
|
||||
&QCont {
|
||||
context: rp.get_context(ClientType::Desktop, true, None).await,
|
||||
continuation,
|
||||
},
|
||||
)
|
||||
.raw(ClientType::Desktop, "next", &QCont { continuation })
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(res.contains("\"frameworkUpdates\""))
|
||||
|
|
@ -375,7 +356,6 @@ pub async fn channel_shorts_lockup(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: rp.get_context(ClientType::Desktop, true, None).await,
|
||||
browse_id: id,
|
||||
params: Some("EgZzaG9ydHPyBgUKA5oBAA%3D%3D"),
|
||||
},
|
||||
|
|
@ -392,7 +372,6 @@ pub async fn playlist_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool>
|
|||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: rp.get_context(ClientType::Desktop, true, None).await,
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -95,11 +95,7 @@ struct HeaderRenderer {
|
|||
}
|
||||
|
||||
async fn get_album_type(query: &RustyPipeQuery, id: &str) -> String {
|
||||
let context = query
|
||||
.get_context(ClientType::DesktopMusic, true, None)
|
||||
.await;
|
||||
let body = QBrowse {
|
||||
context,
|
||||
browse_id: id,
|
||||
params: None,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -350,7 +350,6 @@ async fn get_channel(query: &RustyPipeQuery, channel_id: &str) -> Result<Channel
|
|||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: query.get_context(ClientType::Desktop, true, None).await,
|
||||
browse_id: channel_id,
|
||||
params: Some("EgZ2aWRlb3MYASAAMAE"),
|
||||
},
|
||||
|
|
@ -392,7 +391,6 @@ async fn get_channel(query: &RustyPipeQuery, channel_id: &str) -> Result<Channel
|
|||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QCont {
|
||||
context: query.get_context(ClientType::Desktop, true, None).await,
|
||||
continuation: &popular_token,
|
||||
},
|
||||
)
|
||||
|
|
@ -431,9 +429,6 @@ async fn music_channel_subscribers(query: &RustyPipeQuery, channel_id: &str) ->
|
|||
ClientType::DesktopMusic,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: query
|
||||
.get_context(ClientType::DesktopMusic, true, None)
|
||||
.await,
|
||||
browse_id: channel_id,
|
||||
params: None,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -270,7 +270,6 @@ async fn get_channel_vlengths(
|
|||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: query.get_context(ClientType::Desktop, true, None).await,
|
||||
browse_id: channel_id,
|
||||
params: Some("EgZ2aWRlb3MYASAAMAE"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ordered_hash_map::OrderedHashMap;
|
||||
use rustypipe::{client::YTContext, model::AlbumType, param::Language};
|
||||
use rustypipe::{model::AlbumType, param::Language};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
||||
|
||||
|
|
@ -116,7 +116,6 @@ pub enum ExtItemType {
|
|||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QBrowse<'a> {
|
||||
pub context: YTContext<'a>,
|
||||
pub browse_id: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub params: Option<&'a str>,
|
||||
|
|
@ -125,7 +124,6 @@ pub struct QBrowse<'a> {
|
|||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QCont<'a> {
|
||||
pub context: YTContext<'a>,
|
||||
pub continuation: &'a str,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,11 @@ use crate::{
|
|||
util::{self, timeago, ProtoBuilder},
|
||||
};
|
||||
|
||||
use super::{
|
||||
response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext,
|
||||
};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QChannel<'a> {
|
||||
context: YTContext<'a>,
|
||||
browse_id: &'a str,
|
||||
params: ChannelTab,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -63,9 +60,7 @@ impl RustyPipeQuery {
|
|||
operation: &str,
|
||||
) -> Result<Channel<Paginator<VideoItem>>, Error> {
|
||||
let channel_id = channel_id.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QChannel {
|
||||
context,
|
||||
browse_id: channel_id,
|
||||
params,
|
||||
query,
|
||||
|
|
@ -125,12 +120,10 @@ impl RustyPipeQuery {
|
|||
tab: ChannelVideoTab,
|
||||
order: ChannelOrder,
|
||||
) -> Result<Paginator<VideoItem>, Error> {
|
||||
let visitor_data = Some(self.get_visitor_data().await?);
|
||||
|
||||
self.continuation(
|
||||
order_ctoken(channel_id.as_ref(), tab, order, &random_target()),
|
||||
ContinuationEndpoint::Browse,
|
||||
visitor_data.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -158,9 +151,7 @@ impl RustyPipeQuery {
|
|||
channel_id: S,
|
||||
) -> Result<Channel<Paginator<PlaylistItem>>, Error> {
|
||||
let channel_id = channel_id.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QChannel {
|
||||
context,
|
||||
browse_id: channel_id,
|
||||
params: ChannelTab::Playlists,
|
||||
query: None,
|
||||
|
|
@ -183,9 +174,7 @@ impl RustyPipeQuery {
|
|||
channel_id: S,
|
||||
) -> Result<ChannelInfo, Error> {
|
||||
let channel_id = channel_id.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, false, None).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: &channel_info_ctoken(channel_id, &random_target()),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ impl ClientType {
|
|||
/// YouTube context request parameter
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct YTContext<'a> {
|
||||
struct YTContext<'a> {
|
||||
client: ClientInfo<'a>,
|
||||
/// only used on desktop
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -119,8 +119,7 @@ struct ClientInfo<'a> {
|
|||
platform: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
original_url: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
visitor_data: Option<&'a str>,
|
||||
visitor_data: &'a str,
|
||||
hl: Language,
|
||||
gl: Country,
|
||||
time_zone: &'a str,
|
||||
|
|
@ -136,7 +135,7 @@ impl Default for ClientInfo<'_> {
|
|||
device_model: None,
|
||||
platform: "",
|
||||
original_url: None,
|
||||
visitor_data: None,
|
||||
visitor_data: "",
|
||||
hl: Language::En,
|
||||
gl: Country::Us,
|
||||
time_zone: "UTC",
|
||||
|
|
@ -173,17 +172,22 @@ struct ThirdParty<'a> {
|
|||
embed_url: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct QBody<'a, T> {
|
||||
context: YTContext<'a>,
|
||||
#[serde(flatten)]
|
||||
body: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QBrowse<'a> {
|
||||
context: YTContext<'a>,
|
||||
browse_id: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QBrowseParams<'a> {
|
||||
context: YTContext<'a>,
|
||||
browse_id: &'a str,
|
||||
params: &'a str,
|
||||
}
|
||||
|
|
@ -191,7 +195,6 @@ struct QBrowseParams<'a> {
|
|||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QContinuation<'a> {
|
||||
context: YTContext<'a>,
|
||||
continuation: &'a str,
|
||||
}
|
||||
|
||||
|
|
@ -1126,18 +1129,17 @@ impl RustyPipeQuery {
|
|||
/// # Parameters
|
||||
/// - `ctype`: Client type (`Desktop`, `DesktopMusic`, `Android`, ...)
|
||||
/// - `localized`: Whether to include the configured language and country
|
||||
pub async fn get_context<'a>(
|
||||
async fn get_context<'a>(
|
||||
&'a self,
|
||||
ctype: ClientType,
|
||||
localized: bool,
|
||||
visitor_data: Option<&'a str>,
|
||||
visitor_data: &'a str,
|
||||
) -> YTContext {
|
||||
let (hl, gl) = if localized {
|
||||
(self.opts.lang, self.opts.country)
|
||||
} else {
|
||||
(Language::En, Country::Us)
|
||||
};
|
||||
let visitor_data = visitor_data.or(self.opts.visitor_data.as_deref());
|
||||
|
||||
match ctype {
|
||||
ClientType::Desktop => YTContext {
|
||||
|
|
@ -1451,11 +1453,22 @@ impl RustyPipeQuery {
|
|||
) -> Result<M, Error> {
|
||||
tracing::debug!("getting {}({})", operation, id);
|
||||
|
||||
let visitor_data = ctx_src
|
||||
.visitor_data
|
||||
.or(self.opts.visitor_data.as_deref())
|
||||
.map(Cow::Borrowed)
|
||||
.unwrap_or_else(|| util::random_visitor_data(self.opts.country).into());
|
||||
|
||||
let context = self
|
||||
.get_context(ctype, !ctx_src.unlocalized, &visitor_data)
|
||||
.await;
|
||||
let req_body = QBody { context, body };
|
||||
|
||||
let ctx = MapRespCtx {
|
||||
id,
|
||||
lang: self.opts.lang,
|
||||
deobf: ctx_src.deobf,
|
||||
visitor_data: ctx_src.visitor_data.or(self.opts.visitor_data.as_deref()),
|
||||
visitor_data: Some(&visitor_data),
|
||||
client_type: ctype,
|
||||
artist: ctx_src.artist,
|
||||
};
|
||||
|
|
@ -1463,7 +1476,7 @@ impl RustyPipeQuery {
|
|||
let request = self
|
||||
.request_builder(ctype, endpoint, ctx.visitor_data)
|
||||
.await
|
||||
.json(body)
|
||||
.json(&req_body)
|
||||
.build()?;
|
||||
|
||||
let req_res = self.yt_request::<R, M>(&request, &ctx).await?;
|
||||
|
|
@ -1563,47 +1576,6 @@ impl RustyPipeQuery {
|
|||
.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
|
||||
///
|
||||
/// # Parameters
|
||||
|
|
@ -1616,10 +1588,20 @@ impl RustyPipeQuery {
|
|||
endpoint: &str,
|
||||
body: &B,
|
||||
) -> Result<String, Error> {
|
||||
let visitor_data = self
|
||||
.opts
|
||||
.visitor_data
|
||||
.as_deref()
|
||||
.map(Cow::Borrowed)
|
||||
.unwrap_or_else(|| util::random_visitor_data(self.opts.country).into());
|
||||
|
||||
let context = self.get_context(ctype, true, &visitor_data).await;
|
||||
let req_body = QBody { context, body };
|
||||
|
||||
let request = self
|
||||
.request_builder(ctype, endpoint, None)
|
||||
.await
|
||||
.json(body)
|
||||
.json(&req_body)
|
||||
.build()?;
|
||||
|
||||
self.client.http_request_txt(&request).await
|
||||
|
|
@ -1646,6 +1628,7 @@ struct MapRespCtxSource<'a> {
|
|||
visitor_data: Option<&'a str>,
|
||||
deobf: Option<&'a DeobfData>,
|
||||
artist: Option<ArtistId>,
|
||||
unlocalized: bool,
|
||||
}
|
||||
|
||||
impl<'a> MapRespCtx<'a> {
|
||||
|
|
@ -1663,15 +1646,6 @@ impl<'a> MapRespCtx<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> MapRespCtxSource<'a> {
|
||||
fn visitor_data(visitor_data: &'a str) -> Self {
|
||||
Self {
|
||||
visitor_data: Some(visitor_data),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement this for YouTube API response structs that need to be mapped to
|
||||
/// RustyPipe models.
|
||||
trait MapResponse<T> {
|
||||
|
|
|
|||
|
|
@ -41,9 +41,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
async fn _music_artist(&self, artist_id: &str, all_albums: bool) -> Result<MusicArtist, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: artist_id,
|
||||
};
|
||||
|
||||
|
|
@ -84,24 +82,18 @@ impl RustyPipeQuery {
|
|||
filter: Option<AlbumFilter>,
|
||||
order: Option<AlbumOrder>,
|
||||
) -> Result<Vec<AlbumItem>, Error> {
|
||||
let visitor_data = self.get_visitor_data().await?;
|
||||
let context = self
|
||||
.get_context(ClientType::DesktopMusic, true, Some(&visitor_data))
|
||||
.await;
|
||||
let request_body = QBrowseParams {
|
||||
context: context.clone(),
|
||||
browse_id: &format!("{}{}", util::ARTIST_DISCOGRAPHY_PREFIX, artist_id),
|
||||
params: &albums_param(filter, order),
|
||||
};
|
||||
|
||||
let first_page = self
|
||||
.execute_request_ctx::<response::MusicArtistAlbums, _, _>(
|
||||
.execute_request::<response::MusicArtistAlbums, _, _>(
|
||||
ClientType::DesktopMusic,
|
||||
"music_artist_albums",
|
||||
artist_id,
|
||||
"browse",
|
||||
&request_body,
|
||||
MapRespCtxSource::visitor_data(&visitor_data),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
@ -109,10 +101,7 @@ impl RustyPipeQuery {
|
|||
let mut ctoken = first_page.ctoken;
|
||||
|
||||
while let Some(tkn) = &ctoken {
|
||||
let request_body = QContinuation {
|
||||
context: context.clone(),
|
||||
continuation: tkn,
|
||||
};
|
||||
let request_body = QContinuation { continuation: tkn };
|
||||
let resp: Paginator<MusicItem> = self
|
||||
.execute_request_ctx::<response::MusicContinuation, Paginator<MusicItem>, _>(
|
||||
ClientType::DesktopMusic,
|
||||
|
|
@ -122,7 +111,6 @@ impl RustyPipeQuery {
|
|||
&request_body,
|
||||
MapRespCtxSource {
|
||||
artist: Some(first_page.artist.clone()),
|
||||
visitor_data: Some(&visitor_data),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType},
|
||||
ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapResponse, RustyPipeQuery,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QCharts<'a> {
|
||||
context: YTContext<'a>,
|
||||
browse_id: &'a str,
|
||||
params: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -34,9 +33,7 @@ impl RustyPipeQuery {
|
|||
/// Get the YouTube Music charts for a given country
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_charts(&self, country: Option<Country>) -> Result<MusicCharts, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QCharts {
|
||||
context,
|
||||
browse_id: "FEmusic_charts",
|
||||
params: "sgYPRkVtdXNpY19leHBsb3Jl",
|
||||
form_data: country.map(|c| FormData {
|
||||
|
|
|
|||
|
|
@ -16,12 +16,11 @@ use super::{
|
|||
self,
|
||||
music_item::{map_queue_item, MusicListMapper},
|
||||
},
|
||||
ClientType, MapRespCtx, MapRespCtxSource, MapResponse, QBrowse, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct QMusicDetails<'a> {
|
||||
context: YTContext<'a>,
|
||||
video_id: &'a str,
|
||||
enable_persistent_playlist_panel: bool,
|
||||
is_audio_only: bool,
|
||||
|
|
@ -30,7 +29,6 @@ struct QMusicDetails<'a> {
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct QRadio<'a> {
|
||||
context: YTContext<'a>,
|
||||
playlist_id: &'a str,
|
||||
params: &'a str,
|
||||
enable_persistent_playlist_panel: bool,
|
||||
|
|
@ -46,9 +44,7 @@ impl RustyPipeQuery {
|
|||
video_id: S,
|
||||
) -> Result<TrackDetails, Error> {
|
||||
let video_id = video_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QMusicDetails {
|
||||
context,
|
||||
video_id,
|
||||
enable_persistent_playlist_panel: true,
|
||||
is_audio_only: true,
|
||||
|
|
@ -71,9 +67,7 @@ impl RustyPipeQuery {
|
|||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_lyrics<S: AsRef<str> + Debug>(&self, lyrics_id: S) -> Result<Lyrics, Error> {
|
||||
let lyrics_id = lyrics_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: lyrics_id,
|
||||
};
|
||||
|
||||
|
|
@ -96,9 +90,7 @@ impl RustyPipeQuery {
|
|||
related_id: S,
|
||||
) -> Result<MusicRelated, Error> {
|
||||
let related_id = related_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: related_id,
|
||||
};
|
||||
|
||||
|
|
@ -121,12 +113,7 @@ impl RustyPipeQuery {
|
|||
radio_id: S,
|
||||
) -> Result<Paginator<TrackItem>, Error> {
|
||||
let radio_id = radio_id.as_ref();
|
||||
let visitor_data = self.get_visitor_data().await?;
|
||||
let context = self
|
||||
.get_context(ClientType::DesktopMusic, true, Some(&visitor_data))
|
||||
.await;
|
||||
let request_body = QRadio {
|
||||
context,
|
||||
playlist_id: radio_id,
|
||||
params: "wAEB8gECeAE%3D",
|
||||
enable_persistent_playlist_panel: true,
|
||||
|
|
@ -134,13 +121,12 @@ impl RustyPipeQuery {
|
|||
tuner_setting_value: "AUTOMIX_SETTING_NORMAL",
|
||||
};
|
||||
|
||||
self.execute_request_ctx::<response::MusicDetails, _, _>(
|
||||
self.execute_request::<response::MusicDetails, _, _>(
|
||||
ClientType::DesktopMusic,
|
||||
"music_radio",
|
||||
radio_id,
|
||||
"next",
|
||||
&request_body,
|
||||
MapRespCtxSource::visitor_data(&visitor_data),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ impl RustyPipeQuery {
|
|||
/// Get a list of moods and genres from YouTube Music
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: "FEmusic_moods_and_genres",
|
||||
};
|
||||
|
||||
|
|
@ -38,9 +36,7 @@ impl RustyPipeQuery {
|
|||
genre_id: S,
|
||||
) -> Result<MusicGenre, Error> {
|
||||
let genre_id = genre_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowseParams {
|
||||
context,
|
||||
browse_id: "FEmusic_moods_and_genres_category",
|
||||
params: genre_id,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ impl RustyPipeQuery {
|
|||
/// Get the new albums that were released on YouTube Music
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_new_albums(&self) -> Result<Vec<AlbumItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: "FEmusic_new_releases_albums",
|
||||
};
|
||||
|
||||
|
|
@ -32,9 +30,7 @@ impl RustyPipeQuery {
|
|||
/// Get the new music videos that were released on YouTube Music
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_new_videos(&self) -> Result<Vec<TrackItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: "FEmusic_new_releases_videos",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use super::{
|
|||
self,
|
||||
music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper},
|
||||
},
|
||||
ClientType, MapRespCtx, MapRespCtxSource, MapResponse, QBrowse, RustyPipeQuery,
|
||||
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
|
||||
};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
|
|
@ -28,30 +28,16 @@ impl RustyPipeQuery {
|
|||
playlist_id: S,
|
||||
) -> Result<MusicPlaylist, Error> {
|
||||
let playlist_id = playlist_id.as_ref();
|
||||
// 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 {
|
||||
context,
|
||||
browse_id: &format!("VL{playlist_id}"),
|
||||
};
|
||||
|
||||
self.execute_request_ctx::<response::MusicPlaylist, _, _>(
|
||||
self.execute_request::<response::MusicPlaylist, _, _>(
|
||||
ClientType::DesktopMusic,
|
||||
"music_playlist",
|
||||
playlist_id,
|
||||
"browse",
|
||||
&request_body,
|
||||
MapRespCtxSource {
|
||||
visitor_data: visitor_data.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -63,9 +49,7 @@ impl RustyPipeQuery {
|
|||
album_id: S,
|
||||
) -> Result<MusicAlbum, Error> {
|
||||
let album_id = album_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: album_id,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,11 @@ use crate::{
|
|||
serializer::MapResult,
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, RustyPipeQuery};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QSearch<'a> {
|
||||
context: YTContext<'a>,
|
||||
query: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
params: Option<&'a str>,
|
||||
|
|
@ -29,7 +28,6 @@ struct QSearch<'a> {
|
|||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QSearchSuggestion<'a> {
|
||||
context: YTContext<'a>,
|
||||
input: &'a str,
|
||||
}
|
||||
|
||||
|
|
@ -44,9 +42,7 @@ impl RustyPipeQuery {
|
|||
filter: Option<MusicSearchFilter>,
|
||||
) -> Result<MusicSearchResult<T>, Error> {
|
||||
let query = query.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QSearch {
|
||||
context,
|
||||
query,
|
||||
params: filter.map(MusicSearchFilter::params),
|
||||
};
|
||||
|
|
@ -132,11 +128,7 @@ impl RustyPipeQuery {
|
|||
query: S,
|
||||
) -> Result<MusicSearchSuggestion, Error> {
|
||||
let query = query.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QSearchSuggestion {
|
||||
context,
|
||||
input: query,
|
||||
};
|
||||
let request_body = QSearchSuggestion { input: query };
|
||||
|
||||
self.execute_request::<response::MusicSearchSuggestion, _, _>(
|
||||
ClientType::DesktopMusic,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::error::{Error, ExtractionError};
|
||||
|
|
@ -25,16 +24,7 @@ impl RustyPipeQuery {
|
|||
) -> Result<Paginator<T>, Error> {
|
||||
let ctoken = ctoken.as_ref();
|
||||
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
|
||||
.get_context(ClientType::DesktopMusic, true, Some(&visitor_data))
|
||||
.await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
};
|
||||
|
||||
|
|
@ -45,59 +35,60 @@ impl RustyPipeQuery {
|
|||
ctoken,
|
||||
endpoint.as_str(),
|
||||
&request_body,
|
||||
MapRespCtxSource::visitor_data(&visitor_data),
|
||||
MapRespCtxSource {
|
||||
visitor_data,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(map_ytm_paginator(p, Some(&visitor_data), endpoint))
|
||||
Ok(map_ytm_paginator(p, endpoint))
|
||||
} else {
|
||||
let context = self
|
||||
.get_context(ClientType::Desktop, true, visitor_data)
|
||||
.await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
};
|
||||
|
||||
let p = self
|
||||
.execute_request::<response::Continuation, Paginator<YouTubeItem>, _>(
|
||||
.execute_request_ctx::<response::Continuation, Paginator<YouTubeItem>, _>(
|
||||
ClientType::Desktop,
|
||||
"continuation",
|
||||
ctoken,
|
||||
endpoint.as_str(),
|
||||
&request_body,
|
||||
MapRespCtxSource {
|
||||
visitor_data,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(map_yt_paginator(p, visitor_data, endpoint))
|
||||
Ok(map_yt_paginator(p, endpoint))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_yt_paginator<T: FromYtItem>(
|
||||
p: Paginator<YouTubeItem>,
|
||||
visitor_data: Option<&str>,
|
||||
endpoint: ContinuationEndpoint,
|
||||
) -> Paginator<T> {
|
||||
Paginator {
|
||||
count: p.count,
|
||||
items: p.items.into_iter().filter_map(T::from_yt_item).collect(),
|
||||
ctoken: p.ctoken,
|
||||
visitor_data: visitor_data.map(str::to_owned),
|
||||
visitor_data: p.visitor_data,
|
||||
endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_ytm_paginator<T: FromYtItem>(
|
||||
p: Paginator<MusicItem>,
|
||||
visitor_data: Option<&str>,
|
||||
endpoint: ContinuationEndpoint,
|
||||
) -> Paginator<T> {
|
||||
Paginator {
|
||||
count: p.count,
|
||||
items: p.items.into_iter().filter_map(T::from_ytm_item).collect(),
|
||||
ctoken: p.ctoken,
|
||||
visitor_data: visitor_data.map(str::to_owned),
|
||||
visitor_data: p.visitor_data,
|
||||
endpoint,
|
||||
}
|
||||
}
|
||||
|
|
@ -430,7 +421,7 @@ mod tests {
|
|||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<VideoItem> =
|
||||
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
|
||||
map_yt_paginator(map_res.c, ContinuationEndpoint::Browse);
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
@ -453,7 +444,7 @@ mod tests {
|
|||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<PlaylistItem> =
|
||||
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
|
||||
map_yt_paginator(map_res.c, ContinuationEndpoint::Browse);
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
@ -476,7 +467,7 @@ mod tests {
|
|||
let map_res: MapResult<Paginator<MusicItem>> =
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<TrackItem> =
|
||||
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
||||
map_ytm_paginator(map_res.c, ContinuationEndpoint::MusicBrowse);
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
@ -497,7 +488,7 @@ mod tests {
|
|||
let map_res: MapResult<Paginator<MusicItem>> =
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<MusicPlaylistItem> =
|
||||
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
||||
map_ytm_paginator(map_res.c, ContinuationEndpoint::MusicBrowse);
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
|||
|
|
@ -24,14 +24,13 @@ use super::{
|
|||
self,
|
||||
player::{self, Format},
|
||||
},
|
||||
ClientType, MapRespCtx, MapRespCtxSource, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapRespCtxSource, MapResponse, MapResult, RustyPipeQuery,
|
||||
DEFAULT_PLAYER_CLIENT_ORDER,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QPlayer<'a> {
|
||||
context: YTContext<'a>,
|
||||
/// Website playback context
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
playback_context: Option<QPlaybackContext<'a>>,
|
||||
|
|
@ -124,16 +123,10 @@ impl RustyPipeQuery {
|
|||
client_type: ClientType,
|
||||
) -> Result<VideoPlayer, Error> {
|
||||
let video_id = video_id.as_ref();
|
||||
// let vdata = self.get_visitor_data().await?;
|
||||
let (context, deobf) = tokio::join!(
|
||||
self.get_context(client_type, false, None),
|
||||
self.client.get_deobf_data()
|
||||
);
|
||||
let deobf = deobf?;
|
||||
let deobf = self.client.get_deobf_data().await?;
|
||||
|
||||
let request_body = if client_type.is_web() {
|
||||
QPlayer {
|
||||
context,
|
||||
playback_context: Some(QPlaybackContext {
|
||||
content_playback_context: QContentPlaybackContext {
|
||||
signature_timestamp: &deobf.sts,
|
||||
|
|
@ -148,7 +141,6 @@ impl RustyPipeQuery {
|
|||
}
|
||||
} else {
|
||||
QPlayer {
|
||||
context,
|
||||
playback_context: None,
|
||||
cpn: Some(util::generate_content_playback_nonce()),
|
||||
video_id,
|
||||
|
|
|
|||
|
|
@ -13,40 +13,23 @@ use crate::{
|
|||
util::{self, dictionary, timeago, TryRemove},
|
||||
};
|
||||
|
||||
use super::{
|
||||
response, ClientType, MapRespCtx, MapRespCtxSource, MapResponse, MapResult, QBrowse,
|
||||
RustyPipeQuery,
|
||||
};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, QBrowse, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a YouTube playlist
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn playlist<S: AsRef<str> + Debug>(&self, playlist_id: S) -> Result<Playlist, Error> {
|
||||
let playlist_id = playlist_id.as_ref();
|
||||
// YTM playlists require visitor data for continuations to work
|
||||
let visitor_data: Option<String> = if playlist_id.starts_with("RD") {
|
||||
Some(self.get_visitor_data().await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let context = self
|
||||
.get_context(ClientType::Desktop, true, visitor_data.as_deref())
|
||||
.await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: &format!("VL{playlist_id}"),
|
||||
};
|
||||
|
||||
self.execute_request_ctx::<response::Playlist, _, _>(
|
||||
self.execute_request::<response::Playlist, _, _>(
|
||||
ClientType::Desktop,
|
||||
"playlist",
|
||||
playlist_id,
|
||||
"browse",
|
||||
&request_body,
|
||||
MapRespCtxSource {
|
||||
visitor_data: visitor_data.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,11 @@ use crate::{
|
|||
param::search_filter::SearchFilter,
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QSearch<'a> {
|
||||
context: YTContext<'a>,
|
||||
query: &'a str,
|
||||
params: &'a str,
|
||||
}
|
||||
|
|
@ -30,9 +29,7 @@ impl RustyPipeQuery {
|
|||
query: S,
|
||||
) -> Result<SearchResult<T>, Error> {
|
||||
let query = query.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QSearch {
|
||||
context,
|
||||
query,
|
||||
params: "8AEB",
|
||||
};
|
||||
|
|
@ -55,9 +52,7 @@ impl RustyPipeQuery {
|
|||
filter: &SearchFilter,
|
||||
) -> Result<SearchResult<T>, Error> {
|
||||
let query = query.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QSearch {
|
||||
context,
|
||||
query,
|
||||
params: &filter.encode(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ impl RustyPipeQuery {
|
|||
/// Get the videos from the YouTube trending page
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn trending(&self) -> Result<Vec<VideoItem>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QBrowseParams {
|
||||
context,
|
||||
browse_id: "FEtrending",
|
||||
params: "4gIOGgxtb3N0X3BvcHVsYXI%3D",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
response::{self, url_endpoint::NavigationEndpoint},
|
||||
ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapResponse, RustyPipeQuery,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QResolveUrl<'a> {
|
||||
context: YTContext<'a>,
|
||||
struct QResolveUrl {
|
||||
url: String,
|
||||
}
|
||||
|
||||
|
|
@ -299,9 +298,7 @@ impl RustyPipeQuery {
|
|||
url_path: &str,
|
||||
ctype: ClientType,
|
||||
) -> Result<UrlTarget, Error> {
|
||||
let context = self.get_context(ctype, true, None).await;
|
||||
let request_body = QResolveUrl {
|
||||
context,
|
||||
url: format!(
|
||||
"https://{}.youtube.com{}",
|
||||
match ctype {
|
||||
|
|
|
|||
|
|
@ -15,12 +15,11 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
response::{self, video_details::Payload, IconType},
|
||||
ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct QVideo<'a> {
|
||||
context: YTContext<'a>,
|
||||
/// YouTube video ID
|
||||
video_id: &'a str,
|
||||
/// Set to true to allow extraction of streams with sensitive content
|
||||
|
|
@ -37,9 +36,7 @@ impl RustyPipeQuery {
|
|||
video_id: S,
|
||||
) -> Result<VideoDetails, Error> {
|
||||
let video_id = video_id.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QVideo {
|
||||
context,
|
||||
video_id,
|
||||
content_check_ok: true,
|
||||
racy_check_ok: true,
|
||||
|
|
@ -63,11 +60,7 @@ impl RustyPipeQuery {
|
|||
visitor_data: Option<&str>,
|
||||
) -> Result<Paginator<Comment>, Error> {
|
||||
let ctoken = ctoken.as_ref();
|
||||
let context = self
|
||||
.get_context(ClientType::Desktop, true, visitor_data)
|
||||
.await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,29 @@ pub fn random_uuid() -> String {
|
|||
)
|
||||
}
|
||||
|
||||
/// Generate a random visitor data cookie
|
||||
pub fn random_visitor_data(country: Country) -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let mut pb_e2 = ProtoBuilder::new();
|
||||
pb_e2.string(2, "");
|
||||
pb_e2.varint(4, rng.gen_range(1..256));
|
||||
|
||||
let mut pb_e = ProtoBuilder::new();
|
||||
pb_e.string(1, &country.to_string());
|
||||
pb_e.embedded(2, pb_e2);
|
||||
|
||||
let mut pb = ProtoBuilder::new();
|
||||
pb.string(1, &random_string(CONTENT_PLAYBACK_NONCE_ALPHABET, 11));
|
||||
pb.varint(
|
||||
5,
|
||||
(time::OffsetDateTime::now_utc().unix_timestamp() as u64)
|
||||
.saturating_sub(rng.gen_range(0..600_000)),
|
||||
);
|
||||
pb.embedded(6, pb_e);
|
||||
pb.to_base64()
|
||||
}
|
||||
|
||||
/// Split an URL into its base string and parameter map
|
||||
///
|
||||
/// Example:
|
||||
|
|
|
|||
Reference in a new issue