fix: add support for new channel about data model

This commit is contained in:
ThetaDev 2023-08-22 22:58:28 +02:00
parent 57628d1392
commit e2eda901b1
3 changed files with 66 additions and 10 deletions

View file

@ -14,7 +14,7 @@ use crate::{
}, },
param::Language, param::Language,
serializer::{ serializer::{
text::{AccessibilityText, Text, TextComponent}, text::{AccessibilityText, AttributedText, Text, TextComponent},
MapResult, MapResult,
}, },
util::{self, timeago, TryRemove}, util::{self, timeago, TryRemove},
@ -369,6 +369,9 @@ pub(crate) struct ChannelFullMetadata {
#[serde(default)] #[serde(default)]
#[serde_as(as = "VecSkipError<_>")] #[serde_as(as = "VecSkipError<_>")]
pub primary_links: Vec<PrimaryLink>, pub primary_links: Vec<PrimaryLink>,
#[serde(default)]
// #[serde_as(as = "VecSkipError<_>")]
pub links: Vec<ExternalLink>,
} }
#[serde_as] #[serde_as]
@ -380,6 +383,22 @@ pub(crate) struct PrimaryLink {
pub navigation_endpoint: NavigationEndpoint, 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 { trait IsLive {
fn is_live(&self) -> bool; fn is_live(&self) -> bool;
} }
@ -726,6 +745,18 @@ impl YouTubeListMapper<YouTubeItem> {
self.corrected_query = Some(corrected_query); self.corrected_query = Some(corrected_query);
} }
YouTubeListItem::ChannelAboutFullMetadataRenderer(meta) => { 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 { self.channel_info = Some(ChannelInfo {
create_date: timeago::parse_textual_date_or_warn( create_date: timeago::parse_textual_date_or_warn(
self.lang, self.lang,
@ -736,11 +767,7 @@ impl YouTubeListMapper<YouTubeItem> {
view_count: meta view_count: meta
.view_count_text .view_count_text
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut self.warnings)), .and_then(|txt| util::parse_numeric_or_warn(&txt, &mut self.warnings)),
links: meta links,
.primary_links
.into_iter()
.filter_map(|l| l.navigation_endpoint.url().map(|url| (l.title, url)))
.collect(),
}); });
} }
YouTubeListItem::RichItemRenderer { content } => { YouTubeListItem::RichItemRenderer { content } => {

View file

@ -200,8 +200,12 @@ impl<'de> Deserialize<'de> for TextComponent {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let mut text = RichTextInternal::deserialize(deserializer)?; let text = RichTextInternal::deserialize(deserializer)?;
Ok(text.runs.swap_remove(0).into()) text.runs
.into_iter()
.next()
.map(TextComponent::from)
.ok_or(serde::de::Error::invalid_length(0, &"at least 1"))
} }
} }
@ -289,6 +293,20 @@ impl<'de> DeserializeAs<'de, TextComponents> for AttributedText {
} }
} }
impl<'de> DeserializeAs<'de, TextComponent> for AttributedText {
fn deserialize_as<D>(deserializer: D) -> Result<TextComponent, D::Error>
where
D: Deserializer<'de>,
{
let components: TextComponents = AttributedText::deserialize_as(deserializer)?;
components
.0
.into_iter()
.next()
.ok_or(serde::de::Error::invalid_length(0, &"at least 1"))
}
}
impl TryFrom<TextComponent> for crate::model::ChannelId { impl TryFrom<TextComponent> for crate::model::ChannelId {
type Error = (); type Error = ();
@ -404,6 +422,17 @@ impl TextComponent {
} }
} }
impl From<TextComponent> for String {
fn from(value: TextComponent) -> Self {
match value {
TextComponent::Video { text, .. }
| TextComponent::Browse { text, .. }
| TextComponent::Web { text, .. }
| TextComponent::Text { text } => text,
}
}
}
impl TextComponents { impl TextComponents {
/// Return the string representation of the first text component /// Return the string representation of the first text component
pub fn first_str(&self) -> &str { pub fn first_str(&self) -> &str {

View file

@ -224,7 +224,7 @@ pub fn retry_delay(
/// Also strips google analytics tracking parameters /// Also strips google analytics tracking parameters
/// (`utm_source`, `utm_medium`, `utm_campaign`, `utm_content`) because google analytics is bad. /// (`utm_source`, `utm_medium`, `utm_campaign`, `utm_content`) because google analytics is bad.
pub fn sanitize_yt_url(url: &str) -> String { pub fn sanitize_yt_url(url: &str) -> String {
fn sanitize_yt_url_inner(url: &str) -> Option<String> { fn try_sanitize_yt_url(url: &str) -> Option<String> {
let mut parsed_url = Url::parse(url).ok()?; let mut parsed_url = Url::parse(url).ok()?;
// Convert redirect url // Convert redirect url
@ -260,7 +260,7 @@ pub fn sanitize_yt_url(url: &str) -> String {
Some(parsed_url.to_string()) Some(parsed_url.to_string())
} }
sanitize_yt_url_inner(url).unwrap_or_else(|| url.to_string()) try_sanitize_yt_url(url).unwrap_or_else(|| url.to_string())
} }
pub fn div_ceil(a: u32, b: u32) -> u32 { pub fn div_ceil(a: u32, b: u32) -> u32 {