ytmapi_rs/parse/
library.rs

1use super::{
2    BADGE_LABEL, CONTINUATION_PARAMS, GRID_CONTINUATION, MENU_LIKE_STATUS,
3    MUSIC_SHELF_CONTINUATION, ParseFrom, ParsedPodcastChannel, ProcessedResult, SUBTITLE,
4    SUBTITLE_BADGE_LABEL, SUBTITLE2, SUBTITLE3, SearchResultAlbum, THUMBNAILS, TableListSong,
5    fixed_column_item_pointer, parse_flex_column_item, parse_library_management_items_from_menu,
6    parse_podcast_channel,
7};
8use crate::Result;
9use crate::common::{
10    ApiOutcome, ArtistChannelID, ContinuationParams, Explicit, PlaylistID, PodcastChannelID,
11    PodcastID, Thumbnail,
12};
13use crate::continuations::ParseFromContinuable;
14use crate::nav_consts::{
15    GRID, ITEM_SECTION, MENU_ITEMS, MRLIR, MTRIR, MUSIC_SHELF, NAVIGATION_BROWSE_ID,
16    NAVIGATION_PLAYLIST_ID, PLAY_BUTTON, SECTION_LIST, SECTION_LIST_ITEM, SINGLE_COLUMN_TAB,
17    SUBTITLE_BADGE_ICON, THUMBNAIL_RENDERER, TITLE, TITLE_TEXT, WATCH_VIDEO_ID,
18};
19use crate::query::library::{GetLibraryChannelsQuery, GetLibraryPodcastsQuery};
20use crate::query::{
21    EditSongLibraryStatusQuery, GetContinuationsQuery, GetLibraryAlbumsQuery,
22    GetLibraryArtistSubscriptionsQuery, GetLibraryArtistsQuery, GetLibraryPlaylistsQuery,
23    GetLibrarySongsQuery,
24};
25use crate::youtube_enums::YoutubeMusicBadgeRendererIcon;
26use const_format::concatcp;
27use json_crawler::{CrawlerResult, JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerOwned};
28use serde::{Deserialize, Serialize};
29
30#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
31#[non_exhaustive]
32// Very similar to LibraryArtist struct
33pub struct GetLibraryArtistSubscription {
34    pub name: String,
35    pub subscribers: String,
36    pub channel_id: ArtistChannelID<'static>,
37    pub thumbnails: Vec<Thumbnail>,
38}
39
40#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
41#[non_exhaustive]
42// Very similar to LibraryArtist struct
43pub struct LibraryArtistSubscription {
44    pub name: String,
45    pub subscribers: String,
46    pub channel_id: ArtistChannelID<'static>,
47    pub thumbnails: Vec<Thumbnail>,
48}
49
50#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
51#[non_exhaustive]
52pub struct LibraryPlaylist {
53    pub playlist_id: PlaylistID<'static>,
54    pub title: String,
55    pub thumbnails: Vec<Thumbnail>,
56    pub count: Option<usize>,
57    pub description: Option<String>,
58    pub author: Option<String>,
59}
60#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
61#[non_exhaustive]
62pub struct LibraryArtist {
63    pub channel_id: ArtistChannelID<'static>,
64    pub artist: String,
65    pub byline: String, // e.g 16 songs or 17.8k subscribers
66}
67#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
68#[non_exhaustive]
69pub struct LibraryPodcast {
70    pub title: String,
71    pub channels: Vec<ParsedPodcastChannel>,
72    pub podcast_id: PodcastID<'static>,
73    pub thumbnails: Vec<Thumbnail>,
74    pub podcast_source: PodcastSource,
75}
76#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
77#[non_exhaustive]
78pub struct LibraryChannel {
79    pub title: String,
80    pub subscribers: String,
81    pub channel_id: PodcastChannelID<'static>,
82    pub thumbnails: Vec<Thumbnail>,
83}
84
85#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
86pub enum PodcastSource {
87    Rss,
88    YouTube,
89}
90
91impl ParseFromContinuable<GetLibraryArtistSubscriptionsQuery> for Vec<LibraryArtistSubscription> {
92    fn parse_from_continuable(
93        p: ProcessedResult<GetLibraryArtistSubscriptionsQuery>,
94    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
95        let json_crawler: JsonCrawlerOwned = p.into();
96        let music_shelf = json_crawler.navigate_pointer(concatcp!(
97            SINGLE_COLUMN_TAB,
98            SECTION_LIST_ITEM,
99            MUSIC_SHELF,
100        ))?;
101        parse_library_artist_subscriptions(music_shelf)
102    }
103    fn parse_continuation(
104        p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryArtistSubscriptionsQuery>>,
105    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
106        let json_crawler: JsonCrawlerOwned = p.into();
107        let music_shelf = json_crawler.navigate_pointer(MUSIC_SHELF_CONTINUATION)?;
108        parse_library_artist_subscriptions(music_shelf)
109    }
110}
111
112impl ParseFromContinuable<GetLibraryAlbumsQuery> for Vec<SearchResultAlbum> {
113    fn parse_from_continuable(
114        p: ProcessedResult<GetLibraryAlbumsQuery>,
115    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
116        let json_crawler: JsonCrawlerOwned = p.into();
117        let grid_renderer =
118            json_crawler.navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST_ITEM, GRID))?;
119        parse_library_albums(grid_renderer)
120    }
121    fn parse_continuation(
122        p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryAlbumsQuery>>,
123    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
124        let json_crawler: JsonCrawlerOwned = p.into();
125        let grid_items = json_crawler.navigate_pointer(GRID_CONTINUATION)?;
126        parse_library_albums(grid_items)
127    }
128}
129
130impl ParseFromContinuable<GetLibrarySongsQuery> for Vec<TableListSong> {
131    fn parse_from_continuable(
132        p: ProcessedResult<GetLibrarySongsQuery>,
133    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
134        let json_crawler: JsonCrawlerOwned = p.into();
135        let music_shelf = json_crawler.navigate_pointer(concatcp!(
136            SINGLE_COLUMN_TAB,
137            SECTION_LIST_ITEM,
138            MUSIC_SHELF,
139        ))?;
140        parse_library_songs(music_shelf)
141    }
142    fn parse_continuation(
143        p: ProcessedResult<GetContinuationsQuery<'_, GetLibrarySongsQuery>>,
144    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
145        let json_crawler: JsonCrawlerOwned = p.into();
146        let music_shelf = json_crawler.navigate_pointer(MUSIC_SHELF_CONTINUATION)?;
147        parse_library_songs(music_shelf)
148    }
149}
150
151impl ParseFromContinuable<GetLibraryArtistsQuery> for Vec<LibraryArtist> {
152    fn parse_from_continuable(
153        p: ProcessedResult<GetLibraryArtistsQuery>,
154    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
155        let json_crawler = p.into();
156        let maybe_music_shelf = process_library_contents_music_shelf(json_crawler);
157        if let Some(music_shelf) = maybe_music_shelf {
158            parse_content_list_artists(music_shelf)
159        } else {
160            Ok((Vec::new(), None))
161        }
162    }
163    fn parse_continuation(
164        p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryArtistsQuery>>,
165    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
166        let json_crawler = JsonCrawlerOwned::from(p);
167        let music_shelf = json_crawler.navigate_pointer(MUSIC_SHELF_CONTINUATION)?;
168        parse_content_list_artists(music_shelf)
169    }
170}
171
172impl ParseFromContinuable<GetLibraryPlaylistsQuery> for Vec<LibraryPlaylist> {
173    fn parse_from_continuable(
174        p: ProcessedResult<GetLibraryPlaylistsQuery>,
175    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
176        // TODO: Implement count and author fields
177        let json_crawler = p.into();
178        let maybe_grid_renderer = process_library_contents_grid(json_crawler);
179        if let Some(grid_renderer) = maybe_grid_renderer {
180            parse_library_playlists(grid_renderer)
181        } else {
182            Ok((vec![], None))
183        }
184    }
185    fn parse_continuation(
186        p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryPlaylistsQuery>>,
187    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
188        let json_crawler: JsonCrawlerOwned = p.into();
189        let grid_renderer = json_crawler.navigate_pointer(GRID_CONTINUATION)?;
190        parse_library_playlists(grid_renderer)
191    }
192}
193
194impl ParseFromContinuable<GetLibraryPodcastsQuery> for Vec<LibraryPodcast> {
195    fn parse_from_continuable(
196        p: ProcessedResult<GetLibraryPodcastsQuery>,
197    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
198        let json_crawler: JsonCrawlerOwned = p.into();
199        let maybe_grid_renderer = process_library_contents_grid(json_crawler);
200        if let Some(grid_renderer) = maybe_grid_renderer {
201            parse_library_podcasts(grid_renderer)
202        } else {
203            Ok((vec![], None))
204        }
205    }
206    fn parse_continuation(
207        p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryPodcastsQuery>>,
208    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
209        let json_crawler: JsonCrawlerOwned = p.into();
210        let grid_renderer = json_crawler.navigate_pointer(GRID_CONTINUATION)?;
211        parse_library_podcasts(grid_renderer)
212    }
213}
214
215impl ParseFromContinuable<GetLibraryChannelsQuery> for Vec<LibraryChannel> {
216    fn parse_from_continuable(
217        p: ProcessedResult<GetLibraryChannelsQuery>,
218    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
219        let json_crawler = p.into();
220        let maybe_music_shelf = process_library_contents_music_shelf(json_crawler);
221        if let Some(music_shelf) = maybe_music_shelf {
222            parse_content_list_channels(music_shelf)
223        } else {
224            Ok((Vec::new(), None))
225        }
226    }
227    fn parse_continuation(
228        p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryChannelsQuery>>,
229    ) -> crate::Result<(Self, Option<ContinuationParams<'static>>)> {
230        let json_crawler = JsonCrawlerOwned::from(p);
231        let music_shelf = json_crawler.navigate_pointer(MUSIC_SHELF_CONTINUATION)?;
232        parse_content_list_channels(music_shelf)
233    }
234}
235
236impl ParseFrom<EditSongLibraryStatusQuery<'_>> for Vec<ApiOutcome> {
237    fn parse_from(p: super::ProcessedResult<EditSongLibraryStatusQuery>) -> Result<Self> {
238        let json_crawler = JsonCrawlerOwned::from(p);
239        json_crawler
240            .navigate_pointer("/feedbackResponses")?
241            .try_into_iter()?
242            .map(|mut response| {
243                response
244                    .take_value_pointer::<bool>("/isProcessed")
245                    .map(|p| {
246                        if p {
247                            return ApiOutcome::Success;
248                        }
249                        ApiOutcome::Failure
250                    })
251            })
252            .rev()
253            .collect::<CrawlerResult<_>>()
254            .map_err(Into::into)
255    }
256}
257
258fn parse_library_albums(
259    mut grid_renderer: JsonCrawlerOwned,
260) -> Result<(Vec<SearchResultAlbum>, Option<ContinuationParams<'static>>)> {
261    let continuation_params = grid_renderer.take_value_pointer(CONTINUATION_PARAMS).ok();
262    let albums = grid_renderer
263        .navigate_pointer("/items")?
264        .try_into_iter()?
265        .map(parse_item_list_album)
266        .collect::<Result<_>>()?;
267    Ok((albums, continuation_params))
268}
269fn parse_library_songs(
270    mut music_shelf: JsonCrawlerOwned,
271) -> Result<(Vec<TableListSong>, Option<ContinuationParams<'static>>)> {
272    let continuation_params = music_shelf.take_value_pointer(CONTINUATION_PARAMS).ok();
273    let songs = music_shelf
274        .navigate_pointer("/contents")?
275        .try_into_iter()?
276        .map(|mut item| {
277            let Ok(mut data) = item.borrow_pointer(MRLIR) else {
278                return Ok(None);
279            };
280            let title = super::parse_flex_column_item(&mut data, 0, 0)?;
281            if title == "Shuffle all" {
282                return Ok(None);
283            }
284            Ok(Some(parse_table_list_song(title, data)?))
285        })
286        .filter_map(Result::transpose)
287        .collect::<Result<_>>()?;
288    Ok((songs, continuation_params))
289}
290fn parse_library_artist_subscriptions(
291    mut music_shelf: JsonCrawlerOwned,
292) -> Result<(
293    Vec<LibraryArtistSubscription>,
294    Option<ContinuationParams<'static>>,
295)> {
296    let continuation_params = music_shelf.take_value_pointer(CONTINUATION_PARAMS).ok();
297    let subscriptions = music_shelf
298        .navigate_pointer("/contents")?
299        .try_into_iter()?
300        .map(parse_content_list_artist_subscription)
301        .collect::<Result<_>>()?;
302    Ok((subscriptions, continuation_params))
303}
304
305fn parse_library_playlists(
306    mut grid_renderer: JsonCrawlerOwned,
307) -> Result<(Vec<LibraryPlaylist>, Option<ContinuationParams<'static>>)> {
308    let continuation_params = grid_renderer.take_value_pointer(CONTINUATION_PARAMS).ok();
309    let playlists = grid_renderer
310        .navigate_pointer("/items")?
311        .try_into_iter()?
312        // First result is just a link to create a new playlist.
313        .skip(1)
314        .map(parse_content_list_playlist)
315        .collect::<Result<_>>()?;
316    Ok((playlists, continuation_params))
317}
318fn parse_library_podcasts(
319    mut grid_renderer: impl JsonCrawler,
320) -> Result<(Vec<LibraryPodcast>, Option<ContinuationParams<'static>>)> {
321    let continuation_params = grid_renderer.take_value_pointer(CONTINUATION_PARAMS).ok();
322    let res = grid_renderer
323        .navigate_pointer("/items")?
324        .try_into_iter()?
325        // First result is just a link to create a new podcast.
326        .skip(1)
327        .filter_map(|item| parse_content_list_podcast(item).transpose())
328        .collect::<Result<_>>()?;
329    Ok((res, continuation_params))
330}
331
332// Consider returning ProcessedLibraryContents
333// TODO: Move to process
334fn process_library_contents_grid(mut json_crawler: JsonCrawlerOwned) -> Option<JsonCrawlerOwned> {
335    let section = json_crawler.borrow_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST));
336    // Assume empty library in this case.
337    if let Ok(section) = section {
338        if section.path_exists("/itemSectionRenderer") {
339            json_crawler
340                .navigate_pointer(concatcp!(ITEM_SECTION, GRID))
341                .ok()
342        } else {
343            json_crawler
344                .navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST_ITEM, GRID))
345                .ok()
346        }
347    } else {
348        None
349    }
350}
351// Consider returning ProcessedLibraryContents
352// TODO: Move to process
353fn process_library_contents_music_shelf(
354    mut json_crawler: JsonCrawlerOwned,
355) -> Option<JsonCrawlerOwned> {
356    let section = json_crawler.borrow_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST));
357    // Assume empty library in this case.
358    if let Ok(section) = section {
359        if section.path_exists("itemSectionRenderer") {
360            json_crawler
361                .navigate_pointer(concatcp!(ITEM_SECTION, MUSIC_SHELF))
362                .ok()
363        } else {
364            json_crawler
365                .navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST_ITEM, MUSIC_SHELF))
366                .ok()
367        }
368    } else {
369        None
370    }
371}
372
373fn parse_item_list_album(mut json_crawler: JsonCrawlerOwned) -> Result<SearchResultAlbum> {
374    let mut data = json_crawler.borrow_pointer("/musicTwoRowItemRenderer")?;
375    let browse_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
376    let thumbnails = data.take_value_pointer(THUMBNAIL_RENDERER)?;
377    let title = data.take_value_pointer(TITLE_TEXT)?;
378    let artist = data.take_value_pointer(SUBTITLE2)?;
379    let year = data.take_value_pointer(SUBTITLE3)?;
380    let album_type = data.take_value_pointer(SUBTITLE)?;
381    let explicit = if data.path_exists(SUBTITLE_BADGE_LABEL) {
382        Explicit::IsExplicit
383    } else {
384        Explicit::NotExplicit
385    };
386    Ok(SearchResultAlbum {
387        title,
388        artist,
389        year,
390        explicit,
391        album_id: browse_id,
392        album_type,
393        thumbnails,
394    })
395}
396
397fn parse_content_list_artist_subscription(
398    mut json_crawler: JsonCrawlerOwned,
399) -> Result<LibraryArtistSubscription> {
400    let mut data = json_crawler.borrow_pointer(MRLIR)?;
401    let channel_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
402    let name = parse_flex_column_item(&mut data, 0, 0)?;
403    let subscribers = parse_flex_column_item(&mut data, 1, 0)?;
404    let thumbnails = data.take_value_pointer(THUMBNAILS)?;
405    Ok(LibraryArtistSubscription {
406        name,
407        subscribers,
408        channel_id,
409        thumbnails,
410    })
411}
412
413fn parse_content_list_artists(
414    mut json_crawler: JsonCrawlerOwned,
415) -> Result<(Vec<LibraryArtist>, Option<ContinuationParams<'static>>)> {
416    let continuation_params = json_crawler.take_value_pointer(CONTINUATION_PARAMS).ok();
417    let artists = json_crawler
418        .navigate_pointer("/contents")?
419        .try_iter_mut()?
420        .map(|item| {
421            let mut data = item.navigate_pointer(MRLIR)?;
422            let channel_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
423            let artist = parse_flex_column_item(&mut data, 0, 0)?;
424            let byline = parse_flex_column_item(&mut data, 1, 0)?;
425            Ok(LibraryArtist {
426                channel_id,
427                artist,
428                byline,
429            })
430        })
431        .collect::<Result<_>>()?;
432    Ok((artists, continuation_params))
433}
434
435fn parse_content_list_channels(
436    mut json_crawler: JsonCrawlerOwned,
437) -> Result<(Vec<LibraryChannel>, Option<ContinuationParams<'static>>)> {
438    let continuation_params = json_crawler.take_value_pointer(CONTINUATION_PARAMS).ok();
439    let artists = json_crawler
440        .navigate_pointer("/contents")?
441        .try_iter_mut()?
442        .map(|item| {
443            let mut data = item.navigate_pointer(MRLIR)?;
444            let channel_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
445            let title = parse_flex_column_item(&mut data, 0, 0)?;
446            let subscribers = parse_flex_column_item(&mut data, 1, 0)?;
447            let thumbnails = data.take_value_pointer(THUMBNAILS)?;
448            Ok(LibraryChannel {
449                title,
450                subscribers,
451                channel_id,
452                thumbnails,
453            })
454        })
455        .collect::<Result<_>>()?;
456    Ok((artists, continuation_params))
457}
458
459fn parse_table_list_song(title: String, mut data: JsonCrawlerBorrowed) -> Result<TableListSong> {
460    let video_id = data.take_value_pointer(concatcp!(
461        PLAY_BUTTON,
462        "/playNavigationEndpoint",
463        WATCH_VIDEO_ID
464    ))?;
465    let library_management =
466        parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
467    let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
468    let artists = super::parse_song_artists(&mut data, 1)?;
469    let album = super::parse_song_album(&mut data, 2)?;
470    let duration = data
471        .borrow_pointer(fixed_column_item_pointer(0))?
472        .take_value_pointers(&["/text/simpleText", "/text/runs/0/text"])?;
473    let thumbnails = data.take_value_pointer(THUMBNAILS)?;
474    let is_available = data
475        .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
476        .map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
477        .unwrap_or(true);
478
479    let explicit = if data.path_exists(BADGE_LABEL) {
480        Explicit::IsExplicit
481    } else {
482        Explicit::NotExplicit
483    };
484    let playlist_id = data.take_value_pointer(concatcp!(
485        MENU_ITEMS,
486        "/0/menuNavigationItemRenderer",
487        NAVIGATION_PLAYLIST_ID
488    ))?;
489    Ok(TableListSong {
490        video_id,
491        duration,
492        library_management,
493        title,
494        artists,
495        like_status,
496        thumbnails,
497        explicit,
498        album,
499        playlist_id,
500        is_available,
501    })
502}
503
504fn parse_content_list_playlist(item: JsonCrawlerOwned) -> Result<LibraryPlaylist> {
505    // TODO: Implement count and author fields
506    let mut mtrir = item.navigate_pointer(MTRIR)?;
507    let title = mtrir.take_value_pointer(TITLE_TEXT)?;
508    let playlist_id: PlaylistID = mtrir
509        .borrow_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?
510        // ytmusicapi uses range index [2:] here but doesn't seem to be required.
511        // Revisit later if we crash.
512        .take_value()?;
513    let thumbnails: Vec<Thumbnail> = mtrir.take_value_pointer(THUMBNAIL_RENDERER)?;
514    let mut description = None;
515    let count = None;
516    let author = None;
517    if let Ok(mut subtitle) = mtrir.borrow_pointer("/subtitle") {
518        let runs = subtitle.borrow_pointer("/runs")?.try_into_iter()?;
519        // Extract description from runs.
520        // Collect the iterator of Result<String> into a single Result<String>
521        description = Some(
522            runs.map(|mut c| c.take_value_pointer::<String>("/text"))
523                .collect::<std::result::Result<String, _>>()?,
524        );
525    }
526    Ok(LibraryPlaylist {
527        description,
528        author,
529        playlist_id,
530        title,
531        thumbnails,
532        count,
533    })
534}
535
536fn parse_content_list_podcast(item: impl JsonCrawler) -> Result<Option<LibraryPodcast>> {
537    let mut mtrir = item.navigate_pointer(MTRIR)?;
538    let title = mtrir.take_value_pointer(TITLE_TEXT)?;
539    // There are some potential non-podcast special playlist results. This is one
540    // way to filter them out.
541    // TODO: i18n or more robust method of filtering.
542    if title == "New Episodes" || title == "Episodes for Later" {
543        return Ok(None);
544    }
545    let podcast_id: PodcastID = mtrir
546        .borrow_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?
547        // ytmusicapi uses range index [2:] here but doesn't seem to be required.
548        // Revisit later if we crash.
549        .take_value()?;
550    let thumbnails: Vec<Thumbnail> = mtrir.take_value_pointer(THUMBNAIL_RENDERER)?;
551    let maybe_badge_icon = mtrir
552        .take_value_pointer::<YoutubeMusicBadgeRendererIcon>(SUBTITLE_BADGE_ICON)
553        .ok();
554    let podcast_source = match maybe_badge_icon {
555        Some(YoutubeMusicBadgeRendererIcon::Rss) => PodcastSource::Rss,
556        _ => PodcastSource::YouTube,
557    };
558    let channels = mtrir
559        .navigate_pointer("/subtitle/runs")?
560        .try_into_iter()?
561        .map(parse_podcast_channel)
562        .collect::<Result<Vec<_>>>()?;
563    Ok(Some(LibraryPodcast {
564        title,
565        thumbnails,
566        channels,
567        podcast_id,
568        podcast_source,
569    }))
570}
571
572#[cfg(test)]
573mod tests {
574    use crate::auth::BrowserToken;
575
576    #[tokio::test]
577    async fn test_library_playlists_dummy_json() {
578        parse_with_matching_continuation_test!(
579            "./test_json/get_library_playlists.json",
580            "./test_json/get_library_playlists_continuation_mock.json",
581            "./test_json/get_library_playlists_output.txt",
582            crate::query::GetLibraryPlaylistsQuery,
583            BrowserToken
584        );
585    }
586    #[tokio::test]
587    async fn test_get_library_artists_dummy_json() {
588        parse_with_matching_continuation_test!(
589            "./test_json/get_library_artists.json",
590            "./test_json/get_library_artists_continuation_mock.json",
591            "./test_json/get_library_artists_output.txt",
592            crate::query::GetLibraryArtistsQuery::default(),
593            BrowserToken
594        );
595    }
596    #[tokio::test]
597    async fn test_get_library_albums() {
598        parse_with_matching_continuation_test!(
599            "./test_json/get_library_albums_20240701.json",
600            "./test_json/get_library_albums_continuation_mock.json",
601            "./test_json/get_library_albums_20240701_output.txt",
602            crate::query::GetLibraryAlbumsQuery::default(),
603            BrowserToken
604        );
605    }
606    #[tokio::test]
607    async fn test_get_library_songs() {
608        parse_test!(
609            "./test_json/get_library_songs_20240701.json",
610            "./test_json/get_library_songs_20240701_output.txt",
611            crate::query::GetLibrarySongsQuery::default(),
612            BrowserToken
613        );
614    }
615    #[tokio::test]
616    async fn test_get_library_songs_continuation() {
617        parse_continuations_test!(
618            "./test_json/get_library_songs_continuation_20240910.json",
619            "./test_json/get_library_songs_continuation_20240910_output.txt",
620            crate::query::GetLibrarySongsQuery::default(),
621            BrowserToken
622        );
623    }
624    #[tokio::test]
625    async fn test_get_library_artist_subscriptions() {
626        parse_with_matching_continuation_test!(
627            "./test_json/get_library_artist_subscriptions_20240701.json",
628            "./test_json/get_library_artist_subscriptions_continuation_mock.json",
629            "./test_json/get_library_artist_subscriptions_20240701_output.txt",
630            crate::query::GetLibraryArtistSubscriptionsQuery::default(),
631            BrowserToken
632        );
633    }
634    #[tokio::test]
635    async fn test_get_library_podcasts() {
636        parse_with_matching_continuation_test!(
637            "./test_json/get_library_podcasts_20250626.json",
638            "./test_json/get_library_podcasts_continuation_20250626.json",
639            "./test_json/get_library_podcasts_20250626_output.txt",
640            crate::query::GetLibraryPodcastsQuery::default(),
641            BrowserToken
642        );
643    }
644    #[tokio::test]
645    async fn test_get_library_channels() {
646        parse_with_matching_continuation_test!(
647            "./test_json/get_library_channels_20250626.json",
648            "./test_json/get_library_channels_continuation_20250626.json",
649            "./test_json/get_library_channels_20250626_output.txt",
650            crate::query::GetLibraryChannelsQuery::default(),
651            BrowserToken
652        );
653    }
654    #[tokio::test]
655    async fn test_edit_song_library_status() {
656        // Note - same files as remove_histry_items
657        parse_test!(
658            "./test_json/remove_history_items_20240704.json",
659            "./test_json/remove_history_items_20240704_output.txt",
660            crate::query::EditSongLibraryStatusQuery::new_from_add_to_library_feedback_tokens(
661                Vec::new()
662            )
663            .with_remove_from_library_feedback_tokens(vec![]),
664            BrowserToken
665        );
666    }
667}