1use super::{
2 parse_flex_column_item, parse_library_management_items_from_menu, ParseFrom, ProcessedResult,
3 SearchResultAlbum, TableListSong, BADGE_LABEL, CONTINUATION_PARAMS, GRID_CONTINUATION,
4 MENU_LIKE_STATUS, MUSIC_SHELF_CONTINUATION, SUBTITLE, SUBTITLE2, SUBTITLE3,
5 SUBTITLE_BADGE_LABEL, THUMBNAILS,
6};
7use crate::common::{
8 ApiOutcome, ArtistChannelID, ContinuationParams, Explicit, PlaylistID, Thumbnail,
9};
10use crate::continuations::Continuable;
11use crate::nav_consts::{
12 GRID, ITEM_SECTION, MENU_ITEMS, MRLIR, MTRIR, MUSIC_SHELF, NAVIGATION_BROWSE_ID,
13 NAVIGATION_PLAYLIST_ID, PLAY_BUTTON, SECTION_LIST, SECTION_LIST_ITEM, SINGLE_COLUMN_TAB,
14 THUMBNAIL_RENDERER, TITLE, TITLE_TEXT, WATCH_VIDEO_ID,
15};
16use crate::process::fixed_column_item_pointer;
17use crate::query::{
18 EditSongLibraryStatusQuery, GetContinuationsQuery, GetLibraryAlbumsQuery,
19 GetLibraryArtistSubscriptionsQuery, GetLibraryArtistsQuery, GetLibraryPlaylistsQuery,
20 GetLibrarySongsQuery,
21};
22use crate::Result;
23use const_format::concatcp;
24use json_crawler::{CrawlerResult, JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerOwned};
25use serde::{Deserialize, Serialize};
26
27#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
28#[non_exhaustive]
29pub struct GetLibraryArtistSubscription {
31 pub name: String,
32 pub subscribers: String,
33 pub channel_id: ArtistChannelID<'static>,
34 pub thumbnails: Vec<Thumbnail>,
35}
36
37#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
38pub struct GetLibrarySongs {
41 pub songs: Vec<TableListSong>,
42 pub continuation_params: Option<ContinuationParams<'static>>,
43}
44
45#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
46pub struct GetLibraryArtistSubscriptions {
48 pub subscriptions: Vec<LibraryArtistSubscription>,
49 pub continuation_params: Option<ContinuationParams<'static>>,
50}
51
52#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
53pub struct GetLibraryPlaylists {
55 pub playlists: Vec<LibraryPlaylist>,
56 pub continuation_params: Option<ContinuationParams<'static>>,
57}
58
59#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
60pub struct GetLibraryArtists {
62 pub artists: Vec<LibraryArtist>,
63 pub continuation_params: Option<ContinuationParams<'static>>,
64}
65
66#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
67pub struct GetLibraryAlbums {
69 pub albums: Vec<SearchResultAlbum>,
70 pub continuation_params: Option<ContinuationParams<'static>>,
71}
72
73#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
74#[non_exhaustive]
75pub struct LibraryArtistSubscription {
77 pub name: String,
78 pub subscribers: String,
79 pub channel_id: ArtistChannelID<'static>,
80 pub thumbnails: Vec<Thumbnail>,
81}
82
83#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
84#[non_exhaustive]
85pub struct LibraryPlaylist {
86 pub playlist_id: PlaylistID<'static>,
87 pub title: String,
88 pub thumbnails: Vec<Thumbnail>,
89 pub count: Option<usize>,
90 pub description: Option<String>,
91 pub author: Option<String>,
92}
93#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
94#[non_exhaustive]
95pub struct LibraryArtist {
96 pub channel_id: ArtistChannelID<'static>,
97 pub artist: String,
98 pub byline: String, }
100
101impl ParseFrom<GetLibraryArtistSubscriptionsQuery> for GetLibraryArtistSubscriptions {
102 fn parse_from(p: ProcessedResult<GetLibraryArtistSubscriptionsQuery>) -> Result<Self> {
103 let json_crawler: JsonCrawlerOwned = p.into();
104 let music_shelf = json_crawler.navigate_pointer(concatcp!(
105 SINGLE_COLUMN_TAB,
106 SECTION_LIST_ITEM,
107 MUSIC_SHELF,
108 ))?;
109 parse_library_artist_subscriptions(music_shelf)
110 }
111}
112impl Continuable<GetLibraryArtistSubscriptionsQuery> for GetLibraryArtistSubscriptions {
113 fn take_continuation_params(&mut self) -> Option<ContinuationParams<'static>> {
114 self.continuation_params.take()
115 }
116 fn parse_continuation(
117 p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryArtistSubscriptionsQuery>>,
118 ) -> Result<Self> {
119 let json_crawler: JsonCrawlerOwned = p.into();
120 let music_shelf = json_crawler.navigate_pointer(MUSIC_SHELF_CONTINUATION)?;
121 parse_library_artist_subscriptions(music_shelf)
122 }
123}
124
125impl ParseFrom<GetLibraryAlbumsQuery> for GetLibraryAlbums {
126 fn parse_from(p: ProcessedResult<GetLibraryAlbumsQuery>) -> Result<Self> {
127 let json_crawler: JsonCrawlerOwned = p.into();
128 let grid_renderer =
129 json_crawler.navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST_ITEM, GRID))?;
130 parse_library_albums(grid_renderer)
131 }
132}
133impl Continuable<GetLibraryAlbumsQuery> for GetLibraryAlbums {
134 fn take_continuation_params(&mut self) -> Option<ContinuationParams<'static>> {
135 self.continuation_params.take()
136 }
137 fn parse_continuation(
138 p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryAlbumsQuery>>,
139 ) -> Result<Self> {
140 let json_crawler: JsonCrawlerOwned = p.into();
141 let grid_items = json_crawler.navigate_pointer(GRID_CONTINUATION)?;
142 parse_library_albums(grid_items)
143 }
144}
145
146impl ParseFrom<GetLibrarySongsQuery> for GetLibrarySongs {
147 fn parse_from(p: ProcessedResult<GetLibrarySongsQuery>) -> Result<Self> {
148 let json_crawler: JsonCrawlerOwned = p.into();
149 let music_shelf = json_crawler.navigate_pointer(concatcp!(
150 SINGLE_COLUMN_TAB,
151 SECTION_LIST_ITEM,
152 MUSIC_SHELF,
153 ))?;
154 parse_library_songs(music_shelf)
155 }
156}
157
158impl Continuable<GetLibrarySongsQuery> for GetLibrarySongs {
159 fn take_continuation_params(&mut self) -> Option<ContinuationParams<'static>> {
160 self.continuation_params.take()
161 }
162 fn parse_continuation(
163 p: ProcessedResult<GetContinuationsQuery<GetLibrarySongsQuery>>,
164 ) -> Result<Self> {
165 let json_crawler: JsonCrawlerOwned = p.into();
166 let music_shelf = json_crawler.navigate_pointer(MUSIC_SHELF_CONTINUATION)?;
167 parse_library_songs(music_shelf)
168 }
169}
170
171impl ParseFrom<GetLibraryArtistsQuery> for GetLibraryArtists {
172 fn parse_from(p: ProcessedResult<GetLibraryArtistsQuery>) -> Result<Self> {
173 let json_crawler = p.into();
174 let maybe_music_shelf = process_library_contents_music_shelf(json_crawler);
175 if let Some(music_shelf) = maybe_music_shelf {
176 parse_content_list_artists(music_shelf)
177 } else {
178 Ok(GetLibraryArtists {
179 artists: Vec::new(),
180 continuation_params: None,
181 })
182 }
183 }
184}
185impl Continuable<GetLibraryArtistsQuery> for GetLibraryArtists {
186 fn take_continuation_params(&mut self) -> Option<ContinuationParams<'static>> {
187 self.continuation_params.take()
188 }
189 fn parse_continuation(
190 p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryArtistsQuery>>,
191 ) -> Result<Self> {
192 let json_crawler = JsonCrawlerOwned::from(p);
193 let music_shelf = json_crawler.navigate_pointer(MUSIC_SHELF_CONTINUATION)?;
194 parse_content_list_artists(music_shelf)
195 }
196}
197
198impl ParseFrom<GetLibraryPlaylistsQuery> for GetLibraryPlaylists {
199 fn parse_from(p: ProcessedResult<GetLibraryPlaylistsQuery>) -> Result<Self> {
200 let json_crawler = p.into();
202 let maybe_grid_renderer = process_library_contents_grid(json_crawler);
203 if let Some(grid_renderer) = maybe_grid_renderer {
204 parse_library_playlists(grid_renderer)
205 } else {
206 Ok(GetLibraryPlaylists {
207 playlists: vec![],
208 continuation_params: None,
209 })
210 }
211 }
212}
213impl Continuable<GetLibraryPlaylistsQuery> for GetLibraryPlaylists {
214 fn take_continuation_params(&mut self) -> Option<ContinuationParams<'static>> {
215 self.continuation_params.take()
216 }
217 fn parse_continuation(
218 p: ProcessedResult<GetContinuationsQuery<'_, GetLibraryPlaylistsQuery>>,
219 ) -> Result<Self> {
220 let json_crawler: JsonCrawlerOwned = p.into();
221 let grid_renderer = json_crawler.navigate_pointer(GRID_CONTINUATION)?;
222 parse_library_playlists(grid_renderer)
223 }
224}
225
226impl ParseFrom<EditSongLibraryStatusQuery<'_>> for Vec<ApiOutcome> {
227 fn parse_from(p: super::ProcessedResult<EditSongLibraryStatusQuery>) -> Result<Self> {
228 let json_crawler = JsonCrawlerOwned::from(p);
229 json_crawler
230 .navigate_pointer("/feedbackResponses")?
231 .try_into_iter()?
232 .map(|mut response| {
233 response
234 .take_value_pointer::<bool>("/isProcessed")
235 .map(|p| {
236 if p {
237 return ApiOutcome::Success;
238 }
239 ApiOutcome::Failure
240 })
241 })
242 .rev()
243 .collect::<CrawlerResult<_>>()
244 .map_err(Into::into)
245 }
246}
247
248fn parse_library_albums(mut grid_renderer: JsonCrawlerOwned) -> Result<GetLibraryAlbums> {
249 let continuation_params = grid_renderer.take_value_pointer(CONTINUATION_PARAMS).ok();
250 let songs = grid_renderer
251 .navigate_pointer("/items")?
252 .try_into_iter()?
253 .map(parse_item_list_album)
254 .collect::<Result<_>>()?;
255 Ok(GetLibraryAlbums {
256 albums: songs,
257 continuation_params,
258 })
259}
260fn parse_library_songs(
261 mut music_shelf: JsonCrawlerOwned,
262) -> std::prelude::v1::Result<GetLibrarySongs, crate::Error> {
263 let continuation_params = music_shelf.take_value_pointer(CONTINUATION_PARAMS).ok();
264 let songs = music_shelf
265 .navigate_pointer("/contents")?
266 .try_into_iter()?
267 .map(|mut item| {
268 let Ok(mut data) = item.borrow_pointer(MRLIR) else {
269 return Ok(None);
270 };
271 let title = super::parse_flex_column_item(&mut data, 0, 0)?;
272 if title == "Shuffle all" {
273 return Ok(None);
274 }
275 Ok(Some(parse_table_list_song(title, data)?))
276 })
277 .filter_map(Result::transpose)
278 .collect::<Result<_>>()?;
279 Ok(GetLibrarySongs {
280 songs,
281 continuation_params,
282 })
283}
284fn parse_library_artist_subscriptions(
285 mut music_shelf: JsonCrawlerOwned,
286) -> Result<GetLibraryArtistSubscriptions> {
287 let continuation_params = music_shelf.take_value_pointer(CONTINUATION_PARAMS).ok();
288 let subscriptions = music_shelf
289 .navigate_pointer("/contents")?
290 .try_into_iter()?
291 .map(parse_content_list_artist_subscription)
292 .collect::<Result<_>>()?;
293 Ok(GetLibraryArtistSubscriptions {
294 subscriptions,
295 continuation_params,
296 })
297}
298
299fn parse_library_playlists(mut grid_renderer: JsonCrawlerOwned) -> Result<GetLibraryPlaylists> {
300 let continuation_params = grid_renderer.take_value_pointer(CONTINUATION_PARAMS).ok();
301 let playlists = grid_renderer
302 .navigate_pointer("/items")?
303 .try_into_iter()?
304 .skip(1)
306 .map(parse_content_list_playlist)
307 .collect::<Result<_>>()?;
308 Ok(GetLibraryPlaylists {
309 playlists,
310 continuation_params,
311 })
312}
313
314fn process_library_contents_grid(mut json_crawler: JsonCrawlerOwned) -> Option<JsonCrawlerOwned> {
317 let section = json_crawler.borrow_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST));
318 if let Ok(section) = section {
320 if section.path_exists("/itemSectionRenderer") {
321 json_crawler
322 .navigate_pointer(concatcp!(ITEM_SECTION, GRID))
323 .ok()
324 } else {
325 json_crawler
326 .navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST_ITEM, GRID))
327 .ok()
328 }
329 } else {
330 None
331 }
332}
333fn process_library_contents_music_shelf(
336 mut json_crawler: JsonCrawlerOwned,
337) -> Option<JsonCrawlerOwned> {
338 let section = json_crawler.borrow_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST));
339 if let Ok(section) = section {
341 if section.path_exists("itemSectionRenderer") {
342 json_crawler
343 .navigate_pointer(concatcp!(ITEM_SECTION, MUSIC_SHELF))
344 .ok()
345 } else {
346 json_crawler
347 .navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST_ITEM, MUSIC_SHELF))
348 .ok()
349 }
350 } else {
351 None
352 }
353}
354
355fn parse_item_list_album(mut json_crawler: JsonCrawlerOwned) -> Result<SearchResultAlbum> {
356 let mut data = json_crawler.borrow_pointer("/musicTwoRowItemRenderer")?;
357 let browse_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
358 let thumbnails = data.take_value_pointer(THUMBNAIL_RENDERER)?;
359 let title = data.take_value_pointer(TITLE_TEXT)?;
360 let artist = data.take_value_pointer(SUBTITLE2)?;
361 let year = data.take_value_pointer(SUBTITLE3)?;
362 let album_type = data.take_value_pointer(SUBTITLE)?;
363 let explicit = if data.path_exists(SUBTITLE_BADGE_LABEL) {
364 Explicit::IsExplicit
365 } else {
366 Explicit::NotExplicit
367 };
368 Ok(SearchResultAlbum {
369 title,
370 artist,
371 year,
372 explicit,
373 album_id: browse_id,
374 album_type,
375 thumbnails,
376 })
377}
378
379fn parse_content_list_artist_subscription(
380 mut json_crawler: JsonCrawlerOwned,
381) -> Result<LibraryArtistSubscription> {
382 let mut data = json_crawler.borrow_pointer(MRLIR)?;
383 let channel_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
384 let name = parse_flex_column_item(&mut data, 0, 0)?;
385 let subscribers = parse_flex_column_item(&mut data, 1, 0)?;
386 let thumbnails = data.take_value_pointer(THUMBNAILS)?;
387 Ok(LibraryArtistSubscription {
388 name,
389 subscribers,
390 channel_id,
391 thumbnails,
392 })
393}
394
395fn parse_content_list_artists(mut json_crawler: JsonCrawlerOwned) -> Result<GetLibraryArtists> {
396 let continuation_params = json_crawler.take_value_pointer(CONTINUATION_PARAMS).ok();
397 let songs = json_crawler
398 .navigate_pointer("/contents")?
399 .try_iter_mut()?
400 .map(|item| {
401 let mut data = item.navigate_pointer(MRLIR)?;
402 let channel_id = data.take_value_pointer(NAVIGATION_BROWSE_ID)?;
403 let artist = parse_flex_column_item(&mut data, 0, 0)?;
404 let byline = parse_flex_column_item(&mut data, 1, 0)?;
405 Ok(LibraryArtist {
406 channel_id,
407 artist,
408 byline,
409 })
410 })
411 .collect::<Result<_>>()?;
412 Ok(GetLibraryArtists {
413 artists: songs,
414 continuation_params,
415 })
416}
417
418fn parse_table_list_song(title: String, mut data: JsonCrawlerBorrowed) -> Result<TableListSong> {
419 let video_id = data.take_value_pointer(concatcp!(
420 PLAY_BUTTON,
421 "/playNavigationEndpoint",
422 WATCH_VIDEO_ID
423 ))?;
424 let library_management =
425 parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
426 let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
427 let artists = super::parse_song_artists(&mut data, 1)?;
428 let album = super::parse_song_album(&mut data, 2)?;
429 let duration = data
430 .borrow_pointer(fixed_column_item_pointer(0))?
431 .take_value_pointers(&["/text/simpleText", "/text/runs/0/text"])?;
432 let thumbnails = data.take_value_pointer(THUMBNAILS)?;
433 let is_available = data
434 .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
435 .map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
436 .unwrap_or(true);
437
438 let explicit = if data.path_exists(BADGE_LABEL) {
439 Explicit::IsExplicit
440 } else {
441 Explicit::NotExplicit
442 };
443 let playlist_id = data.take_value_pointer(concatcp!(
444 MENU_ITEMS,
445 "/0/menuNavigationItemRenderer",
446 NAVIGATION_PLAYLIST_ID
447 ))?;
448 Ok(TableListSong {
449 video_id,
450 duration,
451 library_management,
452 title,
453 artists,
454 like_status,
455 thumbnails,
456 explicit,
457 album,
458 playlist_id,
459 is_available,
460 })
461}
462
463fn parse_content_list_playlist(item: JsonCrawlerOwned) -> Result<LibraryPlaylist> {
464 let mut mtrir = item.navigate_pointer(MTRIR)?;
466 let title = mtrir.take_value_pointer(TITLE_TEXT)?;
467 let playlist_id: PlaylistID = mtrir
468 .borrow_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?
469 .take_value()?;
472 let thumbnails: Vec<Thumbnail> = mtrir.take_value_pointer(THUMBNAIL_RENDERER)?;
473 let mut description = None;
474 let count = None;
475 let author = None;
476 if let Ok(mut subtitle) = mtrir.borrow_pointer("/subtitle") {
477 let runs = subtitle.borrow_pointer("/runs")?.try_into_iter()?;
478 description = Some(
481 runs.map(|mut c| c.take_value_pointer::<String>("/text"))
482 .collect::<std::result::Result<String, _>>()?,
483 );
484 }
485 Ok(LibraryPlaylist {
486 description,
487 author,
488 playlist_id,
489 title,
490 thumbnails,
491 count,
492 })
493}
494
495#[cfg(test)]
496mod tests {
497 use crate::auth::BrowserToken;
498
499 #[tokio::test]
501 async fn test_library_playlists_dummy_json() {
502 parse_test!(
503 "./test_json/get_library_playlists.json",
504 "./test_json/get_library_playlists_output.txt",
505 crate::query::GetLibraryPlaylistsQuery,
506 BrowserToken
507 );
508 }
509 #[tokio::test]
510 async fn test_get_library_playlists_continuation() {
511 parse_continuations_test!(
512 "./test_json/get_library_playlists_continuation_mock.json",
513 "./test_json/get_library_playlists_output.txt",
514 crate::query::GetLibraryPlaylistsQuery,
515 BrowserToken
516 );
517 }
518 #[tokio::test]
519 async fn test_library_artists_dummy_json() {
520 parse_test!(
521 "./test_json/get_library_artists.json",
522 "./test_json/get_library_artists_output.txt",
523 crate::query::GetLibraryArtistsQuery::default(),
524 BrowserToken
525 );
526 }
527 #[tokio::test]
528 async fn test_get_library_artists_continuation() {
529 parse_continuations_test!(
530 "./test_json/get_library_artists_continuation_mock.json",
531 "./test_json/get_library_artists_output.txt",
532 crate::query::GetLibraryArtistsQuery::default(),
533 BrowserToken
534 );
535 }
536 #[tokio::test]
537 async fn test_get_library_albums() {
538 parse_test!(
539 "./test_json/get_library_albums_20240701.json",
540 "./test_json/get_library_albums_20240701_output.txt",
541 crate::query::GetLibraryAlbumsQuery::default(),
542 BrowserToken
543 );
544 }
545 #[tokio::test]
546 async fn test_get_library_albums_continuation() {
547 parse_continuations_test!(
548 "./test_json/get_library_albums_continuation_mock.json",
549 "./test_json/get_library_albums_20240701_output.txt",
550 crate::query::GetLibraryAlbumsQuery::default(),
551 BrowserToken
552 );
553 }
554 #[tokio::test]
555 async fn test_get_library_songs() {
556 parse_test!(
557 "./test_json/get_library_songs_20240701.json",
558 "./test_json/get_library_songs_20240701_output.txt",
559 crate::query::GetLibrarySongsQuery::default(),
560 BrowserToken
561 );
562 }
563 #[tokio::test]
564 async fn test_get_library_songs_continuation() {
565 parse_continuations_test!(
566 "./test_json/get_library_songs_continuation_20240910.json",
567 "./test_json/get_library_songs_continuation_20240910_output.txt",
568 crate::query::GetLibrarySongsQuery::default(),
569 BrowserToken
570 );
571 }
572 #[tokio::test]
573 async fn test_get_library_artist_subscriptions() {
574 parse_test!(
575 "./test_json/get_library_artist_subscriptions_20240701.json",
576 "./test_json/get_library_artist_subscriptions_20240701_output.txt",
577 crate::query::GetLibraryArtistSubscriptionsQuery::default(),
578 BrowserToken
579 );
580 }
581 #[tokio::test]
582 async fn test_get_library_artist_subscriptions_continuation() {
583 parse_continuations_test!(
584 "./test_json/get_library_artist_subscriptions_continuation_mock.json",
585 "./test_json/get_library_artist_subscriptions_20240701_output.txt",
586 crate::query::GetLibraryArtistSubscriptionsQuery::default(),
587 BrowserToken
588 );
589 }
590 #[tokio::test]
591 async fn test_edit_song_library_status() {
592 parse_test!(
594 "./test_json/remove_history_items_20240704.json",
595 "./test_json/remove_history_items_20240704_output.txt",
596 crate::query::EditSongLibraryStatusQuery::new_from_add_to_library_feedback_tokens(
597 Vec::new()
598 )
599 .with_remove_from_library_feedback_tokens(vec![]),
600 BrowserToken
601 );
602 }
603}