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