fix: a/b test 10: channel about modal
This commit is contained in:
parent
cced125390
commit
ba06e2c8c8
17 changed files with 1686 additions and 2932 deletions
|
|
@ -1,20 +1,21 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
Channel, ChannelInfo, PlaylistItem, VideoItem, YouTubeItem,
|
||||
Channel, ChannelInfo, PlaylistItem, VideoItem,
|
||||
},
|
||||
param::{ChannelOrder, ChannelVideoTab, Language},
|
||||
serializer::MapResult,
|
||||
util::{self, ProtoBuilder},
|
||||
serializer::{text::TextComponent, MapResult},
|
||||
util::{self, timeago, ProtoBuilder},
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
|
||||
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -36,8 +37,6 @@ enum ChannelTab {
|
|||
Live,
|
||||
#[serde(rename = "EglwbGF5bGlzdHMgAQ%3D%3D")]
|
||||
Playlists,
|
||||
#[serde(rename = "EgVhYm91dPIGBAoCEgA%3D")]
|
||||
Info,
|
||||
#[serde(rename = "EgZzZWFyY2jyBgQKAloA")]
|
||||
Search,
|
||||
}
|
||||
|
|
@ -126,7 +125,7 @@ impl RustyPipeQuery {
|
|||
let visitor_data = Some(self.get_visitor_data().await?);
|
||||
|
||||
self.continuation(
|
||||
order_ctoken(channel_id.as_ref(), tab, order),
|
||||
order_ctoken(channel_id.as_ref(), tab, order, &random_target()),
|
||||
ContinuationEndpoint::Browse,
|
||||
visitor_data.as_deref(),
|
||||
)
|
||||
|
|
@ -179,19 +178,17 @@ impl RustyPipeQuery {
|
|||
pub async fn channel_info<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
) -> Result<Channel<ChannelInfo>, Error> {
|
||||
) -> Result<ChannelInfo, Error> {
|
||||
let channel_id = channel_id.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QChannel {
|
||||
let context = self.get_context(ClientType::Desktop, false, None).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
browse_id: channel_id,
|
||||
params: ChannelTab::Info,
|
||||
query: None,
|
||||
continuation: &channel_info_ctoken(channel_id, &random_target()),
|
||||
};
|
||||
|
||||
self.execute_request::<response::Channel, _, _>(
|
||||
self.execute_request::<response::ChannelAbout, _, _>(
|
||||
ClientType::Desktop,
|
||||
"channel_info",
|
||||
"channel_info2",
|
||||
channel_id,
|
||||
"browse",
|
||||
&request_body,
|
||||
|
|
@ -290,46 +287,64 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
|||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Channel<ChannelInfo>> for response::Channel {
|
||||
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
|
||||
let content = map_channel_content(id, self.contents, self.alerts)?;
|
||||
let channel_data = map_channel(
|
||||
MapChannelData {
|
||||
header: self.header,
|
||||
metadata: self.metadata,
|
||||
microformat: self.microformat,
|
||||
visitor_data: self
|
||||
.response_context
|
||||
.visitor_data
|
||||
.or_else(|| vdata.map(str::to_owned)),
|
||||
has_shorts: content.has_shorts,
|
||||
has_live: content.has_live,
|
||||
},
|
||||
id,
|
||||
lang,
|
||||
)?;
|
||||
_visitor_data: Option<&str>,
|
||||
) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
||||
let ep = self
|
||||
.on_response_received_endpoints
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?;
|
||||
let continuations = ep.append_continuation_items_action.continuation_items;
|
||||
let about = continuations
|
||||
.c
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData("no aboutChannel data".into()))?
|
||||
.about_channel_renderer
|
||||
.metadata
|
||||
.about_channel_view_model;
|
||||
let mut warnings = continuations.warnings;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
|
||||
mapper.map_response(content.content);
|
||||
let mut warnings = mapper.warnings;
|
||||
|
||||
let cinfo = mapper.channel_info.unwrap_or_else(|| {
|
||||
warnings.push("no aboutFullMetadata".to_owned());
|
||||
ChannelInfo {
|
||||
create_date: None,
|
||||
view_count: None,
|
||||
links: Vec::new(),
|
||||
}
|
||||
});
|
||||
let links = about
|
||||
.links
|
||||
.into_iter()
|
||||
.filter_map(|l| {
|
||||
let lv = l.channel_external_link_view_model;
|
||||
if let TextComponent::Web { url, .. } = lv.link {
|
||||
Some((String::from(lv.title), util::sanitize_yt_url(&url)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(MapResult {
|
||||
c: combine_channel_data(channel_data.c, cinfo),
|
||||
c: ChannelInfo {
|
||||
id: about.channel_id,
|
||||
url: about.canonical_channel_url,
|
||||
description: about.description,
|
||||
subscriber_count: about
|
||||
.subscriber_count_text
|
||||
.and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)),
|
||||
video_count: about
|
||||
.video_count_text
|
||||
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
||||
create_date: about.joined_date_text.and_then(|txt| {
|
||||
timeago::parse_textual_date_or_warn(lang, &txt, &mut warnings)
|
||||
.map(OffsetDateTime::date)
|
||||
}),
|
||||
view_count: about
|
||||
.view_count_text
|
||||
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
||||
country: about.country.and_then(|c| util::country_from_name(&c)),
|
||||
links,
|
||||
},
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
|
@ -549,18 +564,7 @@ fn combine_channel_data<T>(channel_data: Channel<()>, content: T) -> Channel<T>
|
|||
}
|
||||
|
||||
/// Get the continuation token to fetch channel videos in the given order
|
||||
fn order_ctoken(channel_id: &str, tab: ChannelVideoTab, order: ChannelOrder) -> String {
|
||||
_order_ctoken(
|
||||
channel_id,
|
||||
tab,
|
||||
order,
|
||||
&format!("\n${}", util::random_uuid()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the continuation token to fetch channel videos in the given order
|
||||
/// (fixed targetId for testing)
|
||||
fn _order_ctoken(
|
||||
fn order_ctoken(
|
||||
channel_id: &str,
|
||||
tab: ChannelVideoTab,
|
||||
order: ChannelOrder,
|
||||
|
|
@ -589,6 +593,32 @@ fn _order_ctoken(
|
|||
pb.to_base64()
|
||||
}
|
||||
|
||||
/// Get the continuation token to fetch channel
|
||||
fn channel_info_ctoken(channel_id: &str, target_id: &str) -> String {
|
||||
let mut pb_3 = ProtoBuilder::new();
|
||||
pb_3.string(19, target_id);
|
||||
|
||||
let mut pb_110 = ProtoBuilder::new();
|
||||
pb_110.embedded(3, pb_3);
|
||||
|
||||
let mut pbi = ProtoBuilder::new();
|
||||
pbi.embedded(110, pb_110);
|
||||
|
||||
let mut pb_80226972 = ProtoBuilder::new();
|
||||
pb_80226972.string(2, channel_id);
|
||||
pb_80226972.string(3, &pbi.to_base64());
|
||||
|
||||
let mut pb = ProtoBuilder::new();
|
||||
pb.embedded(80_226_972, pb_80226972);
|
||||
|
||||
pb.to_base64()
|
||||
}
|
||||
|
||||
/// Create a random UUId to build continuation tokens
|
||||
fn random_target() -> String {
|
||||
format!("\n${}", util::random_uuid())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
|
@ -604,7 +634,7 @@ mod tests {
|
|||
util::tests::TESTFILES,
|
||||
};
|
||||
|
||||
use super::_order_ctoken;
|
||||
use super::{channel_info_ctoken, order_ctoken};
|
||||
|
||||
#[rstest]
|
||||
#[case::base("videos_base", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||
|
|
@ -668,10 +698,10 @@ mod tests {
|
|||
let json_path = path!(*TESTFILES / "channel" / "channel_info.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let channel: response::Channel =
|
||||
let channel: response::ChannelAbout =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Channel<ChannelInfo>> = channel
|
||||
.map_response("UC2DjFE7Xf11URZqWBigcVOQ", Language::En, None, None)
|
||||
let map_res: MapResult<ChannelInfo> = channel
|
||||
.map_response("UC2DjFE7Xf11U-RZqWBigcVOQ", Language::En, None, None)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
|
|
@ -683,10 +713,10 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn order_ctoken() {
|
||||
fn t_order_ctoken() {
|
||||
let channel_id = "UCXuqSBlHAE6Xw-yeJA0Tunw";
|
||||
|
||||
let videos_popular_token = _order_ctoken(
|
||||
let videos_popular_token = order_ctoken(
|
||||
channel_id,
|
||||
ChannelVideoTab::Videos,
|
||||
ChannelOrder::Popular,
|
||||
|
|
@ -694,7 +724,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(videos_popular_token, "4qmFsgJkEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaSDhnWXVHaXg2S2hJbUNpUTJORFl4WkRkak9DMHdNREF3TFRJd05EQXRPRGRoWVMwd09EbGxNRGd5TjJVME1qQVlBZyUzRCUzRA%3D%3D");
|
||||
|
||||
let shorts_popular_token = _order_ctoken(
|
||||
let shorts_popular_token = order_ctoken(
|
||||
channel_id,
|
||||
ChannelVideoTab::Shorts,
|
||||
ChannelOrder::Popular,
|
||||
|
|
@ -702,7 +732,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(shorts_popular_token, "4qmFsgJkEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaSDhnWXVHaXhTS2hJbUNpUTJORFkzT1dabVlpMHdNREF3TFRJMllqTXRZVEZpWkMwMU9ESTBNamxrTW1NM09UUVlBZyUzRCUzRA%3D%3D");
|
||||
|
||||
let live_popular_token = _order_ctoken(
|
||||
let live_popular_token = order_ctoken(
|
||||
channel_id,
|
||||
ChannelVideoTab::Live,
|
||||
ChannelOrder::Popular,
|
||||
|
|
@ -710,4 +740,12 @@ mod tests {
|
|||
);
|
||||
assert_eq!(live_popular_token, "4qmFsgJkEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaSDhnWXVHaXh5S2hJbUNpUTJORFk1TXpBMk9TMHdNREF3TFRKaE1XVXRPR00zWkMwMU9ESTBNamxpWkRWaVlUZ1lBZyUzRCUzRA%3D%3D");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t_channel_info_ctoken() {
|
||||
let channel_id = "UCh8gHdtzO2tXd593_bjErWg";
|
||||
|
||||
let token = channel_info_ctoken(channel_id, "\n$655b339a-0000-20b9-92dc-582429d254b4");
|
||||
assert_eq!(token, "4qmFsgJgEhhVQ2g4Z0hkdHpPMnRYZDU5M19iakVyV2caRDhnWXJHaW1hQVNZS0pEWTFOV0l6TXpsaExUQXdNREF0TWpCaU9TMDVNbVJqTFRVNE1qUXlPV1F5TlRSaU5BJTNEJTNE");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ use serde::Deserialize;
|
|||
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
||||
|
||||
use super::{
|
||||
video_item::YouTubeListRenderer, Alert, ChannelBadge, ContentsRenderer, ResponseContext,
|
||||
Thumbnails, TwoColumnBrowseResults,
|
||||
video_item::YouTubeListRenderer, Alert, ChannelBadge, ContentsRenderer, ContinuationActionWrap,
|
||||
ResponseContext, Thumbnails, TwoColumnBrowseResults,
|
||||
};
|
||||
use crate::serializer::text::Text;
|
||||
use crate::serializer::text::{AttributedText, Text, TextComponent};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -145,3 +145,66 @@ pub(crate) struct MicroformatDataRenderer {
|
|||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ChannelAbout {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub on_response_received_endpoints: Vec<ContinuationActionWrap<AboutChannelRendererWrap>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AboutChannelRendererWrap {
|
||||
pub about_channel_renderer: AboutChannelRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AboutChannelRenderer {
|
||||
pub metadata: ChannelMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ChannelMetadata {
|
||||
pub about_channel_view_model: ChannelMetadataView,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ChannelMetadataView {
|
||||
pub channel_id: String,
|
||||
pub canonical_channel_url: String,
|
||||
pub country: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub joined_date_text: Option<String>,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub subscriber_count_text: Option<String>,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub video_count_text: Option<String>,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub view_count_text: Option<String>,
|
||||
#[serde(default)]
|
||||
pub links: Vec<ExternalLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ExternalLink {
|
||||
pub channel_external_link_view_model: ExternalLinkInner,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ExternalLinkInner {
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub title: TextComponent,
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub link: TextComponent,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub(crate) mod video_details;
|
|||
pub(crate) mod video_item;
|
||||
|
||||
pub(crate) use channel::Channel;
|
||||
pub(crate) use channel::ChannelAbout;
|
||||
pub(crate) use music_artist::MusicArtist;
|
||||
pub(crate) use music_artist::MusicArtistAlbums;
|
||||
pub(crate) use music_charts::MusicCharts;
|
||||
|
|
@ -208,7 +209,7 @@ pub(crate) struct Continuation {
|
|||
alias = "onResponseReceivedEndpoints"
|
||||
)]
|
||||
#[serde_as(as = "Option<VecSkipError<_>>")]
|
||||
pub on_response_received_actions: Option<Vec<ContinuationActionWrap>>,
|
||||
pub on_response_received_actions: Option<Vec<ContinuationActionWrap<YouTubeListItem>>>,
|
||||
/// Used for channel video rich grid renderer
|
||||
///
|
||||
/// A/B test seen on 19.10.2022
|
||||
|
|
@ -217,15 +218,15 @@ pub(crate) struct Continuation {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ContinuationActionWrap {
|
||||
pub(crate) struct ContinuationActionWrap<T> {
|
||||
#[serde(alias = "reloadContinuationItemsCommand")]
|
||||
pub append_continuation_items_action: ContinuationAction,
|
||||
pub append_continuation_items_action: ContinuationAction<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ContinuationAction {
|
||||
pub continuation_items: MapResult<Vec<YouTubeListItem>>,
|
||||
pub(crate) struct ContinuationAction<T> {
|
||||
pub continuation_items: MapResult<Vec<T>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DefaultOnError};
|
||||
|
||||
use crate::{model::UrlTarget, util};
|
||||
use crate::model::UrlTarget;
|
||||
|
||||
/// navigation/resolve_url response model
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -280,14 +280,4 @@ impl NavigationEndpoint {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the sanitized URL from a url endpoint
|
||||
pub(crate) fn url(&self) -> Option<String> {
|
||||
match self {
|
||||
NavigationEndpoint::Url { url_endpoint } => {
|
||||
Some(util::sanitize_yt_url(&url_endpoint.url))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ use serde_with::{
|
|||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::{url_endpoint::NavigationEndpoint, ChannelBadge, ContinuationEndpoint, Thumbnails};
|
||||
use super::{ChannelBadge, ContinuationEndpoint, Thumbnails};
|
||||
use crate::{
|
||||
model::{
|
||||
Channel, ChannelId, ChannelInfo, ChannelItem, ChannelTag, PlaylistItem, Verification,
|
||||
VideoItem, YouTubeItem,
|
||||
Channel, ChannelId, ChannelItem, ChannelTag, PlaylistItem, Verification, VideoItem,
|
||||
YouTubeItem,
|
||||
},
|
||||
param::Language,
|
||||
serializer::{
|
||||
text::{AccessibilityText, AttributedText, Text, TextComponent},
|
||||
text::{AccessibilityText, Text, TextComponent},
|
||||
MapResult,
|
||||
},
|
||||
util::{self, timeago, TryRemove},
|
||||
|
|
@ -48,9 +48,6 @@ pub(crate) enum YouTubeListItem {
|
|||
corrected_query: String,
|
||||
},
|
||||
|
||||
/// Channel metadata (about tab)
|
||||
ChannelAboutFullMetadataRenderer(ChannelFullMetadata),
|
||||
|
||||
/// Contains video on startpage
|
||||
///
|
||||
/// Seems to be currently A/B tested on the channel page,
|
||||
|
|
@ -358,47 +355,6 @@ pub(crate) struct ReelPlayerHeaderRenderer {
|
|||
pub timestamp_text: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ChannelFullMetadata {
|
||||
#[serde_as(as = "Text")]
|
||||
pub joined_date_text: String,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub view_count_text: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub primary_links: Vec<PrimaryLink>,
|
||||
#[serde(default)]
|
||||
// #[serde_as(as = "VecSkipError<_>")]
|
||||
pub links: Vec<ExternalLink>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PrimaryLink {
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
pub navigation_endpoint: NavigationEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ExternalLink {
|
||||
pub channel_external_link_view_model: ExternalLinkInner,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ExternalLinkInner {
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub title: TextComponent,
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub link: TextComponent,
|
||||
}
|
||||
|
||||
trait IsLive {
|
||||
fn is_live(&self) -> bool;
|
||||
}
|
||||
|
|
@ -446,7 +402,6 @@ pub(crate) struct YouTubeListMapper<T> {
|
|||
pub warnings: Vec<String>,
|
||||
pub ctoken: Option<String>,
|
||||
pub corrected_query: Option<String>,
|
||||
pub channel_info: Option<ChannelInfo>,
|
||||
}
|
||||
|
||||
impl<T> YouTubeListMapper<T> {
|
||||
|
|
@ -458,7 +413,6 @@ impl<T> YouTubeListMapper<T> {
|
|||
warnings: Vec::new(),
|
||||
ctoken: None,
|
||||
corrected_query: None,
|
||||
channel_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -476,7 +430,6 @@ impl<T> YouTubeListMapper<T> {
|
|||
warnings,
|
||||
ctoken: None,
|
||||
corrected_query: None,
|
||||
channel_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -744,32 +697,6 @@ impl YouTubeListMapper<YouTubeItem> {
|
|||
YouTubeListItem::ShowingResultsForRenderer { corrected_query } => {
|
||||
self.corrected_query = Some(corrected_query);
|
||||
}
|
||||
YouTubeListItem::ChannelAboutFullMetadataRenderer(meta) => {
|
||||
let mut links = meta
|
||||
.primary_links
|
||||
.into_iter()
|
||||
.filter_map(|l| l.navigation_endpoint.url().map(|url| (l.title, url)))
|
||||
.collect::<Vec<_>>();
|
||||
for l in meta.links {
|
||||
let l = l.channel_external_link_view_model;
|
||||
if let TextComponent::Web { url, .. } = l.link {
|
||||
links.push((l.title.into(), util::sanitize_yt_url(&url)));
|
||||
}
|
||||
}
|
||||
|
||||
self.channel_info = Some(ChannelInfo {
|
||||
create_date: timeago::parse_textual_date_or_warn(
|
||||
self.lang,
|
||||
&meta.joined_date_text,
|
||||
&mut self.warnings,
|
||||
)
|
||||
.map(OffsetDateTime::date),
|
||||
view_count: meta
|
||||
.view_count_text
|
||||
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut self.warnings)),
|
||||
links,
|
||||
});
|
||||
}
|
||||
YouTubeListItem::RichItemRenderer { content } => {
|
||||
self.map_item(*content);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,166 +2,28 @@
|
|||
source: src/client/channel.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
Channel(
|
||||
ChannelInfo(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
subscriber_count: Some(881000),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
|
||||
width: 48,
|
||||
height: 48,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s88-c-k-c0x00ffffff-no-rj",
|
||||
width: 88,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s176-c-k-c0x00ffffff-no-rj",
|
||||
width: 176,
|
||||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
url: "http://www.youtube.com/@EEVblog",
|
||||
description: "NO SCRIPT, NO FEAR, ALL OPINION\nAn off-the-cuff Video Blog about Electronics Engineering, for engineers, hobbyists, enthusiasts, hackers and Makers\nHosted by Dave Jones from Sydney Australia\n\nDONATIONS:\nBitcoin: 3KqyH1U3qrMPnkLufM2oHDU7YB4zVZeFyZ\nEthereum: 0x99ccc4d2654ba40744a1f678d9868ecb15e91206\nPayPal: david@alternatezone.com\n\nPatreon: https://www.patreon.com/eevblog\n\nEEVblog2: http://www.youtube.com/EEVblog2\nEEVdiscover: https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ\n\nEMAIL:\nAdvertising/Commercial: eevblog+business@gmail.com\nFan mail: eevblog+fan@gmail.com\nHate Mail: eevblog+hate@gmail.com\n\nI DON\'T DO PAID VIDEO SPONSORSHIPS, DON\'T ASK!\n\nPLEASE:\nDo NOT ask for personal advice on something, post it in the EEVblog forum.\nI read ALL email, but please don\'t be offended if I don\'t have time to reply, I get a LOT of email.\n\nMailbag\nPO Box 7949\nBaulkham Hills NSW 2153\nAUSTRALIA",
|
||||
tags: [
|
||||
"electronics",
|
||||
"engineering",
|
||||
"maker",
|
||||
"hacker",
|
||||
"design",
|
||||
"circuit",
|
||||
"hardware",
|
||||
"pic",
|
||||
"atmel",
|
||||
"oscilloscope",
|
||||
"multimeter",
|
||||
"diy",
|
||||
"hobby",
|
||||
"review",
|
||||
"teardown",
|
||||
"microcontroller",
|
||||
"arduino",
|
||||
"video",
|
||||
"blog",
|
||||
"tutorial",
|
||||
"how-to",
|
||||
"interview",
|
||||
"rant",
|
||||
"industry",
|
||||
"news",
|
||||
"mailbag",
|
||||
"dumpster diving",
|
||||
"debunking",
|
||||
subscriber_count: Some(920000),
|
||||
video_count: Some(1920),
|
||||
create_date: Some("2009-04-04"),
|
||||
view_count: Some(199087682),
|
||||
country: Some(AU),
|
||||
links: [
|
||||
("EEVblog Web Site", "http://www.eevblog.com/"),
|
||||
("Twitter", "http://www.twitter.com/eevblog"),
|
||||
("Facebook", "http://www.facebook.com/EEVblog"),
|
||||
("EEVdiscover", "https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ"),
|
||||
("The EEVblog Forum", "http://www.eevblog.com/forum"),
|
||||
("EEVblog Merchandise (T-Shirts)", "http://www.eevblog.com/merch"),
|
||||
("EEVblog Donations", "http://www.eevblog.com/donations/"),
|
||||
("Patreon", "https://www.patreon.com/eevblog"),
|
||||
("SubscribeStar", "https://www.subscribestar.com/eevblog"),
|
||||
("The AmpHour Radio Show", "http://www.theamphour.com/"),
|
||||
("Flickr", "http://www.flickr.com/photos/eevblog"),
|
||||
("EEVblog AMAZON Store", "http://www.amazon.com/gp/redirect.html?ie=UTF8&location=http%3A%2F%2Fwww.amazon.com%2F&tag=ee04-20&linkCode=ur2&camp=1789&creative=390957"),
|
||||
("2nd EEVblog Channel", "http://www.youtube.com/EEVblog2"),
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1060,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1138-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1138,
|
||||
height: 188,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1707,
|
||||
height: 283,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2276-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 2276,
|
||||
height: 377,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 2560,
|
||||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: false,
|
||||
visitor_data: Some("CgszMUUzZDlGLWxiRSipqr2ZBg%3D%3D"),
|
||||
content: ChannelInfo(
|
||||
create_date: Some("2009-04-04"),
|
||||
view_count: Some(186854342),
|
||||
links: [
|
||||
("EEVblog Web Site", "http://www.eevblog.com/"),
|
||||
("Twitter", "http://www.twitter.com/eevblog"),
|
||||
("Facebook", "http://www.facebook.com/EEVblog"),
|
||||
("EEVdiscover", "https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ"),
|
||||
("The EEVblog Forum", "http://www.eevblog.com/forum"),
|
||||
("EEVblog Merchandise (T-Shirts)", "http://www.eevblog.com/merch"),
|
||||
("EEVblog Donations", "http://www.eevblog.com/donations/"),
|
||||
("Patreon", "https://www.patreon.com/eevblog"),
|
||||
("SubscribeStar", "https://www.subscribestar.com/eevblog"),
|
||||
("The AmpHour Radio Show", "http://www.theamphour.com/"),
|
||||
("Flickr", "http://www.flickr.com/photos/eevblog"),
|
||||
("EEVblog AMAZON Store", "http://www.amazon.com/gp/redirect.html?ie=UTF8&location=http%3A%2F%2Fwww.amazon.com%2F&tag=ee04-20&linkCode=ur2&camp=1789&creative=390957"),
|
||||
("2nd EEVblog Channel", "http://www.youtube.com/EEVblog2"),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Reference in a new issue