ytmapi_rs/parse/
artist.rs

1use super::{
2    parse_flex_column_item, parse_song_album, parse_song_artists, parse_upload_song_album,
3    parse_upload_song_artists, search::SearchResultVideo, EpisodeDate, EpisodeDuration, ParseFrom,
4    ParsedSongAlbum, ParsedSongArtist, ParsedUploadArtist, ParsedUploadSongAlbum, ProcessedResult,
5    Thumbnail,
6};
7use crate::{
8    common::{
9        AlbumID, AlbumType, ArtistChannelID, BrowseParams, EpisodeID, Explicit, LibraryManager,
10        LibraryStatus, LikeStatus, PlaylistID, UploadEntityID, VideoID,
11    },
12    nav_consts::*,
13    process::{fixed_column_item_pointer, flex_column_item_pointer},
14    query::*,
15    youtube_enums::YoutubeMusicVideoType,
16    Result,
17};
18use const_format::concatcp;
19use json_crawler::{JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerIterator, JsonCrawlerOwned};
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[non_exhaustive]
24pub struct ArtistParams {
25    pub description: String,
26    pub views: String,
27    pub name: String,
28    pub channel_id: String,
29    pub shuffle_id: Option<String>,
30    pub radio_id: Option<String>,
31    pub subscribers: Option<String>,
32    pub subscribed: Option<String>,
33    pub thumbnails: Option<String>,
34    pub top_releases: GetArtistTopReleases,
35}
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[non_exhaustive]
39pub struct GetArtistAlbumsAlbum {
40    pub title: String,
41    // TODO: Use type system
42    pub playlist_id: Option<String>,
43    // TODO: Use type system
44    pub browse_id: AlbumID<'static>,
45    pub category: Option<String>, // TODO change to enum
46    pub thumbnails: Vec<Thumbnail>,
47    pub year: Option<String>,
48}
49
50fn parse_artist_song(json: &mut JsonCrawlerBorrowed) -> Result<ArtistSong> {
51    let mut data = json.borrow_pointer(MRLIR)?;
52    let title = parse_flex_column_item(&mut data, 0, 0)?;
53    let plays = parse_flex_column_item(&mut data, 2, 0)?;
54    let artists = parse_song_artists(&mut data, 1)?;
55    let album = parse_song_album(&mut data, 3)?;
56    let video_id = data.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
57    let explicit = if data.path_exists(BADGE_LABEL) {
58        Explicit::IsExplicit
59    } else {
60        Explicit::NotExplicit
61    };
62    let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
63    let library_management =
64        parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
65    Ok(ArtistSong {
66        video_id,
67        plays,
68        album,
69        artists,
70        library_management,
71        title,
72        like_status,
73        explicit,
74    })
75}
76fn parse_artist_songs(json: &mut JsonCrawlerBorrowed) -> Result<GetArtistSongs> {
77    // Unsure if this should be optional or not.
78    let browse_id = json.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
79    let results = json
80        .borrow_pointer("/contents")?
81        .try_into_iter()?
82        .map(|mut item| parse_artist_song(&mut item))
83        .collect::<Result<Vec<ArtistSong>>>()?;
84    Ok(GetArtistSongs { results, browse_id })
85}
86
87impl<'a> ParseFrom<GetArtistQuery<'a>> for ArtistParams {
88    // While this function gets improved, we'll allow this lint for the creation of
89    // GetArtistTopReleases.
90    #[allow(clippy::field_reassign_with_default)]
91    fn parse_from(p: ProcessedResult<GetArtistQuery<'a>>) -> crate::Result<Self> {
92        // TODO: Make this optional.
93        let mut json_crawler: JsonCrawlerOwned = p.into();
94        let mut results =
95            json_crawler.borrow_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST))?;
96        //        artist = {'description': None, 'views': None}
97        let mut description = String::default();
98        let mut views = String::default();
99        //descriptionShelf = find_object_by_key(results, DESCRIPTION_SHELF[0],
100        // is_key=True) XXX Functional way to take description:
101        // let x: String = results
102        //     .as_array_iter_mut()
103        //     .map(|mut r| {
104        //         r.find_map(|a| a.navigate_pointer(DESCRIPTION_SHELF).ok())
105        //             .and_then(|mut d| d.take_value_pointer(DESCRIPTION).ok())
106        //     })
107        //     .unwrap_or(Some(String::new()))
108        //     .unwrap_or(String::new());
109        if let Ok(results_array) = results.try_iter_mut() {
110            for r in results_array {
111                if let Ok(mut description_shelf) = r.navigate_pointer(DESCRIPTION_SHELF) {
112                    description = description_shelf.take_value_pointer(DESCRIPTION)?;
113                    if let Ok(mut subheader) = description_shelf.borrow_pointer("/subheader") {
114                        views = subheader.take_value_pointer("/runs/0/text")?;
115                    }
116                    break;
117                }
118            }
119        }
120        let mut top_releases = GetArtistTopReleases::default();
121        top_releases.songs = results
122            .borrow_pointer(concatcp!("/0", MUSIC_SHELF))
123            .ok()
124            .map(|mut j| parse_artist_songs(&mut j))
125            .transpose()?;
126        // TODO: Check if Carousel Title is in list of categories.
127        // TODO: Actually pass these variables in the return
128        // XXX: Looks to be two loops over results here.
129        // XXX: if there are multiple results for each category we only want to look at
130        // the first one.
131        for mut r in results
132            .try_iter_mut()
133            .into_iter()
134            .flatten()
135            .filter_map(|r| r.navigate_pointer("/musicCarouselShelfRenderer").ok())
136        {
137            // XXX: Should this only be on the first result per category?
138            let category = r.take_value_pointer(concatcp!(CAROUSEL_TITLE, "/text"))?;
139            // Likely optional, need to confirm.
140            // XXX: Errors here
141            let browse_id: Option<ArtistChannelID> = r
142                .take_value_pointer(concatcp!(CAROUSEL_TITLE, NAVIGATION_BROWSE_ID))
143                .ok();
144            // XXX should only be mandatory for albums, singles, playlists
145            // as a result leaving as optional for now.
146            let params = r
147                .take_value_pointer(concatcp!(
148                    CAROUSEL_TITLE,
149                    "/navigationEndpoint/browseEndpoint/params"
150                ))
151                .ok();
152            // TODO: finish other categories
153            match category {
154                ArtistTopReleaseCategory::Related => (),
155                ArtistTopReleaseCategory::Videos => (),
156                ArtistTopReleaseCategory::Singles => (),
157                ArtistTopReleaseCategory::Albums => {
158                    let mut results = Vec::new();
159                    for i in r.navigate_pointer("/contents")?.try_iter_mut()? {
160                        results.push(parse_album_from_mtrir(i.navigate_pointer(MTRIR)?)?);
161                    }
162                    let albums = GetArtistAlbums {
163                        browse_id,
164                        params,
165                        results,
166                    };
167                    top_releases.albums = Some(albums);
168                }
169                ArtistTopReleaseCategory::Playlists => (),
170                ArtistTopReleaseCategory::None => (),
171            }
172        }
173        // Assume header exists, assumption may be incorrect.
174        // I think Json is owned by someone else here?
175        // I think I can do another self.get_navigable()
176        let mut header = json_crawler.navigate_pointer("/header/musicImmersiveHeaderRenderer")?;
177        let name = header.take_value_pointer(TITLE_TEXT)?;
178        let shuffle_id = header
179            .take_value_pointer(concatcp!(
180                "/playButton/buttonRenderer",
181                NAVIGATION_WATCH_PLAYLIST_ID
182            ))
183            .ok();
184        let radio_id = header
185            .take_value_pointer(concatcp!(
186                "/startRadioButton/buttonRenderer",
187                NAVIGATION_WATCH_PLAYLIST_ID
188            ))
189            .ok();
190        // TODO: Validate if this could instead be returned as a Thumbnails struct.
191        let thumbnails = header.take_value_pointer(THUMBNAILS).ok();
192        // Assume subscription button exists, assumption may not be correct.
193        let mut subscription_button =
194            header.navigate_pointer("/subscriptionButton/subscribeButtonRenderer")?;
195        let channel_id = subscription_button.take_value_pointer("/channelId")?;
196        let subscribers = subscription_button
197            .take_value_pointer("/subscriberCountText/runs/0/text")
198            .ok();
199        // XXX: Unsure if this is optional. It errors currently, removed the ?.
200        let subscribed = subscription_button.take_value_pointer("/subscribed").ok();
201        //                artist[category]['results'] =
202        // parse_content_list(data[0]['contents'],
203        // categories_parser[i])
204        Ok(ArtistParams {
205            views,
206            description,
207            name,
208            top_releases,
209            thumbnails,
210            subscribed,
211            radio_id,
212            channel_id,
213            shuffle_id,
214            subscribers,
215        })
216    }
217}
218#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
219#[non_exhaustive]
220pub struct GetArtistTopReleases {
221    pub songs: Option<GetArtistSongs>,
222    pub albums: Option<GetArtistAlbums>,
223    pub singles: Option<GetArtistAlbums>,
224    pub videos: Option<GetArtistVideos>,
225    pub related: Option<GetArtistRelated>,
226}
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
228#[non_exhaustive]
229pub struct GetArtistRelated {
230    pub results: Vec<RelatedResult>,
231}
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
233#[non_exhaustive]
234pub struct GetArtistSongs {
235    pub results: Vec<ArtistSong>,
236    pub browse_id: PlaylistID<'static>,
237}
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
239#[non_exhaustive]
240pub struct ArtistSong {
241    pub video_id: VideoID<'static>,
242    pub plays: String,
243    pub album: ParsedSongAlbum,
244    pub artists: Vec<ParsedSongArtist>,
245    /// Library management fields are optional; if a album has already been
246    /// added to your library, you cannot add the individual songs.
247    // https://github.com/nick42d/youtui/issues/138
248    pub library_management: Option<LibraryManager>,
249    pub title: String,
250    pub like_status: LikeStatus,
251    pub explicit: Explicit,
252}
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
254#[non_exhaustive]
255pub struct GetArtistVideos {
256    pub results: Vec<SearchResultVideo>,
257    pub browse_id: PlaylistID<'static>,
258}
259/// The Albums section of the Browse Artist page.
260/// The browse_id and params can be used to get the full list of artist's
261/// albums. If they aren't set, and results is not empty, you can assume that
262/// all albums are displayed here already.
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
264#[non_exhaustive]
265pub struct GetArtistAlbums {
266    pub results: Vec<AlbumResult>,
267    // XXX: Unsure if AlbumID is correct here.
268    pub browse_id: Option<ArtistChannelID<'static>>,
269    pub params: Option<BrowseParams<'static>>,
270}
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
272#[non_exhaustive]
273pub struct RelatedResult {
274    pub browse_id: ArtistChannelID<'static>,
275    pub title: String,
276    pub subscribers: String,
277}
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
279#[non_exhaustive]
280pub struct AlbumResult {
281    pub title: String,
282    #[deprecated = "Future deprecation see https://github.com/nick42d/youtui/issues/211"]
283    pub album_type: Option<AlbumType>,
284    pub year: String,
285    pub album_id: AlbumID<'static>,
286    pub library_status: LibraryStatus,
287    pub thumbnails: Vec<Thumbnail>,
288    pub explicit: Explicit,
289}
290
291#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
292#[non_exhaustive]
293// Could this alternatively be Result<Song>?
294// May need to be enum to track 'Not Available' case.
295pub struct PlaylistSong {
296    pub video_id: VideoID<'static>,
297    pub track_no: usize,
298    pub album: ParsedSongAlbum,
299    pub duration: String,
300    /// Some songs may not have library management features. There could be
301    /// various resons for this.
302    pub library_management: Option<LibraryManager>,
303    pub title: String,
304    pub artists: Vec<super::ParsedSongArtist>,
305    // TODO: Song like feedback tokens.
306    pub like_status: LikeStatus,
307    pub thumbnails: Vec<Thumbnail>,
308    pub explicit: Explicit,
309    pub is_available: bool,
310    /// Id of the playlist that will get created when pressing 'Start Radio'.
311    pub playlist_id: PlaylistID<'static>,
312}
313
314#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
315#[non_exhaustive]
316pub struct PlaylistVideo {
317    pub video_id: VideoID<'static>,
318    pub track_no: usize,
319    pub duration: String,
320    pub title: String,
321    // Could be 'ParsedVideoChannel'
322    pub channel_name: String,
323    pub channel_id: ArtistChannelID<'static>,
324    // TODO: Song like feedback tokens.
325    pub like_status: LikeStatus,
326    pub thumbnails: Vec<Thumbnail>,
327    pub is_available: bool,
328    /// Id of the playlist that will get created when pressing 'Start Radio'.
329    pub playlist_id: PlaylistID<'static>,
330}
331
332#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
333#[non_exhaustive]
334pub struct PlaylistEpisode {
335    pub episode_id: EpisodeID<'static>,
336    pub track_no: usize,
337    pub date: EpisodeDate,
338    pub duration: EpisodeDuration,
339    pub title: String,
340    pub podcast_name: String,
341    pub podcast_id: PlaylistID<'static>,
342    // TODO: Song like feedback tokens.
343    pub like_status: LikeStatus,
344    pub thumbnails: Vec<Thumbnail>,
345    pub is_available: bool,
346}
347
348#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
349#[non_exhaustive]
350pub struct PlaylistUploadSong {
351    pub entity_id: UploadEntityID<'static>,
352    pub video_id: VideoID<'static>,
353    pub track_no: usize,
354    pub duration: String,
355    pub album: ParsedUploadSongAlbum,
356    pub title: String,
357    pub artists: Vec<ParsedUploadArtist>,
358    // TODO: Song like feedback tokens.
359    pub like_status: LikeStatus,
360    pub thumbnails: Vec<Thumbnail>,
361}
362
363#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
364#[non_exhaustive]
365// Could this alternatively be Result<Song>?
366// May need to be enum to track 'Not Available' case.
367// NOTE: Difference between this and PlaylistSong is no trackId.
368pub struct TableListSong {
369    pub video_id: VideoID<'static>,
370    pub album: ParsedSongAlbum,
371    pub duration: String,
372    /// Some songs may not have library management features. There could be
373    /// various resons for this.
374    pub library_management: Option<LibraryManager>,
375    pub title: String,
376    pub artists: Vec<super::ParsedSongArtist>,
377    // TODO: Song like feedback tokens.
378    pub like_status: LikeStatus,
379    pub thumbnails: Vec<Thumbnail>,
380    pub explicit: Explicit,
381    pub is_available: bool,
382    /// Id of the playlist that will get created when pressing 'Start Radio'.
383    pub playlist_id: PlaylistID<'static>,
384}
385
386#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
387pub enum PlaylistItem {
388    Song(PlaylistSong),
389    Video(PlaylistVideo),
390    Episode(PlaylistEpisode),
391    UploadSong(PlaylistUploadSong),
392}
393
394// Should be at higher level in mod structure.
395#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
396enum ArtistTopReleaseCategory {
397    #[serde(alias = "albums")]
398    Albums,
399    #[serde(alias = "singles")]
400    Singles,
401    #[serde(alias = "videos")]
402    Videos,
403    #[serde(alias = "playlists")]
404    Playlists,
405    #[serde(alias = "fans might also like")]
406    Related,
407    #[serde(other)]
408    None,
409}
410
411/// Google A/B change pending
412pub(crate) fn parse_album_from_mtrir(mut navigator: JsonCrawlerBorrowed) -> Result<AlbumResult> {
413    let title = navigator.take_value_pointer(TITLE_TEXT)?;
414
415    let (year, album_type) = match navigator.borrow_pointer(SUBTITLE2) {
416        Ok(mut subtitle2) => {
417            // See https://github.com/nick42d/youtui/issues/211
418            ab_warn!();
419            (
420                subtitle2.take_value()?,
421                navigator.take_value_pointer(SUBTITLE)?,
422            )
423        }
424        Err(_) => (navigator.take_value_pointer(SUBTITLE)?, None),
425    };
426
427    let album_id = navigator.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
428    let thumbnails = navigator.take_value_pointer(THUMBNAIL_RENDERER)?;
429    let explicit = if navigator.path_exists(concatcp!(SUBTITLE_BADGE_LABEL)) {
430        Explicit::IsExplicit
431    } else {
432        Explicit::NotExplicit
433    };
434    let mut library_menu = navigator
435        .borrow_pointer(MENU_ITEMS)?
436        .try_into_iter()?
437        .find_path("/toggleMenuServiceItemRenderer")?;
438    let library_status = library_menu.take_value_pointer("/defaultIcon/iconType")?;
439    Ok(AlbumResult {
440        title,
441        album_type,
442        year,
443        album_id,
444        library_status,
445        thumbnails,
446        explicit,
447    })
448}
449
450pub(crate) fn parse_library_management_items_from_menu(
451    menu: JsonCrawlerBorrowed,
452) -> Result<Option<LibraryManager>> {
453    let Some((status, add_to_library_token, remove_from_library_token)) = menu
454        .try_into_iter()?
455        .filter_map(|menu_item| {
456            menu_item
457                .navigate_pointer("/toggleMenuServiceItemRenderer")
458                .ok()
459        })
460        .filter_map(|mut toggle_menu| {
461            let Ok(status) = toggle_menu.take_value_pointer("/defaultIcon/iconType") else {
462                // In this case the toggle_menu is not the right type, e.g might be Pin to
463                // Listen Again.
464                //
465                // e.g: https://github.com/nick42d/youtui/issues/193
466                return None;
467            };
468            let (add_to_library_token, remove_from_library_token) = match status {
469                LibraryStatus::InLibrary => (
470                    toggle_menu.take_value_pointer(TOGGLED_ENDPOINT),
471                    toggle_menu.take_value_pointer(DEFAULT_ENDPOINT),
472                ),
473                LibraryStatus::NotInLibrary => (
474                    toggle_menu.take_value_pointer(DEFAULT_ENDPOINT),
475                    toggle_menu.take_value_pointer(TOGGLED_ENDPOINT),
476                ),
477            };
478            Some((status, add_to_library_token, remove_from_library_token))
479        })
480        .next()
481    else {
482        // In this case there is no toggle_menu, so returning None is not an error.
483        return Ok(None);
484    };
485    Ok(Some(LibraryManager {
486        status,
487        add_to_library_token: add_to_library_token?,
488        remove_from_library_token: remove_from_library_token?,
489    }))
490}
491
492pub(crate) fn parse_playlist_song(
493    title: String,
494    track_no: usize,
495    mut data: JsonCrawlerBorrowed,
496) -> Result<PlaylistSong> {
497    let video_id = data.take_value_pointer(concatcp!(
498        PLAY_BUTTON,
499        "/playNavigationEndpoint",
500        WATCH_VIDEO_ID
501    ))?;
502    let library_management =
503        parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
504    let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
505    let artists = super::parse_song_artists(&mut data, 1)?;
506    // Some playlist types (Potentially just Featured Playlists) have a 'Plays'
507    // field between Artist and Album.
508    // TODO: Find a more efficient way, and potentially parse Featured Playlists
509    // differently.
510    let album_col_idx = if data.path_exists("/flexColumns/3") {
511        3
512    } else {
513        2
514    };
515    let album = super::parse_song_album(&mut data, album_col_idx)?;
516    let duration = data
517        .borrow_pointer(fixed_column_item_pointer(0))?
518        .take_value_pointers(&["/text/simpleText", "/text/runs/0/text"])?;
519    let thumbnails = data.take_value_pointer(THUMBNAILS)?;
520    let is_available = data
521        .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
522        .map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
523        .unwrap_or(true);
524
525    let explicit = if data.path_exists(BADGE_LABEL) {
526        Explicit::IsExplicit
527    } else {
528        Explicit::NotExplicit
529    };
530    let playlist_id = data.take_value_pointer(concatcp!(
531        MENU_ITEMS,
532        "/0/menuNavigationItemRenderer",
533        NAVIGATION_PLAYLIST_ID
534    ))?;
535    Ok(PlaylistSong {
536        video_id,
537        track_no,
538        duration,
539        library_management,
540        title,
541        artists,
542        like_status,
543        thumbnails,
544        explicit,
545        album,
546        playlist_id,
547        is_available,
548    })
549}
550pub(crate) fn parse_playlist_upload_song(
551    title: String,
552    track_no: usize,
553    mut data: JsonCrawlerBorrowed,
554) -> Result<PlaylistUploadSong> {
555    let duration = data
556        .borrow_pointer(fixed_column_item_pointer(0))?
557        .take_value_pointer(TEXT_RUN_TEXT)?;
558    let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
559    let video_id = data.take_value_pointer(concatcp!(
560        PLAY_BUTTON,
561        "/playNavigationEndpoint/watchEndpoint/videoId"
562    ))?;
563    let thumbnails = data.take_value_pointer(THUMBNAILS)?;
564    let artists = parse_upload_song_artists(data.borrow_mut(), 1)?;
565    let album = parse_upload_song_album(data.borrow_mut(), 2)?;
566    let mut menu = data.navigate_pointer(MENU_ITEMS)?;
567    let entity_id = menu
568        .try_iter_mut()?
569        .find_path(DELETION_ENTITY_ID)?
570        .take_value()?;
571    Ok(PlaylistUploadSong {
572        entity_id,
573        video_id,
574        album,
575        duration,
576        like_status,
577        title,
578        artists,
579        thumbnails,
580        track_no,
581    })
582}
583pub(crate) fn parse_playlist_episode(
584    title: String,
585    track_no: usize,
586    mut data: JsonCrawlerBorrowed,
587) -> Result<PlaylistEpisode> {
588    let video_id = data.take_value_pointer(concatcp!(
589        PLAY_BUTTON,
590        "/playNavigationEndpoint",
591        WATCH_VIDEO_ID
592    ))?;
593    let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
594    let is_live = data.path_exists(LIVE_BADGE_LABEL);
595    let (duration, date) = match is_live {
596        true => (EpisodeDuration::Live, EpisodeDate::Live),
597        false => {
598            let date = parse_flex_column_item(&mut data, 2, 0)?;
599            let duration =
600                data.borrow_pointer(fixed_column_item_pointer(0))
601                    .and_then(|mut i| {
602                        i.take_value_pointer("/text/simpleText")
603                            .or_else(|_| i.take_value_pointer("/text/runs/0/text"))
604                    })?;
605            (
606                EpisodeDuration::Recorded { duration },
607                EpisodeDate::Recorded { date },
608            )
609        }
610    };
611    let podcast_name = parse_flex_column_item(&mut data, 1, 0)?;
612    let podcast_id = data
613        .borrow_pointer(flex_column_item_pointer(1))?
614        .take_value_pointer(concatcp!(TEXT_RUN, NAVIGATION_BROWSE_ID))?;
615    let thumbnails = data.take_value_pointer(THUMBNAILS)?;
616    let is_available = data
617        .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
618        .map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
619        .unwrap_or(true);
620    Ok(PlaylistEpisode {
621        episode_id: video_id,
622        duration,
623        title,
624        like_status,
625        thumbnails,
626        date,
627        podcast_name,
628        podcast_id,
629        is_available,
630        track_no,
631    })
632}
633pub(crate) fn parse_playlist_video(
634    title: String,
635    track_no: usize,
636    mut data: JsonCrawlerBorrowed,
637) -> Result<PlaylistVideo> {
638    let video_id = data.take_value_pointer(concatcp!(
639        PLAY_BUTTON,
640        "/playNavigationEndpoint",
641        WATCH_VIDEO_ID
642    ))?;
643    let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
644    let channel_name = parse_flex_column_item(&mut data, 1, 0)?;
645    let channel_id = data
646        .borrow_pointer(flex_column_item_pointer(1))?
647        .take_value_pointer(concatcp!(TEXT_RUN, NAVIGATION_BROWSE_ID))?;
648    let duration = data
649        .borrow_pointer(fixed_column_item_pointer(0))?
650        .take_value_pointers(&["/text/simpleText", "/text/runs/0/text"])?;
651    let thumbnails = data.take_value_pointer(THUMBNAILS)?;
652    let is_available = data
653        .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
654        .map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
655        .unwrap_or(true);
656
657    let playlist_id = data.take_value_pointer(concatcp!(
658        MENU_ITEMS,
659        "/0/menuNavigationItemRenderer",
660        NAVIGATION_PLAYLIST_ID
661    ))?;
662    Ok(PlaylistVideo {
663        video_id,
664        track_no,
665        duration,
666        title,
667        like_status,
668        thumbnails,
669        playlist_id,
670        is_available,
671        channel_name,
672        channel_id,
673    })
674}
675
676pub(crate) fn parse_playlist_item(
677    track_no: usize,
678    json: &mut JsonCrawlerBorrowed,
679) -> Result<Option<PlaylistItem>> {
680    let Ok(mut data) = json.borrow_pointer(MRLIR) else {
681        return Ok(None);
682    };
683    let title = super::parse_flex_column_item(&mut data, 0, 0)?;
684    if title == "Song deleted" {
685        return Ok(None);
686    }
687    let video_type_path = concatcp!(
688        PLAY_BUTTON,
689        "/playNavigationEndpoint",
690        NAVIGATION_VIDEO_TYPE
691    );
692    let video_type: YoutubeMusicVideoType = data.take_value_pointer(video_type_path)?;
693    // TODO: Deserialize to enum
694    let item = match video_type {
695        YoutubeMusicVideoType::Ugc | YoutubeMusicVideoType::Omv => Some(PlaylistItem::Video(
696            parse_playlist_video(title, track_no, data)?,
697        )),
698        YoutubeMusicVideoType::Atv => Some(PlaylistItem::Song(parse_playlist_song(
699            title, track_no, data,
700        )?)),
701        YoutubeMusicVideoType::Upload => Some(PlaylistItem::UploadSong(
702            parse_playlist_upload_song(title, track_no, data)?,
703        )),
704        YoutubeMusicVideoType::Episode => Some(PlaylistItem::Episode(parse_playlist_episode(
705            title, track_no, data,
706        )?)),
707    };
708    Ok(item)
709}
710//TODO: Menu entries
711//TODO: Consider rename
712pub(crate) fn parse_playlist_items(json: JsonCrawlerBorrowed) -> Result<Vec<PlaylistItem>> {
713    json.try_into_iter()
714        .into_iter()
715        .flatten()
716        .enumerate()
717        .filter_map(|(idx, mut item)| parse_playlist_item(idx + 1, &mut item).transpose())
718        .collect()
719}
720impl<'a> ParseFrom<GetArtistAlbumsQuery<'a>> for Vec<GetArtistAlbumsAlbum> {
721    fn parse_from(p: ProcessedResult<GetArtistAlbumsQuery<'a>>) -> crate::Result<Self> {
722        let json_crawler: JsonCrawlerOwned = p.into();
723        let mut albums = Vec::new();
724        let mut json_crawler = json_crawler.navigate_pointer(concatcp!(
725            SINGLE_COLUMN_TAB,
726            SECTION_LIST_ITEM,
727            GRID_ITEMS
728        ))?;
729        for mut r in json_crawler
730            .borrow_mut()
731            .try_into_iter()?
732            .flat_map(|i| i.navigate_pointer(MTRIR))
733        {
734            let browse_id = r.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
735            let playlist_id = r.take_value_pointer(MENU_PLAYLIST_ID).ok();
736            let title = r.take_value_pointer(TITLE_TEXT)?;
737            let thumbnails = r.take_value_pointer(THUMBNAIL_RENDERER)?;
738            // TODO: category
739            let category = r.take_value_pointer(SUBTITLE).ok();
740            albums.push(GetArtistAlbumsAlbum {
741                browse_id,
742                year: None,
743                title,
744                category,
745                thumbnails,
746                playlist_id,
747            });
748        }
749        Ok(albums)
750    }
751}
752#[cfg(test)]
753mod tests {
754    use crate::common::ArtistChannelID;
755    use crate::{
756        auth::BrowserToken,
757        common::{BrowseParams, YoutubeID},
758        query::GetArtistAlbumsQuery,
759    };
760
761    #[tokio::test]
762    async fn test_get_artist_albums_query() {
763        parse_test!(
764            // Radiohead's albums.
765            "./test_json/browse_artist_albums.json",
766            "./test_json/browse_artist_albums_output.txt",
767            GetArtistAlbumsQuery::new(ArtistChannelID::from_raw(""), BrowseParams::from_raw("")),
768            BrowserToken
769        );
770    }
771
772    // Old as of https://github.com/nick42d/youtui/issues/211
773    #[tokio::test]
774    async fn test_get_artist_old_1() {
775        parse_test!(
776            "./test_json/get_artist_20240705.json",
777            "./test_json/get_artist_20240705_output.txt",
778            crate::query::GetArtistQuery::new(ArtistChannelID::from_raw("")),
779            BrowserToken
780        );
781    }
782
783    #[tokio::test]
784    async fn test_get_artist() {
785        parse_test!(
786            "./test_json/get_artist_20250310.json",
787            "./test_json/get_artist_20250310_output.txt",
788            crate::query::GetArtistQuery::new(ArtistChannelID::from_raw("")),
789            BrowserToken
790        );
791    }
792}