Compare commits
3 commits
4ebee5856e
...
8026b08e2d
Author | SHA1 | Date | |
---|---|---|---|
8026b08e2d | |||
17f71dc9f5 | |||
1c0c64a8bf |
48 changed files with 27834 additions and 100 deletions
|
@ -22,6 +22,8 @@ pub async fn download_testfiles(project_root: &Path) {
|
||||||
comments_latest(&testfiles).await;
|
comments_latest(&testfiles).await;
|
||||||
recommendations(&testfiles).await;
|
recommendations(&testfiles).await;
|
||||||
channel_videos(&testfiles).await;
|
channel_videos(&testfiles).await;
|
||||||
|
channel_shorts(&testfiles).await;
|
||||||
|
channel_livestreams(&testfiles).await;
|
||||||
channel_playlists(&testfiles).await;
|
channel_playlists(&testfiles).await;
|
||||||
channel_info(&testfiles).await;
|
channel_info(&testfiles).await;
|
||||||
channel_videos_cont(&testfiles).await;
|
channel_videos_cont(&testfiles).await;
|
||||||
|
@ -258,6 +260,36 @@ async fn channel_videos(testfiles: &Path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn channel_shorts(testfiles: &Path) {
|
||||||
|
let mut json_path = testfiles.to_path_buf();
|
||||||
|
json_path.push("channel");
|
||||||
|
json_path.push("channel_shorts.json");
|
||||||
|
if json_path.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rp = rp_testfile(&json_path);
|
||||||
|
rp.query()
|
||||||
|
.channel_shorts("UCh8gHdtzO2tXd593_bjErWg")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn channel_livestreams(testfiles: &Path) {
|
||||||
|
let mut json_path = testfiles.to_path_buf();
|
||||||
|
json_path.push("channel");
|
||||||
|
json_path.push("channel_livestreams.json");
|
||||||
|
if json_path.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rp = rp_testfile(&json_path);
|
||||||
|
rp.query()
|
||||||
|
.channel_livestreams("UC2DjFE7Xf11URZqWBigcVOQ")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
async fn channel_playlists(testfiles: &Path) {
|
async fn channel_playlists(testfiles: &Path) {
|
||||||
let mut json_path = testfiles.to_path_buf();
|
let mut json_path = testfiles.to_path_buf();
|
||||||
json_path.push("channel");
|
json_path.push("channel");
|
||||||
|
|
|
@ -13,7 +13,10 @@ use crate::{
|
||||||
util::{self, TryRemove},
|
util::{self, TryRemove},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
|
use super::{
|
||||||
|
response::{self, channel::ChannelContent},
|
||||||
|
ClientType, MapResponse, RustyPipeQuery, YTContext,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -27,6 +30,10 @@ struct QChannel<'a> {
|
||||||
enum Params {
|
enum Params {
|
||||||
#[serde(rename = "EgZ2aWRlb3PyBgQKAjoA")]
|
#[serde(rename = "EgZ2aWRlb3PyBgQKAjoA")]
|
||||||
Videos,
|
Videos,
|
||||||
|
#[serde(rename = "EgZzaG9ydHPyBgUKA5oBAA%3D%3D")]
|
||||||
|
Shorts,
|
||||||
|
#[serde(rename = "EgdzdHJlYW1z8gYECgJ6AA%3D%3D")]
|
||||||
|
Live,
|
||||||
#[serde(rename = "EglwbGF5bGlzdHMgAQ%3D%3D")]
|
#[serde(rename = "EglwbGF5bGlzdHMgAQ%3D%3D")]
|
||||||
Playlists,
|
Playlists,
|
||||||
#[serde(rename = "EgVhYm91dPIGBAoCEgA%3D")]
|
#[serde(rename = "EgVhYm91dPIGBAoCEgA%3D")]
|
||||||
|
@ -34,20 +41,22 @@ enum Params {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
pub async fn channel_videos(
|
async fn _channel_videos(
|
||||||
&self,
|
&self,
|
||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
|
params: Params,
|
||||||
|
operation: &str,
|
||||||
) -> Result<Channel<Paginator<VideoItem>>, Error> {
|
) -> Result<Channel<Paginator<VideoItem>>, Error> {
|
||||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||||
let request_body = QChannel {
|
let request_body = QChannel {
|
||||||
context,
|
context,
|
||||||
browse_id: channel_id,
|
browse_id: channel_id,
|
||||||
params: Params::Videos,
|
params,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.execute_request::<response::Channel, _, _>(
|
self.execute_request::<response::Channel, _, _>(
|
||||||
ClientType::Desktop,
|
ClientType::Desktop,
|
||||||
"channel_videos",
|
operation,
|
||||||
channel_id,
|
channel_id,
|
||||||
"browse",
|
"browse",
|
||||||
&request_body,
|
&request_body,
|
||||||
|
@ -55,6 +64,30 @@ impl RustyPipeQuery {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn channel_videos(
|
||||||
|
&self,
|
||||||
|
channel_id: &str,
|
||||||
|
) -> Result<Channel<Paginator<VideoItem>>, Error> {
|
||||||
|
self._channel_videos(channel_id, Params::Videos, "channel_videos")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn channel_shorts(
|
||||||
|
&self,
|
||||||
|
channel_id: &str,
|
||||||
|
) -> Result<Channel<Paginator<VideoItem>>, Error> {
|
||||||
|
self._channel_videos(channel_id, Params::Shorts, "channel_shorts")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn channel_livestreams(
|
||||||
|
&self,
|
||||||
|
channel_id: &str,
|
||||||
|
) -> Result<Channel<Paginator<VideoItem>>, Error> {
|
||||||
|
self._channel_videos(channel_id, Params::Live, "channel_livestreams")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn channel_playlists(
|
pub async fn channel_playlists(
|
||||||
&self,
|
&self,
|
||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
|
@ -102,8 +135,8 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||||
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
|
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, id, self.alerts)?;
|
let content = map_channel_content(self.contents, self.alerts)?;
|
||||||
let grid = match content {
|
let grid = match content.content {
|
||||||
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
@ -112,10 +145,15 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: map_channel(
|
c: map_channel(
|
||||||
self.header,
|
MapChannelData {
|
||||||
self.metadata,
|
header: self.header,
|
||||||
self.microformat,
|
metadata: self.metadata,
|
||||||
v_res.c,
|
microformat: self.microformat,
|
||||||
|
visitor_data: self.response_context.visitor_data,
|
||||||
|
has_shorts: content.has_shorts,
|
||||||
|
has_live: content.has_live,
|
||||||
|
content: v_res.c,
|
||||||
|
},
|
||||||
id,
|
id,
|
||||||
lang,
|
lang,
|
||||||
)?,
|
)?,
|
||||||
|
@ -131,8 +169,8 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||||
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
|
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, id, self.alerts)?;
|
let content = map_channel_content(self.contents, self.alerts)?;
|
||||||
let grid = match content {
|
let grid = match content.content {
|
||||||
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
@ -143,10 +181,15 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: map_channel(
|
c: map_channel(
|
||||||
self.header,
|
MapChannelData {
|
||||||
self.metadata,
|
header: self.header,
|
||||||
self.microformat,
|
metadata: self.metadata,
|
||||||
p_res.c,
|
microformat: self.microformat,
|
||||||
|
visitor_data: self.response_context.visitor_data,
|
||||||
|
has_shorts: content.has_shorts,
|
||||||
|
has_live: content.has_live,
|
||||||
|
content: p_res.c,
|
||||||
|
},
|
||||||
id,
|
id,
|
||||||
lang,
|
lang,
|
||||||
)?,
|
)?,
|
||||||
|
@ -162,9 +205,9 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||||
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
|
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, id, self.alerts)?;
|
let content = map_channel_content(self.contents, self.alerts)?;
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
let meta = match content {
|
let meta = match content.content {
|
||||||
response::channel::ChannelContent::ChannelAboutFullMetadataRenderer(meta) => Some(meta),
|
response::channel::ChannelContent::ChannelAboutFullMetadataRenderer(meta) => Some(meta),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
@ -201,10 +244,15 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: map_channel(
|
c: map_channel(
|
||||||
self.header,
|
MapChannelData {
|
||||||
self.metadata,
|
header: self.header,
|
||||||
self.microformat,
|
metadata: self.metadata,
|
||||||
cinfo,
|
microformat: self.microformat,
|
||||||
|
visitor_data: self.response_context.visitor_data,
|
||||||
|
has_shorts: content.has_shorts,
|
||||||
|
has_live: content.has_live,
|
||||||
|
content: cinfo,
|
||||||
|
},
|
||||||
id,
|
id,
|
||||||
lang,
|
lang,
|
||||||
)?,
|
)?,
|
||||||
|
@ -251,23 +299,35 @@ fn map_vanity_url(url: &str, id: &str) -> Option<String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_channel<T>(
|
struct MapChannelData<T> {
|
||||||
header: Option<response::channel::Header>,
|
header: Option<response::channel::Header>,
|
||||||
metadata: Option<response::channel::Metadata>,
|
metadata: Option<response::channel::Metadata>,
|
||||||
microformat: Option<response::channel::Microformat>,
|
microformat: Option<response::channel::Microformat>,
|
||||||
|
visitor_data: Option<String>,
|
||||||
|
has_shorts: bool,
|
||||||
|
has_live: bool,
|
||||||
content: T,
|
content: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_channel<T>(
|
||||||
|
d: MapChannelData<T>,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
) -> Result<Channel<T>, ExtractionError> {
|
) -> Result<Channel<T>, ExtractionError> {
|
||||||
let header = header.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
let header = d
|
||||||
|
.header
|
||||||
|
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
||||||
"channel not found",
|
"channel not found",
|
||||||
)))?;
|
)))?;
|
||||||
let metadata = metadata
|
let metadata = d
|
||||||
|
.metadata
|
||||||
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
||||||
"channel not found",
|
"channel not found",
|
||||||
)))?
|
)))?
|
||||||
.channel_metadata_renderer;
|
.channel_metadata_renderer;
|
||||||
let microformat = microformat.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
let microformat = d
|
||||||
|
.microformat
|
||||||
|
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
|
||||||
"channel not found",
|
"channel not found",
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
|
@ -298,7 +358,10 @@ fn map_channel<T>(
|
||||||
banner: header.banner.into(),
|
banner: header.banner.into(),
|
||||||
mobile_banner: header.mobile_banner.into(),
|
mobile_banner: header.mobile_banner.into(),
|
||||||
tv_banner: header.tv_banner.into(),
|
tv_banner: header.tv_banner.into(),
|
||||||
content,
|
has_shorts: d.has_shorts,
|
||||||
|
has_live: d.has_live,
|
||||||
|
visitor_data: d.visitor_data,
|
||||||
|
content: d.content,
|
||||||
},
|
},
|
||||||
response::channel::Header::CarouselHeaderRenderer(carousel) => {
|
response::channel::Header::CarouselHeaderRenderer(carousel) => {
|
||||||
let hdata = carousel
|
let hdata = carousel
|
||||||
|
@ -332,17 +395,25 @@ fn map_channel<T>(
|
||||||
banner: Vec::new(),
|
banner: Vec::new(),
|
||||||
mobile_banner: Vec::new(),
|
mobile_banner: Vec::new(),
|
||||||
tv_banner: Vec::new(),
|
tv_banner: Vec::new(),
|
||||||
content,
|
has_shorts: d.has_shorts,
|
||||||
|
has_live: d.has_live,
|
||||||
|
visitor_data: d.visitor_data,
|
||||||
|
content: d.content,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct MappedChannelContent {
|
||||||
|
content: response::channel::ChannelContent,
|
||||||
|
has_shorts: bool,
|
||||||
|
has_live: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn map_channel_content(
|
fn map_channel_content(
|
||||||
contents: Option<response::channel::Contents>,
|
contents: Option<response::channel::Contents>,
|
||||||
id: &str,
|
|
||||||
alerts: Option<Vec<response::Alert>>,
|
alerts: Option<Vec<response::Alert>>,
|
||||||
) -> Result<response::channel::ChannelContent, ExtractionError> {
|
) -> Result<MappedChannelContent, ExtractionError> {
|
||||||
match contents {
|
match contents {
|
||||||
Some(contents) => {
|
Some(contents) => {
|
||||||
let tabs = contents.two_column_browse_results_renderer.tabs;
|
let tabs = contents.two_column_browse_results_renderer.tabs;
|
||||||
|
@ -352,42 +423,70 @@ fn map_channel_content(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (channel_content, target_id) = tabs
|
let cmp_url_suffix = |endpoint: &response::channel::ChannelTabEndpoint,
|
||||||
|
expect: &str| {
|
||||||
|
endpoint
|
||||||
|
.command_metadata
|
||||||
|
.web_command_metadata
|
||||||
|
.url
|
||||||
|
.ends_with(expect)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut has_shorts = false;
|
||||||
|
let mut has_live = false;
|
||||||
|
let mut featured_tab = false;
|
||||||
|
|
||||||
|
for tab in &tabs {
|
||||||
|
if cmp_url_suffix(&tab.tab_renderer.endpoint, "/featured")
|
||||||
|
&& (tab.tab_renderer.content.section_list_renderer.is_some()
|
||||||
|
|| tab.tab_renderer.content.rich_grid_renderer.is_some())
|
||||||
|
{
|
||||||
|
featured_tab = true;
|
||||||
|
} else if cmp_url_suffix(&tab.tab_renderer.endpoint, "/shorts") {
|
||||||
|
has_shorts = true;
|
||||||
|
} else if cmp_url_suffix(&tab.tab_renderer.endpoint, "/streams") {
|
||||||
|
has_live = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel_content = tabs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|tab| {
|
.filter_map(|tab| {
|
||||||
let content = tab.tab_renderer.content;
|
let content = tab.tab_renderer.content;
|
||||||
match (content.section_list_renderer, content.rich_grid_renderer) {
|
match (content.rich_grid_renderer, content.section_list_renderer) {
|
||||||
(Some(mut section_list_renderer), _) => {
|
(Some(rich_grid), _) => Some(ChannelContent::GridRenderer {
|
||||||
let content =
|
items: rich_grid.contents,
|
||||||
section_list_renderer.contents.try_swap_remove(0).and_then(
|
}),
|
||||||
|mut i| i.item_section_renderer.contents.try_swap_remove(0),
|
(None, Some(section_list)) => {
|
||||||
);
|
let mut contents = section_list.contents;
|
||||||
|
contents.try_swap_remove(0).and_then(|mut i| {
|
||||||
content.map(|c| (c, section_list_renderer.target_id))
|
i.item_section_renderer.contents.try_swap_remove(0)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
(None, Some(rich_grid_renderer)) => Some((
|
|
||||||
response::channel::ChannelContent::GridRenderer {
|
|
||||||
items: rich_grid_renderer.contents,
|
|
||||||
},
|
|
||||||
rich_grid_renderer.target_id,
|
|
||||||
)),
|
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.next()
|
.next();
|
||||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
|
|
||||||
|
let content = match channel_content {
|
||||||
|
Some(content) => content,
|
||||||
|
None => {
|
||||||
|
// YouTube may show the "Featured" tab if the requested tab is empty/does not exist
|
||||||
|
if featured_tab {
|
||||||
|
response::channel::ChannelContent::None
|
||||||
|
} else {
|
||||||
|
return Err(ExtractionError::InvalidData(Cow::Borrowed(
|
||||||
"could not extract content",
|
"could not extract content",
|
||||||
)))?;
|
)));
|
||||||
|
|
||||||
if let Some(target_id) = target_id {
|
|
||||||
// YouTube falls back to the featured page if the channel does not have a "videos" tab.
|
|
||||||
// This is the case for YouTube Music channels.
|
|
||||||
if target_id.starts_with(&format!("browse-feed{}featured", id)) {
|
|
||||||
return Ok(response::channel::ChannelContent::None);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(channel_content)
|
Ok(MappedChannelContent {
|
||||||
|
content,
|
||||||
|
has_shorts,
|
||||||
|
has_live,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
None => Err(response::alerts_to_err(alerts)),
|
None => Err(response::alerts_to_err(alerts)),
|
||||||
}
|
}
|
||||||
|
@ -407,16 +506,18 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::base("base", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
#[case::base("videos_base", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||||
#[case::music("music", "UC_vmjW5e1xEHhYjY2a0kK1A")]
|
#[case::music("videos_music", "UC_vmjW5e1xEHhYjY2a0kK1A")]
|
||||||
|
#[case::withshorts("videos_shorts", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||||
|
#[case::live("videos_live", "UChs0pSaEoNLV4mevBFGaoKA")]
|
||||||
|
#[case::empty("videos_empty", "UCxBa895m48H5idw5li7h-0g")]
|
||||||
|
#[case::upcoming("videos_upcoming", "UCcvfHa-GHSOHFAjU0-Ie57A")]
|
||||||
|
#[case::richgrid("videos_20221011_richgrid", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||||
|
#[case::richgrid2("videos_20221011_richgrid2", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||||
#[case::shorts("shorts", "UCh8gHdtzO2tXd593_bjErWg")]
|
#[case::shorts("shorts", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||||
#[case::live("live", "UChs0pSaEoNLV4mevBFGaoKA")]
|
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||||
#[case::empty("empty", "UCxBa895m48H5idw5li7h-0g")]
|
|
||||||
#[case::upcoming("upcoming", "UCcvfHa-GHSOHFAjU0-Ie57A")]
|
|
||||||
#[case::richgrid("20221011_richgrid", "UCh8gHdtzO2tXd593_bjErWg")]
|
|
||||||
#[case::richgrid2("20221011_richgrid2", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
|
||||||
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
||||||
let filename = format!("testfiles/channel/channel_videos_{}.json", name);
|
let filename = format!("testfiles/channel/channel_{}.json", name);
|
||||||
let json_path = Path::new(&filename);
|
let json_path = Path::new(&filename);
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
|
@ -431,12 +532,12 @@ mod tests {
|
||||||
map_res.warnings
|
map_res.warnings
|
||||||
);
|
);
|
||||||
|
|
||||||
if name == "upcoming" {
|
if name == "videos_upcoming" {
|
||||||
insta::assert_ron_snapshot!(format!("map_channel_videos_{}", name), map_res.c, {
|
insta::assert_ron_snapshot!(format!("map_channel_{}", name), map_res.c, {
|
||||||
".content.items[1:].publish_date" => "[date]",
|
".content.items[1:].publish_date" => "[date]",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
insta::assert_ron_snapshot!(format!("map_channel_videos_{}", name), map_res.c, {
|
insta::assert_ron_snapshot!(format!("map_channel_{}", name), map_res.c, {
|
||||||
".content.items[].publish_date" => "[date]",
|
".content.items[].publish_date" => "[date]",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,6 +275,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
expires_in_seconds: streaming_data.expires_in_seconds,
|
expires_in_seconds: streaming_data.expires_in_seconds,
|
||||||
hls_manifest_url: streaming_data.hls_manifest_url,
|
hls_manifest_url: streaming_data.hls_manifest_url,
|
||||||
dash_manifest_url: streaming_data.dash_manifest_url,
|
dash_manifest_url: streaming_data.dash_manifest_url,
|
||||||
|
visitor_data: self.response_context.visitor_data,
|
||||||
},
|
},
|
||||||
warnings,
|
warnings,
|
||||||
})
|
})
|
||||||
|
|
|
@ -169,6 +169,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
channel,
|
channel,
|
||||||
last_update,
|
last_update,
|
||||||
last_update_txt,
|
last_update_txt,
|
||||||
|
visitor_data: self.response_context.visitor_data,
|
||||||
},
|
},
|
||||||
warnings,
|
warnings,
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,9 +3,7 @@ use serde_with::serde_as;
|
||||||
use serde_with::{DefaultOnError, VecSkipError};
|
use serde_with::{DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use super::url_endpoint::NavigationEndpoint;
|
use super::url_endpoint::NavigationEndpoint;
|
||||||
use super::{Alert, ChannelBadge};
|
use super::{Alert, ChannelBadge, ContentsRenderer, ResponseContext, Thumbnails, YouTubeListItem};
|
||||||
use super::{ContentRenderer, ContentsRenderer};
|
|
||||||
use super::{Thumbnails, YouTubeListItem};
|
|
||||||
use crate::serializer::ignore_any;
|
use crate::serializer::ignore_any;
|
||||||
use crate::serializer::{text::Text, MapResult, VecLogError};
|
use crate::serializer::{text::Text, MapResult, VecLogError};
|
||||||
|
|
||||||
|
@ -21,6 +19,7 @@ pub(crate) struct Channel {
|
||||||
pub microformat: Option<Microformat>,
|
pub microformat: Option<Microformat>,
|
||||||
#[serde_as(as = "Option<DefaultOnError>")]
|
#[serde_as(as = "Option<DefaultOnError>")]
|
||||||
pub alerts: Option<Vec<Alert>>,
|
pub alerts: Option<Vec<Alert>>,
|
||||||
|
pub response_context: ResponseContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -42,11 +41,19 @@ pub(crate) struct TabsRenderer {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct TabRendererWrap {
|
pub(crate) struct TabRendererWrap {
|
||||||
pub tab_renderer: ContentRenderer<TabContent>,
|
pub tab_renderer: TabRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct TabRenderer {
|
||||||
|
#[serde(default)]
|
||||||
|
pub content: TabContent,
|
||||||
|
pub endpoint: ChannelTabEndpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Default, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct TabContent {
|
pub(crate) struct TabContent {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -58,14 +65,28 @@ pub(crate) struct TabContent {
|
||||||
pub rich_grid_renderer: Option<RichGridRenderer>,
|
pub rich_grid_renderer: Option<RichGridRenderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct ChannelTabEndpoint {
|
||||||
|
pub command_metadata: ChannelTabCommandMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct ChannelTabCommandMetadata {
|
||||||
|
pub web_command_metadata: ChannelTabWebCommandMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct ChannelTabWebCommandMetadata {
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct SectionListRenderer {
|
pub(crate) struct SectionListRenderer {
|
||||||
pub contents: Vec<ItemSectionRendererWrap>,
|
pub contents: Vec<ItemSectionRendererWrap>,
|
||||||
/// - **Videos**: browse-feedUC2DjFE7Xf11URZqWBigcVOQvideos (...)
|
|
||||||
/// - **Playlists**: browse-feedUC2DjFE7Xf11URZqWBigcVOQplaylists104 (...)
|
|
||||||
/// - **Info**: None
|
|
||||||
pub target_id: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seems to be currently A/B tested, as of 11.10.2022
|
/// Seems to be currently A/B tested, as of 11.10.2022
|
||||||
|
@ -75,10 +96,6 @@ pub(crate) struct SectionListRenderer {
|
||||||
pub(crate) struct RichGridRenderer {
|
pub(crate) struct RichGridRenderer {
|
||||||
#[serde_as(as = "VecLogError<_>")]
|
#[serde_as(as = "VecLogError<_>")]
|
||||||
pub contents: MapResult<Vec<YouTubeListItem>>,
|
pub contents: MapResult<Vec<YouTubeListItem>>,
|
||||||
/// - **Videos**: browse-feedUC2DjFE7Xf11URZqWBigcVOQvideos (...)
|
|
||||||
/// - **Playlists**: browse-feedUC2DjFE7Xf11URZqWBigcVOQplaylists104 (...)
|
|
||||||
/// - **Info**: None
|
|
||||||
pub target_id: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
@ -4,7 +4,7 @@ use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::{json::JsonString, DefaultOnError};
|
use serde_with::{json::JsonString, DefaultOnError};
|
||||||
|
|
||||||
use super::Thumbnails;
|
use super::{ResponseContext, Thumbnails};
|
||||||
use crate::serializer::{text::Text, MapResult, VecLogError};
|
use crate::serializer::{text::Text, MapResult, VecLogError};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -14,6 +14,7 @@ pub(crate) struct Player {
|
||||||
pub streaming_data: Option<StreamingData>,
|
pub streaming_data: Option<StreamingData>,
|
||||||
pub captions: Option<Captions>,
|
pub captions: Option<Captions>,
|
||||||
pub video_details: Option<VideoDetails>,
|
pub video_details: Option<VideoDetails>,
|
||||||
|
pub response_context: ResponseContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
@ -6,7 +6,8 @@ use crate::serializer::{ignore_any, MapResult, VecLogError};
|
||||||
use crate::util::MappingError;
|
use crate::util::MappingError;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Alert, ContentRenderer, ContentsRenderer, ContinuationEndpoint, Thumbnails, ThumbnailsWrap,
|
Alert, ContentRenderer, ContentsRenderer, ContinuationEndpoint, ResponseContext, Thumbnails,
|
||||||
|
ThumbnailsWrap,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
@ -18,6 +19,7 @@ pub(crate) struct Playlist {
|
||||||
pub sidebar: Option<Sidebar>,
|
pub sidebar: Option<Sidebar>,
|
||||||
#[serde_as(as = "Option<DefaultOnError>")]
|
#[serde_as(as = "Option<DefaultOnError>")]
|
||||||
pub alerts: Option<Vec<Alert>>,
|
pub alerts: Option<Vec<Alert>>,
|
||||||
|
pub response_context: ResponseContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::{json::JsonString, serde_as};
|
use serde_with::{json::JsonString, serde_as};
|
||||||
|
|
||||||
use super::video_item::YouTubeListRendererWrap;
|
use super::{video_item::YouTubeListRendererWrap, ResponseContext};
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -10,6 +10,7 @@ pub(crate) struct Search {
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "Option<JsonString>")]
|
||||||
pub estimated_results: Option<u64>,
|
pub estimated_results: Option<u64>,
|
||||||
pub contents: Contents,
|
pub contents: Contents,
|
||||||
|
pub response_context: ResponseContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use fancy_regex::Regex;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::{json::JsonString, serde_as, DefaultOnError, VecSkipError};
|
use serde_with::{json::JsonString, serde_as, DefaultOnError, VecSkipError};
|
||||||
use time::OffsetDateTime;
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
use super::{ChannelBadge, ContinuationEndpoint, Thumbnails};
|
use super::{ChannelBadge, ContinuationEndpoint, Thumbnails};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -8,7 +10,7 @@ use crate::{
|
||||||
param::Language,
|
param::Language,
|
||||||
serializer::{
|
serializer::{
|
||||||
ignore_any,
|
ignore_any,
|
||||||
text::{Text, TextComponent},
|
text::{AccessibilityText, Text, TextComponent},
|
||||||
MapResult, VecLogError,
|
MapResult, VecLogError,
|
||||||
},
|
},
|
||||||
timeago,
|
timeago,
|
||||||
|
@ -21,6 +23,7 @@ use crate::{
|
||||||
pub(crate) enum YouTubeListItem {
|
pub(crate) enum YouTubeListItem {
|
||||||
#[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")]
|
#[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")]
|
||||||
VideoRenderer(VideoRenderer),
|
VideoRenderer(VideoRenderer),
|
||||||
|
ReelItemRenderer(ReelItemRenderer),
|
||||||
|
|
||||||
#[serde(alias = "gridPlaylistRenderer")]
|
#[serde(alias = "gridPlaylistRenderer")]
|
||||||
PlaylistRenderer(PlaylistRenderer),
|
PlaylistRenderer(PlaylistRenderer),
|
||||||
|
@ -98,6 +101,7 @@ pub(crate) struct VideoRenderer {
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub badges: Vec<VideoBadge>,
|
pub badges: Vec<VideoBadge>,
|
||||||
/// Contains Short/Live tag
|
/// Contains Short/Live tag
|
||||||
|
#[serde(default)]
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub thumbnail_overlays: Vec<TimeOverlay>,
|
pub thumbnail_overlays: Vec<TimeOverlay>,
|
||||||
/// Abbreviated video description (on startpage)
|
/// Abbreviated video description (on startpage)
|
||||||
|
@ -110,6 +114,27 @@ pub(crate) struct VideoRenderer {
|
||||||
pub upcoming_event_data: Option<UpcomingEventData>,
|
pub upcoming_event_data: Option<UpcomingEventData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Short video item
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct ReelItemRenderer {
|
||||||
|
pub video_id: String,
|
||||||
|
pub thumbnail: Thumbnails,
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub headline: String,
|
||||||
|
/// Contains `No views` if the view count is zero
|
||||||
|
#[serde_as(as = "Option<Text>")]
|
||||||
|
pub view_count_text: Option<String>,
|
||||||
|
/// video duration
|
||||||
|
///
|
||||||
|
/// Example: `the horror maze - 44 seconds - play video`
|
||||||
|
///
|
||||||
|
/// Dashes may be `\u2013` (emdash)
|
||||||
|
#[serde_as(as = "Option<AccessibilityText>")]
|
||||||
|
pub accessibility: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Playlist displayed in search results
|
/// Playlist displayed in search results
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -363,6 +388,39 @@ impl<T> YouTubeListMapper<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_short_video(&self, video: ReelItemRenderer) -> VideoItem {
|
||||||
|
static ACCESSIBILITY_SEP_REGEX: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(" [-\u{2013}] (.+) [-\u{2013}] ").unwrap());
|
||||||
|
|
||||||
|
VideoItem {
|
||||||
|
id: video.video_id,
|
||||||
|
title: video.headline,
|
||||||
|
length: video.accessibility.and_then(|acc| {
|
||||||
|
ACCESSIBILITY_SEP_REGEX
|
||||||
|
.captures(&acc)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|cap| {
|
||||||
|
cap.get(1).and_then(|c| {
|
||||||
|
timeago::parse_timeago(self.lang, c.as_str())
|
||||||
|
.map(|ta| Duration::from(ta).whole_seconds() as u32)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
thumbnail: video.thumbnail.into(),
|
||||||
|
channel: None,
|
||||||
|
publish_date: None,
|
||||||
|
publish_date_txt: None,
|
||||||
|
view_count: video
|
||||||
|
.view_count_text
|
||||||
|
.map(|txt| util::parse_numeric(&txt).unwrap_or_default()),
|
||||||
|
is_live: false,
|
||||||
|
is_short: true,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_playlist(playlist: PlaylistRenderer) -> PlaylistItem {
|
fn map_playlist(playlist: PlaylistRenderer) -> PlaylistItem {
|
||||||
PlaylistItem {
|
PlaylistItem {
|
||||||
id: playlist.playlist_id,
|
id: playlist.playlist_id,
|
||||||
|
@ -413,6 +471,10 @@ impl YouTubeListMapper<YouTubeItem> {
|
||||||
YouTubeListItem::VideoRenderer(video) => {
|
YouTubeListItem::VideoRenderer(video) => {
|
||||||
self.items.push(YouTubeItem::Video(self.map_video(video)));
|
self.items.push(YouTubeItem::Video(self.map_video(video)));
|
||||||
}
|
}
|
||||||
|
YouTubeListItem::ReelItemRenderer(video) => {
|
||||||
|
self.items
|
||||||
|
.push(YouTubeItem::Video(self.map_short_video(video)));
|
||||||
|
}
|
||||||
YouTubeListItem::PlaylistRenderer(playlist) => self
|
YouTubeListItem::PlaylistRenderer(playlist) => self
|
||||||
.items
|
.items
|
||||||
.push(YouTubeItem::Playlist(Self::map_playlist(playlist))),
|
.push(YouTubeItem::Playlist(Self::map_playlist(playlist))),
|
||||||
|
@ -449,6 +511,9 @@ impl YouTubeListMapper<VideoItem> {
|
||||||
YouTubeListItem::VideoRenderer(video) => {
|
YouTubeListItem::VideoRenderer(video) => {
|
||||||
self.items.push(self.map_video(video));
|
self.items.push(self.map_video(video));
|
||||||
}
|
}
|
||||||
|
YouTubeListItem::ReelItemRenderer(video) => {
|
||||||
|
self.items.push(self.map_short_video(video));
|
||||||
|
}
|
||||||
YouTubeListItem::ContinuationItemRenderer {
|
YouTubeListItem::ContinuationItemRenderer {
|
||||||
continuation_endpoint,
|
continuation_endpoint,
|
||||||
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
||||||
|
|
|
@ -109,6 +109,7 @@ impl MapResponse<SearchResult> for response::Search {
|
||||||
c: SearchResult {
|
c: SearchResult {
|
||||||
items: Paginator::new(self.estimated_results, mapper.items, mapper.ctoken),
|
items: Paginator::new(self.estimated_results, mapper.items, mapper.ctoken),
|
||||||
corrected_query: mapper.corrected_query,
|
corrected_query: mapper.corrected_query,
|
||||||
|
visitor_data: self.response_context.visitor_data,
|
||||||
},
|
},
|
||||||
warnings: mapper.warnings,
|
warnings: mapper.warnings,
|
||||||
})
|
})
|
||||||
|
|
|
@ -142,6 +142,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("CgszMUUzZDlGLWxiRSipqr2ZBg%3D%3D"),
|
||||||
content: ChannelInfo(
|
content: ChannelInfo(
|
||||||
create_date: Some("2009-04-04"),
|
create_date: Some("2009-04-04"),
|
||||||
view_count: Some(186854342),
|
view_count: Some(186854342),
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -142,6 +142,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("CgttaWpyTVpUN1AyZyioqr2ZBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: None,
|
count: None,
|
||||||
items: [
|
items: [
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -113,6 +113,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: true,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("CgtQdE9zVVR3NVBDbyjz0ZKaBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: None,
|
count: None,
|
||||||
items: [
|
items: [
|
||||||
|
|
|
@ -142,6 +142,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: true,
|
||||||
|
visitor_data: Some("Cgs4ZFVmMzVlU1dxbyiBqpeaBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: None,
|
count: None,
|
||||||
items: [
|
items: [
|
||||||
|
|
|
@ -142,6 +142,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("CgszNU5rbDVZS2hMcyim4K2ZBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: None,
|
count: None,
|
||||||
items: [
|
items: [
|
||||||
|
|
|
@ -30,6 +30,9 @@ Channel(
|
||||||
banner: [],
|
banner: [],
|
||||||
mobile_banner: [],
|
mobile_banner: [],
|
||||||
tv_banner: [],
|
tv_banner: [],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("Cgtvc2s4UllvTGl6byigxseZBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: Some(0),
|
count: Some(0),
|
||||||
items: [],
|
items: [],
|
||||||
|
|
|
@ -126,6 +126,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("CgtkYXJITElwYmd4OCj85a2ZBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: Some(21),
|
count: Some(21),
|
||||||
items: [
|
items: [
|
||||||
|
|
|
@ -113,6 +113,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("CgtCV1l2R2Rzb2ZSZyiu4a2ZBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: Some(0),
|
count: Some(0),
|
||||||
items: [],
|
items: [],
|
||||||
|
|
|
@ -113,6 +113,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("CgtneXVRbGtSMWtlYyj75a2ZBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: None,
|
count: None,
|
||||||
items: [
|
items: [
|
||||||
|
|
|
@ -130,6 +130,9 @@ Channel(
|
||||||
height: 1192,
|
height: 1192,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
has_shorts: false,
|
||||||
|
has_live: false,
|
||||||
|
visitor_data: Some("Cgs4Ri1tLW1KNWozNCjGk8yZBg%3D%3D"),
|
||||||
content: Paginator(
|
content: Paginator(
|
||||||
count: None,
|
count: None,
|
||||||
items: [
|
items: [
|
||||||
|
|
|
@ -424,4 +424,5 @@ VideoPlayer(
|
||||||
expires_in_seconds: 21540,
|
expires_in_seconds: 21540,
|
||||||
hls_manifest_url: None,
|
hls_manifest_url: None,
|
||||||
dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtOPEYSBgQeHmqbwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/1/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/initcwndbps/1527500/vprv/1/mt/1659459429/fvip/4/itag_bl/376%2C377%2C384%2C385%2C612%2C613%2C617%2C619%2C623%2C628%2C655%2C656%2C660%2C662%2C666%2C671/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cvprv%2Citag/sig/AOq0QJ8wRAIgMm4a_MIHA3YUszKeruSy3exs5JwNjJAyLAwxL0yPdNMCIANb9GDMSTp_NT-PPhbvYMwRULJ5a9BO6MYD9FuWprC1/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgETSOwhwWVMy7gmrFXZlJu655ToLzSwOEsT16oRyrWhACIQDkvOEw1fImz5omu4iVIRNFe-z-JC9v8WUyx281dW2NOw%3D%3D"),
|
dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtOPEYSBgQeHmqbwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/1/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/initcwndbps/1527500/vprv/1/mt/1659459429/fvip/4/itag_bl/376%2C377%2C384%2C385%2C612%2C613%2C617%2C619%2C623%2C628%2C655%2C656%2C660%2C662%2C666%2C671/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cvprv%2Citag/sig/AOq0QJ8wRAIgMm4a_MIHA3YUszKeruSy3exs5JwNjJAyLAwxL0yPdNMCIANb9GDMSTp_NT-PPhbvYMwRULJ5a9BO6MYD9FuWprC1/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgETSOwhwWVMy7gmrFXZlJu655ToLzSwOEsT16oRyrWhACIQDkvOEw1fImz5omu4iVIRNFe-z-JC9v8WUyx281dW2NOw%3D%3D"),
|
||||||
|
visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -541,4 +541,5 @@ VideoPlayer(
|
||||||
expires_in_seconds: 21540,
|
expires_in_seconds: 21540,
|
||||||
hls_manifest_url: None,
|
hls_manifest_url: None,
|
||||||
dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtq3BJCX1gKVyJGQDg/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C26/mn/sn-h0jelnez%2Csn-4g5edn6k/ms/au%2Conr/mv/m/mvi/4/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1513750/spc/lT-KhrZGE2opztWyVdAtyUNlb8dXPDs/vprv/1/mt/1659459429/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRgIhAPEjHK19PKVHqQeia6WF4qubuMYk74LGi8F8lk5ZMPkFAiEAsaB2pKQWBvuPnNUnbdQXHc-izgsHJUP793woC2xNJlg%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgOY4xu4H9wqPVZ7vF2i0hFcOnqrur1XGoA43a7ZEuuSUCIQCyPxBKXUQrKFmknNEGpX5GSWySKgMw_xHBikWpKpKwvg%3D%3D"),
|
dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtq3BJCX1gKVyJGQDg/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C26/mn/sn-h0jelnez%2Csn-4g5edn6k/ms/au%2Conr/mv/m/mvi/4/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1513750/spc/lT-KhrZGE2opztWyVdAtyUNlb8dXPDs/vprv/1/mt/1659459429/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRgIhAPEjHK19PKVHqQeia6WF4qubuMYk74LGi8F8lk5ZMPkFAiEAsaB2pKQWBvuPnNUnbdQXHc-izgsHJUP793woC2xNJlg%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgOY4xu4H9wqPVZ7vF2i0hFcOnqrur1XGoA43a7ZEuuSUCIQCyPxBKXUQrKFmknNEGpX5GSWySKgMw_xHBikWpKpKwvg%3D%3D"),
|
||||||
|
visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -365,4 +365,5 @@ VideoPlayer(
|
||||||
expires_in_seconds: 21540,
|
expires_in_seconds: 21540,
|
||||||
hls_manifest_url: None,
|
hls_manifest_url: None,
|
||||||
dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659487474/ei/knDpYub6BojEgAf6jbLgDw/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1418750/spc/lT-Khox4YuJQ2wmH79zYALRvsWTPCUc/vprv/1/mt/1659465669/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRAIgErABhAEaoKHUDu9dDbpxE_8gR4b8WWAi61fnu8UKnuICIEYrEKcHvqHdO4V3R7cvSGwi_HGH34IlQsKbziOfMBov/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgJxHmH0Sxo3cY_pW_ZzQ3hW9-7oz6K_pZWcUdrDDQ2sQCIQDJYNINQwLgKelgbO3CZYx7sMxdUAFpWdokmRBQ77vwvw%3D%3D"),
|
dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659487474/ei/knDpYub6BojEgAf6jbLgDw/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1418750/spc/lT-Khox4YuJQ2wmH79zYALRvsWTPCUc/vprv/1/mt/1659465669/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRAIgErABhAEaoKHUDu9dDbpxE_8gR4b8WWAi61fnu8UKnuICIEYrEKcHvqHdO4V3R7cvSGwi_HGH34IlQsKbziOfMBov/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgJxHmH0Sxo3cY_pW_ZzQ3hW9-7oz6K_pZWcUdrDDQ2sQCIQDJYNINQwLgKelgbO3CZYx7sMxdUAFpWdokmRBQ77vwvw%3D%3D"),
|
||||||
|
visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -160,4 +160,5 @@ VideoPlayer(
|
||||||
expires_in_seconds: 21540,
|
expires_in_seconds: 21540,
|
||||||
hls_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1659481355/ei/q1jpYq-xHs7NgQev0bfwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jelnez%2Csn-h0jeenek/ms/au%2Crdu/mv/m/mvi/4/pl/37/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1513750/vprv/1/go/1/mt/1659459429/fvip/5/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24001373%2C24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRQIhAIYnEHvIgJtJ8hehAXNtVY3qsgsq_GdOhWf2hkJZe6lCAiBxaRY_nubYp6hBizcAg_KFkKnkG-t2XYLRQ5wGdM3AjA%3D%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRgIhAM_91Kk_0VLuSsR6nLCY7LdtWojyRAzXSScd_X9ShRROAiEA1AF4VY04F71NsAI8_j3iqjuXnWL9s6NoXHq7P8-bHx8%3D/file/index.m3u8"),
|
hls_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1659481355/ei/q1jpYq-xHs7NgQev0bfwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jelnez%2Csn-h0jeenek/ms/au%2Crdu/mv/m/mvi/4/pl/37/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1513750/vprv/1/go/1/mt/1659459429/fvip/5/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24001373%2C24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRQIhAIYnEHvIgJtJ8hehAXNtVY3qsgsq_GdOhWf2hkJZe6lCAiBxaRY_nubYp6hBizcAg_KFkKnkG-t2XYLRQ5wGdM3AjA%3D%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRgIhAM_91Kk_0VLuSsR6nLCY7LdtWojyRAzXSScd_X9ShRROAiEA1AF4VY04F71NsAI8_j3iqjuXnWL9s6NoXHq7P8-bHx8%3D/file/index.m3u8"),
|
||||||
dash_manifest_url: None,
|
dash_manifest_url: None,
|
||||||
|
visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -541,4 +541,5 @@ VideoPlayer(
|
||||||
expires_in_seconds: 21540,
|
expires_in_seconds: 21540,
|
||||||
hls_manifest_url: None,
|
hls_manifest_url: None,
|
||||||
dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYv-eJ9uF6dsPhvyH8As/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jelnez%2Csn-h0jeenek/ms/au%2Crdu/mv/m/mvi/4/pl/37/hfr/all/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/initcwndbps/1527500/vprv/1/mt/1659459429/fvip/5/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cvprv%2Citag/sig/AOq0QJ8wRQIhANKWS7GCN4pSoHIQ6BMZdOaHAD0I25nHwRj7ds4qrxdEAiBsd9l8WIceqF7-2xyR82DGecCiS9hgUIPJhdNhkwVpHg%3D%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgMbu-wTOcXGCwGh27y0YZHktumKM1sopgxfQf8LCcCnECIQDnhFbgddOxwiQbnMOIcCn6ncpN54UyALRNigUSCp9Deg%3D%3D"),
|
dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYv-eJ9uF6dsPhvyH8As/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jelnez%2Csn-h0jeenek/ms/au%2Crdu/mv/m/mvi/4/pl/37/hfr/all/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/initcwndbps/1527500/vprv/1/mt/1659459429/fvip/5/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cvprv%2Citag/sig/AOq0QJ8wRQIhANKWS7GCN4pSoHIQ6BMZdOaHAD0I25nHwRj7ds4qrxdEAiBsd9l8WIceqF7-2xyR82DGecCiS9hgUIPJhdNhkwVpHg%3D%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgMbu-wTOcXGCwGh27y0YZHktumKM1sopgxfQf8LCcCnECIQDnhFbgddOxwiQbnMOIcCn6ncpN54UyALRNigUSCp9Deg%3D%3D"),
|
||||||
|
visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -3142,4 +3142,5 @@ Playlist(
|
||||||
)),
|
)),
|
||||||
last_update: "[date]",
|
last_update: "[date]",
|
||||||
last_update_txt: Some("Last updated on Aug 7, 2022"),
|
last_update_txt: Some("Last updated on Aug 7, 2022"),
|
||||||
|
visitor_data: Some("CgtZdi1GV3N3TnBuQSi46K-YBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -2088,4 +2088,5 @@ Playlist(
|
||||||
)),
|
)),
|
||||||
last_update: "[date]",
|
last_update: "[date]",
|
||||||
last_update_txt: Some("Last updated on Jul 2, 2014"),
|
last_update_txt: Some("Last updated on Jul 2, 2014"),
|
||||||
|
visitor_data: Some("CgtXY0lqNWN5VlNmTSik8q-YBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -3041,4 +3041,5 @@ Playlist(
|
||||||
channel: None,
|
channel: None,
|
||||||
last_update: "[date]",
|
last_update: "[date]",
|
||||||
last_update_txt: Some("Updated today"),
|
last_update_txt: Some("Updated today"),
|
||||||
|
visitor_data: Some("CgtLZVdRQ1dkM2VDVSi46K-YBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -734,4 +734,5 @@ SearchResult(
|
||||||
endpoint: browse,
|
endpoint: browse,
|
||||||
),
|
),
|
||||||
corrected_query: Some("doobydobap"),
|
corrected_query: Some("doobydobap"),
|
||||||
|
visitor_data: Some("Cgs4MEJMc3FmVzVadyiNy4-aBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,4 +10,5 @@ SearchResult(
|
||||||
endpoint: browse,
|
endpoint: browse,
|
||||||
),
|
),
|
||||||
corrected_query: None,
|
corrected_query: None,
|
||||||
|
visitor_data: Some("Cgs1Q0NxX3llelBxWSi85ZGaBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -691,4 +691,5 @@ SearchResult(
|
||||||
endpoint: browse,
|
endpoint: browse,
|
||||||
),
|
),
|
||||||
corrected_query: None,
|
corrected_query: None,
|
||||||
|
visitor_data: Some("CgstZjhyS1IyR1R6dyiX4JGaBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -750,4 +750,5 @@ VideoDetails(
|
||||||
ctoken: Some("Eg0SC1plZXJybnVMaTVFGAYyOCIRIgtaZWVycm51TGk1RTABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
ctoken: Some("Eg0SC1plZXJybnVMaTVFGAYyOCIRIgtaZWVycm51TGk1RTABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("CgtCeURHR09uNlJ5TSjOiLqZBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -824,4 +824,5 @@ VideoDetails(
|
||||||
ctoken: Some("Eg0SC1plZXJybnVMaTVFGAYyOCIRIgtaZWVycm51TGk1RTABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
ctoken: Some("Eg0SC1plZXJybnVMaTVFGAYyOCIRIgtaZWVycm51TGk1RTABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("Cgs2V0p6ZW5ab1ozTSjkrpaaBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1282,4 +1282,5 @@ VideoDetails(
|
||||||
ctoken: Some("Eg0SC25GREJ4QlVmRTc0GAYyOCIRIgtuRkRCeEJVZkU3NDABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
ctoken: Some("Eg0SC25GREJ4QlVmRTc0GAYyOCIRIgtuRkRCeEJVZkU3NDABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("Cgtidzg4MlRTb3FKSSiqipeaBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -54,4 +54,5 @@ VideoDetails(
|
||||||
ctoken: Some("Eg0SC0hSS3UwY3Zycl9vGAYyOCIRIgtIUkt1MGN2cnJfbzABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
ctoken: Some("Eg0SC0hSS3UwY3Zycl9vGAYyOCIRIgtIUkt1MGN2cnJfbzABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("CgtxUUdrc1VSVE54Zyja1KiZBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -771,4 +771,5 @@ VideoDetails(
|
||||||
ctoken: Some("Eg0SCzByYjlDZk92b2prGAYyOCIRIgswcmI5Q2ZPdm9qazABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
ctoken: Some("Eg0SCzByYjlDZk92b2prGAYyOCIRIgswcmI5Q2ZPdm9qazABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("CgtoY1pQUF8wNW1qayjSjpSZBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1239,4 +1239,5 @@ VideoDetails(
|
||||||
ctoken: Some("Eg0SC25GREJ4QlVmRTc0GAYyOCIRIgtuRkRCeEJVZkU3NDABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
ctoken: Some("Eg0SC25GREJ4QlVmRTc0GAYyOCIRIgtuRkRCeEJVZkU3NDABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("CgtIV0JjSUtDQm9LQSjUjpSZBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -830,4 +830,5 @@ VideoDetails(
|
||||||
ctoken: None,
|
ctoken: None,
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("CgtnQS1WdzlNNkNCSSiSmKiZBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -576,4 +576,5 @@ VideoDetails(
|
||||||
ctoken: None,
|
ctoken: None,
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("CgtzclhqZVpoajVhVSi76qeZBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -787,4 +787,5 @@ VideoDetails(
|
||||||
ctoken: Some("Eg0SC1plZXJybnVMaTVFGAYyOCIRIgtaZWVycm51TGk1RTABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
ctoken: Some("Eg0SC1plZXJybnVMaTVFGAYyOCIRIgtaZWVycm51TGk1RTABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||||
endpoint: next,
|
endpoint: next,
|
||||||
),
|
),
|
||||||
|
visitor_data: Some("Cgtjemd0bDVxU1N1QSjRjpSZBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -252,7 +252,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
let mut res = map_recommendations(
|
let mut res = map_recommendations(
|
||||||
r,
|
r,
|
||||||
sr.secondary_results.continuations,
|
sr.secondary_results.continuations,
|
||||||
self.response_context.visitor_data,
|
self.response_context.visitor_data.clone(),
|
||||||
lang,
|
lang,
|
||||||
);
|
);
|
||||||
warnings.append(&mut res.warnings);
|
warnings.append(&mut res.warnings);
|
||||||
|
@ -343,6 +343,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
None,
|
None,
|
||||||
crate::param::ContinuationEndpoint::Next,
|
crate::param::ContinuationEndpoint::Next,
|
||||||
),
|
),
|
||||||
|
visitor_data: self.response_context.visitor_data,
|
||||||
},
|
},
|
||||||
warnings,
|
warnings,
|
||||||
})
|
})
|
||||||
|
|
|
@ -112,8 +112,12 @@ pub struct VideoPlayer {
|
||||||
pub subtitles: Vec<Subtitle>,
|
pub subtitles: Vec<Subtitle>,
|
||||||
/// Lifetime of the stream URLs in seconds
|
/// Lifetime of the stream URLs in seconds
|
||||||
pub expires_in_seconds: u32,
|
pub expires_in_seconds: u32,
|
||||||
|
/// HLS manifest URL (for livestreams)
|
||||||
pub hls_manifest_url: Option<String>,
|
pub hls_manifest_url: Option<String>,
|
||||||
|
/// Dash manifest URL (for livestreams)
|
||||||
pub dash_manifest_url: Option<String>,
|
pub dash_manifest_url: Option<String>,
|
||||||
|
/// YouTube visitor data cookie
|
||||||
|
pub visitor_data: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Video metadata from the player
|
/// Video metadata from the player
|
||||||
|
@ -461,6 +465,8 @@ pub struct Playlist {
|
||||||
pub last_update: Option<Date>,
|
pub last_update: Option<Date>,
|
||||||
/// Textual last update date
|
/// Textual last update date
|
||||||
pub last_update_txt: Option<String>,
|
pub last_update_txt: Option<String>,
|
||||||
|
/// YouTube visitor data cookie
|
||||||
|
pub visitor_data: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YouTube video extracted from a playlist
|
/// YouTube video extracted from a playlist
|
||||||
|
@ -543,6 +549,8 @@ pub struct VideoDetails {
|
||||||
///
|
///
|
||||||
/// Is initially empty.
|
/// Is initially empty.
|
||||||
pub latest_comments: Paginator<Comment>,
|
pub latest_comments: Paginator<Comment>,
|
||||||
|
/// YouTube visitor data cookie
|
||||||
|
pub visitor_data: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chapter of a video
|
/// Chapter of a video
|
||||||
|
@ -675,6 +683,12 @@ pub struct Channel<T> {
|
||||||
pub mobile_banner: Vec<Thumbnail>,
|
pub mobile_banner: Vec<Thumbnail>,
|
||||||
/// Banner image shown above the channel (16:9 fullscreen format for TV)
|
/// Banner image shown above the channel (16:9 fullscreen format for TV)
|
||||||
pub tv_banner: Vec<Thumbnail>,
|
pub tv_banner: Vec<Thumbnail>,
|
||||||
|
/// Does the channel have a *Shorts* tab?
|
||||||
|
pub has_shorts: bool,
|
||||||
|
/// Does the channel have a *Live* tab?
|
||||||
|
pub has_live: bool,
|
||||||
|
/// YouTube visitor data cookie
|
||||||
|
pub visitor_data: Option<String>,
|
||||||
/// Content fetched from the channel
|
/// Content fetched from the channel
|
||||||
pub content: T,
|
pub content: T,
|
||||||
}
|
}
|
||||||
|
@ -745,6 +759,8 @@ pub struct SearchResult {
|
||||||
/// for the corrected search term and displays it on top of the
|
/// for the corrected search term and displays it on top of the
|
||||||
/// search results page.
|
/// search results page.
|
||||||
pub corrected_query: Option<String>,
|
pub corrected_query: Option<String>,
|
||||||
|
/// YouTube visitor data cookie
|
||||||
|
pub visitor_data: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YouTube item (Video/Channel/Playlist)
|
/// YouTube item (Video/Channel/Playlist)
|
||||||
|
|
|
@ -82,17 +82,27 @@ impl Mul<u8> for TimeAgo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<TimeAgo> for Duration {
|
||||||
|
fn from(ta: TimeAgo) -> Self {
|
||||||
|
match ta.unit {
|
||||||
|
TimeUnit::Second => Duration::seconds(ta.n as i64),
|
||||||
|
TimeUnit::Minute => Duration::minutes(ta.n as i64),
|
||||||
|
TimeUnit::Hour => Duration::hours(ta.n as i64),
|
||||||
|
TimeUnit::Day => Duration::days(ta.n as i64),
|
||||||
|
TimeUnit::Week => Duration::weeks(ta.n as i64),
|
||||||
|
TimeUnit::Month => Duration::days(ta.n as i64 * 30),
|
||||||
|
TimeUnit::Year => Duration::days(ta.n as i64 * 365),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<TimeAgo> for OffsetDateTime {
|
impl From<TimeAgo> for OffsetDateTime {
|
||||||
fn from(ta: TimeAgo) -> Self {
|
fn from(ta: TimeAgo) -> Self {
|
||||||
let ts = util::now_sec();
|
let ts = util::now_sec();
|
||||||
match ta.unit {
|
match ta.unit {
|
||||||
TimeUnit::Second => ts - Duration::seconds(ta.n as i64),
|
|
||||||
TimeUnit::Minute => ts - Duration::minutes(ta.n as i64),
|
|
||||||
TimeUnit::Hour => ts - Duration::hours(ta.n as i64),
|
|
||||||
TimeUnit::Day => ts - Duration::days(ta.n as i64),
|
|
||||||
TimeUnit::Week => ts - Duration::weeks(ta.n as i64),
|
|
||||||
TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -(ta.n as i32))),
|
TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -(ta.n as i32))),
|
||||||
TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -(ta.n as i32))),
|
TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -(ta.n as i32))),
|
||||||
|
_ => ts - Duration::from(ta),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12323
testfiles/channel/channel_livestreams.json
Normal file
12323
testfiles/channel/channel_livestreams.json
Normal file
File diff suppressed because it is too large
Load diff
12750
testfiles/channel/channel_shorts.json
Normal file
12750
testfiles/channel/channel_shorts.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -12,6 +12,8 @@ use rustypipe::model::{
|
||||||
};
|
};
|
||||||
use rustypipe::param::search_filter::{self, SearchFilter};
|
use rustypipe::param::search_filter::{self, SearchFilter};
|
||||||
|
|
||||||
|
const VISITOR_DATA_3TAB_CHANNEL_LAYOUT: &str = "CgtOa256ckVkcG5YVSiirbyaBg%3D%3D";
|
||||||
|
|
||||||
//#PLAYER
|
//#PLAYER
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
@ -908,6 +910,75 @@ async fn channel_videos() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn channel_shorts() {
|
||||||
|
let rp = RustyPipe::builder()
|
||||||
|
.strict()
|
||||||
|
.visitor_data(VISITOR_DATA_3TAB_CHANNEL_LAYOUT)
|
||||||
|
.build();
|
||||||
|
let channel = rp
|
||||||
|
.query()
|
||||||
|
.channel_shorts("UCh8gHdtzO2tXd593_bjErWg")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&channel);
|
||||||
|
assert_eq!(channel.id, "UCh8gHdtzO2tXd593_bjErWg");
|
||||||
|
assert_eq!(channel.name, "Doobydobap");
|
||||||
|
assert!(
|
||||||
|
channel.subscriber_count.unwrap() > 2800000,
|
||||||
|
"expected >2.8M subscribers, got {}",
|
||||||
|
channel.subscriber_count.unwrap()
|
||||||
|
);
|
||||||
|
assert!(!channel.avatar.is_empty(), "got no thumbnails");
|
||||||
|
assert_eq!(channel.verification, Verification::Verified);
|
||||||
|
assert!(channel
|
||||||
|
.description
|
||||||
|
.contains("Hi, I\u{2019}m Tina, aka Doobydobap"));
|
||||||
|
assert_eq!(
|
||||||
|
channel.vanity_url.as_ref().unwrap(),
|
||||||
|
"https://www.youtube.com/c/Doobydobap"
|
||||||
|
);
|
||||||
|
assert!(!channel.banner.is_empty(), "got no banners");
|
||||||
|
assert!(!channel.mobile_banner.is_empty(), "got no mobile banners");
|
||||||
|
assert!(!channel.tv_banner.is_empty(), "got no tv banners");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!channel.content.items.is_empty() && !channel.content.is_exhausted(),
|
||||||
|
"got no shorts"
|
||||||
|
);
|
||||||
|
|
||||||
|
let next = channel.content.next(&rp.query()).await.unwrap().unwrap();
|
||||||
|
assert!(
|
||||||
|
!next.is_exhausted() && !next.items.is_empty(),
|
||||||
|
"no more shorts"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn channel_livestreams() {
|
||||||
|
let rp = RustyPipe::builder()
|
||||||
|
.visitor_data(VISITOR_DATA_3TAB_CHANNEL_LAYOUT)
|
||||||
|
.strict()
|
||||||
|
.build();
|
||||||
|
let channel = rp
|
||||||
|
.query()
|
||||||
|
.channel_livestreams("UC2DjFE7Xf11URZqWBigcVOQ")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&channel);
|
||||||
|
assert_channel_eevblog(&channel);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!channel.content.items.is_empty() && !channel.content.is_exhausted(),
|
||||||
|
"got no streams"
|
||||||
|
);
|
||||||
|
|
||||||
|
let next = channel.content.next(&rp.query()).await.unwrap().unwrap();
|
||||||
|
assert!(!next.items.is_empty(), "no more streams");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn channel_playlists() {
|
async fn channel_playlists() {
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
|
Loading…
Add table
Reference in a new issue