1use super::{
2 ParseFrom, ParsedSongArtist, ProcessedResult, fixed_column_item_pointer,
3 parse_flex_column_item, parse_library_management_items_from_menu, parse_song_artist,
4};
5use crate::Result;
6use crate::common::{
7 AlbumType, Explicit, LibraryManager, LibraryStatus, LikeStatus, PlaylistID, Thumbnail, VideoID,
8};
9use crate::nav_consts::*;
10use crate::query::*;
11use const_format::concatcp;
12use json_crawler::{
13 CrawlerResult, JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerIterator, JsonCrawlerOwned,
14};
15use serde::{Deserialize, Serialize};
16
17#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
19pub enum InLikedSongs {
20 Liked,
21 Indifferent,
22}
23
24#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
25#[non_exhaustive]
26pub struct AlbumSong {
27 pub video_id: VideoID<'static>,
28 pub track_no: usize,
29 pub duration: String,
30 pub plays: String,
31 pub library_management: Option<LibraryManager>,
35 pub title: String,
36 pub like_status: LikeStatus,
37 pub explicit: Explicit,
38}
39
40#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
43#[non_exhaustive]
44pub struct GetAlbum {
45 pub title: String,
46 pub category: AlbumType,
47 pub thumbnails: Vec<Thumbnail>,
48 pub artist_thumbnails: Vec<Thumbnail>,
49 pub description: Option<String>,
50 pub artists: Vec<ParsedSongArtist>,
51 pub year: String,
52 pub track_count_text: Option<String>,
53 pub duration: String,
54 pub audio_playlist_id: Option<PlaylistID<'static>>,
55 pub tracks: Vec<AlbumSong>,
57 pub library_status: LibraryStatus,
58}
59
60impl<'a> ParseFrom<GetAlbumQuery<'a>> for GetAlbum {
61 fn parse_from(p: ProcessedResult<GetAlbumQuery<'a>>) -> crate::Result<Self> {
62 parse_album_query(p)
63 }
64}
65
66fn parse_album_track(json: &mut JsonCrawlerBorrowed) -> Result<Option<AlbumSong>> {
67 let mut data = json.borrow_pointer(MRLIR)?;
68 if let Ok("MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT") = data
71 .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
72 .as_deref()
73 {
74 return Ok(None);
75 }
76 let title = super::parse_flex_column_item(&mut data, 0, 0)?;
77 let library_management =
78 parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
79 let video_id = data.take_value_pointer(concatcp!(
80 PLAY_BUTTON,
81 "/playNavigationEndpoint",
82 WATCH_VIDEO_ID
83 ))?;
84 let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
85 let duration = data
86 .borrow_pointer(fixed_column_item_pointer(0))
87 .and_then(|mut i| {
88 i.take_value_pointer("/text/simpleText")
89 .or_else(|_| i.take_value_pointer("/text/runs/0/text"))
90 })?;
91 let plays = parse_flex_column_item(&mut data, 2, 0)?;
92 let track_no = data
93 .borrow_pointer(concatcp!("/index", RUN_TEXT))?
94 .take_and_parse_str()?;
95 let explicit = if data.path_exists(BADGE_LABEL) {
96 Explicit::IsExplicit
97 } else {
98 Explicit::NotExplicit
99 };
100 Ok(Some(AlbumSong {
101 video_id,
102 track_no,
103 duration,
104 plays,
105 library_management,
106 title,
107 like_status,
108 explicit,
109 }))
110}
111
112fn parse_album_query(p: ProcessedResult<GetAlbumQuery>) -> Result<GetAlbum> {
114 let json_crawler = JsonCrawlerOwned::from(p);
115 let mut columns = json_crawler.navigate_pointer(TWO_COLUMN)?;
116 let mut header =
117 columns.borrow_pointer(concatcp!(TAB_CONTENT, SECTION_LIST_ITEM, RESPONSIVE_HEADER))?;
118 let title = header.take_value_pointer(TITLE_TEXT)?;
119 let category = header.take_value_pointer(SUBTITLE)?;
120 let year = header.take_value_pointer(SUBTITLE2)?;
121 let artists = header
122 .borrow_pointer("/straplineTextOne/runs")?
123 .try_into_iter()?
124 .step_by(2)
125 .map(|mut item| parse_song_artist(&mut item))
126 .collect::<Result<Vec<ParsedSongArtist>>>()?;
127 let description = header
128 .borrow_pointer(DESCRIPTION_SHELF_RUNS)
129 .and_then(|d| d.try_into_iter())
130 .ok()
131 .map(|r| {
132 r.map(|mut r| r.take_value_pointer::<String>("/text"))
133 .collect::<CrawlerResult<String>>()
134 })
135 .transpose()?;
136 let artist_thumbnails = header
138 .take_value_pointer(STRAPLINE_THUMBNAIL)
139 .unwrap_or_default();
140 let thumbnails = header.take_value_pointer(THUMBNAILS)?;
141 let duration = header.take_value_pointer("/secondSubtitle/runs/2/text")?;
142 let track_count_text = header.take_value_pointer("/secondSubtitle/runs/0/text")?;
143 let mut buttons = header.borrow_pointer("/buttons")?;
144 let audio_playlist_id = buttons
147 .try_iter_mut()?
148 .find_path("/musicPlayButtonRenderer")?
149 .take_value_pointers(&[
150 "/playNavigationEndpoint/watchEndpoint/playlistId",
151 "/playNavigationEndpoint/watchPlaylistEndpoint/playlistId",
152 ])?;
153 let library_status = buttons
154 .try_iter_mut()?
155 .find_path("/toggleButtonRenderer")?
156 .take_value_pointer("/defaultIcon/iconType")?;
157 let tracks = columns
158 .borrow_pointer(
159 "/secondaryContents/sectionListRenderer/contents/0/musicShelfRenderer/contents",
160 )?
161 .try_into_iter()?
162 .filter_map(|mut track| parse_album_track(&mut track).transpose())
163 .collect::<Result<Vec<AlbumSong>>>()?;
164 Ok(GetAlbum {
165 library_status,
166 title,
167 description,
168 artist_thumbnails,
169 duration,
170 category,
171 track_count_text,
172 audio_playlist_id,
173 year,
174 tracks,
175 artists,
176 thumbnails,
177 })
178}
179
180#[cfg(test)]
181mod tests {
182 use crate::auth::BrowserToken;
183 use crate::auth::noauth::NoAuthToken;
184 use crate::common::{AlbumID, YoutubeID};
185 use crate::parse::album::GetAlbumQuery;
186
187 #[tokio::test]
188 async fn test_get_album_query() {
189 parse_test!(
190 "./test_json/get_album_20240724.json",
191 "./test_json/get_album_20240724_output.txt",
192 GetAlbumQuery::new(AlbumID::from_raw("")),
193 BrowserToken
194 );
195 }
196 #[tokio::test]
197 async fn test_get_album_query_no_artist_thumbnail() {
198 parse_test!(
199 "./test_json/get_album_various_artists_no_thumbnail_20240818.json",
200 "./test_json/get_album_various_artists_no_thumbnail_20240818_output.txt",
201 GetAlbumQuery::new(AlbumID::from_raw("")),
202 BrowserToken
203 );
204 }
205 #[tokio::test]
206 async fn test_get_album_query_not_signed_in() {
207 parse_test!(
208 "./test_json/get_album_not_signed_in_20250611.json",
209 "./test_json/get_album_not_signed_in_20250611_output.txt",
210 GetAlbumQuery::new(AlbumID::from_raw("")),
211 NoAuthToken
212 );
213 }
214}