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