1use super::{ParseFrom, DELETION_ENTITY_ID, HEADER_DETAIL, SECOND_SUBTITLE_RUNS, SUBTITLE};
2use crate::{
3 common::{
4 AlbumType, LikeStatus, Thumbnail, UploadAlbumID, UploadArtistID, UploadEntityID, VideoID,
5 },
6 nav_consts::{
7 GRID_ITEMS, INDEX_TEXT, MENU_ITEMS, MENU_LIKE_STATUS, MRLIR, MUSIC_SHELF,
8 NAVIGATION_BROWSE_ID, PLAY_BUTTON, SECTION_LIST_ITEM, SINGLE_COLUMN_TAB,
9 SINGLE_COLUMN_TABS, SUBTITLE2, SUBTITLE3, TAB_RENDERER, TEXT_RUN_TEXT, THUMBNAILS,
10 THUMBNAIL_CROPPED, THUMBNAIL_RENDERER, TITLE_TEXT, WATCH_VIDEO_ID,
11 },
12 parse::{parse_fixed_column_item, parse_flex_column_item},
13 process::{fixed_column_item_pointer, flex_column_item_pointer},
14 query::{
15 DeleteUploadEntityQuery, GetLibraryUploadAlbumQuery, GetLibraryUploadAlbumsQuery,
16 GetLibraryUploadArtistQuery, GetLibraryUploadArtistsQuery, GetLibraryUploadSongsQuery,
17 },
18 Result,
19};
20use const_format::concatcp;
21use json_crawler::{JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerIterator, JsonCrawlerOwned};
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct ParsedUploadArtist {
27 pub name: String,
28 pub id: Option<UploadArtistID<'static>>,
29}
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct ParsedUploadSongAlbum {
33 pub name: String,
34 pub id: UploadAlbumID<'static>,
35}
36
37#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
38#[non_exhaustive]
39pub struct TableListUploadSong {
41 pub entity_id: UploadEntityID<'static>,
42 pub video_id: VideoID<'static>,
43 pub album: ParsedUploadSongAlbum,
44 pub duration: String,
45 pub like_status: LikeStatus,
46 pub title: String,
47 pub artists: Vec<ParsedUploadArtist>,
48 pub thumbnails: Vec<Thumbnail>,
49}
50
51#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
52#[non_exhaustive]
53pub struct UploadAlbum {
54 pub title: String,
55 pub artist: String,
56 pub year: Option<String>,
58 pub entity_id: UploadEntityID<'static>,
59 pub album_id: UploadAlbumID<'static>,
60 pub thumbnails: Vec<Thumbnail>,
61}
62
63#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
64#[non_exhaustive]
65pub struct UploadArtist {
66 pub artist_name: String,
67 pub song_count: String,
68 pub artist_id: UploadArtistID<'static>,
69 pub thumbnails: Vec<Thumbnail>,
70}
71
72#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
73#[non_exhaustive]
74pub struct GetLibraryUploadAlbum {
75 pub title: String,
76 pub artist_name: String,
77 pub album_type: AlbumType,
78 pub song_count: String,
79 pub duration: String,
80 pub entity_id: UploadEntityID<'static>,
81 pub songs: Vec<GetLibraryUploadAlbumSong>,
82 pub thumbnails: Vec<Thumbnail>,
83}
84
85#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
86#[non_exhaustive]
87pub struct GetLibraryUploadAlbumSong {
89 pub title: String,
90 pub track_no: i64,
91 pub entity_id: UploadEntityID<'static>,
92 pub video_id: VideoID<'static>,
93 pub album: ParsedUploadSongAlbum,
94 pub duration: String,
95 pub like_status: LikeStatus,
96}
97
98impl ParseFrom<GetLibraryUploadSongsQuery> for Vec<TableListUploadSong> {
99 fn parse_from(p: super::ProcessedResult<GetLibraryUploadSongsQuery>) -> Result<Self> {
100 let crawler: JsonCrawlerOwned = p.into();
101 let contents = get_uploads_tab(crawler)?.navigate_pointer(concatcp!(
102 TAB_RENDERER,
103 SECTION_LIST_ITEM,
104 MUSIC_SHELF,
105 "/contents"
106 ))?;
107 contents
108 .try_into_iter()?
109 .map(|mut item| {
110 let Ok(mut data) = item.borrow_pointer(MRLIR) else {
111 return Ok(None);
112 };
113 let title = parse_flex_column_item(&mut data, 0, 0)?;
114 if title == "Shuffle all" {
115 return Ok(None);
116 };
117 Ok(Some(parse_table_list_upload_song(title, data)?))
118 })
119 .filter_map(Result::transpose)
120 .collect()
121 }
122}
123impl ParseFrom<GetLibraryUploadAlbumsQuery> for Vec<UploadAlbum> {
124 fn parse_from(p: super::ProcessedResult<GetLibraryUploadAlbumsQuery>) -> Result<Self> {
125 fn parse_item_list_upload_album(mut json_crawler: JsonCrawlerOwned) -> Result<UploadAlbum> {
126 let mut data = json_crawler.borrow_pointer("/musicTwoRowItemRenderer")?;
127 let album_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
128 let thumbnails = data.take_value_pointer(THUMBNAIL_RENDERER)?;
129 let title = data.take_value_pointer(TITLE_TEXT)?;
130 let artist = data.take_value_pointer(SUBTITLE2)?;
131 let year = data.take_value_pointer(SUBTITLE3).ok();
132 let entity_id = data
133 .borrow_pointer(MENU_ITEMS)?
134 .try_iter_mut()?
135 .find_path(DELETION_ENTITY_ID)?
136 .take_value()?;
137 Ok(UploadAlbum {
138 title,
139 year,
140 thumbnails,
141 artist,
142 entity_id,
143 album_id,
144 })
145 }
146 let crawler: JsonCrawlerOwned = p.into();
147 let items = get_uploads_tab(crawler)?.navigate_pointer(concatcp!(
148 TAB_RENDERER,
149 SECTION_LIST_ITEM,
150 GRID_ITEMS
151 ))?;
152 items
153 .try_into_iter()?
154 .map(parse_item_list_upload_album)
155 .collect()
156 }
157}
158impl ParseFrom<GetLibraryUploadArtistsQuery> for Vec<UploadArtist> {
159 fn parse_from(p: super::ProcessedResult<GetLibraryUploadArtistsQuery>) -> Result<Self> {
160 fn parse_item_list_upload_artist(
161 mut json_crawler: JsonCrawlerOwned,
162 ) -> Result<UploadArtist> {
163 let mut data = json_crawler.borrow_pointer(MRLIR)?;
164 let artist_name = parse_flex_column_item(&mut data.borrow_mut(), 0, 0)?;
165 let songs = parse_flex_column_item(&mut data.borrow_mut(), 1, 0)?;
166 let thumbnails = data.take_value_pointer(THUMBNAILS)?;
167 let artist_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
168 Ok(UploadArtist {
169 thumbnails,
170 artist_name,
171 song_count: songs,
172 artist_id,
173 })
174 }
175 let crawler: JsonCrawlerOwned = p.into();
176 let items = get_uploads_tab(crawler)?.navigate_pointer(concatcp!(
177 TAB_RENDERER,
178 SECTION_LIST_ITEM,
179 MUSIC_SHELF,
180 "/contents"
181 ))?;
182 items
183 .try_into_iter()?
184 .map(parse_item_list_upload_artist)
185 .collect()
186 }
187}
188impl ParseFrom<GetLibraryUploadAlbumQuery<'_>> for GetLibraryUploadAlbum {
189 fn parse_from(p: super::ProcessedResult<GetLibraryUploadAlbumQuery>) -> Result<Self> {
190 fn parse_playlist_upload_song(
191 mut json_crawler: JsonCrawlerOwned,
192 ) -> Result<GetLibraryUploadAlbumSong> {
193 let mut data = json_crawler.borrow_pointer(MRLIR)?;
194 let title = parse_flex_column_item(&mut data.borrow_mut(), 0, 0)?;
195 let album = parse_upload_song_album(data.borrow_mut(), 2)?;
196 let duration = parse_fixed_column_item(&mut data.borrow_mut(), 0)?;
197 let track_no = data.borrow_pointer(INDEX_TEXT)?.take_and_parse_str()?;
198 let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
199 let video_id = data.take_value_pointer(concatcp!(
200 PLAY_BUTTON,
201 "/playNavigationEndpoint",
202 WATCH_VIDEO_ID
203 ))?;
204 let entity_id = data
205 .borrow_pointer(MENU_ITEMS)?
206 .try_iter_mut()?
207 .find_path(DELETION_ENTITY_ID)?
208 .take_value()?;
209 Ok(GetLibraryUploadAlbumSong {
210 title,
211 track_no,
212 entity_id,
213 video_id,
214 album,
215 duration,
216 like_status,
217 })
218 }
219 let mut crawler: JsonCrawlerOwned = p.into();
220 let mut header = crawler.borrow_pointer(HEADER_DETAIL)?;
221 let title = header.take_value_pointer(TITLE_TEXT)?;
222 let album_type = header.take_value_pointer(SUBTITLE)?;
223 let artist_name = header.take_value_pointer(SUBTITLE2)?;
224 let song_count = header.take_value_pointer(concatcp!(SECOND_SUBTITLE_RUNS, "/0/text"))?;
225 let duration = header.take_value_pointer(concatcp!(SECOND_SUBTITLE_RUNS, "/2/text"))?;
226 let thumbnails = header.take_value_pointer(THUMBNAIL_CROPPED)?;
227 let entity_id = header
228 .navigate_pointer(MENU_ITEMS)?
229 .try_into_iter()?
230 .find_path(DELETION_ENTITY_ID)?
231 .take_value()?;
232 let songs = crawler
233 .navigate_pointer(concatcp!(
234 SINGLE_COLUMN_TAB,
235 SECTION_LIST_ITEM,
236 MUSIC_SHELF,
237 "/contents"
238 ))?
239 .try_into_iter()?
240 .map(parse_playlist_upload_song)
241 .collect::<Result<Vec<_>>>()?;
242 Ok(GetLibraryUploadAlbum {
243 title,
244 artist_name,
245 album_type,
246 song_count,
247 duration,
248 entity_id,
249 songs,
250 thumbnails,
251 })
252 }
253}
254impl ParseFrom<GetLibraryUploadArtistQuery<'_>> for Vec<TableListUploadSong> {
255 fn parse_from(p: super::ProcessedResult<GetLibraryUploadArtistQuery>) -> Result<Self> {
256 let crawler: JsonCrawlerOwned = p.into();
257 let contents = get_uploads_tab(crawler)?.navigate_pointer(concatcp!(
258 TAB_RENDERER,
259 SECTION_LIST_ITEM,
260 MUSIC_SHELF,
261 "/contents"
262 ))?;
263 contents
264 .try_into_iter()?
265 .map(|mut item| {
266 let Ok(mut data) = item.borrow_pointer(MRLIR) else {
267 return Ok(None);
268 };
269 let title = parse_flex_column_item(&mut data, 0, 0)?;
270 if title == "Shuffle all" {
271 return Ok(None);
272 };
273 Ok(Some(parse_table_list_upload_song(title, data)?))
274 })
275 .filter_map(Result::transpose)
276 .collect()
277 }
278}
279impl<'a> ParseFrom<DeleteUploadEntityQuery<'a>> for () {
280 fn parse_from(p: super::ProcessedResult<DeleteUploadEntityQuery<'a>>) -> crate::Result<Self> {
281 let crawler: JsonCrawlerOwned = p.into();
282 crawler
286 .navigate_pointer("/actions")?
287 .try_into_iter()?
288 .find_path("/addToToastAction")
289 .map(|_| ())
290 .map_err(Into::into)
291 }
292}
293pub(crate) fn parse_upload_song_artists(
294 data: JsonCrawlerBorrowed,
295 col_idx: usize,
296) -> Result<Vec<ParsedUploadArtist>> {
297 data.navigate_pointer(format!("{}/text/runs", flex_column_item_pointer(col_idx)))?
298 .try_into_iter()?
299 .step_by(2)
300 .map(|mut item| parse_upload_song_artist(&mut item))
301 .collect()
302}
303fn parse_upload_song_artist(data: &mut JsonCrawlerBorrowed) -> Result<ParsedUploadArtist> {
304 Ok(ParsedUploadArtist {
305 name: data.take_value_pointer("/text")?,
306 id: data.take_value_pointer(NAVIGATION_BROWSE_ID).ok(),
307 })
308}
309pub(crate) fn parse_upload_song_album(
310 mut data: JsonCrawlerBorrowed,
311 col_idx: usize,
312) -> Result<ParsedUploadSongAlbum> {
313 Ok(ParsedUploadSongAlbum {
314 name: parse_flex_column_item(&mut data, col_idx, 0)?,
315 id: data.take_value_pointer(format!(
316 "{}/text/runs/0{NAVIGATION_BROWSE_ID}",
317 flex_column_item_pointer(col_idx)
318 ))?,
319 })
320}
321pub(crate) fn parse_table_list_upload_song(
322 title: String,
323 mut crawler: JsonCrawlerBorrowed,
324) -> Result<TableListUploadSong> {
325 let duration =
326 crawler.take_value_pointer(format!("{}{TEXT_RUN_TEXT}", fixed_column_item_pointer(0)))?;
327 let like_status = crawler.take_value_pointer(MENU_LIKE_STATUS)?;
328 let video_id = crawler.take_value_pointer(concatcp!(
329 PLAY_BUTTON,
330 "/playNavigationEndpoint/watchEndpoint/videoId"
331 ))?;
332 let thumbnails = crawler.take_value_pointer(THUMBNAILS)?;
333 let artists = parse_upload_song_artists(crawler.borrow_mut(), 1)?;
334 let album = parse_upload_song_album(crawler.borrow_mut(), 2)?;
335 let entity_id = crawler
336 .navigate_pointer(MENU_ITEMS)?
337 .try_into_iter()?
338 .find_path(DELETION_ENTITY_ID)?
339 .take_value()?;
340 Ok(TableListUploadSong {
341 entity_id,
342 video_id,
343 album,
344 duration,
345 like_status,
346 title,
347 artists,
348 thumbnails,
349 })
350}
351
352fn get_uploads_tab(json: JsonCrawlerOwned) -> Result<JsonCrawlerOwned> {
353 let tabs_path = concatcp!(SINGLE_COLUMN_TABS);
354 json.navigate_pointer(tabs_path)?
355 .try_into_iter()?
356 .try_last()
357 .map_err(Into::into)
358}
359
360#[cfg(test)]
361mod tests {
362 use crate::{
363 auth::BrowserToken,
364 common::{UploadAlbumID, UploadArtistID, UploadEntityID, YoutubeID},
365 };
366 #[tokio::test]
367 async fn test_get_library_upload_songs() {
368 parse_test!(
369 "./test_json/get_library_upload_songs_20240712.json",
370 "./test_json/get_library_upload_songs_20240712_output.txt",
371 crate::query::GetLibraryUploadSongsQuery::default(),
372 BrowserToken
373 );
374 }
375 #[tokio::test]
376 async fn test_get_library_upload_albums() {
377 parse_test!(
378 "./test_json/get_library_upload_albums_20240712.json",
379 "./test_json/get_library_upload_albums_20240712_output.txt",
380 crate::query::GetLibraryUploadAlbumsQuery::default(),
381 BrowserToken
382 );
383 }
384 #[tokio::test]
385 async fn test_get_library_upload_artists() {
386 parse_test!(
387 "./test_json/get_library_upload_artists_20240712.json",
388 "./test_json/get_library_upload_artists_20240712_output.txt",
389 crate::query::GetLibraryUploadArtistsQuery::default(),
390 BrowserToken
391 );
392 }
393 #[tokio::test]
394 async fn test_get_library_upload_artist() {
395 parse_test!(
396 "./test_json/get_library_upload_artist_20240712.json",
397 "./test_json/get_library_upload_artist_20240712_output.txt",
398 crate::query::GetLibraryUploadArtistQuery::new(UploadArtistID::from_raw("")),
399 BrowserToken
400 );
401 }
402 #[tokio::test]
403 async fn test_get_library_upload_album() {
404 parse_test!(
405 "./test_json/get_library_upload_album_20240712.json",
406 "./test_json/get_library_upload_album_20240712_output.txt",
407 crate::query::GetLibraryUploadAlbumQuery::new(UploadAlbumID::from_raw("")),
408 BrowserToken
409 );
410 }
411 #[tokio::test]
412 async fn test_delete_upload_entity() {
413 parse_test_value!(
414 "./test_json/delete_upload_entity_20240715.json",
415 (),
416 crate::query::DeleteUploadEntityQuery::new(UploadEntityID::from_raw("")),
417 BrowserToken
418 );
419 }
420}