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