Compare commits

..

No commits in common. "449fc0128e04c55637e5fd2258913f80ca5660e9" and "79c504954ef856332d7e7ff05e712818f010588d" have entirely different histories.

15 changed files with 138 additions and 18242 deletions

View file

@ -1,8 +1,5 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
on: [push, pull_request]
jobs:
Test:

View file

@ -2,16 +2,6 @@
All notable changes to this project will be documented in this file.
## [v0.1.3](https://code.thetadev.de/ThetaDev/rustypipe/compare/rustypipe/v0.1.2..v0.1.3) - 2024-04-01
### 🐛 Bug Fixes
- Parse new comment model (A/B#14 frameworkUpdates) - ([b0331f7](https://code.thetadev.de/ThetaDev/rustypipe/commit/b0331f7250f5d7d61a45209150739d2cb08b4280))
### ◀️ Revert
- "fix: improve VecLogErr messages" (leads to infinite loop) - ([348c852](https://code.thetadev.de/ThetaDev/rustypipe/commit/348c8523fe847f2f6ce98317375a7ab65e778ed2))
## [v0.1.2](https://code.thetadev.de/ThetaDev/rustypipe/compare/rustypipe/v0.1.1..v0.1.2) - 2024-03-26
### 🐛 Bug Fixes

View file

@ -1,6 +1,6 @@
[package]
name = "rustypipe"
version = "0.1.3"
version = "0.1.2"
edition.workspace = true
authors.workspace = true
license.workspace = true

View file

@ -36,7 +36,7 @@ Client for the public YouTube / YouTube Music API (Innertube), inspired by
```toml
[dependencies]
rustypipe = "0.1.3"
rustypipe = "0.1.2"
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
```

View file

@ -13,8 +13,6 @@ use rustypipe::param::ChannelVideoTab;
use serde::de::IgnoredAny;
use serde::{Deserialize, Serialize};
use crate::model::QCont;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, TryFromPrimitive, Serialize, Deserialize,
)]
@ -33,15 +31,10 @@ pub enum ABTest {
LikeButtonViewmodel = 11,
ChannelPageHeader = 12,
MusicPlaylistTwoColumn = 13,
CommentsFrameworkUpdate = 14,
}
/// List of active A/B tests that are run when none is manually specified
const TESTS_TO_RUN: [ABTest; 3] = [
ABTest::ChannelPageHeader,
ABTest::MusicPlaylistTwoColumn,
ABTest::CommentsFrameworkUpdate,
];
const TESTS_TO_RUN: [ABTest; 2] = [ABTest::ChannelPageHeader, ABTest::MusicPlaylistTwoColumn];
#[derive(Debug, Serialize, Deserialize)]
pub struct ABTestRes {
@ -111,7 +104,6 @@ pub async fn run_test(
ABTest::LikeButtonViewmodel => like_button_viewmodel(&query).await,
ABTest::ChannelPageHeader => channel_page_header(&query).await,
ABTest::MusicPlaylistTwoColumn => music_playlist_two_column(&query).await,
ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
}
.unwrap();
pb.inc(1);
@ -364,20 +356,3 @@ pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
.unwrap();
Ok(res.contains("\"musicResponsiveHeaderRenderer\""))
}
pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
let continuation =
"Eg0SC3dMZHBSN2d1S3k4GAYyJSIRIgt3TGRwUjdndUt5ODAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D";
let res = rp
.raw(
ClientType::Desktop,
"next",
&QCont {
context: rp.get_context(ClientType::Desktop, true, None).await,
continuation,
},
)
.await
.unwrap();
Ok(res.contains("\"frameworkUpdates\""))
}

View file

@ -592,6 +592,7 @@ be accomodated. There are also no mobile/TV header images available any more.
}
```
## [13] Music album/playlist 2-column layout
- **Encountered on:** 29.02.2024
@ -601,149 +602,5 @@ be accomodated. There are also no mobile/TV header images available any more.
![A/B test 13 screenshot](./_img/ab_13.png)
YouTube Music updated the layout of album and playlist pages. The new layout shows the
cover on the left side of the playlist content.
## [14] Comments Framework update
- **Encountered on:** 31.01.2024
- **Impact:** 🟢 Low
- **Endpoint:** next
- **Status:** Common (50%)
YouTube changed the data model for YouTube comments, now putting the content into a
seperate framework update object
```json
{
"frameworkUpdates": {
"onResponseReceivedEndpoints": [
{
"clickTrackingParams": "CAAQg2ciEwi64q3dmKGFAxWvy0IFHc14BKM=",
"reloadContinuationItemsCommand": {
"targetId": "comments-section",
"continuationItems": [
{
"commentThreadRenderer": {
"replies": {
"commentRepliesRenderer": {
"contents": [
{
"continuationItemRenderer": {
"trigger": "CONTINUATION_TRIGGER_ON_ITEM_SHOWN",
"continuationEndpoint": {
"clickTrackingParams": "CHgQvnUiEwi64q3dmKGFAxWvy0IFHc14BKM=",
"commandMetadata": {
"webCommandMetadata": {
"sendPost": true,
"apiUrl": "/youtubei/v1/next"
}
},
"continuationCommand": {
"token": "Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd5TlRUOHV4REVqZ1lxeWJJRjRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZw%3D%3D",
"request": "CONTINUATION_REQUEST_TYPE_WATCH_NEXT"
}
}
}
}
],
"trackingParams": "CHgQvnUiEwi64q3dmKGFAxWvy0IFHc14BKM=",
"viewReplies": {
"buttonRenderer": {
"text": { "runs": [{ "text": "220 replies" }] },
"icon": { "iconType": "ARROW_DROP_DOWN" },
"trackingParams": "CHoQosAEIhMIuuKt3ZihhQMVr8tCBR3NeASj",
"iconPosition": "BUTTON_ICON_POSITION_TYPE_LEFT_OF_TEXT"
}
},
"hideReplies": {
"buttonRenderer": {
"text": { "runs": [{ "text": "220 replies" }] },
"icon": { "iconType": "ARROW_DROP_UP" },
"trackingParams": "CHkQ280EIhMIuuKt3ZihhQMVr8tCBR3NeASj",
"iconPosition": "BUTTON_ICON_POSITION_TYPE_LEFT_OF_TEXT"
}
},
"targetId": "comment-replies-item-UgyNTT8uxDEjgYqybIF4AaABAg"
}
},
"trackingParams": "CHYQwnUYywEiEwi64q3dmKGFAxWvy0IFHc14BKM=",
"renderingPriority": "RENDERING_PRIORITY_PINNED_COMMENT",
"isModeratedElqComment": false,
"commentViewModel": {
"commentViewModel": {
"commentId": "UgyNTT8uxDEjgYqybIF4AaABAg"
}
}
}
}
]
}
}
],
"entityBatchUpdate": {
"mutations": [
{
"entityKey": "EhpVZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZyAoKAE%3D",
"type": "ENTITY_MUTATION_TYPE_REPLACE",
"payload": {
"commentEntityPayload": {
"key": "EhpVZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZyAoKAE%3D",
"properties": {
"commentId": "UgyNTT8uxDEjgYqybIF4AaABAg",
"content": {
"content": "⚠️ Important notice: if you put any symbol immediately after markup, it will not work: *here is the comma*, without space.\n\nYou should leave space before and after , to make it work.\n\nSame for _underscore_, and -hyphen-.\n\nLeave space before opening and after closing underscore and hyphen. Put all dots and commas inside markup.",
"styleRuns": [
{
"startIndex": 135,
"length": 28,
"weightLabel": "FONT_WEIGHT_MEDIUM"
},
{
"startIndex": 267,
"length": 10,
"weightLabel": "FONT_WEIGHT_NORMAL",
"italic": true
},
{
"startIndex": 282,
"length": 7,
"weightLabel": "FONT_WEIGHT_NORMAL",
"strikethrough": "LINE_STYLE_SINGLE"
}
]
},
"publishedTime": "2 years ago (edited)",
"replyLevel": 0,
"authorButtonA11y": "@kibizoid",
"toolbarStateKey": "EhpVZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZyAsKAE%3D",
"translateButtonEntityKey": "EhpVZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZyD_ASgB"
},
"author": {
"channelId": "UCUJfyiofeHQTmxKwZ6cCwIg",
"displayName": "@kibizoid",
"avatarThumbnailUrl": "https://yt3.ggpht.com/ytc/AIdro_nY2PkIyojDqs9Bk5RY6J90-U7wePswTYl799DNJQ=s88-c-k-c0x00ffffff-no-rj",
"isVerified": false,
"isCurrentUser": false,
"isCreator": false,
"isArtist": false
},
"avatar": {
"image": {
"sources": [
{
"url": "https://yt3.ggpht.com/ytc/AIdro_nY2PkIyojDqs9Bk5RY6J90-U7wePswTYl799DNJQ=s88-c-k-c0x00ffffff-no-rj",
"width": 88,
"height": 88
}
]
}
}
}
}
}
]
}
}
}
```
YouTube Music updated the layout of album and playlist pages. The new layout shows
the cover on the left side of the playlist content.

View file

@ -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, ImageView, ResponseContext, Thumbnails, TwoColumnBrowseResults,
ContinuationActionWrap, ResponseContext, Thumbnails, TwoColumnBrowseResults,
};
use crate::serializer::text::{AttributedText, Text, TextComponent};
@ -224,6 +224,12 @@ 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 {

View file

@ -48,7 +48,6 @@ 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::{
@ -107,12 +106,6 @@ 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)]
@ -381,87 +374,3 @@ 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>))
}
}

View file

@ -3,8 +3,9 @@
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, TextComponent, TextComponents},
text::{AccessibilityText, AttributedText, Text, TextComponents},
MapResult,
};
@ -12,10 +13,7 @@ use super::{
url_endpoint::BrowseEndpointWrap, ContinuationEndpoint, ContinuationItemRenderer, Icon,
MusicContinuationData, Thumbnails,
};
use super::{
ChannelBadge, ContentsRendererLogged, FrameworkUpdates, ImageView, ResponseContext,
YouTubeListItem,
};
use super::{ChannelBadge, ContentsRendererLogged, ResponseContext, YouTubeListItem};
/*
#VIDEO DETAILS
@ -478,7 +476,6 @@ 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
@ -501,13 +498,23 @@ pub(crate) struct AppendComments {
#[serde(rename_all = "camelCase")]
pub(crate) enum CommentListItem {
/// Top-level comment
CommentThreadRenderer(CommentThreadRenderer),
#[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,
},
/// Reply comment
CommentRenderer(CommentRenderer),
/// Reply comment (A/B #14)
CommentViewModel(CommentViewModel),
/// Continuation token to fetch more comments
ContinuationItemRenderer(ContinuationItemVariants),
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
/// Header of the comment section (contains number of comments)
#[serde(rename_all = "camelCase")]
CommentsHeaderRenderer {
@ -517,46 +524,6 @@ 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 {
@ -597,7 +564,7 @@ pub(crate) struct CommentRenderer {
pub action_buttons: CommentActionButtons,
}
#[derive(Default, Clone, Copy, Debug, Deserialize)]
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum CommentPriority {
/// Default rendering priority
@ -607,26 +574,6 @@ 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)]
@ -690,85 +637,3 @@ 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,
}

View file

@ -1,691 +0,0 @@
---
source: src/client/video_details.rs
expression: map_res.c
---
Paginator(
count: Some(20617),
items: [
Comment(
id: "UgyNTT8uxDEjgYqybIF4AaABAg",
text: RichText([
Text(
text: "⚠\u{fe0f} Important notice: if you put any symbol immediately after markup, it will not work: *here is the comma*, without space.\n\nYou should leave space before and after , to make it work.\n\nSame for _underscore_, and -hyphen-.\n\nLeave space before opening and after closing underscore and hyphen. Put all dots and commas inside markup.",
),
]),
author: Some(ChannelTag(
id: "UCUJfyiofeHQTmxKwZ6cCwIg",
name: "@kibizoid",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AIdro_nY2PkIyojDqs9Bk5RY6J90-U7wePswTYl799DNJQ=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 years ago (edited)",
like_count: Some(293),
reply_count: 220,
replies: Paginator(
count: Some(220),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd5TlRUOHV4REVqZ1lxeWJJRjRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: true,
hearted: true,
),
Comment(
id: "UgycWgNOoon0A4EV9LZ4AaABAg",
text: RichText([
Text(
text: "Me: tests out fonts\nFriend: Why are you doing this?\nMe: my goals are beyond your understanding",
),
]),
author: Some(ChannelTag(
id: "UCr0PeEY_am9P-GobbfvKECw",
name: "@userfjdrg",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/Zr2PmQsrD4obL2n5HS18X3jKXGJ-HFjIJS_OcZv4I5VAk5HuLRCpzFprY5Hh7n23-FCURVJi=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 years ago",
like_count: Some(80),
reply_count: 34,
replies: Paginator(
count: Some(34),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd5Y1dnTk9vb24wQTRFVjlMWjRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3ljV2dOT29vbjBBNEVWOUxaNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: true,
),
Comment(
id: "Ugy5iq4M1c3WS3lGmih4AaABAg",
text: RichText([
Text(
text: "To-do list\n• be dumb\n• get kicked out when i can legally live alone\n• spend money on pointless things",
),
]),
author: Some(ChannelTag(
id: "UCDB5XvpUB8cEvjbWewlp28w",
name: "@T0r0xFan",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/PPL5vj6-pXFpaLa41yet34OHGcEYt06WPQLmruaiFJSM0eLmn9ZQW0QgTtdafDBO-kNy2oukVA=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 years ago (edited)",
like_count: Some(48),
reply_count: 22,
replies: Paginator(
count: Some(22),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd5NWlxNE0xYzNXUzNsR21paDRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3k1aXE0TTFjM1dTM2xHbWloNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: true,
),
Comment(
id: "UgxqDIVVcoigjtx4Dtl4AaABAg",
text: RichText([
Text(
text: "omg thank you! Ive been looking for this tutorial for a year forever",
),
]),
author: Some(ChannelTag(
id: "UCxa4xER0-cFbcIYp0ZIeVaw",
name: "@LunasVibe",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/P5Io1kQb7GRwe4pgNsaYFEm30hDl_T7Tp5rZo7aYWFkqbV6Yp_lCYVuaaK7O3SEsnIX_5iC1Hw=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "1 month ago",
like_count: Some(0),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgxDQfVQdYaWR-VUM-94AaABAg",
text: RichText([
Text(
text: "tysm\ni finally learned it\nother channel never go straight to the point",
),
]),
author: Some(ChannelTag(
id: "UC8cojSRuyZT74Bs_b5AecTA",
name: "@Bp_bts_skz_for_life",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/0zFBRYv8wo2JqzkyMk29xgC8zD1nKYNSSoD3Zo9XP8t9rHrbTYEEt0gdu0O3XS7Scpza3JJKog=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "1 month ago (edited)",
like_count: Some(1),
reply_count: 1,
replies: Paginator(
count: Some(1),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd4RFFmVlFkWWFXUi1WVU0tOTRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3hEUWZWUWRZYVdSLVZVTS05NEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgxFvrmwec-jmfQyGRR4AaABAg",
text: RichText([
Text(
text: "I like how this was straight to the point. Unlike other channels lol Thank you!",
),
]),
author: Some(ChannelTag(
id: "UCCyIVS_s1-jA48pPft8AifA",
name: "@ishouldbesleepingalready",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/N90y_XdoDP6Rd1v6Z5OpDH8jMtvqpU1qnF6DJoIL6qcLiWfZK7ok8u_IxqSxJazaQH6oqhEbqA=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 years ago",
like_count: Some(241),
reply_count: 65,
replies: Paginator(
count: Some(65),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd4RnZybXdlYy1qbWZReUdSUjRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3hGdnJtd2VjLWptZlF5R1JSNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugy-3OYEcwxkvyrrCqN4AaABAg",
text: RichText([
Text(
text: "To the person who is reading this: You\'re intelligent and smart, stay safe",
),
]),
author: Some(ChannelTag(
id: "UCQklgcA8quxZm5pgNAsVJAQ",
name: "@blocking948",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AIdro_nAg9bEjW4otWlryJwqAgiDRLzy8ZX-ROqkDY1ksQ=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "3 years ago (edited)",
like_count: Some(711),
reply_count: 250,
replies: Paginator(
count: Some(250),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd5LTNPWUVjd3hrdnlyckNxTjRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3ktM09ZRWN3eGt2eXJyQ3FONEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: true,
),
Comment(
id: "Ugylw3ss_xv9svWbRud4AaABAg",
text: RichText([
Text(
text: " life could be a dream, life could be a dream ",
),
]),
author: Some(ChannelTag(
id: "UCSyjdP7Duhns4Ybncy6ObZA",
name: "@malarchee0899",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/dSwRzOfoKrN4yc58uhFlIyqBXmbi6B14-On-wEEM_S6Nr6aDHTkG-xVkI1-u-uBwqKqodEgrMro=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "13 days ago",
like_count: Some(2),
reply_count: 1,
replies: Paginator(
count: Some(1),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd5bHczc3NfeHY5c3ZXYlJ1ZDRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3lsdzNzc194djlzdldiUnVkNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgydXobRB0F5dW1KVsF4AaABAg",
text: RichText([
Text(
text: "Woah! thank you for showing me this I really needed it!",
),
]),
author: Some(ChannelTag(
id: "UC9f9uJgwsCBBHA4CioIzdkA",
name: "@fatimagarcia3162",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/cKmBVKqq9mkW4F355y3UtUw4POwTWKi-0LUYLDx85vffRd7pU-LECXvudUrHH_9qobo6A1kM=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "1 month ago",
like_count: Some(0),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgwmFn6ejKltcZ_BZvl4AaABAg",
text: RichText([
Text(
text: "The fitness gram pacer test is a multistage aerobic capacity test that progressively gets more difficult as it continues.",
),
]),
author: Some(ChannelTag(
id: "UCIymYi-_AJ10pYrh8sqTBTg",
name: "@No-du9is",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AIdro_lPWhGGvIIA08s4u_-Lwyx88rGSRksOFeYHipE=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 years ago",
like_count: Some(22),
reply_count: 12,
replies: Paginator(
count: Some(12),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd3bUZuNmVqS2x0Y1pfQlp2bDRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3dtRm42ZWpLbHRjWl9CWnZsNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: true,
),
Comment(
id: "UgxtXH6bWRWm8ahavfR4AaABAg",
text: RichText([
Text(
text: "YouTube got a new update(or probably it\'s a bug) and for that it\'s not showing bold/strikethrough/italic on the app but it\'s showing on other places.",
),
]),
author: Some(ChannelTag(
id: "UCyaflzzOH0GC683DlQ-fzwg",
name: "@HaruXen",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/YMmDD0zp5wT6l5ozqVMEMuqm5W07QFqmMHzOJ9QKGnSf9xpgEQ0rznstfXlBDxlFpLIrltQxRg=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "3 years ago",
like_count: Some(167),
reply_count: 337,
replies: Paginator(
count: Some(337),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd4dFhINmJXUldtOGFoYXZmUjRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3h0WEg2YldSV204YWhhdmZSNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: true,
pinned: false,
hearted: false,
),
Comment(
id: "UgyHg3XnjBV935da_Lh4AaABAg",
text: RichText([
Text(
text: "omg it works i actuallly cant believe this ive been wanting to do this for ages thankyou so much!",
),
]),
author: Some(ChannelTag(
id: "UCFL5d8rMCfbxppODSbRLOgQ",
name: "@Auf-dem-weg-zum-sieg",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/oi4vbSV3Cx9G97QcHkSMWL98LksC6rnTLoq93T5sOO8MNuZPXWEXq9Nqkp8XYF93L2WklHADmNY=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "4 days ago",
like_count: Some(0),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgxIL5emXyn42htlfZZ4AaABAg",
text: RichText([
Text(
text: "I did know how to do that writing where the text is highlighted before, and now after I watched this video I knew even how to write those styles of text.",
),
]),
author: Some(ChannelTag(
id: "UChkVaXCYN_QcaE50zETAMOg",
name: "@CasamTheAnimator",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ctUbv8QpWPZRZumEBTVhlSSxg0JfiyvJ40nrWj_0ivOy5s6OoPK7iNp01diskRLs1Hig4ZE82w=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "3 weeks ago",
like_count: Some(0),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgwMKY-89XCdCVB9bXp4AaABAg",
text: RichText([
Text(
text: "Nobody asked for, but everyone needed",
),
]),
author: Some(ChannelTag(
id: "UCDezbPSXn3awzhxVm7qhGtg",
name: "@0_Ed",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/4DWdgRIJ0lEV-e4GZFrdf8MGxQBtML2aix2orKBt3iM6QBrh7Kg1ur1FZlyRmqWpWnRPRIex9w=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "3 years ago",
like_count: Some(12),
reply_count: 5,
replies: Paginator(
count: Some(5),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd3TUtZLTg5WENkQ1ZCOWJYcDRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3dNS1ktODlYQ2RDVkI5YlhwNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: true,
),
Comment(
id: "UgyfuG2sCDvgnRUYHJp4AaABAg",
text: RichText([
Text(
text: "me: types bold\n\nHaruTutorial: your bald",
),
]),
author: Some(ChannelTag(
id: "UCge96FdHXkARBjzPhdYl8Sg",
name: "@stargazeu",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/JoeW_qNuf9b6BusB3E6JShizqRLB4jR3NaTnsnzvpUQ1KW88OcS74_Sx1h6vjZiXK2uOxnrUNeY=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "3 years ago",
like_count: Some(42),
reply_count: 15,
replies: Paginator(
count: Some(15),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd5ZnVHMnNDRHZnblJVWUhKcDRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3lmdUcyc0NEdmduUlVZSEpwNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: true,
),
Comment(
id: "UgwAXndNNEa1h-VVIC94AaABAg",
text: RichText([
Text(
text: "the McDonalds dont feel like turning the Icecream machine on",
),
]),
author: Some(ChannelTag(
id: "UCkmY4kQ8e8gDRllV485Rd9g",
name: "@Flowershowrise",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/c2iMsq-wFjKRjTDqPy14UpMI1B9hNms4moW9H7xtPjOMI0vjaHwN94me23upYar-8CE3s6QkFw=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "1 month ago",
like_count: Some(2),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgwwEBqareQ0tpsW7RR4AaABAg",
text: RichText([
Text(
text: "YOOO THIS IS SICK! THANK YOU MAN!",
),
]),
author: Some(ChannelTag(
id: "UChIbg4dGguUwzg7O-xmi57g",
name: "@ziaaaaa.",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/HEAdH93oAQu2ScXNmiKIISapv5O9dKSVLuT3gD1zJhSgHqTaptL7JPun6A5GZqg58_C75_OPkQ=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "1 month ago",
like_count: Some(0),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgxnFMLrpvbCWzHidml4AaABAg",
text: RichText([
Text(
text: "Someone must honor him , this man is the best , no , he is a LEGEND . We must all thank him for his video and for getting to the point immediately.",
),
]),
author: Some(ChannelTag(
id: "UCeGJuvHZqqebHTE_Kz2zyug",
name: "@Dahackabarade",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/csf-cEr2z4uWg8ZpVEPqgS2D2ZUHKBAJWnIbnzQCRtAlioSlUbtQZAyx76tnyfpXpixrsKke6DE=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 years ago (edited)",
like_count: Some(11),
reply_count: 5,
replies: Paginator(
count: Some(5),
items: [],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd4bkZNTHJwdmJDV3pIaWRtbDRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3huRk1McnB2YkNXekhpZG1sNEFhQUJBZw%3D%3D"),
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "UgwCIwmF6synP7UF_wV4AaABAg",
text: RichText([
Text(
text: "Never gonna give you up. Im gonna let u down",
),
]),
author: Some(ChannelTag(
id: "UCyrDrBrWvXwIhf2s2F1dq-Q",
name: "@imnotjust...2326",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/Clzb1GD_KnRm9u7mOAN165HZO_H0jhXQlRG8YvEjqkDuBUNibGkclRyRZIdhi-yJhC4hHorGLQ=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 years ago",
like_count: Some(14),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: true,
),
Comment(
id: "Ugyb5Wy91Yon69o3wLh4AaABAg",
text: RichText([
Text(
text: "Thank you for being A Legend No, The Goat Lets go dude",
),
]),
author: Some(ChannelTag(
id: "UCPCgaC_EJlS5RpRRWPHWvKA",
name: "@gfghdgfghd6391",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AIdro_n0lpwvjOP9HO_XHxzInwQoqQ7qIXeR0SqZVbCE=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "1 month ago",
like_count: Some(0),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
],
ctoken: Some("Eg0SC1FpcDFWa1R1TTcwGAYy4gIKuAJnZXRfcmFua2VkX3N0cmVhbXMtLUNxWUJDSUFFRlJlMzBUZ2Ftd0VLbGdFSTJGOFFnQVFZQnlLTEFSdUNtdFZ5a3dZMFFzVVVvM3I0LUY0OWU2d3RGSGFjbDIxS0Nsd3M4ZFZNaGdDdm9VWFhac2NZNXVncURIaUNiQVpveUczUEh6MTRPQ0tJV1BZTm9PTnN6dlFPVDZkaFZXMGRiSlZNelJXSW5QTm5QY0pyTmhQbzAyT1ZuamlVcHJTTHc1UEZxVHFBRkxlYXEtSHQtdU5uZkp1SzItMXVhQkp2aWE3S183QzgzOURiekJhY2tFeVRzUUFRRkJJRkNJZ2dHQUFTQndpSElCQUJHQUFTQlFpb0lCZ0FFZ1VJaVNBWUFCSUhDSVVnRUFrWUFSSUhDSVFnRUFzWUFSZ0EiESILUWlwMVZrVHVNNzAwAHgBKBRCEGNvbW1lbnRzLXNlY3Rpb24%3D"),
endpoint: browse,
)

View file

@ -1,351 +0,0 @@
---
source: src/client/video_details.rs
expression: map_res.c
---
Paginator(
count: None,
items: [
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-GzThFMUcw",
text: RichText([
Text(
text: "Fact🙌🏻",
),
]),
author: Some(ChannelTag(
id: "UC4I0-MXGyTRsc1tsJrDMh2A",
name: "@Sadaf788",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/uIXOBEBIhYn6Y8cQHlhaGGnCqOqS8PI5YR_Cx28qR_Y_p1qkjHC8V68iwxfeJ20eQ3zp81owJ64=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(800),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-H295I5iMZ",
text: RichText([
Text(
text: "Facts",
),
]),
author: Some(ChannelTag(
id: "UCdJ0CAWWa1rRjRbVrQUrU_w",
name: "@Biggest_Onceu",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/dHe_Zmr-7EueUz0R-BbuFoGwRyNMXwa3gb_GJMgAie9yU5PM6LbgTlNJ1zivRxnjiFg2nrlF1Es=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(530),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-H73oLoHkI",
text: RichText([
Text(
text: "Faacttts",
),
]),
author: Some(ChannelTag(
id: "UCPPhfcNhQ768F0Hhk3-25hA",
name: "@neni996",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/t__fCiOIhKCg2BwaxaRog9F_a5uemd8rTEvwzWYl6WeLn-nN9xEW0FvxUtM0fQrh2Dj_6ENsGQ=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(412),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-H7nBbPD5z",
text: RichText([
Text(
text: "REAL",
),
]),
author: Some(ChannelTag(
id: "UCQyomFJDEQtC2lbQ6E7QUGA",
name: "@momolvs",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/nt6GjcwAs8NPgaF29pn-cHTPmPAwQC_e_lXQHGDjZJGSRKzsH4s3le8Wpg0ByAUvPwTSHWe0OA=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(312),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-H9s3LvStZ",
text: RichText([
Text(
text: "FR!!",
),
]),
author: Some(ChannelTag(
id: "UC9HOPOf3gD3aw6Ej9WZ-rYg",
name: "@user-vv9yp1fh8w",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AIdro_nqyUCWW7jqWrg_39XNQ18-acPouL6wyHeQnZOMbmlSa9x2YGWINkfU1DLcvaXw=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(187),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-HB52Dv3SL",
text: RichText([
Text(
text: "fato.",
),
]),
author: Some(ChannelTag(
id: "UCW6ua0VDEFz7SyVMX01fTCA",
name: "@millenatwice",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/rInaNzUF3JLL_pCNfZtZlf2cHipf1yM4grr8VGJRHocwOQiuq1x7kUVi24q3ydtDC0j8bqbw2vA=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(165),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-HDLrM1OPD",
text: RichText([
Text(
text: "For sure!! TWICE is always TWICE!! They always give GOOD MUSIC",
),
]),
author: Some(ChannelTag(
id: "UCqDSps4SV0v8Dzf8esr6ScQ",
name: "@Its_me_hi_good",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AIdro_nfh9rWK7_gae1YkUgKuq13G9OUpxQCqrXAAi1hfPkCvHeHORbq3DUTYm7b5eoy=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(341),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-HIAbm3Him",
text: RichText([
Text(
text: "Fr",
),
]),
author: Some(ChannelTag(
id: "UCFA4BaLyvM1DDNsFyE_BHqQ",
name: "@amanpreetbrar7836",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AIdro_n1M-xxgSLIqe17kDv-i-tPn23FT1ywabpRAQ=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(84),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-HIrpOKIi7",
text: RichText([
Text(
text: "Presave I got you and with youth on spotify",
),
]),
author: Some(ChannelTag(
id: "UCxMDESp088wGItVM4xXACgw",
name: "@RitaOnce9",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/hVTumbLCpmtJw0c2mq1B-ES5W3kdYPqnNrtzEcUhxCoUN6dAutXc6exaPRnBMLM6Jw1ILPoBDg=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(112),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
Comment(
id: "Ugzu-t48vV9SjdeWIMh4AaABAg.A-Grr7qN9uaA-HKMOSBnLK",
text: RichText([
Text(
text: "real",
),
]),
author: Some(ChannelTag(
id: "UCqeOr9ddrs_d6OgboKjk6zw",
name: "@twiceupremacy",
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AIdro_lpKYjxtRm1HSjv3tFvGwrvnRILmJoQrPTBBOFG=s88-c-k-c0x00ffffff-no-rj",
width: 88,
height: 88,
),
],
verification: None,
subscriber_count: None,
)),
publish_date: "[date]",
publish_date_txt: "2 months ago",
like_count: Some(75),
reply_count: 0,
replies: Paginator(
count: Some(0),
items: [],
ctoken: None,
endpoint: browse,
),
by_owner: false,
pinned: false,
hearted: false,
),
],
ctoken: Some("Eg0SC2hhZjY3ZUtGMHVvGAYy1wEKUGdldF9jb21tZW50X3dpdGhfcmVwbGllc19zdHJlYW0tLUNnZ0lnQVFWRjdmUk9CSUZDS0FnR0FFWUFDSU9DZ3dJeXUzdXJRWVE2Tkt2NGdFGlASGlVnenUtdDQ4dlY5U2pkZVdJTWg0QWFBQkFnIgIIACoYVUNhTzZUWXRsQzhVNXR0ejYyaFRyWmdnMgtoYWY2N2VLRjB1b0AASDKCAQIIASgKQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3p1LXQ0OHZWOVNqZGVXSU1oNEFhQUJBZw%3D%3D"),
endpoint: browse,
)

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, fmt::Debug};
use std::fmt::Debug;
use serde::Serialize;
@ -6,7 +6,7 @@ use crate::{
error::{Error, ExtractionError},
model::{
paginator::{ContinuationEndpoint, Paginator},
ChannelTag, Chapter, Comment, Verification, VideoDetails, VideoItem,
ChannelTag, Chapter, Comment, VideoDetails, VideoItem,
},
param::Language,
serializer::MapResult,
@ -14,7 +14,7 @@ use crate::{
};
use super::{
response::{self, video_details::Payload, IconType},
response::{self, IconType},
ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext,
};
@ -391,73 +391,44 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
_vdata: Option<&str>,
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
let received_endpoints = self.on_response_received_endpoints;
let mut warnings = Vec::new();
let mut warnings = received_endpoints.warnings;
let mut comments = Vec::new();
let mut comment_count = None;
let mut ctoken = None;
let mut mutations = if let Some(upd) = self.framework_updates {
let mut m = upd.entity_batch_update.mutations;
warnings.append(&mut m.warnings);
m.items
} else {
HashMap::new()
};
received_endpoints.c.into_iter().for_each(|citem| {
let mut items = citem.append_continuation_items_action.continuation_items;
warnings.append(&mut items.warnings);
items.c.into_iter().for_each(|item| match item {
response::video_details::CommentListItem::CommentThreadRenderer(thread) => {
if let Some(comment) = thread.comment {
comments.push(map_comment(
response::video_details::CommentListItem::CommentThreadRenderer {
comment,
replies,
rendering_priority,
} => {
let mut res = map_comment(
comment.comment_renderer,
Some(thread.replies),
thread.rendering_priority,
Some(replies),
rendering_priority,
lang,
&mut warnings,
));
} else if let Some(vm) = thread.comment_view_model {
if let Some(c) = map_comment_vm(
vm.comment_view_model,
&mut mutations,
Some(thread.replies),
thread.rendering_priority,
lang,
&mut warnings,
) {
comments.push(c);
}
} else {
warnings.push(
"comment does not contain comment or commentViewModel field".to_owned(),
);
}
comments.push(res.c);
warnings.append(&mut res.warnings);
}
response::video_details::CommentListItem::CommentRenderer(comment) => {
comments.push(map_comment(
let mut res = map_comment(
comment,
None,
response::video_details::CommentPriority::RenderingPriorityUnknown,
lang,
&mut warnings,
));
);
comments.push(res.c);
warnings.append(&mut res.warnings);
}
response::video_details::CommentListItem::CommentViewModel(vm) => {
if let Some(c) = map_comment_vm(
vm,
&mut mutations,
None,
response::video_details::CommentPriority::RenderingPriorityUnknown,
lang,
&mut warnings,
) {
comments.push(c);
}
}
response::video_details::CommentListItem::ContinuationItemRenderer(cont) => {
ctoken = Some(cont.token());
response::video_details::CommentListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
ctoken = Some(continuation_endpoint.continuation_command.token);
}
response::video_details::CommentListItem::CommentsHeaderRenderer { count_text } => {
comment_count = count_text
@ -500,50 +471,44 @@ fn map_recommendations(
}
}
fn map_replies(
fn map_comment(
c: response::video_details::CommentRenderer,
replies: Option<response::video_details::Replies>,
priority: response::video_details::CommentPriority,
lang: Language,
warnings: &mut Vec<String>,
) -> (Vec<Comment>, Option<String>) {
) -> MapResult<Comment> {
let mut warnings = Vec::new();
let mut reply_ctoken = None;
let replies = replies
.map(|replies| {
let replies = replies.map(|replies| {
replies
.comment_replies_renderer
.contents
.into_iter()
.filter_map(|item| match item {
response::video_details::CommentListItem::CommentRenderer(comment) => {
Some(map_comment(
let mut res = map_comment(
comment,
None,
response::video_details::CommentPriority::default(),
lang,
warnings,
))
);
warnings.append(&mut res.warnings);
Some(res.c)
}
response::video_details::CommentListItem::ContinuationItemRenderer(cont) => {
reply_ctoken = Some(cont.token());
response::video_details::CommentListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
reply_ctoken = Some(continuation_endpoint.continuation_command.token);
None
}
_ => None,
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
(replies, reply_ctoken)
}
});
fn map_comment(
c: response::video_details::CommentRenderer,
replies: Option<response::video_details::Replies>,
priority: response::video_details::CommentPriority,
lang: Language,
warnings: &mut Vec<String>,
) -> Comment {
let (replies, reply_ctoken) = map_replies(replies, lang, warnings);
Comment {
MapResult {
c: Comment {
id: c.comment_id,
text: c.content_text.into(),
author: match (c.author_endpoint, c.author_text) {
@ -559,91 +524,32 @@ fn map_comment(
}),
_ => None,
},
publish_date: timeago::parse_timeago_dt_or_warn(lang, &c.published_time_text, warnings),
publish_date: timeago::parse_timeago_dt_or_warn(
lang,
&c.published_time_text,
&mut warnings,
),
publish_date_txt: c.published_time_text,
like_count: match c.vote_count {
Some(txt) => util::parse_numeric_or_warn(&txt, warnings),
Some(txt) => util::parse_numeric_or_warn(&txt, &mut warnings),
None => Some(0),
},
reply_count: c.reply_count as u32,
replies: Paginator::new(Some(c.reply_count), replies, reply_ctoken),
replies: replies
.map(|items| Paginator::new(Some(c.reply_count), items, reply_ctoken))
.unwrap_or_default(),
by_owner: c.author_is_channel_owner,
pinned: priority.into(),
pinned: priority
== response::video_details::CommentPriority::RenderingPriorityPinnedComment,
hearted: c
.action_buttons
.comment_action_buttons_renderer
.creator_heart
.map(|h| h.creator_heart_renderer.is_hearted)
.unwrap_or_default(),
}
}
fn map_comment_vm(
vm: response::video_details::CommentViewModel,
mutations: &mut HashMap<String, response::video_details::Payload>,
replies: Option<response::video_details::Replies>,
priority: response::video_details::CommentPriority,
lang: Language,
warnings: &mut Vec<String>,
) -> Option<Comment> {
let (replies, reply_ctoken) = map_replies(replies, lang, warnings);
let ce = if let Some(Payload::CommentEntityPayload(ce)) = mutations.remove(&vm.comment_key) {
ce
} else {
warnings.push(format!(
"comment `{}` does not have entity payload (key: `{}`)",
vm.comment_id, vm.comment_key
));
return None;
};
let hearted = if let Some(Payload::EngagementToolbarStateEntityPayload { heart_state }) =
mutations.get(&vm.toolbar_state_key)
{
(*heart_state).into()
} else {
false
};
let mut parse_num = |s: &str| -> Option<u32> {
if s.is_empty() || s == " " {
Some(0)
} else {
util::parse_large_numstr_or_warn(s, lang, warnings)
}
};
let reply_count = parse_num(&ce.toolbar.reply_count).unwrap_or_default();
Some(Comment {
id: vm.comment_id,
text: ce.properties.content.into(),
by_owner: ce.author.as_ref().map(|a| a.is_creator).unwrap_or_default(),
author: ce.author.map(|a| ChannelTag {
id: a.channel_id,
name: a.display_name,
avatar: ce.avatar.image.into(),
verification: if a.is_artist {
Verification::Artist
} else if a.is_verified {
Verification::Verified
} else {
Verification::None
},
subscriber_count: None,
}),
like_count: parse_num(&ce.toolbar.like_count_notliked),
reply_count,
replies: Paginator::new(Some(reply_count.into()), replies, reply_ctoken),
publish_date: timeago::parse_timeago_dt_or_warn(
lang,
&ce.properties.published_time,
warnings,
),
publish_date_txt: ce.properties.published_time,
pinned: priority.into(),
hearted,
})
}
}
#[cfg(test)]
@ -708,8 +614,6 @@ mod tests {
#[rstest]
#[case::top("top")]
#[case::latest("latest")]
#[case::frameworkupd("20240401_frameworkupd")]
#[case::frameworkupd_reply("20240401_frameworkupd_reply")]
fn map_comments(#[case] name: &str) {
let json_path = path!(*TESTFILES / "video_details" / format!("comments_{name}.json"));
let json_file = File::open(json_path).unwrap();

View file

@ -20,13 +20,6 @@ where
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum GoodOrError<T> {
Good(T),
Error(serde_json::Value),
}
struct SeqVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for SeqVisitor<T>
@ -46,16 +39,14 @@ where
let mut values = Vec::with_capacity(seq.size_hint().unwrap_or_default());
let mut warnings = Vec::new();
while let Some(value) = seq.next_element()? {
match value {
GoodOrError::<T>::Good(value) => {
values.push(value);
}
GoodOrError::<T>::Error(value) => {
warnings.push(format!(
"error deserializing item: {}",
serde_json::to_string(&value).unwrap_or_default()
));
loop {
match seq.next_element::<T>() {
Ok(val) => match val {
Some(val) => values.push(val),
None => break,
},
Err(e) => {
warnings.push(format!("error deserializing item: {e}"));
}
}
}
@ -186,8 +177,8 @@ mod tests {
insta::assert_debug_snapshot!(res.items.warnings, @r###"
[
"error deserializing item: {\"xyz\":\"i2\"}",
"error deserializing item: {\"namra\":\"i4\"}",
"error deserializing item: missing field `name` at line 1 column 40",
"error deserializing item: missing field `name` at line 1 column 73",
]
"###);
}

File diff suppressed because it is too large Load diff