fix: add support for A/B-13 (2-column layout for music playlists/albums)
This commit is contained in:
parent
2fb210b363
commit
c402f463b4
14 changed files with 63535 additions and 89 deletions
|
|
@ -30,9 +30,11 @@ pub enum ABTest {
|
||||||
ChannelAboutModal = 10,
|
ChannelAboutModal = 10,
|
||||||
LikeButtonViewmodel = 11,
|
LikeButtonViewmodel = 11,
|
||||||
ChannelPageHeader = 12,
|
ChannelPageHeader = 12,
|
||||||
|
MusicPlaylistTwoColumn = 13,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TESTS_TO_RUN: [ABTest; 1] = [ABTest::ChannelPageHeader];
|
/// List of active A/B tests that are run when none is manually specified
|
||||||
|
const TESTS_TO_RUN: [ABTest; 2] = [ABTest::ChannelPageHeader, ABTest::MusicPlaylistTwoColumn];
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ABTestRes {
|
pub struct ABTestRes {
|
||||||
|
|
@ -101,6 +103,7 @@ pub async fn run_test(
|
||||||
ABTest::ChannelAboutModal => channel_about_modal(&query).await,
|
ABTest::ChannelAboutModal => channel_about_modal(&query).await,
|
||||||
ABTest::LikeButtonViewmodel => like_button_viewmodel(&query).await,
|
ABTest::LikeButtonViewmodel => like_button_viewmodel(&query).await,
|
||||||
ABTest::ChannelPageHeader => channel_page_header(&query).await,
|
ABTest::ChannelPageHeader => channel_page_header(&query).await,
|
||||||
|
ABTest::MusicPlaylistTwoColumn => music_playlist_two_column(&query).await,
|
||||||
}
|
}
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pb.inc(1);
|
pb.inc(1);
|
||||||
|
|
@ -336,3 +339,20 @@ pub async fn channel_page_header(rp: &RustyPipeQuery) -> Result<bool> {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(channel.mobile_banner.is_empty() && channel.tv_banner.is_empty())
|
Ok(channel.mobile_banner.is_empty() && channel.tv_banner.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
|
||||||
|
let id = "VLRDCLAK5uy_kb7EBi6y3GrtJri4_ZH56Ms786DFEimbM";
|
||||||
|
let res = rp
|
||||||
|
.raw(
|
||||||
|
ClientType::DesktopMusic,
|
||||||
|
"browse",
|
||||||
|
&QBrowse {
|
||||||
|
context: rp.get_context(ClientType::DesktopMusic, true, None).await,
|
||||||
|
browse_id: id,
|
||||||
|
params: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Ok(res.contains("\"musicResponsiveHeaderRenderer\""))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,13 @@ to the new feature.
|
||||||
- 🔴 **High** Changes to the functionality of YouTube that will require API changes for
|
- 🔴 **High** Changes to the functionality of YouTube that will require API changes for
|
||||||
alternative clients
|
alternative clients
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
|
||||||
|
- Experimental (<3%)
|
||||||
|
- Common (>3%)
|
||||||
|
- Frequent (>40%)
|
||||||
|
- Stabilized (100%)
|
||||||
|
|
||||||
If you want to check how often these A/B tests occur, you can use the `codegen` tool
|
If you want to check how often these A/B tests occur, you can use the `codegen` tool
|
||||||
with the following command: `rustypipe-codegen ab-test <id>`.
|
with the following command: `rustypipe-codegen ab-test <id>`.
|
||||||
|
|
||||||
|
|
@ -584,3 +591,16 @@ 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
|
||||||
|
- **Impact:** 🟢 Low
|
||||||
|
- **Endpoint:** browse
|
||||||
|
- **Status:** Common (6%)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
YouTube Music updated the layout of album and playlist pages. The new layout shows
|
||||||
|
the cover on the left side of the playlist content.
|
||||||
|
|
|
||||||
BIN
notes/_img/ab_13.png
Normal file
BIN
notes/_img/ab_13.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{borrow::Cow, fmt::Debug};
|
use std::{borrow::Cow, fmt::Debug};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
client::response::url_endpoint::NavigationEndpoint,
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{
|
model::{
|
||||||
paginator::{ContinuationEndpoint, Paginator},
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
|
@ -143,16 +144,35 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
let music_contents = self
|
let (header, music_contents) = match self.contents {
|
||||||
.contents
|
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => (
|
||||||
.single_column_browse_results_renderer
|
self.header,
|
||||||
.contents
|
c.contents
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
||||||
.tab_renderer
|
.tab_renderer
|
||||||
.content
|
.content
|
||||||
.section_list_renderer;
|
.section_list_renderer,
|
||||||
|
),
|
||||||
|
response::music_playlist::Contents::TwoColumnBrowseResultsRenderer {
|
||||||
|
secondary_contents,
|
||||||
|
tabs,
|
||||||
|
} => (
|
||||||
|
tabs.into_iter()
|
||||||
|
.next()
|
||||||
|
.and_then(|t| {
|
||||||
|
t.tab_renderer
|
||||||
|
.content
|
||||||
|
.section_list_renderer
|
||||||
|
.contents
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
})
|
||||||
|
.or(self.header),
|
||||||
|
secondary_contents.section_list_renderer,
|
||||||
|
),
|
||||||
|
};
|
||||||
let shelf = music_contents
|
let shelf = music_contents
|
||||||
.contents
|
.contents
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -183,7 +203,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
.map(|cont| cont.next_continuation_data.continuation);
|
.map(|cont| cont.next_continuation_data.continuation);
|
||||||
|
|
||||||
let track_count = if ctoken.is_some() {
|
let track_count = if ctoken.is_some() {
|
||||||
self.header.as_ref().and_then(|h| {
|
header.as_ref().and_then(|h| {
|
||||||
let parts = h
|
let parts = h
|
||||||
.music_detail_header_renderer
|
.music_detail_header_renderer
|
||||||
.second_subtitle
|
.second_subtitle
|
||||||
|
|
@ -203,23 +223,24 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
.next()
|
.next()
|
||||||
.map(|c| c.next_continuation_data.continuation);
|
.map(|c| c.next_continuation_data.continuation);
|
||||||
|
|
||||||
let (from_ytm, channel, name, thumbnail, description) = match self.header {
|
let (from_ytm, channel, name, thumbnail, description) = match header {
|
||||||
Some(header) => {
|
Some(header) => {
|
||||||
let h = header.music_detail_header_renderer;
|
let h = header.music_detail_header_renderer;
|
||||||
|
|
||||||
let from_ytm = h.subtitle.0.iter().any(util::is_ytm);
|
let st = match h.strapline_text_one {
|
||||||
let channel = h
|
Some(s) => s,
|
||||||
.subtitle
|
None => h.subtitle,
|
||||||
.0
|
};
|
||||||
.into_iter()
|
|
||||||
.find_map(|c| ChannelId::try_from(c).ok());
|
let from_ytm = st.0.iter().any(util::is_ytm);
|
||||||
|
let channel = st.0.into_iter().find_map(|c| ChannelId::try_from(c).ok());
|
||||||
|
|
||||||
(
|
(
|
||||||
from_ytm,
|
from_ytm,
|
||||||
channel,
|
channel,
|
||||||
h.title,
|
h.title,
|
||||||
h.thumbnail.into(),
|
h.thumbnail.into(),
|
||||||
h.description,
|
h.description.map(String::from),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -288,23 +309,40 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
let header = self
|
let (header, sections) = match self.contents {
|
||||||
.header
|
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => (
|
||||||
|
self.header,
|
||||||
|
c.contents
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
||||||
|
.tab_renderer
|
||||||
|
.content
|
||||||
|
.section_list_renderer
|
||||||
|
.contents,
|
||||||
|
),
|
||||||
|
response::music_playlist::Contents::TwoColumnBrowseResultsRenderer {
|
||||||
|
secondary_contents,
|
||||||
|
tabs,
|
||||||
|
} => (
|
||||||
|
tabs.into_iter()
|
||||||
|
.next()
|
||||||
|
.and_then(|t| {
|
||||||
|
t.tab_renderer
|
||||||
|
.content
|
||||||
|
.section_list_renderer
|
||||||
|
.contents
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
})
|
||||||
|
.or(self.header),
|
||||||
|
secondary_contents.section_list_renderer.contents,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let header = header
|
||||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no header")))?
|
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no header")))?
|
||||||
.music_detail_header_renderer;
|
.music_detail_header_renderer;
|
||||||
|
|
||||||
let sections = self
|
|
||||||
.contents
|
|
||||||
.single_column_browse_results_renderer
|
|
||||||
.contents
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
|
||||||
.tab_renderer
|
|
||||||
.content
|
|
||||||
.section_list_renderer
|
|
||||||
.contents;
|
|
||||||
|
|
||||||
let mut shelf = None;
|
let mut shelf = None;
|
||||||
let mut album_variants = None;
|
let mut album_variants = None;
|
||||||
for section in sections {
|
for section in sections {
|
||||||
|
|
@ -322,27 +360,37 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
|
|
||||||
let mut subtitle_split = header.subtitle.split(util::DOT_SEPARATOR);
|
let mut subtitle_split = header.subtitle.split(util::DOT_SEPARATOR);
|
||||||
|
|
||||||
let (year_txt, artists_p) = match subtitle_split.len() {
|
let (year_txt, artists_p) = match header.strapline_text_one {
|
||||||
3.. => {
|
Some(sl) => {
|
||||||
let year_txt = subtitle_split
|
let year_txt = subtitle_split
|
||||||
.swap_remove(2)
|
.swap_remove(1)
|
||||||
.0
|
.0
|
||||||
.first()
|
.first()
|
||||||
.map(|c| c.as_str().to_owned());
|
.map(|c| c.as_str().to_owned());
|
||||||
(year_txt, subtitle_split.try_swap_remove(1))
|
(year_txt, Some(sl))
|
||||||
}
|
}
|
||||||
2 => {
|
None => match subtitle_split.len() {
|
||||||
// The second part may either be the year or the artist
|
3.. => {
|
||||||
let p2 = subtitle_split.swap_remove(1);
|
let year_txt = subtitle_split
|
||||||
let is_year =
|
.swap_remove(2)
|
||||||
p2.0.len() == 1 && p2.0[0].as_str().chars().all(|c| c.is_ascii_digit());
|
.0
|
||||||
if is_year {
|
.first()
|
||||||
(Some(p2.0[0].as_str().to_owned()), None)
|
.map(|c| c.as_str().to_owned());
|
||||||
} else {
|
(year_txt, subtitle_split.try_swap_remove(1))
|
||||||
(None, Some(p2))
|
|
||||||
}
|
}
|
||||||
}
|
2 => {
|
||||||
_ => (None, None),
|
// The second part may either be the year or the artist
|
||||||
|
let p2 = subtitle_split.swap_remove(1);
|
||||||
|
let is_year =
|
||||||
|
p2.0.len() == 1 && p2.0[0].as_str().chars().all(|c| c.is_ascii_digit());
|
||||||
|
if is_year {
|
||||||
|
(Some(p2.0[0].as_str().to_owned()), None)
|
||||||
|
} else {
|
||||||
|
(None, Some(p2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (None, None),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let (artists, by_va) = map_artists(artists_p);
|
let (artists, by_va) = map_artists(artists_p);
|
||||||
|
|
@ -355,21 +403,34 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
let album_type = map_album_type(album_type_txt.as_str(), lang);
|
let album_type = map_album_type(album_type_txt.as_str(), lang);
|
||||||
let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());
|
let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());
|
||||||
|
|
||||||
let (artist_id, playlist_id) = header
|
fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> {
|
||||||
|
if let NavigationEndpoint::WatchPlaylist {
|
||||||
|
watch_playlist_endpoint,
|
||||||
|
} = ep
|
||||||
|
{
|
||||||
|
Some(watch_playlist_endpoint.playlist_id.to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (playlist_id, artist_id) = header
|
||||||
.menu
|
.menu
|
||||||
|
.or_else(|| header.buttons.into_iter().next())
|
||||||
.map(|menu| {
|
.map(|menu| {
|
||||||
(
|
(
|
||||||
map_artist_id(menu.menu_renderer.items),
|
|
||||||
menu.menu_renderer
|
menu.menu_renderer
|
||||||
.top_level_buttons
|
.top_level_buttons
|
||||||
.into_iter()
|
.iter()
|
||||||
.next()
|
.find_map(|btn| map_playlist_id(&btn.button_renderer.navigation_endpoint))
|
||||||
.map(|btn| {
|
.or_else(|| {
|
||||||
btn.button_renderer
|
menu.menu_renderer.items.iter().find_map(|itm| {
|
||||||
.navigation_endpoint
|
map_playlist_id(
|
||||||
.watch_playlist_endpoint
|
&itm.menu_navigation_item_renderer.navigation_endpoint,
|
||||||
.playlist_id
|
)
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
|
map_artist_id(menu.menu_renderer.items),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
@ -403,7 +464,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
cover: header.thumbnail.into(),
|
cover: header.thumbnail.into(),
|
||||||
artists,
|
artists,
|
||||||
artist_id,
|
artist_id,
|
||||||
description: header.description,
|
description: header.description.map(String::from),
|
||||||
album_type,
|
album_type,
|
||||||
year,
|
year,
|
||||||
by_va,
|
by_va,
|
||||||
|
|
@ -429,6 +490,8 @@ mod tests {
|
||||||
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
||||||
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
|
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
|
||||||
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
||||||
|
#[case::two_columns("20240228_twoColumns", "RDCLAK5uy_kb7EBi6y3GrtJri4_ZH56Ms786DFEimbM")]
|
||||||
|
#[case::n_album("20240228_album", "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0")]
|
||||||
fn map_music_playlist(#[case] name: &str, #[case] id: &str) {
|
fn map_music_playlist(#[case] name: &str, #[case] id: &str) {
|
||||||
let json_path = path!(*TESTFILES / "music_playlist" / format!("playlist_{name}.json"));
|
let json_path = path!(*TESTFILES / "music_playlist" / format!("playlist_{name}.json"));
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
@ -454,6 +517,8 @@ mod tests {
|
||||||
#[case::single("single", "MPREb_bHfHGoy7vuv")]
|
#[case::single("single", "MPREb_bHfHGoy7vuv")]
|
||||||
#[case::description("description", "MPREb_PiyfuVl6aYd")]
|
#[case::description("description", "MPREb_PiyfuVl6aYd")]
|
||||||
#[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
|
#[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
|
||||||
|
#[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
|
||||||
|
#[case::two_columns("20240228_twoColumns", "MPREb_bHfHGoy7vuv")]
|
||||||
fn map_music_album(#[case] name: &str, #[case] id: &str) {
|
fn map_music_album(#[case] name: &str, #[case] id: &str) {
|
||||||
let json_path = path!(*TESTFILES / "music_playlist" / format!("album_{name}.json"));
|
let json_path = path!(*TESTFILES / "music_playlist" / format!("album_{name}.json"));
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ pub(crate) struct ContentRenderer<T> {
|
||||||
pub content: T,
|
pub content: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserializes any object with an array field named `contents`, `tabs` or `items`.
|
||||||
|
///
|
||||||
|
/// Invalid items are skipped
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct ContentsRenderer<T> {
|
pub(crate) struct ContentsRenderer<T> {
|
||||||
pub contents: Vec<T>,
|
pub contents: Vec<T>,
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,37 @@ use crate::serializer::text::{Text, TextComponents};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
music_item::{
|
music_item::{
|
||||||
ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicThumbnailRenderer,
|
Button, ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicThumbnailRenderer,
|
||||||
SingleColumnBrowseResult,
|
|
||||||
},
|
},
|
||||||
Tab,
|
ContentsRenderer, SectionList, Tab,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Response model for YouTube Music playlists and albums
|
/// Response model for YouTube Music playlists and albums
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct MusicPlaylist {
|
pub(crate) struct MusicPlaylist {
|
||||||
pub contents: SingleColumnBrowseResult<Tab<SectionList>>,
|
pub contents: Contents,
|
||||||
pub header: Option<Header>,
|
pub header: Option<Header>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) enum Contents {
|
||||||
|
SingleColumnBrowseResultsRenderer(ContentsRenderer<Tab<PlSectionList>>),
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
TwoColumnBrowseResultsRenderer {
|
||||||
|
/// List content
|
||||||
|
secondary_contents: PlSectionList,
|
||||||
|
/// Header
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
tabs: Vec<Tab<SectionList<Header>>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct SectionList {
|
pub(crate) struct PlSectionList {
|
||||||
/// Includes a continuation token for fetching recommendations
|
/// Includes a continuation token for fetching recommendations
|
||||||
pub section_list_renderer: MusicContentsRenderer<ItemSection>,
|
pub section_list_renderer: MusicContentsRenderer<ItemSection>,
|
||||||
}
|
}
|
||||||
|
|
@ -29,6 +43,7 @@ pub(crate) struct SectionList {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct Header {
|
pub(crate) struct Header {
|
||||||
|
#[serde(alias = "musicResponsiveHeaderRenderer")]
|
||||||
pub music_detail_header_renderer: HeaderRenderer,
|
pub music_detail_header_renderer: HeaderRenderer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,12 +63,13 @@ pub(crate) struct HeaderRenderer {
|
||||||
pub subtitle: TextComponents,
|
pub subtitle: TextComponents,
|
||||||
/// Playlist/album description. May contain hashtags which are
|
/// Playlist/album description. May contain hashtags which are
|
||||||
/// displayed as search links on the YouTube website.
|
/// displayed as search links on the YouTube website.
|
||||||
#[serde_as(as = "Option<Text>")]
|
pub description: Option<Description>,
|
||||||
pub description: Option<String>,
|
|
||||||
/// Playlist thumbnail / album cover.
|
/// Playlist thumbnail / album cover.
|
||||||
/// Missing on artist_tracks view.
|
/// Missing on artist_tracks view.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub thumbnail: MusicThumbnailRenderer,
|
pub thumbnail: MusicThumbnailRenderer,
|
||||||
|
/// Channel (only on TwoColumnBrowseResultsRenderer)
|
||||||
|
pub strapline_text_one: Option<TextComponents>,
|
||||||
/// Number of tracks + playtime.
|
/// Number of tracks + playtime.
|
||||||
/// Missing on artist_tracks view.
|
/// Missing on artist_tracks view.
|
||||||
///
|
///
|
||||||
|
|
@ -66,6 +82,28 @@ pub(crate) struct HeaderRenderer {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "DefaultOnError")]
|
#[serde_as(as = "DefaultOnError")]
|
||||||
pub menu: Option<HeaderMenu>,
|
pub menu: Option<HeaderMenu>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub buttons: Vec<HeaderMenu>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub(crate) enum Description {
|
||||||
|
Text(#[serde_as(as = "Text")] String),
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
Shelf {
|
||||||
|
music_description_shelf_renderer: DescriptionShelf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct DescriptionShelf {
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -80,31 +118,18 @@ pub(crate) struct HeaderMenu {
|
||||||
pub(crate) struct HeaderMenuRenderer {
|
pub(crate) struct HeaderMenuRenderer {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub top_level_buttons: Vec<TopLevelButton>,
|
pub top_level_buttons: Vec<Button>,
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub items: Vec<MusicItemMenuEntry>,
|
pub items: Vec<MusicItemMenuEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
impl From<Description> for String {
|
||||||
#[serde(rename_all = "camelCase")]
|
fn from(value: Description) -> Self {
|
||||||
pub(crate) struct TopLevelButton {
|
match value {
|
||||||
pub button_renderer: ButtonRenderer,
|
Description::Text(v) => v,
|
||||||
}
|
Description::Shelf {
|
||||||
|
music_description_shelf_renderer,
|
||||||
#[derive(Debug, Deserialize)]
|
} => music_description_shelf_renderer.description,
|
||||||
#[serde(rename_all = "camelCase")]
|
}
|
||||||
pub(crate) struct ButtonRenderer {
|
}
|
||||||
pub navigation_endpoint: PlaylistEndpoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub(crate) struct PlaylistEndpoint {
|
|
||||||
pub watch_playlist_endpoint: PlaylistWatchEndpoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub(crate) struct PlaylistWatchEndpoint {
|
|
||||||
pub playlist_id: String,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ pub(crate) enum NavigationEndpoint {
|
||||||
},
|
},
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Url { url_endpoint: UrlEndpoint },
|
Url { url_endpoint: UrlEndpoint },
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
WatchPlaylist {
|
||||||
|
watch_playlist_endpoint: WatchPlaylistEndpoint,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -54,6 +58,12 @@ pub(crate) struct BrowseEndpointWrap {
|
||||||
pub browse_endpoint: BrowseEndpoint,
|
pub browse_endpoint: BrowseEndpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct WatchPlaylistEndpoint {
|
||||||
|
pub playlist_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for BrowseEndpoint {
|
impl<'de> Deserialize<'de> for BrowseEndpoint {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
|
@ -294,6 +304,12 @@ impl NavigationEndpoint {
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
NavigationEndpoint::Url { .. } => None,
|
NavigationEndpoint::Url { .. } => None,
|
||||||
|
NavigationEndpoint::WatchPlaylist {
|
||||||
|
watch_playlist_endpoint,
|
||||||
|
} => Some(MusicPage {
|
||||||
|
id: watch_playlist_endpoint.playlist_id,
|
||||||
|
typ: MusicPageType::Playlist,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
---
|
||||||
|
source: src/client/music_playlist.rs
|
||||||
|
expression: map_res.c
|
||||||
|
---
|
||||||
|
MusicAlbum(
|
||||||
|
id: "MPREb_bHfHGoy7vuv",
|
||||||
|
playlist_id: Some("OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0"),
|
||||||
|
name: "Der Himmel reißt auf",
|
||||||
|
cover: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w60-h60-l90-rj",
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w120-h120-l90-rj",
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w226-h226-l90-rj",
|
||||||
|
width: 226,
|
||||||
|
height: 226,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w544-h544-l90-rj",
|
||||||
|
width: 544,
|
||||||
|
height: 544,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
artists: [
|
||||||
|
ArtistId(
|
||||||
|
id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||||
|
name: "Joel Brandenstein",
|
||||||
|
),
|
||||||
|
ArtistId(
|
||||||
|
id: Some("UCFTcSVPYRWlDoHisR-ZKwgw"),
|
||||||
|
name: "Vanessa Mai",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
artist_id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||||
|
description: None,
|
||||||
|
album_type: Single,
|
||||||
|
year: Some(2020),
|
||||||
|
by_va: false,
|
||||||
|
tracks: [
|
||||||
|
TrackItem(
|
||||||
|
id: "XX0epju-YvY",
|
||||||
|
name: "Der Himmel reißt auf",
|
||||||
|
duration: Some(183),
|
||||||
|
cover: [],
|
||||||
|
artists: [
|
||||||
|
ArtistId(
|
||||||
|
id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||||
|
name: "Joel Brandenstein",
|
||||||
|
),
|
||||||
|
ArtistId(
|
||||||
|
id: Some("UCFTcSVPYRWlDoHisR-ZKwgw"),
|
||||||
|
name: "Vanessa Mai",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
artist_id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||||
|
album: Some(AlbumId(
|
||||||
|
id: "MPREb_bHfHGoy7vuv",
|
||||||
|
name: "Der Himmel reißt auf",
|
||||||
|
)),
|
||||||
|
view_count: Some(12000000),
|
||||||
|
is_video: true,
|
||||||
|
track_nr: Some(1),
|
||||||
|
by_va: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
variants: [],
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
source: src/client/music_playlist.rs
|
||||||
|
expression: map_res.c
|
||||||
|
---
|
||||||
|
MusicPlaylist(
|
||||||
|
id: "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0",
|
||||||
|
name: "Der Himmel reißt auf",
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w60-h60-l90-rj",
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w120-h120-l90-rj",
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: None,
|
||||||
|
description: None,
|
||||||
|
track_count: Some(1),
|
||||||
|
from_ytm: true,
|
||||||
|
tracks: Paginator(
|
||||||
|
count: Some(1),
|
||||||
|
items: [
|
||||||
|
TrackItem(
|
||||||
|
id: "VU6lEv0PKAo",
|
||||||
|
name: "Der Himmel reißt auf",
|
||||||
|
duration: Some(183),
|
||||||
|
cover: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w60-h60-l90-rj",
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w120-h120-l90-rj",
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
artists: [
|
||||||
|
ArtistId(
|
||||||
|
id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||||
|
name: "Joel Brandenstein",
|
||||||
|
),
|
||||||
|
ArtistId(
|
||||||
|
id: Some("UCFTcSVPYRWlDoHisR-ZKwgw"),
|
||||||
|
name: "Vanessa Mai",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
artist_id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||||
|
album: Some(AlbumId(
|
||||||
|
id: "MPREb_bHfHGoy7vuv",
|
||||||
|
name: "Der Himmel reißt auf",
|
||||||
|
)),
|
||||||
|
view_count: None,
|
||||||
|
is_video: false,
|
||||||
|
track_nr: None,
|
||||||
|
by_va: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ctoken: None,
|
||||||
|
endpoint: music_browse,
|
||||||
|
),
|
||||||
|
related_playlists: Paginator(
|
||||||
|
count: Some(0),
|
||||||
|
items: [],
|
||||||
|
ctoken: None,
|
||||||
|
endpoint: music_browse,
|
||||||
|
),
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -195,6 +195,13 @@ fn map_text_component(text: String, nav: Option<NavigationEndpoint>) -> TextComp
|
||||||
text,
|
text,
|
||||||
url: url_endpoint.url,
|
url: url_endpoint.url,
|
||||||
},
|
},
|
||||||
|
Some(NavigationEndpoint::WatchPlaylist {
|
||||||
|
watch_playlist_endpoint,
|
||||||
|
}) => TextComponent::Browse {
|
||||||
|
text,
|
||||||
|
page_type: PageType::Playlist,
|
||||||
|
browse_id: watch_playlist_endpoint.playlist_id,
|
||||||
|
},
|
||||||
None => TextComponent::Text { text },
|
None => TextComponent::Text { text },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1225
testfiles/music_playlist/album_20240228_twoColumns.json
Normal file
1225
testfiles/music_playlist/album_20240228_twoColumns.json
Normal file
File diff suppressed because it is too large
Load diff
691
testfiles/music_playlist/playlist_20240228_album.json
Normal file
691
testfiles/music_playlist/playlist_20240228_album.json
Normal file
|
|
@ -0,0 +1,691 @@
|
||||||
|
{
|
||||||
|
"responseContext": {
|
||||||
|
"serviceTrackingParams": [
|
||||||
|
{
|
||||||
|
"service": "GFEEDBACK",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "has_unlimited_entitlement",
|
||||||
|
"value": "False"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "browse_id",
|
||||||
|
"value": "VLOLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "browse_id_prefix",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "logged_in",
|
||||||
|
"value": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "e",
|
||||||
|
"value": "23804281,23946420,23966208,23983296,23998056,24004644,24007246,24036947,24077241,24080738,24120819,24135310,24140247,24181174,24186126,24187377,24241378,24255543,24255545,24288664,24290971,24299875,24377598,24385728,24390675,24439361,24445497,24451319,24458317,24458324,24458329,24468724,24485421,24506784,24515423,24524098,24537200,24542367,24542452,24546059,24548627,24548629,24560416,24566687,24690006,24697068,51003636,51004018,51006181,51009781,51010235,51014091,51016856,51017346,51019626,51020570,51021953,51025415,51026715,51027696,51027870,51030101,51033106,51033399,51037342,51037349,51037540,51041280,51047537,51048489,51050361,51053689,51057746,51057757,51057844,51057851,51059571,51060353,51061001,51063643,51065188,51069269,51073088,51078191,51079299,51079353,51080342,51080397,51080511,51082385,51083232,51083797,51084279,51084290,51087987,51089007,51089175,51089441,51089956,51091331,51092289,51092660,51092916,51092929,51094171,51094197,51094202,51094207,51094886,51095273,51096389,51096577,51096646,51096961,51096989,51098297,51098299,51099412,51099452,51101454,51102968,51103046,51103088,51104190,51105801,51105868,51105978,51106628,51106957,51106995,51107338,51107774,51108006,51108584,51108626,51108977,51110159,51111738,51112355,51113658,51113661,51117318,51118058,51118293,51118931,51119935,51120306,51121444,51121888,51122466,51123075,51124104,51124359,51125645,51128545,51128582,51130055,51130172"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "CSI",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "c",
|
||||||
|
"value": "WEB_REMIX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "cver",
|
||||||
|
"value": "1.20240221.01.01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "yt_li",
|
||||||
|
"value": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GetBrowsePlaylistDetailPage_rid",
|
||||||
|
"value": "0xde7c93e5674d0e49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "ECATCHER",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "client.version",
|
||||||
|
"value": "1.20000101"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "client.name",
|
||||||
|
"value": "WEB_REMIX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "client.fexp",
|
||||||
|
"value": "51079353,24458317,51063643,51003636,51084290,24077241,51016856,24186126,24004644,24458324,51050361,23983296,51089007,24187377,51096961,51092660,24255545,51128545,51105868,51094207,51094171,24690006,51026715,51089956,51065188,51091331,51107774,24506784,24542367,51017346,51130055,24445497,51033106,51101454,24524098,51057746,51096989,51087987,51099412,24036947,24697068,51118293,23998056,51128582,24135310,51106995,51113661,24255543,51096646,51009781,51120306,51108584,51105801,51110159,51121888,51037540,51104190,51041280,51095273,51123075,24120819,51094202,24007246,51106628,51047537,51082385,51102968,51010235,51069269,51092929,51025415,51092289,24560416,51130172,23946420,51073088,51124104,24537200,51033399,24548629,51083797,51118931,51098299,51089441,24542452,24390675,51089175,51019626,51080342,24385728,51092916,51096389,23804281,51106957,24546059,51103088,51121444,51057757,51079299,51084279,51060353,51057844,51014091,51107338,24439361,51057851,24566687,24288664,51083232,24548627,51125645,51099452,51103046,24299875,51122466,51027696,24181174,51094886,24377598,51108626,24515423,51020570,24468724,51078191,23966208,24080738,24140247,51098297,51117318,51048489,51021953,51059571,24290971,51119935,51080397,51113658,51037342,51111738,51037349,51030101,51094197,51124359,51118058,51108977,51004018,51112355,51096577,51108006,51105978,51006181,51061001,24485421,24451319,24241378,51053689,51027870,24458329,51080511"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contents": {
|
||||||
|
"twoColumnBrowseResultsRenderer": {
|
||||||
|
"secondaryContents": {
|
||||||
|
"sectionListRenderer": {
|
||||||
|
"contents": [
|
||||||
|
{
|
||||||
|
"musicPlaylistShelfRenderer": {
|
||||||
|
"contents": [
|
||||||
|
{
|
||||||
|
"musicResponsiveListItemRenderer": {
|
||||||
|
"trackingParams": "CAUQyfQCGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"thumbnail": {
|
||||||
|
"musicThumbnailRenderer": {
|
||||||
|
"thumbnail": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w60-h60-l90-rj",
|
||||||
|
"width": 60,
|
||||||
|
"height": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w120-h120-l90-rj",
|
||||||
|
"width": 120,
|
||||||
|
"height": 120
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED",
|
||||||
|
"thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT",
|
||||||
|
"trackingParams": "CBYQhL8CIhMIk9iTrbLPhAMV0OhCBR2xIwrh"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overlay": {
|
||||||
|
"musicItemThumbnailOverlayRenderer": {
|
||||||
|
"background": {
|
||||||
|
"verticalGradient": {
|
||||||
|
"gradientLayerColors": [
|
||||||
|
"3422552064",
|
||||||
|
"3422552064"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"musicPlayButtonRenderer": {
|
||||||
|
"playNavigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CBUQyN4CIhMIk9iTrbLPhAMV0OhCBR2xIwrh",
|
||||||
|
"watchEndpoint": {
|
||||||
|
"videoId": "VU6lEv0PKAo",
|
||||||
|
"playlistId": "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0",
|
||||||
|
"playlistSetVideoId": "6263132B04ADB7BE",
|
||||||
|
"loggingContext": {
|
||||||
|
"vssLoggingContext": {
|
||||||
|
"serializedContextData": "GilPTEFLNXV5X2tkU1dCWi05QVpEa1lrdXkwUUNjM3AwS085REVIVk5IMA%3D%3D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"watchEndpointMusicSupportedConfigs": {
|
||||||
|
"watchEndpointMusicConfig": {
|
||||||
|
"musicVideoType": "MUSIC_VIDEO_TYPE_ATV"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CBUQyN4CIhMIk9iTrbLPhAMV0OhCBR2xIwrh",
|
||||||
|
"playIcon": {
|
||||||
|
"iconType": "PLAY_ARROW"
|
||||||
|
},
|
||||||
|
"pauseIcon": {
|
||||||
|
"iconType": "PAUSE"
|
||||||
|
},
|
||||||
|
"iconColor": 4294967295,
|
||||||
|
"backgroundColor": 0,
|
||||||
|
"activeBackgroundColor": 0,
|
||||||
|
"loadingIndicatorColor": 4294901760,
|
||||||
|
"playingIcon": {
|
||||||
|
"iconType": "VOLUME_UP"
|
||||||
|
},
|
||||||
|
"iconLoadingColor": 0,
|
||||||
|
"activeScaleFactor": 1,
|
||||||
|
"buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL",
|
||||||
|
"rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF",
|
||||||
|
"accessibilityPlayData": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "Play Der Himmel reißt auf - Joel Brandenstein - 3 minutes, 3 seconds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accessibilityPauseData": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "Pause Der Himmel reißt auf - Joel Brandenstein - 3 minutes, 3 seconds"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED",
|
||||||
|
"displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flexColumns": [
|
||||||
|
{
|
||||||
|
"musicResponsiveListItemFlexColumnRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Der Himmel reißt auf",
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAUQyfQCGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"watchEndpoint": {
|
||||||
|
"videoId": "VU6lEv0PKAo",
|
||||||
|
"playlistId": "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0",
|
||||||
|
"loggingContext": {
|
||||||
|
"vssLoggingContext": {
|
||||||
|
"serializedContextData": "GilPTEFLNXV5X2tkU1dCWi05QVpEa1lrdXkwUUNjM3AwS085REVIVk5IMA%3D%3D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"watchEndpointMusicSupportedConfigs": {
|
||||||
|
"watchEndpointMusicConfig": {
|
||||||
|
"musicVideoType": "MUSIC_VIDEO_TYPE_ATV"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"musicResponsiveListItemFlexColumnRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Joel Brandenstein",
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAUQyfQCGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"browseEndpoint": {
|
||||||
|
"browseId": "UCXGYZ-OhdOpPBamHX3K9YRg",
|
||||||
|
"browseEndpointContextSupportedConfigs": {
|
||||||
|
"browseEndpointContextMusicConfig": {
|
||||||
|
"pageType": "MUSIC_PAGE_TYPE_ARTIST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " & "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Vanessa Mai",
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAUQyfQCGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"browseEndpoint": {
|
||||||
|
"browseId": "UCFTcSVPYRWlDoHisR-ZKwgw",
|
||||||
|
"browseEndpointContextSupportedConfigs": {
|
||||||
|
"browseEndpointContextMusicConfig": {
|
||||||
|
"pageType": "MUSIC_PAGE_TYPE_ARTIST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"musicResponsiveListItemFlexColumnRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Der Himmel reißt auf",
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAUQyfQCGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"browseEndpoint": {
|
||||||
|
"browseId": "MPREb_bHfHGoy7vuv",
|
||||||
|
"browseEndpointContextSupportedConfigs": {
|
||||||
|
"browseEndpointContextMusicConfig": {
|
||||||
|
"pageType": "MUSIC_PAGE_TYPE_ALBUM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fixedColumns": [
|
||||||
|
{
|
||||||
|
"musicResponsiveListItemFixedColumnRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "3:03"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH",
|
||||||
|
"size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menu": {
|
||||||
|
"menuRenderer": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"menuNavigationItemRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Start radio"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"iconType": "MIX"
|
||||||
|
},
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CBQQm_MFGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"watchEndpoint": {
|
||||||
|
"videoId": "VU6lEv0PKAo",
|
||||||
|
"playlistId": "RDAMVMVU6lEv0PKAo",
|
||||||
|
"params": "wAEB",
|
||||||
|
"loggingContext": {
|
||||||
|
"vssLoggingContext": {
|
||||||
|
"serializedContextData": "GhFSREFNVk1WVTZsRXYwUEtBbw%3D%3D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"watchEndpointMusicSupportedConfigs": {
|
||||||
|
"watchEndpointMusicConfig": {
|
||||||
|
"musicVideoType": "MUSIC_VIDEO_TYPE_ATV"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CBQQm_MFGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuServiceItemRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Play next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"iconType": "QUEUE_PLAY_NEXT"
|
||||||
|
},
|
||||||
|
"serviceEndpoint": {
|
||||||
|
"clickTrackingParams": "CBIQvu4FGAEiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"queueAddEndpoint": {
|
||||||
|
"queueTarget": {
|
||||||
|
"videoId": "VU6lEv0PKAo",
|
||||||
|
"onEmptyQueue": {
|
||||||
|
"clickTrackingParams": "CBIQvu4FGAEiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"watchEndpoint": {
|
||||||
|
"videoId": "VU6lEv0PKAo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"clickTrackingParams": "CBIQvu4FGAEiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"addToToastAction": {
|
||||||
|
"item": {
|
||||||
|
"notificationTextRenderer": {
|
||||||
|
"successResponseText": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Song will play next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"trackingParams": "CBMQyscDIhMIk9iTrbLPhAMV0OhCBR2xIwrh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CBIQvu4FGAEiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuServiceItemRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Add to queue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"iconType": "ADD_TO_REMOTE_QUEUE"
|
||||||
|
},
|
||||||
|
"serviceEndpoint": {
|
||||||
|
"clickTrackingParams": "CBAQ--8FGAIiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"queueAddEndpoint": {
|
||||||
|
"queueTarget": {
|
||||||
|
"videoId": "VU6lEv0PKAo",
|
||||||
|
"onEmptyQueue": {
|
||||||
|
"clickTrackingParams": "CBAQ--8FGAIiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"watchEndpoint": {
|
||||||
|
"videoId": "VU6lEv0PKAo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"queueInsertPosition": "INSERT_AT_END",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"clickTrackingParams": "CBAQ--8FGAIiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"addToToastAction": {
|
||||||
|
"item": {
|
||||||
|
"notificationTextRenderer": {
|
||||||
|
"successResponseText": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Song added to queue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"trackingParams": "CBEQyscDIhMIk9iTrbLPhAMV0OhCBR2xIwrh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CBAQ--8FGAIiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuNavigationItemRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Save to playlist"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"iconType": "ADD_TO_PLAYLIST"
|
||||||
|
},
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CA4Qw5QGGAMiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"modalEndpoint": {
|
||||||
|
"modal": {
|
||||||
|
"modalWithTitleAndButtonRenderer": {
|
||||||
|
"title": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Save this for later"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Make playlists and share them after signing in"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"buttonRenderer": {
|
||||||
|
"style": "STYLE_BLUE_TEXT",
|
||||||
|
"isDisabled": false,
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Sign in"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CA8Q8FsiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"signInEndpoint": {
|
||||||
|
"hack": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CA8Q8FsiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CA4Qw5QGGAMiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuNavigationItemRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Go to album"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"iconType": "ALBUM"
|
||||||
|
},
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CA0Qj_sFGAQiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"browseEndpoint": {
|
||||||
|
"browseId": "MPREb_bHfHGoy7vuv",
|
||||||
|
"browseEndpointContextSupportedConfigs": {
|
||||||
|
"browseEndpointContextMusicConfig": {
|
||||||
|
"pageType": "MUSIC_PAGE_TYPE_ALBUM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CA0Qj_sFGAQiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuNavigationItemRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Go to artist"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"iconType": "ARTIST"
|
||||||
|
},
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAwQkPsFGAUiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"browseEndpoint": {
|
||||||
|
"browseId": "UCXGYZ-OhdOpPBamHX3K9YRg",
|
||||||
|
"browseEndpointContextSupportedConfigs": {
|
||||||
|
"browseEndpointContextMusicConfig": {
|
||||||
|
"pageType": "MUSIC_PAGE_TYPE_ARTIST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CAwQkPsFGAUiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menuNavigationItemRenderer": {
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Share"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"iconType": "SHARE"
|
||||||
|
},
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAsQkfsFGAYiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"shareEntityEndpoint": {
|
||||||
|
"serializedShareEntity": "CgtWVTZsRXYwUEtBbw%3D%3D",
|
||||||
|
"sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CAsQkfsFGAYiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trackingParams": "CAcQpzsiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"topLevelButtons": [
|
||||||
|
{
|
||||||
|
"likeButtonRenderer": {
|
||||||
|
"target": {
|
||||||
|
"videoId": "VU6lEv0PKAo"
|
||||||
|
},
|
||||||
|
"likeStatus": "INDIFFERENT",
|
||||||
|
"trackingParams": "CAgQpUEYByITCJPYk62yz4QDFdDoQgUdsSMK4Q==",
|
||||||
|
"likesAllowed": true,
|
||||||
|
"dislikeNavigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAgQpUEYByITCJPYk62yz4QDFdDoQgUdsSMK4Q==",
|
||||||
|
"modalEndpoint": {
|
||||||
|
"modal": {
|
||||||
|
"modalWithTitleAndButtonRenderer": {
|
||||||
|
"title": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Not a fan?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Improve your recommendations after signing in"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"buttonRenderer": {
|
||||||
|
"style": "STYLE_BLUE_TEXT",
|
||||||
|
"isDisabled": false,
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Sign in"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAoQ8FsiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"signInEndpoint": {
|
||||||
|
"hack": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CAoQ8FsiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"likeCommand": {
|
||||||
|
"clickTrackingParams": "CAgQpUEYByITCJPYk62yz4QDFdDoQgUdsSMK4Q==",
|
||||||
|
"modalEndpoint": {
|
||||||
|
"modal": {
|
||||||
|
"modalWithTitleAndButtonRenderer": {
|
||||||
|
"title": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Like this song"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Improve recommendations and save music after signing in"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"buttonRenderer": {
|
||||||
|
"style": "STYLE_BLUE_TEXT",
|
||||||
|
"isDisabled": false,
|
||||||
|
"text": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Sign in"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CAkQ8FsiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"signInEndpoint": {
|
||||||
|
"hack": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CAkQ8FsiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "Action menu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"playlistItemData": {
|
||||||
|
"playlistSetVideoId": "6263132B04ADB7BE",
|
||||||
|
"videoId": "VU6lEv0PKAo"
|
||||||
|
},
|
||||||
|
"multiSelectCheckbox": {
|
||||||
|
"checkboxRenderer": {
|
||||||
|
"onSelectionChangeCommand": {
|
||||||
|
"clickTrackingParams": "CAYQvr4JIhMIk9iTrbLPhAMV0OhCBR2xIwrh",
|
||||||
|
"updateMultiSelectStateCommand": {
|
||||||
|
"multiSelectParams": "CAISKU9MQUs1dXlfa2RTV0JaLTlBWkRrWWt1eTBRQ2MzcDBLTzlERUhWTkgw",
|
||||||
|
"multiSelectItem": "Ch8KC1ZVNmxFdjBQS0FvEhA2MjYzMTMyQjA0QURCN0JF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkedState": "CHECKBOX_CHECKED_STATE_UNCHECKED",
|
||||||
|
"trackingParams": "CAYQvr4JIhMIk9iTrbLPhAMV0OhCBR2xIwrh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collapsedItemCount": 1,
|
||||||
|
"trackingParams": "CAQQ9-MCGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE=",
|
||||||
|
"contentsMultiSelectable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trackingParams": "CAMQui8iEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"tabRenderer": {
|
||||||
|
"content": {
|
||||||
|
"sectionListRenderer": {
|
||||||
|
"trackingParams": "CAIQui8iEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CAEQ8JMBGAAiEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trackingParams": "CAAQhGciEwiT2JOtss-EAxXQ6EIFHbEjCuE="
|
||||||
|
}
|
||||||
57976
testfiles/music_playlist/playlist_20240228_twoColumns.json
Normal file
57976
testfiles/music_playlist/playlist_20240228_twoColumns.json
Normal file
File diff suppressed because it is too large
Load diff
Reference in a new issue