fix: parse new comment model (A/B#14 frameworkUpdates)
This commit is contained in:
parent
348c8523fe
commit
b0331f7250
10 changed files with 18207 additions and 125 deletions
|
|
@ -3,7 +3,7 @@ use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkip
|
|||
|
||||
use super::{
|
||||
video_item::YouTubeListRenderer, Alert, ChannelBadge, ContentRenderer, ContentsRenderer,
|
||||
ContinuationActionWrap, ResponseContext, Thumbnails, TwoColumnBrowseResults,
|
||||
ContinuationActionWrap, ImageView, ResponseContext, Thumbnails, TwoColumnBrowseResults,
|
||||
};
|
||||
use crate::serializer::text::{AttributedText, Text, TextComponent};
|
||||
|
||||
|
|
@ -224,12 +224,6 @@ pub(crate) struct PhAvatarView3 {
|
|||
pub avatar_view_model: ImageView,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ImageView {
|
||||
pub image: Thumbnails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhMetadataView {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ pub(crate) mod channel_rss;
|
|||
pub(crate) use channel_rss::ChannelRss;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use serde::{
|
||||
|
|
@ -106,6 +107,12 @@ pub(crate) struct ThumbnailsWrap {
|
|||
pub thumbnail: Thumbnails,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ImageView {
|
||||
pub image: Thumbnails,
|
||||
}
|
||||
|
||||
/// List of images in different resolutions.
|
||||
/// Not only used for thumbnails, but also for avatars and banners.
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
|
|
@ -374,3 +381,87 @@ pub(crate) fn alerts_to_err(id: &str, alerts: Option<Vec<Alert>>) -> ExtractionE
|
|||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
// FRAMEWORK UPDATES
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct FrameworkUpdates<T> {
|
||||
pub entity_batch_update: EntityBatchUpdate<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct EntityBatchUpdate<T> {
|
||||
pub mutations: FrameworkUpdateMutations<T>,
|
||||
}
|
||||
|
||||
/// List of update mutations that deserializes into a HashMap (entity_key => payload)
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FrameworkUpdateMutations<T> {
|
||||
pub items: HashMap<String, T>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'de, T> Deserialize<'de> for FrameworkUpdateMutations<T>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct SeqVisitor<T>(PhantomData<T>);
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum MutationOrError<T> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Good {
|
||||
entity_key: String,
|
||||
payload: T,
|
||||
},
|
||||
Error(serde_json::Value),
|
||||
}
|
||||
|
||||
impl<'de, T> Visitor<'de> for SeqVisitor<T>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
type Value = FrameworkUpdateMutations<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("sequence of entity mutations")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut items = HashMap::with_capacity(seq.size_hint().unwrap_or_default());
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
while let Some(value) = seq.next_element::<MutationOrError<T>>()? {
|
||||
match value {
|
||||
MutationOrError::Good {
|
||||
entity_key,
|
||||
payload,
|
||||
} => {
|
||||
items.insert(entity_key, payload);
|
||||
}
|
||||
MutationOrError::Error(value) => {
|
||||
warnings.push(format!(
|
||||
"error deserializing item: {}",
|
||||
serde_json::to_string(&value).unwrap_or_default()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FrameworkUpdateMutations { items, warnings })
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_seq(SeqVisitor(PhantomData::<T>))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
||||
|
||||
use crate::serializer::text::TextComponent;
|
||||
use crate::serializer::{
|
||||
text::{AccessibilityText, AttributedText, Text, TextComponents},
|
||||
text::{AccessibilityText, AttributedText, Text, TextComponent, TextComponents},
|
||||
MapResult,
|
||||
};
|
||||
|
||||
|
|
@ -13,7 +12,10 @@ use super::{
|
|||
url_endpoint::BrowseEndpointWrap, ContinuationEndpoint, ContinuationItemRenderer, Icon,
|
||||
MusicContinuationData, Thumbnails,
|
||||
};
|
||||
use super::{ChannelBadge, ContentsRendererLogged, ResponseContext, YouTubeListItem};
|
||||
use super::{
|
||||
ChannelBadge, ContentsRendererLogged, FrameworkUpdates, ImageView, ResponseContext,
|
||||
YouTubeListItem,
|
||||
};
|
||||
|
||||
/*
|
||||
#VIDEO DETAILS
|
||||
|
|
@ -476,6 +478,7 @@ pub(crate) struct VideoComments {
|
|||
/// - n*commentRenderer, continuationItemRenderer:
|
||||
/// replies + continuation
|
||||
pub on_response_received_endpoints: MapResult<Vec<CommentsContItem>>,
|
||||
pub framework_updates: Option<FrameworkUpdates<Payload>>,
|
||||
}
|
||||
|
||||
/// Video comments continuation
|
||||
|
|
@ -498,23 +501,13 @@ pub(crate) struct AppendComments {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum CommentListItem {
|
||||
/// Top-level comment
|
||||
#[serde(rename_all = "camelCase")]
|
||||
CommentThreadRenderer {
|
||||
comment: Comment,
|
||||
/// Continuation token to fetch replies
|
||||
#[serde(default)]
|
||||
replies: Replies,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
rendering_priority: CommentPriority,
|
||||
},
|
||||
CommentThreadRenderer(CommentThreadRenderer),
|
||||
/// Reply comment
|
||||
CommentRenderer(CommentRenderer),
|
||||
/// Reply comment (A/B #14)
|
||||
CommentViewModel(CommentViewModel),
|
||||
/// Continuation token to fetch more comments
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContinuationItemRenderer {
|
||||
continuation_endpoint: ContinuationEndpoint,
|
||||
},
|
||||
ContinuationItemRenderer(ContinuationItemVariants),
|
||||
/// Header of the comment section (contains number of comments)
|
||||
#[serde(rename_all = "camelCase")]
|
||||
CommentsHeaderRenderer {
|
||||
|
|
@ -524,6 +517,46 @@ pub(crate) enum CommentListItem {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum ContinuationItemVariants {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Ep {
|
||||
continuation_endpoint: ContinuationEndpoint,
|
||||
},
|
||||
Btn {
|
||||
button: ContinuationButton,
|
||||
},
|
||||
}
|
||||
|
||||
impl ContinuationItemVariants {
|
||||
pub fn token(self) -> String {
|
||||
match self {
|
||||
ContinuationItemVariants::Ep {
|
||||
continuation_endpoint,
|
||||
} => continuation_endpoint,
|
||||
ContinuationItemVariants::Btn { button } => button.button_renderer.command,
|
||||
}
|
||||
.continuation_command
|
||||
.token
|
||||
}
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommentThreadRenderer {
|
||||
/// Missing on the FrameworkUpdate data model (A/B #14)
|
||||
pub comment: Option<Comment>,
|
||||
pub comment_view_model: Option<CommentViewModelWrap>,
|
||||
/// Continuation token to fetch replies
|
||||
#[serde(default)]
|
||||
pub replies: Replies,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
pub rendering_priority: CommentPriority,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Comment {
|
||||
|
|
@ -564,7 +597,7 @@ pub(crate) struct CommentRenderer {
|
|||
pub action_buttons: CommentActionButtons,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub(crate) enum CommentPriority {
|
||||
/// Default rendering priority
|
||||
|
|
@ -574,6 +607,26 @@ pub(crate) enum CommentPriority {
|
|||
RenderingPriorityPinnedComment,
|
||||
}
|
||||
|
||||
impl From<CommentPriority> for bool {
|
||||
fn from(value: CommentPriority) -> Self {
|
||||
matches!(value, CommentPriority::RenderingPriorityPinnedComment)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommentViewModelWrap {
|
||||
pub comment_view_model: CommentViewModel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommentViewModel {
|
||||
pub comment_id: String,
|
||||
pub comment_key: String,
|
||||
pub toolbar_state_key: String,
|
||||
}
|
||||
|
||||
/// Does not contain replies directly but a continuation token
|
||||
/// for fetching them.
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
|
|
@ -637,3 +690,85 @@ pub(crate) struct AuthorCommentBadgeRenderer {
|
|||
/// Artist: `OFFICIAL_ARTIST_BADGE`
|
||||
pub icon: Icon,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum Payload {
|
||||
CommentEntityPayload(CommentEntityPayload),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
EngagementToolbarStateEntityPayload {
|
||||
heart_state: HeartState,
|
||||
},
|
||||
#[serde(other, deserialize_with = "deserialize_ignore_any")]
|
||||
None,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommentEntityPayload {
|
||||
pub properties: CommentProperties,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub author: Option<CommentAuthor>,
|
||||
pub toolbar: CommentToolbar,
|
||||
#[serde(default)]
|
||||
pub avatar: ImageView,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommentProperties {
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub content: TextComponents,
|
||||
pub published_time: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommentAuthor {
|
||||
pub channel_id: String,
|
||||
pub display_name: String,
|
||||
#[serde(default)]
|
||||
pub is_verified: bool,
|
||||
#[serde(default)]
|
||||
pub is_artist: bool,
|
||||
#[serde(default)]
|
||||
pub is_creator: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommentToolbar {
|
||||
pub like_count_notliked: String,
|
||||
pub reply_count: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub(crate) enum HeartState {
|
||||
ToolbarHeartStateUnhearted,
|
||||
ToolbarHeartStateHearted,
|
||||
}
|
||||
|
||||
impl From<HeartState> for bool {
|
||||
fn from(value: HeartState) -> Self {
|
||||
match value {
|
||||
HeartState::ToolbarHeartStateUnhearted => false,
|
||||
HeartState::ToolbarHeartStateHearted => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ContinuationButton {
|
||||
pub button_renderer: ContinuationButtonRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ContinuationButtonRenderer {
|
||||
pub command: ContinuationEndpoint,
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue