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 pub playlist_id: Option<String>,
43 pub browse_id: AlbumID<'static>,
45 pub category: Option<String>, 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 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 #[allow(clippy::field_reassign_with_default)]
91 fn parse_from(p: ProcessedResult<GetArtistQuery<'a>>) -> crate::Result<Self> {
92 let mut json_crawler: JsonCrawlerOwned = p.into();
94 let mut results =
95 json_crawler.borrow_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST))?;
96 let mut description = String::default();
98 let mut views = String::default();
99 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 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 let category = r.take_value_pointer(concatcp!(CAROUSEL_TITLE, "/text"))?;
139 let browse_id: Option<ArtistChannelID> = r
142 .take_value_pointer(concatcp!(CAROUSEL_TITLE, NAVIGATION_BROWSE_ID))
143 .ok();
144 let params = r
147 .take_value_pointer(concatcp!(
148 CAROUSEL_TITLE,
149 "/navigationEndpoint/browseEndpoint/params"
150 ))
151 .ok();
152 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 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 let thumbnails = header.take_value_pointer(THUMBNAILS).ok();
192 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 let subscribed = subscription_button.take_value_pointer("/subscribed").ok();
201 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
264#[non_exhaustive]
265pub struct GetArtistAlbums {
266 pub results: Vec<AlbumResult>,
267 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]
293pub struct PlaylistSong {
296 pub video_id: VideoID<'static>,
297 pub track_no: usize,
298 pub album: ParsedSongAlbum,
299 pub duration: String,
300 pub library_management: Option<LibraryManager>,
303 pub title: String,
304 pub artists: Vec<super::ParsedSongArtist>,
305 pub like_status: LikeStatus,
307 pub thumbnails: Vec<Thumbnail>,
308 pub explicit: Explicit,
309 pub is_available: bool,
310 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 pub channel_name: String,
323 pub channel_id: ArtistChannelID<'static>,
324 pub like_status: LikeStatus,
326 pub thumbnails: Vec<Thumbnail>,
327 pub is_available: bool,
328 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 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 pub like_status: LikeStatus,
360 pub thumbnails: Vec<Thumbnail>,
361}
362
363#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
364#[non_exhaustive]
365pub struct TableListSong {
369 pub video_id: VideoID<'static>,
370 pub album: ParsedSongAlbum,
371 pub duration: String,
372 pub library_management: Option<LibraryManager>,
375 pub title: String,
376 pub artists: Vec<super::ParsedSongArtist>,
377 pub like_status: LikeStatus,
379 pub thumbnails: Vec<Thumbnail>,
380 pub explicit: Explicit,
381 pub is_available: bool,
382 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#[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
411pub(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 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 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 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 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 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}
710pub(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 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 "./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 #[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}