use super::{
parse_flex_column_item, parse_song_album, parse_song_artists, parse_upload_song_album,
parse_upload_song_artists, search::SearchResultVideo, EpisodeDate, EpisodeDuration, ParseFrom,
ParsedSongAlbum, ParsedSongArtist, ParsedUploadArtist, ParsedUploadSongAlbum, ProcessedResult,
Thumbnail,
};
use crate::{
common::{
AlbumID, AlbumType, ArtistChannelID, BrowseParams, EpisodeID, Explicit, LibraryManager,
LibraryStatus, LikeStatus, PlaylistID, UploadEntityID, VideoID,
},
nav_consts::*,
process::{fixed_column_item_pointer, flex_column_item_pointer},
query::*,
youtube_enums::YoutubeMusicVideoType,
Result,
};
use const_format::concatcp;
use json_crawler::{JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerIterator, JsonCrawlerOwned};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ArtistParams {
pub description: String,
pub views: String,
pub name: String,
pub channel_id: String,
pub shuffle_id: Option<String>,
pub radio_id: Option<String>,
pub subscribers: Option<String>,
pub subscribed: Option<String>,
pub thumbnails: Option<String>,
pub top_releases: GetArtistTopReleases,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetArtistAlbumsAlbum {
pub title: String,
pub playlist_id: Option<String>,
pub browse_id: AlbumID<'static>,
pub category: Option<String>, pub thumbnails: Vec<Thumbnail>,
pub year: Option<String>,
}
fn parse_artist_song(json: &mut JsonCrawlerBorrowed) -> Result<ArtistSong> {
let mut data = json.borrow_pointer(MRLIR)?;
let title = parse_flex_column_item(&mut data, 0, 0)?;
let plays = parse_flex_column_item(&mut data, 2, 0)?;
let artists = parse_song_artists(&mut data, 1)?;
let album = parse_song_album(&mut data, 3)?;
let video_id = data.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
let explicit = if data.path_exists(BADGE_LABEL) {
Explicit::IsExplicit
} else {
Explicit::NotExplicit
};
let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
let library_management =
parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
Ok(ArtistSong {
video_id,
plays,
album,
artists,
library_management,
title,
like_status,
explicit,
})
}
fn parse_artist_songs(json: &mut JsonCrawlerBorrowed) -> Result<GetArtistSongs> {
let browse_id = json.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
let results = json
.borrow_pointer("/contents")?
.try_into_iter()?
.map(|mut item| parse_artist_song(&mut item))
.collect::<Result<Vec<ArtistSong>>>()?;
Ok(GetArtistSongs { results, browse_id })
}
impl<'a> ParseFrom<GetArtistQuery<'a>> for ArtistParams {
#[allow(clippy::field_reassign_with_default)]
fn parse_from(p: ProcessedResult<GetArtistQuery<'a>>) -> crate::Result<Self> {
let mut json_crawler: JsonCrawlerOwned = p.into();
let mut results =
json_crawler.borrow_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST))?;
let mut description = String::default();
let mut views = String::default();
if let Ok(results_array) = results.try_iter_mut() {
for r in results_array {
if let Ok(mut description_shelf) = r.navigate_pointer(DESCRIPTION_SHELF) {
description = description_shelf.take_value_pointer(DESCRIPTION)?;
if let Ok(mut subheader) = description_shelf.borrow_pointer("/subheader") {
views = subheader.take_value_pointer("/runs/0/text")?;
}
break;
}
}
}
let mut top_releases = GetArtistTopReleases::default();
top_releases.songs = results
.borrow_pointer(concatcp!("/0", MUSIC_SHELF))
.ok()
.map(|mut j| parse_artist_songs(&mut j))
.transpose()?;
for mut r in results
.try_iter_mut()
.into_iter()
.flatten()
.filter_map(|r| r.navigate_pointer("/musicCarouselShelfRenderer").ok())
{
let category = r.take_value_pointer(concatcp!(CAROUSEL_TITLE, "/text"))?;
let browse_id: Option<ArtistChannelID> = r
.take_value_pointer(concatcp!(CAROUSEL_TITLE, NAVIGATION_BROWSE_ID))
.ok();
let params = r
.take_value_pointer(concatcp!(
CAROUSEL_TITLE,
"/navigationEndpoint/browseEndpoint/params"
))
.ok();
match category {
ArtistTopReleaseCategory::Related => (),
ArtistTopReleaseCategory::Videos => (),
ArtistTopReleaseCategory::Singles => (),
ArtistTopReleaseCategory::Albums => {
let mut results = Vec::new();
for i in r.navigate_pointer("/contents")?.try_iter_mut()? {
results.push(parse_album_from_mtrir(i.navigate_pointer(MTRIR)?)?);
}
let albums = GetArtistAlbums {
browse_id,
params,
results,
};
top_releases.albums = Some(albums);
}
ArtistTopReleaseCategory::Playlists => (),
ArtistTopReleaseCategory::None => (),
}
}
let mut header = json_crawler.navigate_pointer("/header/musicImmersiveHeaderRenderer")?;
let name = header.take_value_pointer(TITLE_TEXT)?;
let shuffle_id = header
.take_value_pointer(concatcp!(
"/playButton/buttonRenderer",
NAVIGATION_WATCH_PLAYLIST_ID
))
.ok();
let radio_id = header
.take_value_pointer(concatcp!(
"/startRadioButton/buttonRenderer",
NAVIGATION_WATCH_PLAYLIST_ID
))
.ok();
let thumbnails = header.take_value_pointer(THUMBNAILS).ok();
let mut subscription_button =
header.navigate_pointer("/subscriptionButton/subscribeButtonRenderer")?;
let channel_id = subscription_button.take_value_pointer("/channelId")?;
let subscribers = subscription_button
.take_value_pointer("/subscriberCountText/runs/0/text")
.ok();
let subscribed = subscription_button.take_value_pointer("/subscribed").ok();
Ok(ArtistParams {
views,
description,
name,
top_releases,
thumbnails,
subscribed,
radio_id,
channel_id,
shuffle_id,
subscribers,
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct GetArtistTopReleases {
pub songs: Option<GetArtistSongs>,
pub albums: Option<GetArtistAlbums>,
pub singles: Option<GetArtistAlbums>,
pub videos: Option<GetArtistVideos>,
pub related: Option<GetArtistRelated>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct GetArtistRelated {
pub results: Vec<RelatedResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct GetArtistSongs {
pub results: Vec<ArtistSong>,
pub browse_id: PlaylistID<'static>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct ArtistSong {
pub video_id: VideoID<'static>,
pub plays: String,
pub album: ParsedSongAlbum,
pub artists: Vec<ParsedSongArtist>,
pub library_management: Option<LibraryManager>,
pub title: String,
pub like_status: LikeStatus,
pub explicit: Explicit,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct GetArtistVideos {
pub results: Vec<SearchResultVideo>,
pub browse_id: PlaylistID<'static>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct GetArtistAlbums {
pub results: Vec<AlbumResult>,
pub browse_id: Option<ArtistChannelID<'static>>,
pub params: Option<BrowseParams<'static>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct RelatedResult {
pub browse_id: ArtistChannelID<'static>,
pub title: String,
pub subscribers: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
pub struct AlbumResult {
pub title: String,
pub album_type: AlbumType,
pub year: String,
pub album_id: AlbumID<'static>,
pub library_status: LibraryStatus,
pub thumbnails: Vec<Thumbnail>,
pub explicit: Explicit,
}
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PlaylistSong {
pub video_id: VideoID<'static>,
pub track_no: usize,
pub album: ParsedSongAlbum,
pub duration: String,
pub library_management: Option<LibraryManager>,
pub title: String,
pub artists: Vec<super::ParsedSongArtist>,
pub like_status: LikeStatus,
pub thumbnails: Vec<Thumbnail>,
pub explicit: Explicit,
pub is_available: bool,
pub playlist_id: PlaylistID<'static>,
}
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PlaylistVideo {
pub video_id: VideoID<'static>,
pub track_no: usize,
pub duration: String,
pub title: String,
pub channel_name: String,
pub channel_id: ArtistChannelID<'static>,
pub like_status: LikeStatus,
pub thumbnails: Vec<Thumbnail>,
pub is_available: bool,
pub playlist_id: PlaylistID<'static>,
}
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PlaylistEpisode {
pub episode_id: EpisodeID<'static>,
pub track_no: usize,
pub date: EpisodeDate,
pub duration: EpisodeDuration,
pub title: String,
pub podcast_name: String,
pub podcast_id: PlaylistID<'static>,
pub like_status: LikeStatus,
pub thumbnails: Vec<Thumbnail>,
pub is_available: bool,
}
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PlaylistUploadSong {
pub entity_id: UploadEntityID<'static>,
pub video_id: VideoID<'static>,
pub track_no: usize,
pub duration: String,
pub album: ParsedUploadSongAlbum,
pub title: String,
pub artists: Vec<ParsedUploadArtist>,
pub like_status: LikeStatus,
pub thumbnails: Vec<Thumbnail>,
}
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
#[non_exhaustive]
pub struct TableListSong {
pub video_id: VideoID<'static>,
pub album: ParsedSongAlbum,
pub duration: String,
pub library_management: Option<LibraryManager>,
pub title: String,
pub artists: Vec<super::ParsedSongArtist>,
pub like_status: LikeStatus,
pub thumbnails: Vec<Thumbnail>,
pub explicit: Explicit,
pub is_available: bool,
pub playlist_id: PlaylistID<'static>,
}
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
pub enum PlaylistItem {
Song(PlaylistSong),
Video(PlaylistVideo),
Episode(PlaylistEpisode),
UploadSong(PlaylistUploadSong),
}
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
enum ArtistTopReleaseCategory {
#[serde(alias = "albums")]
Albums,
#[serde(alias = "singles")]
Singles,
#[serde(alias = "videos")]
Videos,
#[serde(alias = "playlists")]
Playlists,
#[serde(alias = "fans might also like")]
Related,
#[serde(other)]
None,
}
pub(crate) fn parse_album_from_mtrir(mut navigator: JsonCrawlerBorrowed) -> Result<AlbumResult> {
let title = navigator.take_value_pointer(TITLE_TEXT)?;
let album_type = navigator.take_value_pointer(SUBTITLE)?;
let year = navigator.take_value_pointer(SUBTITLE2)?;
let album_id = navigator.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
let thumbnails = navigator.take_value_pointer(THUMBNAIL_RENDERER)?;
let explicit = if navigator.path_exists(concatcp!(SUBTITLE_BADGE_LABEL)) {
Explicit::IsExplicit
} else {
Explicit::NotExplicit
};
let mut library_menu = navigator
.borrow_pointer(MENU_ITEMS)?
.try_into_iter()?
.find_path("/toggleMenuServiceItemRenderer")?;
let library_status = library_menu.take_value_pointer("/defaultIcon/iconType")?;
Ok(AlbumResult {
title,
album_type,
year,
album_id,
library_status,
thumbnails,
explicit,
})
}
pub(crate) fn parse_library_management_items_from_menu(
menu: JsonCrawlerBorrowed,
) -> Result<Option<LibraryManager>> {
let Ok(mut library_menu) = menu
.try_into_iter()?
.find_path("/toggleMenuServiceItemRenderer")
else {
return Ok(None);
};
let status = library_menu.take_value_pointer("/defaultIcon/iconType")?;
let (add_to_library_token, remove_from_library_token) = match status {
LibraryStatus::InLibrary => (
library_menu.take_value_pointer(TOGGLED_ENDPOINT)?,
library_menu.take_value_pointer(DEFAULT_ENDPOINT)?,
),
LibraryStatus::NotInLibrary => (
library_menu.take_value_pointer(DEFAULT_ENDPOINT)?,
library_menu.take_value_pointer(TOGGLED_ENDPOINT)?,
),
};
Ok(Some(LibraryManager {
status,
add_to_library_token,
remove_from_library_token,
}))
}
pub(crate) fn parse_playlist_song(
title: String,
track_no: usize,
mut data: JsonCrawlerBorrowed,
) -> Result<PlaylistSong> {
let video_id = data.take_value_pointer(concatcp!(
PLAY_BUTTON,
"/playNavigationEndpoint",
WATCH_VIDEO_ID
))?;
let library_management =
parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
let artists = super::parse_song_artists(&mut data, 1)?;
let album_col_idx = if data.path_exists("/flexColumns/3") {
3
} else {
2
};
let album = super::parse_song_album(&mut data, album_col_idx)?;
let duration = data
.borrow_pointer(fixed_column_item_pointer(0))?
.take_value_pointers(vec!["/text/simpleText", "/text/runs/0/text"])?;
let thumbnails = data.take_value_pointer(THUMBNAILS)?;
let is_available = data
.take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
.map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
.unwrap_or(true);
let explicit = if data.path_exists(BADGE_LABEL) {
Explicit::IsExplicit
} else {
Explicit::NotExplicit
};
let playlist_id = data.take_value_pointer(concatcp!(
MENU_ITEMS,
"/0/menuNavigationItemRenderer",
NAVIGATION_PLAYLIST_ID
))?;
Ok(PlaylistSong {
video_id,
track_no,
duration,
library_management,
title,
artists,
like_status,
thumbnails,
explicit,
album,
playlist_id,
is_available,
})
}
pub(crate) fn parse_playlist_upload_song(
title: String,
track_no: usize,
mut data: JsonCrawlerBorrowed,
) -> Result<PlaylistUploadSong> {
let duration = data
.borrow_pointer(fixed_column_item_pointer(0))?
.take_value_pointer(TEXT_RUN_TEXT)?;
let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
let video_id = data.take_value_pointer(concatcp!(
PLAY_BUTTON,
"/playNavigationEndpoint/watchEndpoint/videoId"
))?;
let thumbnails = data.take_value_pointer(THUMBNAILS)?;
let artists = parse_upload_song_artists(data.borrow_mut(), 1)?;
let album = parse_upload_song_album(data.borrow_mut(), 2)?;
let mut menu = data.navigate_pointer(MENU_ITEMS)?;
let entity_id = menu
.try_iter_mut()?
.find_path(DELETION_ENTITY_ID)?
.take_value()?;
Ok(PlaylistUploadSong {
entity_id,
video_id,
album,
duration,
like_status,
title,
artists,
thumbnails,
track_no,
})
}
pub(crate) fn parse_playlist_episode(
title: String,
track_no: usize,
mut data: JsonCrawlerBorrowed,
) -> Result<PlaylistEpisode> {
let video_id = data.take_value_pointer(concatcp!(
PLAY_BUTTON,
"/playNavigationEndpoint",
WATCH_VIDEO_ID
))?;
let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
let is_live = data.path_exists(LIVE_BADGE_LABEL);
let (duration, date) = match is_live {
true => (EpisodeDuration::Live, EpisodeDate::Live),
false => {
let date = parse_flex_column_item(&mut data, 2, 0)?;
let duration =
data.borrow_pointer(fixed_column_item_pointer(0))
.and_then(|mut i| {
i.take_value_pointer("/text/simpleText")
.or_else(|_| i.take_value_pointer("/text/runs/0/text"))
})?;
(
EpisodeDuration::Recorded { duration },
EpisodeDate::Recorded { date },
)
}
};
let podcast_name = parse_flex_column_item(&mut data, 1, 0)?;
let podcast_id = data
.borrow_pointer(flex_column_item_pointer(1))?
.take_value_pointer(concatcp!(TEXT_RUN, NAVIGATION_BROWSE_ID))?;
let thumbnails = data.take_value_pointer(THUMBNAILS)?;
let is_available = data
.take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
.map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
.unwrap_or(true);
Ok(PlaylistEpisode {
episode_id: video_id,
duration,
title,
like_status,
thumbnails,
date,
podcast_name,
podcast_id,
is_available,
track_no,
})
}
pub(crate) fn parse_playlist_video(
title: String,
track_no: usize,
mut data: JsonCrawlerBorrowed,
) -> Result<PlaylistVideo> {
let video_id = data.take_value_pointer(concatcp!(
PLAY_BUTTON,
"/playNavigationEndpoint",
WATCH_VIDEO_ID
))?;
let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
let channel_name = parse_flex_column_item(&mut data, 1, 0)?;
let channel_id = data
.borrow_pointer(flex_column_item_pointer(1))?
.take_value_pointer(concatcp!(TEXT_RUN, NAVIGATION_BROWSE_ID))?;
let duration = data
.borrow_pointer(fixed_column_item_pointer(0))?
.take_value_pointers(vec!["/text/simpleText", "/text/runs/0/text"])?;
let thumbnails = data.take_value_pointer(THUMBNAILS)?;
let is_available = data
.take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
.map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
.unwrap_or(true);
let playlist_id = data.take_value_pointer(concatcp!(
MENU_ITEMS,
"/0/menuNavigationItemRenderer",
NAVIGATION_PLAYLIST_ID
))?;
Ok(PlaylistVideo {
video_id,
track_no,
duration,
title,
like_status,
thumbnails,
playlist_id,
is_available,
channel_name,
channel_id,
})
}
pub(crate) fn parse_playlist_item(
track_no: usize,
json: &mut JsonCrawlerBorrowed,
) -> Result<Option<PlaylistItem>> {
let Ok(mut data) = json.borrow_pointer(MRLIR) else {
return Ok(None);
};
let title = super::parse_flex_column_item(&mut data, 0, 0)?;
if title == "Song deleted" {
return Ok(None);
}
let video_type_path = concatcp!(
PLAY_BUTTON,
"/playNavigationEndpoint",
NAVIGATION_VIDEO_TYPE
);
let video_type: YoutubeMusicVideoType = data.take_value_pointer(video_type_path)?;
let item = match video_type {
YoutubeMusicVideoType::Ugc | YoutubeMusicVideoType::Omv => Some(PlaylistItem::Video(
parse_playlist_video(title, track_no, data)?,
)),
YoutubeMusicVideoType::Atv => Some(PlaylistItem::Song(parse_playlist_song(
title, track_no, data,
)?)),
YoutubeMusicVideoType::Upload => Some(PlaylistItem::UploadSong(
parse_playlist_upload_song(title, track_no, data)?,
)),
YoutubeMusicVideoType::Episode => Some(PlaylistItem::Episode(parse_playlist_episode(
title, track_no, data,
)?)),
};
Ok(item)
}
pub(crate) fn parse_playlist_items(json: JsonCrawlerBorrowed) -> Result<Vec<PlaylistItem>> {
json.try_into_iter()
.into_iter()
.flatten()
.enumerate()
.filter_map(|(idx, mut item)| parse_playlist_item(idx + 1, &mut item).transpose())
.collect()
}
impl<'a> ParseFrom<GetArtistAlbumsQuery<'a>> for Vec<GetArtistAlbumsAlbum> {
fn parse_from(p: ProcessedResult<GetArtistAlbumsQuery<'a>>) -> crate::Result<Self> {
let json_crawler: JsonCrawlerOwned = p.into();
let mut albums = Vec::new();
let mut json_crawler = json_crawler.navigate_pointer(concatcp!(
SINGLE_COLUMN_TAB,
SECTION_LIST_ITEM,
GRID_ITEMS
))?;
for mut r in json_crawler
.borrow_mut()
.try_into_iter()?
.flat_map(|i| i.navigate_pointer(MTRIR))
{
let browse_id = r.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
let playlist_id = r.take_value_pointer(MENU_PLAYLIST_ID).ok();
let title = r.take_value_pointer(TITLE_TEXT)?;
let thumbnails = r.take_value_pointer(THUMBNAIL_RENDERER)?;
let category = r.take_value_pointer(SUBTITLE).ok();
albums.push(GetArtistAlbumsAlbum {
browse_id,
year: None,
title,
category,
thumbnails,
playlist_id,
});
}
Ok(albums)
}
}
#[cfg(test)]
mod tests {
use crate::common::ArtistChannelID;
use crate::{
auth::BrowserToken,
common::{BrowseParams, YoutubeID},
query::GetArtistAlbumsQuery,
};
#[tokio::test]
async fn test_get_artist_albums_query() {
parse_test!(
"./test_json/browse_artist_albums.json",
"./test_json/browse_artist_albums_output.txt",
GetArtistAlbumsQuery::new(ArtistChannelID::from_raw(""), BrowseParams::from_raw("")),
BrowserToken
);
}
#[tokio::test]
async fn test_get_artist() {
parse_test!(
"./test_json/get_artist_20240705.json",
"./test_json/get_artist_20240705_output.txt",
crate::query::GetArtistQuery::new(ArtistChannelID::from_raw("")),
BrowserToken
);
}
}