fix: A/B test 17: channel playlists lockupViewModel

This commit is contained in:
ThetaDev 2024-11-09 05:11:41 +01:00
parent 0919cbd0df
commit 342119dba6
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
13 changed files with 8770 additions and 49 deletions

View file

@ -36,6 +36,7 @@ pub enum ABTest {
CommentsFrameworkUpdate = 14,
ChannelShortsLockup = 15,
PlaylistPageHeader = 16,
ChannelPlaylistsLockup = 17,
}
/// List of active A/B tests that are run when none is manually specified
@ -112,6 +113,7 @@ pub async fn run_test(
ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
ABTest::ChannelShortsLockup => channel_shorts_lockup(&query).await,
ABTest::PlaylistPageHeader => playlist_page_header_renderer(&query).await,
ABTest::ChannelPlaylistsLockup => channel_playlists_lockup(&query).await,
}
.unwrap();
pb.inc(1);
@ -156,7 +158,7 @@ pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
content_check_ok: false,
racy_check_ok: false,
};
let response_txt = rp.raw(ClientType::Desktop, "next", &q).await.unwrap();
let response_txt = rp.raw(ClientType::Desktop, "next", &q).await?;
if !response_txt.contains("\"Black Mamba\"") {
bail!("invalid response data");
@ -166,7 +168,7 @@ pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
}
pub async fn three_tab_channel_layout(rp: &RustyPipeQuery) -> Result<bool> {
let channel = rp.channel_videos("UCR-DXc1voovS8nhAvccRZhg").await.unwrap();
let channel = rp.channel_videos("UCR-DXc1voovS8nhAvccRZhg").await?;
Ok(channel.has_live || channel.has_shorts)
}
@ -233,8 +235,7 @@ pub async fn discography_page(rp: &RustyPipeQuery) -> Result<bool> {
params: None,
},
)
.await
.unwrap();
.await?;
Ok(res.contains(&format!("\"MPAD{id}\"")))
}
@ -296,8 +297,7 @@ pub async fn channel_about_modal(rp: &RustyPipeQuery) -> Result<bool> {
params: None,
},
)
.await
.unwrap();
.await?;
Ok(!res.contains("\"EgVhYm91dPIGBAoCEgA%3D\""))
}
@ -334,8 +334,7 @@ pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
params: None,
},
)
.await
.unwrap();
.await?;
Ok(res.contains("\"musicResponsiveHeaderRenderer\""))
}
@ -344,8 +343,7 @@ pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
"Eg0SC3dMZHBSN2d1S3k4GAYyJSIRIgt3TGRwUjdndUt5ODAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D";
let res = rp
.raw(ClientType::Desktop, "next", &QCont { continuation })
.await
.unwrap();
.await?;
Ok(res.contains("\"frameworkUpdates\""))
}
@ -360,8 +358,7 @@ pub async fn channel_shorts_lockup(rp: &RustyPipeQuery) -> Result<bool> {
params: Some("EgZzaG9ydHPyBgUKA5oBAA%3D%3D"),
},
)
.await
.unwrap();
.await?;
Ok(res.contains("\"shortsLockupViewModel\""))
}
@ -376,7 +373,21 @@ pub async fn playlist_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool>
params: None,
},
)
.await
.unwrap();
.await?;
Ok(res.contains("\"pageHeaderRenderer\""))
}
pub async fn channel_playlists_lockup(rp: &RustyPipeQuery) -> Result<bool> {
let id = "UC2DjFE7Xf11URZqWBigcVOQ";
let res = rp
.raw(
ClientType::Desktop,
"browse",
&QBrowse {
browse_id: id,
params: Some("EglwbGF5bGlzdHMgAQ%3D%3D"),
},
)
.await?;
Ok(res.contains("\"lockupViewModel\""))
}

View file

@ -899,3 +899,73 @@ YouTube changed the data model for the channel shorts tab
}
}
```
## [17] Channel playlists: lockupViewModel
- **Encountered on:** 09.11.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse
- **Status:** Common (50%)
YouTube changed the data model for the channel playlists / podcasts / albums tab
```json
{
"lockupViewModel": {
"contentImage": {
"collectionThumbnailViewModel": {
"primaryThumbnail": {
"thumbnailViewModel": {
"image": {
"sources": [
{
"url": "https://i.ytimg.com/vi/XYdmX8w8xwI/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCqmf6TGfDinNXhgU29ZxOkv2u9sQ",
"width": 480,
"height": 270
}
]
},
"overlays": [
{
"thumbnailOverlayBadgeViewModel": {
"thumbnailBadges": [
{
"thumbnailBadgeViewModel": {
"icon": {
"sources": [
{
"clientResource": {
"imageName": "PLAYLISTS"
}
}
]
},
"text": "5 videos",
"badgeStyle": "THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT",
"backgroundColor": {
"lightTheme": 2370867,
"darkTheme": 2370867
}
}
}
],
"position": "THUMBNAIL_OVERLAY_BADGE_POSITION_BOTTOM_END"
}
}
]
}
}
}
},
"metadata": {
"lockupMetadataViewModel": {
"title": {
"content": "Jellybean Components Series"
}
}
},
"contentId": "PLvOlSehNtuHv268f0mW5m1t_hq_RVGRSA",
"contentType": "LOCKUP_CONTENT_TYPE_PLAYLIST"
}
}
```

View file

@ -776,8 +776,10 @@ mod tests {
}
#[rstest]
fn map_channel_playlists() {
let json_path = path!(*TESTFILES / "channel" / "channel_playlists.json");
#[case::base("base")]
#[case::lockup("20241109_lockup")]
fn map_channel_playlists(#[case] name: &str) {
let json_path = path!(*TESTFILES / "channel" / format!("channel_playlists_{name}.json"));
let json_file = File::open(json_path).unwrap();
let channel: response::Channel =
@ -791,7 +793,7 @@ mod tests {
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!("map_channel_playlists", map_res.c);
insta::assert_ron_snapshot!(format!("map_channel_playlists_{name}"), map_res.c);
}
#[rstest]

View file

@ -188,13 +188,13 @@ pub(crate) enum ChannelBadgeStyle {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Alert {
pub alert_renderer: AlertRenderer,
pub alert_renderer: TextBox,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AlertRenderer {
pub(crate) struct TextBox {
#[serde_as(as = "Text")]
pub text: String,
}
@ -523,3 +523,50 @@ pub(crate) struct AvatarStackViewModel {
#[serde_as(deserialize_as = "AttributedText")]
pub text: TextComponent,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ContentImage {
pub collection_thumbnail_view_model: CollectionThumbnailViewModel,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CollectionThumbnailViewModel {
pub primary_thumbnail: ThumbnailViewModelWrap,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ThumbnailViewModelWrap {
pub thumbnail_view_model: ImageViewOl,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ImageViewOl {
pub image: Thumbnails,
#[serde_as(as = "VecSkipError<_>")]
pub overlays: Vec<ImageViewOverlay>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ImageViewOverlay {
pub thumbnail_overlay_badge_view_model: ThumbnailOverlayBadgeViewModel,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ThumbnailOverlayBadgeViewModel {
#[serde_as(as = "VecSkipError<_>")]
pub thumbnail_badges: Vec<ThumbnailBadges>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ThumbnailBadges {
pub thumbnail_badge_view_model: TextBox,
}

View file

@ -3,8 +3,8 @@ use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::Text;
use super::AlertRenderer;
use super::ContentsRenderer;
use super::TextBox;
use super::{
music_item::{ItemSection, PlaylistPanelRenderer},
ContentRenderer,
@ -115,7 +115,7 @@ pub(crate) struct MusicLyrics {
#[serde(rename_all = "camelCase")]
pub(crate) enum ListOrMessage<T> {
SectionListRenderer(ContentsRenderer<T>),
MessageRenderer(AlertRenderer),
MessageRenderer(TextBox),
}
#[derive(Debug, Deserialize)]

View file

@ -4,9 +4,9 @@ use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::{AttributedText, Text, TextComponent, TextComponents};
use super::{
url_endpoint::NavigationEndpoint, video_item::YouTubeListRenderer, Alert, ContentRenderer,
ContentsRenderer, ImageView, PageHeaderRendererContent, PhMetadataView, ResponseContext,
SectionList, Tab, ThumbnailsWrap, TwoColumnBrowseResults,
url_endpoint::OnTap, video_item::YouTubeListRenderer, Alert, ContentRenderer, ContentsRenderer,
ImageView, PageHeaderRendererContent, PhMetadataView, ResponseContext, SectionList, Tab,
TextBox, ThumbnailsWrap, TwoColumnBrowseResults,
};
#[serde_as]
@ -70,15 +70,7 @@ pub(crate) struct PlaylistHeaderBanner {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Byline {
pub playlist_byline_renderer: BylineRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct BylineRenderer {
#[serde_as(as = "Text")]
pub text: String,
pub playlist_byline_renderer: TextBox,
}
#[derive(Debug, Deserialize)]
@ -187,11 +179,5 @@ pub(crate) struct ButtonAction {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ButtonViewModel {
pub on_tap: ActionOnTap,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ActionOnTap {
pub innertube_command: NavigationEndpoint,
pub on_tap: OnTap,
}

View file

@ -157,6 +157,12 @@ pub(crate) struct WatchEndpointConfig {
pub music_video_type: MusicVideoType,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct OnTap {
pub innertube_command: NavigationEndpoint,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
pub(crate) enum MusicVideoType {
#[default]

View file

@ -4,7 +4,7 @@ use serde_with::{
};
use time::OffsetDateTime;
use super::{ChannelBadge, ContinuationEndpoint, Thumbnails};
use super::{ChannelBadge, ContentImage, ContinuationEndpoint, Thumbnails};
use crate::{
model::{
Channel, ChannelId, ChannelItem, ChannelTag, PlaylistItem, Verification, VideoItem,
@ -33,6 +33,8 @@ pub(crate) enum YouTubeListItem {
ChannelRenderer(ChannelRenderer),
LockupViewModel(LockupViewModel),
/// Continauation items are located at the end of a list
/// and contain the continuation token for progressive loading
#[serde(rename_all = "camelCase")]
@ -165,6 +167,41 @@ pub(crate) struct ShortsOverlayMetadata {
pub secondary_text: Option<String>,
}
/// Generalized list item, currently only used for playlists
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LockupViewModel {
pub content_image: ContentImage,
pub metadata: LockupViewModelMetadata,
pub content_id: String,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub content_type: LockupContentType,
}
#[derive(Default, Debug, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum LockupContentType {
LockupContentTypePlaylist,
#[default]
Unknown,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LockupViewModelMetadata {
pub lockup_metadata_view_model: LockupViewModelMetadataInner,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LockupViewModelMetadataInner {
#[serde_as(as = "AttributedText")]
pub title: String,
}
/// Video displayed in a playlist
#[serde_as]
#[derive(Debug, Deserialize)]
@ -681,6 +718,38 @@ impl<T> YouTubeListMapper<T> {
short_description: channel.description_snippet,
}
}
fn map_lockup(&mut self, lockup: LockupViewModel) -> Option<PlaylistItem> {
let md = lockup.metadata.lockup_metadata_view_model;
let tn = lockup
.content_image
.collection_thumbnail_view_model
.primary_thumbnail
.thumbnail_view_model;
match lockup.content_type {
LockupContentType::LockupContentTypePlaylist => Some(PlaylistItem {
id: lockup.content_id,
name: md.title,
thumbnail: tn.image.into(),
channel: self.channel.clone(),
video_count: tn
.overlays
.first()
.and_then(|ol| {
ol.thumbnail_overlay_badge_view_model
.thumbnail_badges
.first()
})
.and_then(|badge| {
util::parse_numeric_or_warn(
&badge.thumbnail_badge_view_model.text,
&mut self.warnings,
)
}),
}),
LockupContentType::Unknown => None,
}
}
}
impl YouTubeListMapper<YouTubeItem> {
@ -711,6 +780,11 @@ impl YouTubeListMapper<YouTubeItem> {
let mapped = YouTubeItem::Channel(self.map_channel(channel));
self.items.push(mapped);
}
YouTubeListItem::LockupViewModel(lockup) => {
if let Some(mapped) = self.map_lockup(lockup) {
self.items.push(YouTubeItem::Playlist(mapped));
}
}
YouTubeListItem::ContinuationItemRenderer {
continuation_endpoint,
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
@ -784,6 +858,11 @@ impl YouTubeListMapper<PlaylistItem> {
let mapped = self.map_playlist(playlist);
self.items.push(mapped);
}
YouTubeListItem::LockupViewModel(lockup) => {
if let Some(mapped) = self.map_lockup(lockup) {
self.items.push(mapped);
}
}
YouTubeListItem::ContinuationItemRenderer {
continuation_endpoint,
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),

View file

@ -0,0 +1,672 @@
---
source: src/client/channel.rs
expression: map_res.c
---
Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
handle: Some("@EEVblog"),
subscriber_count: Some(952000),
video_count: Some(2),
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/ytc/AIdro_l17lYcTcRSydZeQK-RuiSfEeH5eX9m4irSNQj6109v5MQ=s72-c-k-c0x00ffffff-no-rj",
width: 72,
height: 72,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/ytc/AIdro_l17lYcTcRSydZeQK-RuiSfEeH5eX9m4irSNQj6109v5MQ=s120-c-k-c0x00ffffff-no-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/ytc/AIdro_l17lYcTcRSydZeQK-RuiSfEeH5eX9m4irSNQj6109v5MQ=s160-c-k-c0x00ffffff-no-rj",
width: 160,
height: 160,
),
],
verification: Verified,
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",
],
banner: [
Thumbnail(
url: "https://yt3.googleusercontent.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.googleusercontent.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.googleusercontent.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.googleusercontent.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.googleusercontent.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.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
width: 2560,
height: 424,
),
],
has_shorts: true,
has_live: true,
visitor_data: None,
content: Paginator(
count: None,
items: [
PlaylistItem(
id: "PLvOlSehNtuHv268f0mW5m1t_hq_RVGRSA",
name: "Jellybean Components Series",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/XYdmX8w8xwI/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCqmf6TGfDinNXhgU29ZxOkv2u9sQ",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(5),
),
PlaylistItem(
id: "PLvOlSehNtuHu46I7nFuUg3LC3PpiWTR4f",
name: "Tandy Electronics / Radio Shack & Computers",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/uUXxY6gA-7g/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAlIVvQ4Axx40Xa_i8F56qmppXEXg",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(11),
),
PlaylistItem(
id: "PLvOlSehNtuHuS01_RNCnvpzyk7bycYCmM",
name: "Open Source Hardware",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/m_8jh_MpWBE/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBx6U5iikp5rSO78dIWdy1RQ_BLNQ",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(4),
),
PlaylistItem(
id: "PLvOlSehNtuHuwwQ1fpquOJuA5MSfD4iD6",
name: "Fluke Multimeters",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ymJc5oxthlw/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDAOiw39aJajjAdroLnuj_fh60Ryw",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(22),
),
PlaylistItem(
id: "PLvOlSehNtuHs2LwEdDwTp3n7mxb-MyBbo",
name: "EEVacademy Digital Design Tutorial Series",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/lJ3q9RHIatU/hqdefault.jpg?sqp=-oaymwExCOADEI4CSFryq4qpAyMIARUAAIhCGAHwAQH4Af4JgALQBYoCDAgAEAEYQyBXKGUwDw==&rs=AOn4CLBaaQaTJzi7H-zjwSsTlNJdBsyqvQ",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(5),
),
PlaylistItem(
id: "PLvOlSehNtuHu2v8THrRMt8E9ziHtRXPm7",
name: "AI / ChatGPT",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/g5_Ts9SWbYs/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBmZPW6EiAvTCsI86BFg4BxXLj66A",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(3),
),
PlaylistItem(
id: "PLvOlSehNtuHvXuXRmoBUys09Dwi1heNii",
name: "Shorts",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ndvJtQ8nxV4/hqdefault.jpg?sqp=-oaymwExCOADEI4CSFryq4qpAyMIARUAAIhCGAHwAQH4AbYIgAKAD4oCDAgAEAEYNyBTKH8wDw==&rs=AOn4CLDD0qOLs38KPJtqdG6zCeVLQMf62Q",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(1),
),
PlaylistItem(
id: "PLvOlSehNtuHv3gxNg5BGoZJJu9htoAGB6",
name: "Microcontrollers",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/L9Wrv7nW-S8/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDiAT5izyig1ntMSUhvSOVuYSsG1Q",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(1),
),
PlaylistItem(
id: "PLvOlSehNtuHvllTQ-vwvY26E3Bvrov93Y",
name: "Bypass Capacitors",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1xicZF9glH0/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAFb2FcbpdtAG1xLjmdkdIm1hFvgA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(4),
),
PlaylistItem(
id: "PLvOlSehNtuHtOV3AEwhuea4TnviddKfAj",
name: "MacGyver Project",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/4yosozyeIP4/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAkwsCiJjFkWhYxtcg5NgfnQbkZrA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(3),
),
PlaylistItem(
id: "PLvOlSehNtuHuvHE5GQrQJxWXHdmW2l5IF",
name: "Calculators",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/S3R4r2xvVYQ/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLB7HH5drG-33c1SyRe9kyZBrXvm3A",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(1),
),
PlaylistItem(
id: "PLvOlSehNtuHs6wRwVSaErU0BEnLiHfnKJ",
name: "BM235",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/WPyEFB4cHkA/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAzBuQFV8T9hM8adlPvv58C9TeDug",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(9),
),
PlaylistItem(
id: "PLvOlSehNtuHu4k0ZkKFLsysSB5iava6Qu",
name: "Vibration Measurement",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/uus_cpZiqsU/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCqdsjWVFaLOkEcXgbZD2Eca8MnuQ",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(3),
),
PlaylistItem(
id: "PLvOlSehNtuHtdQF-m5UFZ5GEjABadI3kI",
name: "Component Selection",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/uq1DMWtjL2U/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAbgb1Jdb5P69JGdZQ-a8asLLyYdA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(6),
),
PlaylistItem(
id: "PLvOlSehNtuHtlndPUSOPgsujUdq1c5Mr9",
name: "Solar Roadways",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/oIImmlfCyzo/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBxApgyGu3dNXRGoqLctVUnESpEIA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(23),
),
PlaylistItem(
id: "PLvOlSehNtuHvD6M_7WeN071OVsZFE0_q-",
name: "Electronics Tutorials - AC Circuit Theory Series",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/rrPtvYYJ2-g/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBEVc71xxSjJ-xlA_dDQaYIjdHyUw",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(3),
),
PlaylistItem(
id: "PLvOlSehNtuHtVLq2MDPIz82BWMIZcuwhK",
name: "Electronics Tutorial - DC Fundamentals",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/xSRe_4TQbuo/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDP4V24_MG6vzvUZsHep9WFSCCY6Q",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(8),
),
PlaylistItem(
id: "PLvOlSehNtuHvIDfW3x2p4BY6l4RYgfBJE",
name: "Oscilloscope Probing",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/OiAmER1OJh4/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAXeGAvEc8y3pEsPUxWdsNIP9UmPw",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(14),
),
PlaylistItem(
id: "PLvOlSehNtuHu6Jjb8U82eKQfvKhJVl0Bu",
name: "Thermal Design",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/8ruFVmxf0zs/hqdefault.jpg?sqp=-oaymwExCOADEI4CSFryq4qpAyMIARUAAIhCGAHwAQH4Af4JgALQBYoCDAgAEAEYfyA1KDUwDw==&rs=AOn4CLD6PMawyYXKe8KT1-Y6vWjQc2xIDw",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(9),
),
PlaylistItem(
id: "PLvOlSehNtuHs-X2Awg33PCBNrP2BGFVhC",
name: "Electric Cars",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CPcZm1Tu5VI/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCsm8De0QaHPaeCZqxMp_F464fWzg",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(9),
),
PlaylistItem(
id: "PLvOlSehNtuHuLODLTeq3PM-OJRP2nzNUa",
name: "Designing a better uCurrent",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/0AEVilxXAAo/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCjotFuRjPPBHd2LWzt3lviPj9HaA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(3),
),
PlaylistItem(
id: "PLvOlSehNtuHtvTKP4RTNW1-08Kmzy1pvA",
name: "EMC Compliance & Measurement",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/lYmfVMWbIHQ/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBtygEqMXx7Lwe5SuBWt2q0CSahYA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(8),
),
PlaylistItem(
id: "PLvOlSehNtuHuUTpCrTVX7BdU68l2aVqMv",
name: "Power Counter Display Project",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/nTpE1Nw3Yy4/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAbPl28_i7isizY6A1t2_c6gV8BAQ",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(2),
),
PlaylistItem(
id: "PLvOlSehNtuHvm120Tq40nKrM5SUBlolN3",
name: "Live - Ask Dave",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/gQ7TTuiDH1M/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBMnucUil90WeDSIeFz8mZCOtEv9g",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(3),
),
PlaylistItem(
id: "PLvOlSehNtuHsiF93KOLoF1KAHArmIW9lC",
name: "Padauk Microcontroller",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/r45r4rV5JOI/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCn4kGWcjBOhk3vN8QPMDa9L3mkKA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(10),
),
PlaylistItem(
id: "PLvOlSehNtuHvxTzBLwUFw4My4rtrNFzED",
name: "Other Debunking Videos",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/WopuF9vD7KE/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBv5buh3qMs4feQaPj6Fy6bxl_vuA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(1),
),
PlaylistItem(
id: "PLvOlSehNtuHt2pJ7X5tumuM4Wa3r1OC7Q",
name: "Audio & Speakers",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/qHbkw0Gm7pk/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCJBYXTDttGHTm51j3bfwqxOqVFig",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(9),
),
PlaylistItem(
id: "PLvOlSehNtuHtX7OearWdmqGzqiu4DHKWi",
name: "Cameras",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/g9umAQ1-an4/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCB5jNm9U-rypnpthK_N321LpYWew",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(16),
),
PlaylistItem(
id: "PLvOlSehNtuHu-TaNRp27_PiXjBG5wY9Gv",
name: "Cryptocurrency",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ibPgfzd9zd8/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDe3IXT88HR3XxnxfqrpAxh6pfYMg",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(7),
),
PlaylistItem(
id: "PLvOlSehNtuHvmK-VGcZ33ZuATmcNB8tvH",
name: "LCD Tutorial",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ZYvxgl-9tNM/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDv2WT4Chl1_H2G43AjfSFpPcKVoA",
width: 480,
height: 270,
),
],
channel: Some(ChannelTag(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: Verified,
subscriber_count: Some(952000),
)),
video_count: Some(6),
),
],
ctoken: Some("4qmFsgLCARIYVUMyRGpGRTdYZjExVVJacVdCaWdjVk9RGnRFZ2x3YkdGNWJHbHpkSE1ZQXlBQk1BRTRBZW9EUEVOblRrUlJhbEZUU2tKSmFWVkZlREpVTW5oVVdsZG9UMlJJVmtsa2JURk1URlphU0ZreGIzcE5NWEF4VVZaU2RGa3dOVU5QU0ZJeVUwTm5PQSUzRCUzRJoCL2Jyb3dzZS1mZWVkVUMyRGpGRTdYZjExVVJacVdCaWdjVk9RcGxheWxpc3RzMTA0"),
endpoint: browse,
),
)

View file

@ -5,7 +5,7 @@ use serde_with::{serde_as, DefaultOnError, DeserializeAs, VecSkipError};
use crate::{
client::response::url_endpoint::{
MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, PageType,
MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, OnTap, PageType,
},
model::{richtext::Style, UrlTarget},
util,
@ -159,7 +159,7 @@ pub(crate) struct AttributedText {
struct CommandRun {
start_index: usize,
length: usize,
on_tap: AttributedTextOnTap,
on_tap: OnTap,
#[serde(default)]
#[serde_as(as = "DefaultOnError<_>")]
on_tap_options: Option<AttributedTextOnTapOptions>,
@ -196,12 +196,6 @@ enum Strikethrough {
None,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AttributedTextOnTap {
innertube_command: NavigationEndpoint,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AttributedTextOnTapOptions {

File diff suppressed because it is too large Load diff