1use super::{
2 DESCRIPTION_SHELF_RUNS, EpisodeDate, EpisodeDuration, ParseFrom, ParsedSongAlbum,
3 ParsedUploadArtist, ParsedUploadSongAlbum, ProcessedResult, STRAPLINE_TEXT, TITLE_TEXT,
4 TWO_COLUMN, fixed_column_item_pointer, flex_column_item_pointer, parse_flex_column_item,
5 parse_library_management_items_from_menu, parse_upload_song_album, parse_upload_song_artists,
6};
7use crate::common::{
8 ApiOutcome, ArtistChannelID, ContinuationParams, EpisodeID, Explicit, LibraryManager,
9 LikeStatus, PlaylistID, SetVideoID, Thumbnail, UploadEntityID, VideoID,
10};
11use crate::continuations::ParseFromContinuable;
12use crate::nav_consts::{
13 APPEND_CONTINUATION_ITEMS, BADGE_LABEL, CONTENT, CONTINUATION_RENDERER_COMMAND,
14 DELETION_ENTITY_ID, FACEPILE_AVATAR_URL, FACEPILE_TEXT, LIVE_BADGE_LABEL, MENU_ITEMS,
15 MENU_LIKE_STATUS, MRLIR, MUSIC_PLAYLIST_SHELF, NAVIGATION_BROWSE_ID, NAVIGATION_PLAYLIST_ID,
16 NAVIGATION_VIDEO_ID, NAVIGATION_VIDEO_TYPE, PLAY_BUTTON, PLAYLIST_PANEL_CONTINUATION, PPR,
17 RADIO_CONTINUATION_PARAMS, RESPONSIVE_HEADER, RUN_TEXT, SECOND_SUBTITLE_RUNS,
18 SECONDARY_SECTION_LIST_RENDERER, SECTION_LIST_ITEM, TAB_CONTENT, TEXT_RUN, TEXT_RUN_TEXT,
19 THUMBNAIL, THUMBNAILS, WATCH_NEXT_CONTENT, WATCH_VIDEO_ID,
20};
21use crate::query::playlist::{
22 CreatePlaylistType, GetPlaylistDetailsQuery, GetWatchPlaylistQueryID, PrivacyStatus,
23 SpecialisedQuery,
24};
25use crate::query::{
26 AddPlaylistItemsQuery, CreatePlaylistQuery, DeletePlaylistQuery, EditPlaylistQuery,
27 GetPlaylistTracksQuery, GetWatchPlaylistQuery, RemovePlaylistItemsQuery,
28};
29use crate::youtube_enums::YoutubeMusicVideoType;
30use crate::{Error, Result};
31use const_format::concatcp;
32use json_crawler::{JsonCrawler, JsonCrawlerIterator, JsonCrawlerOwned};
33use serde::{Deserialize, Serialize};
34
35#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
36#[non_exhaustive]
37pub struct GetPlaylistDetails {
38 pub id: PlaylistID<'static>,
39 pub privacy: Option<PrivacyStatus>,
42 pub title: String,
43 pub description: Option<String>,
44 pub author: String,
45 pub author_avatar_url: Option<String>,
46 pub year: String,
47 pub duration: String,
48 pub track_count_text: String,
49 pub views: Option<String>,
51 pub thumbnails: Vec<Thumbnail>,
52}
53#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
54pub struct AddPlaylistItem {
57 pub video_id: VideoID<'static>,
58 pub set_video_id: SetVideoID<'static>,
59}
60#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
61#[non_exhaustive]
62pub struct WatchPlaylistTrack {
63 pub title: String,
64 pub author: String,
65 pub duration: String,
66 pub thumbnails: Vec<Thumbnail>,
67 pub video_id: VideoID<'static>,
68}
69
70#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
71#[non_exhaustive]
72pub struct PlaylistSong {
75 pub video_id: VideoID<'static>,
76 pub track_no: usize,
77 pub album: ParsedSongAlbum,
78 pub duration: String,
79 pub library_management: Option<LibraryManager>,
82 pub title: String,
83 pub artists: Vec<super::ParsedSongArtist>,
84 pub like_status: LikeStatus,
86 pub thumbnails: Vec<Thumbnail>,
87 pub explicit: Explicit,
88 pub is_available: bool,
89 pub playlist_id: PlaylistID<'static>,
91}
92
93#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
94pub enum PlaylistItem {
95 Song(PlaylistSong),
96 Video(PlaylistVideo),
97 Episode(PlaylistEpisode),
98 UploadSong(PlaylistUploadSong),
99}
100
101#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
102#[non_exhaustive]
103pub struct PlaylistVideo {
104 pub video_id: VideoID<'static>,
105 pub track_no: usize,
106 pub duration: String,
107 pub title: String,
108 pub channel_name: String,
110 pub channel_id: ArtistChannelID<'static>,
111 pub like_status: LikeStatus,
113 pub thumbnails: Vec<Thumbnail>,
114 pub is_available: bool,
115 pub playlist_id: PlaylistID<'static>,
117}
118
119#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
120#[non_exhaustive]
121pub struct PlaylistEpisode {
122 pub episode_id: EpisodeID<'static>,
123 pub track_no: usize,
124 pub date: EpisodeDate,
125 pub duration: EpisodeDuration,
126 pub title: String,
127 pub podcast_name: String,
128 pub podcast_id: PlaylistID<'static>,
129 pub like_status: LikeStatus,
131 pub thumbnails: Vec<Thumbnail>,
132 pub is_available: bool,
133}
134
135#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
136#[non_exhaustive]
137pub struct PlaylistUploadSong {
138 pub entity_id: UploadEntityID<'static>,
139 pub video_id: VideoID<'static>,
140 pub track_no: usize,
141 pub duration: String,
142 pub album: ParsedUploadSongAlbum,
143 pub title: String,
144 pub artists: Vec<ParsedUploadArtist>,
145 pub like_status: LikeStatus,
147 pub thumbnails: Vec<Thumbnail>,
148}
149
150impl<'a> ParseFrom<RemovePlaylistItemsQuery<'a>> for () {
151 fn parse_from(_: ProcessedResult<RemovePlaylistItemsQuery<'a>>) -> crate::Result<Self> {
152 Ok(())
153 }
154}
155impl<'a, C: CreatePlaylistType> ParseFrom<CreatePlaylistQuery<'a, C>> for PlaylistID<'static> {
156 fn parse_from(p: ProcessedResult<CreatePlaylistQuery<'a, C>>) -> crate::Result<Self> {
157 let mut json_crawler: JsonCrawlerOwned = p.into();
158 json_crawler
159 .take_value_pointer("/playlistId")
160 .map_err(Into::into)
161 }
162}
163impl<'a, T: SpecialisedQuery> ParseFrom<AddPlaylistItemsQuery<'a, T>> for Vec<AddPlaylistItem> {
164 fn parse_from(p: ProcessedResult<AddPlaylistItemsQuery<'a, T>>) -> crate::Result<Self> {
165 let mut json_crawler: JsonCrawlerOwned = p.into();
166 let status: ApiOutcome = json_crawler.borrow_pointer("/status")?.take_value()?;
167 if let ApiOutcome::Failure = status {
168 return Err(Error::status_failed());
169 }
170 json_crawler
171 .navigate_pointer("/playlistEditResults")?
172 .try_iter_mut()?
173 .map(|r| {
174 let mut r = r.navigate_pointer("/playlistEditVideoAddedResultData")?;
175 Ok(AddPlaylistItem {
176 video_id: r.take_value_pointer("/videoId")?,
177 set_video_id: r.take_value_pointer("/setVideoId")?,
178 })
179 })
180 .collect()
181 }
182}
183impl<'a> ParseFrom<EditPlaylistQuery<'a>> for ApiOutcome {
184 fn parse_from(p: ProcessedResult<EditPlaylistQuery<'a>>) -> crate::Result<Self> {
185 let json_crawler: JsonCrawlerOwned = p.into();
186 json_crawler
187 .navigate_pointer("/status")?
188 .take_value()
189 .map_err(Into::into)
190 }
191}
192impl<'a> ParseFrom<DeletePlaylistQuery<'a>> for () {
193 fn parse_from(_: ProcessedResult<DeletePlaylistQuery<'a>>) -> crate::Result<Self> {
194 Ok(())
195 }
196}
197
198impl<'a> ParseFrom<GetPlaylistDetailsQuery<'a>> for GetPlaylistDetails {
199 fn parse_from(p: ProcessedResult<GetPlaylistDetailsQuery<'a>>) -> crate::Result<Self> {
200 let json_crawler: JsonCrawlerOwned = p.into();
201 get_playlist_details(json_crawler)
202 }
203}
204
205impl<'a> ParseFromContinuable<GetPlaylistTracksQuery<'a>> for Vec<PlaylistItem> {
206 fn parse_from_continuable(
207 p: ProcessedResult<GetPlaylistTracksQuery<'a>>,
208 ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
209 let json_crawler: JsonCrawlerOwned = p.into();
210 let music_playlist_shelf = json_crawler.navigate_pointer(concatcp!(
211 TWO_COLUMN,
212 SECONDARY_SECTION_LIST_RENDERER,
213 CONTENT,
214 MUSIC_PLAYLIST_SHELF,
215 "/contents"
216 ))?;
217 parse_playlist_items(music_playlist_shelf)
218 }
219 fn parse_continuation(
220 p: ProcessedResult<crate::query::GetContinuationsQuery<'_, GetPlaylistTracksQuery<'a>>>,
221 ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
222 let json_crawler: JsonCrawlerOwned = p.into();
223 let continuation_items = json_crawler.navigate_pointer(APPEND_CONTINUATION_ITEMS)?;
224 parse_playlist_items(continuation_items)
225 }
226}
227
228impl<T: GetWatchPlaylistQueryID> ParseFromContinuable<GetWatchPlaylistQuery<T>>
229 for Vec<WatchPlaylistTrack>
230{
231 fn parse_from_continuable(
232 p: ProcessedResult<GetWatchPlaylistQuery<T>>,
233 ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
234 let json_crawler: JsonCrawlerOwned = p.into();
235 let mut playlist_panel =
236 json_crawler.navigate_pointer(concatcp!(WATCH_NEXT_CONTENT, PPR))?;
237 let continuation_params = playlist_panel
238 .take_value_pointer(RADIO_CONTINUATION_PARAMS)
239 .ok();
240 let tracks = playlist_panel
241 .navigate_pointer("/contents")?
242 .try_into_iter()?
243 .map(parse_watch_playlist_track)
244 .collect::<Result<Vec<_>>>()?;
245 Ok((tracks, continuation_params))
246 }
247 fn parse_continuation(
248 p: ProcessedResult<crate::query::GetContinuationsQuery<'_, GetWatchPlaylistQuery<T>>>,
249 ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
250 let json_crawler: JsonCrawlerOwned = p.into();
251 let mut playlist_panel = json_crawler.navigate_pointer(PLAYLIST_PANEL_CONTINUATION)?;
252 let continuation_params = playlist_panel
253 .take_value_pointer(RADIO_CONTINUATION_PARAMS)
254 .ok();
255 let tracks = playlist_panel
256 .navigate_pointer("/contents")?
257 .try_into_iter()?
258 .map(parse_watch_playlist_track)
259 .collect::<Result<Vec<_>>>()?;
260 Ok((tracks, continuation_params))
261 }
262}
263
264fn parse_watch_playlist_track(mut item: impl JsonCrawler) -> Result<WatchPlaylistTrack> {
265 let video_renderer_paths = [
266 "/playlistPanelVideoRenderer",
267 "/playlistPanelVideoWrapperRenderer/primaryRenderer/playlistPanelVideoRenderer",
268 ];
269 item.apply_function_at_paths(
270 &video_renderer_paths,
271 parse_watch_playlist_track_from_video_renderer,
272 )?
273}
274
275fn parse_watch_playlist_track_from_video_renderer<C: JsonCrawler>(
276 mut video_renderer: C::BorrowTo<'_>,
277) -> Result<WatchPlaylistTrack> {
278 let title = video_renderer.take_value_pointer(TITLE_TEXT)?;
279 let author = video_renderer.take_value_pointer(concatcp!("/shortBylineText", RUN_TEXT))?;
280 let duration = video_renderer.take_value_pointer(concatcp!("/lengthText", RUN_TEXT))?;
281 let video_id = video_renderer.take_value_pointer(NAVIGATION_VIDEO_ID)?;
282 let thumbnails = video_renderer.take_value_pointer(THUMBNAIL)?;
283 Ok(WatchPlaylistTrack {
284 title,
285 author,
286 duration,
287 thumbnails,
288 video_id,
289 })
290}
291
292pub(crate) fn parse_playlist_song(
293 title: String,
294 track_no: usize,
295 mut data: impl JsonCrawler,
296) -> Result<PlaylistSong> {
297 let video_id = data.take_value_pointer(concatcp!(
298 PLAY_BUTTON,
299 "/playNavigationEndpoint",
300 WATCH_VIDEO_ID
301 ))?;
302 let library_management =
303 parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
304 let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
305 let artists = super::parse_song_artists(&mut data, 1)?;
306 let album_col_idx = if data.path_exists("/flexColumns/3") {
311 3
312 } else {
313 2
314 };
315 let album = super::parse_song_album(&mut data, album_col_idx)?;
316 let duration = data
317 .borrow_pointer(fixed_column_item_pointer(0))?
318 .take_value_pointers(&["/text/simpleText", "/text/runs/0/text"])?;
319 let thumbnails = data.take_value_pointer(THUMBNAILS)?;
320 let is_available = data
321 .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
322 .map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
323 .unwrap_or(true);
324
325 let explicit = if data.path_exists(BADGE_LABEL) {
326 Explicit::IsExplicit
327 } else {
328 Explicit::NotExplicit
329 };
330 let playlist_id = data.take_value_pointer(concatcp!(
331 MENU_ITEMS,
332 "/0/menuNavigationItemRenderer",
333 NAVIGATION_PLAYLIST_ID
334 ))?;
335 Ok(PlaylistSong {
336 video_id,
337 track_no,
338 duration,
339 library_management,
340 title,
341 artists,
342 like_status,
343 thumbnails,
344 explicit,
345 album,
346 playlist_id,
347 is_available,
348 })
349}
350pub(crate) fn parse_playlist_upload_song(
351 title: String,
352 track_no: usize,
353 mut data: impl JsonCrawler,
354) -> Result<PlaylistUploadSong> {
355 let duration = data
356 .borrow_pointer(fixed_column_item_pointer(0))?
357 .take_value_pointer(TEXT_RUN_TEXT)?;
358 let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
359 let video_id = data.take_value_pointer(concatcp!(
360 PLAY_BUTTON,
361 "/playNavigationEndpoint/watchEndpoint/videoId"
362 ))?;
363 let thumbnails = data.take_value_pointer(THUMBNAILS)?;
364 let artists = parse_upload_song_artists(data.borrow_mut(), 1)?;
365 let album = parse_upload_song_album(data.borrow_mut(), 2)?;
366 let mut menu = data.navigate_pointer(MENU_ITEMS)?;
367 let entity_id = menu
368 .try_iter_mut()?
369 .find_path(DELETION_ENTITY_ID)?
370 .take_value()?;
371 Ok(PlaylistUploadSong {
372 entity_id,
373 video_id,
374 album,
375 duration,
376 like_status,
377 title,
378 artists,
379 thumbnails,
380 track_no,
381 })
382}
383
384pub(crate) fn parse_playlist_episode(
385 title: String,
386 track_no: usize,
387 mut data: impl JsonCrawler,
388) -> Result<PlaylistEpisode> {
389 let video_id = data.take_value_pointer(concatcp!(
390 PLAY_BUTTON,
391 "/playNavigationEndpoint",
392 WATCH_VIDEO_ID
393 ))?;
394 let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
395 let is_live = data.path_exists(LIVE_BADGE_LABEL);
396 let (duration, date) = match is_live {
397 true => (EpisodeDuration::Live, EpisodeDate::Live),
398 false => {
399 let date = parse_flex_column_item(&mut data, 2, 0)?;
400 let duration =
401 data.borrow_pointer(fixed_column_item_pointer(0))
402 .and_then(|mut i| {
403 i.take_value_pointer("/text/simpleText")
404 .or_else(|_| i.take_value_pointer("/text/runs/0/text"))
405 })?;
406 (
407 EpisodeDuration::Recorded { duration },
408 EpisodeDate::Recorded { date },
409 )
410 }
411 };
412 let podcast_name = parse_flex_column_item(&mut data, 1, 0)?;
413 let podcast_id = data
414 .borrow_pointer(flex_column_item_pointer(1))?
415 .take_value_pointer(concatcp!(TEXT_RUN, NAVIGATION_BROWSE_ID))?;
416 let thumbnails = data.take_value_pointer(THUMBNAILS)?;
417 let is_available = data
418 .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
419 .map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
420 .unwrap_or(true);
421 Ok(PlaylistEpisode {
422 episode_id: video_id,
423 duration,
424 title,
425 like_status,
426 thumbnails,
427 date,
428 podcast_name,
429 podcast_id,
430 is_available,
431 track_no,
432 })
433}
434pub(crate) fn parse_playlist_video(
435 title: String,
436 track_no: usize,
437 mut data: impl JsonCrawler,
438) -> Result<PlaylistVideo> {
439 let video_id = data.take_value_pointer(concatcp!(
440 PLAY_BUTTON,
441 "/playNavigationEndpoint",
442 WATCH_VIDEO_ID
443 ))?;
444 let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
445 let channel_name = parse_flex_column_item(&mut data, 1, 0)?;
446 let channel_id = data
447 .borrow_pointer(flex_column_item_pointer(1))?
448 .take_value_pointer(concatcp!(TEXT_RUN, NAVIGATION_BROWSE_ID))?;
449 let duration = data
450 .borrow_pointer(fixed_column_item_pointer(0))?
451 .take_value_pointers(&["/text/simpleText", "/text/runs/0/text"])?;
452 let thumbnails = data.take_value_pointer(THUMBNAILS)?;
453 let is_available = data
454 .take_value_pointer::<String>("/musicItemRendererDisplayPolicy")
455 .map(|m| m != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")
456 .unwrap_or(true);
457
458 let playlist_id = data.take_value_pointer(concatcp!(
459 MENU_ITEMS,
460 "/0/menuNavigationItemRenderer",
461 NAVIGATION_PLAYLIST_ID
462 ))?;
463 Ok(PlaylistVideo {
464 video_id,
465 track_no,
466 duration,
467 title,
468 like_status,
469 thumbnails,
470 playlist_id,
471 is_available,
472 channel_name,
473 channel_id,
474 })
475}
476
477pub(crate) fn parse_playlist_item(
478 track_no: usize,
479 mut json: impl JsonCrawler,
480) -> Result<Option<PlaylistItem>> {
481 let Ok(mut data) = json.borrow_pointer(MRLIR) else {
482 return Ok(None);
483 };
484 let title = super::parse_flex_column_item(&mut data, 0, 0)?;
485 if title == "Song deleted" {
486 return Ok(None);
487 }
488 let video_type_path = concatcp!(
489 PLAY_BUTTON,
490 "/playNavigationEndpoint",
491 NAVIGATION_VIDEO_TYPE
492 );
493 let video_type: YoutubeMusicVideoType = data.take_value_pointer(video_type_path)?;
494 let item = match video_type {
496 YoutubeMusicVideoType::Ugc
497 | YoutubeMusicVideoType::Omv
498 | YoutubeMusicVideoType::Shoulder => Some(PlaylistItem::Video(parse_playlist_video(
499 title, track_no, data,
500 )?)),
501 YoutubeMusicVideoType::Atv => Some(PlaylistItem::Song(parse_playlist_song(
502 title, track_no, data,
503 )?)),
504 YoutubeMusicVideoType::Upload => Some(PlaylistItem::UploadSong(
505 parse_playlist_upload_song(title, track_no, data)?,
506 )),
507 YoutubeMusicVideoType::Episode => Some(PlaylistItem::Episode(parse_playlist_episode(
508 title, track_no, data,
509 )?)),
510 };
511 Ok(item)
512}
513pub(crate) fn parse_playlist_items<C>(
515 json: C,
516) -> Result<(Vec<PlaylistItem>, Option<ContinuationParams<'static>>)>
517where
518 C: JsonCrawler,
519 C::IntoIter: DoubleEndedIterator,
520{
521 let mut items = json.try_into_iter()?;
522 let mut last_item = items.next_back();
523 let continuation_params = last_item.as_mut().and_then(|ref mut last_item| {
524 last_item
525 .take_value_pointer(CONTINUATION_RENDERER_COMMAND)
526 .ok()
527 });
528 let items = items
529 .chain(last_item)
530 .enumerate()
531 .filter_map(|(idx, item)| parse_playlist_item(idx + 1, item).transpose())
532 .collect::<Result<_>>()?;
533 Ok((items, continuation_params))
534}
535
536fn get_playlist_details(json_crawler: JsonCrawlerOwned) -> Result<GetPlaylistDetails> {
538 let mut columns = json_crawler.navigate_pointer(TWO_COLUMN)?;
539 let header =
540 columns.borrow_pointer(concatcp!(TAB_CONTENT, SECTION_LIST_ITEM, RESPONSIVE_HEADER));
541 let mut header = match header {
543 Ok(header) => header,
544 Err(_) => columns.borrow_pointer(concatcp!(
545 TAB_CONTENT,
546 SECTION_LIST_ITEM,
547 "/musicEditablePlaylistDetailHeaderRenderer/header",
548 RESPONSIVE_HEADER
549 ))?,
550 };
551 let title = header.take_value_pointer(TITLE_TEXT)?;
552 let author = header.take_value_pointers(&[STRAPLINE_TEXT, FACEPILE_TEXT])?;
554 let thumbnails: Vec<Thumbnail> = header.take_value_pointer(THUMBNAILS)?;
555 let author_avatar_url: Option<String> = header.take_value_pointer(FACEPILE_AVATAR_URL).ok();
556 let description = header
557 .borrow_pointer(DESCRIPTION_SHELF_RUNS)
558 .and_then(|d| d.try_into_iter())
559 .ok()
560 .map(|r| {
561 r.map(|mut r| r.take_value_pointer::<String>("/text"))
562 .collect::<std::result::Result<String, _>>()
563 })
564 .transpose()?;
565 let mut subtitle = header.borrow_pointer("/subtitle/runs")?;
566 let subtitle_len = subtitle.try_iter_mut()?.len();
567 let privacy = if subtitle_len == 5 {
568 Some(subtitle.take_value_pointer("/2/text")?)
569 } else {
570 None
571 };
572 let year = subtitle.take_value_pointer(format!("/{}/text", subtitle_len.saturating_sub(1)))?;
573 let mut second_subtitle_runs = header.borrow_pointer(SECOND_SUBTITLE_RUNS)?;
574 let duration = second_subtitle_runs
575 .try_iter_mut()?
576 .try_last()?
577 .take_value_pointer("/text")?;
578 let track_count_text = second_subtitle_runs.try_expect(
579 "second subtitle runs should count at least 3 runs",
580 |second_subtitle_runs| {
581 second_subtitle_runs
582 .try_iter_mut()?
583 .rev()
584 .nth(2)
585 .map(|mut run| run.take_value_pointer("/text"))
586 .transpose()
587 },
588 )?;
589 let views = second_subtitle_runs
590 .try_iter_mut()?
591 .rev()
592 .nth(4)
593 .map(|mut item| item.take_value_pointer("/text"))
594 .transpose()?;
595 let id = header
596 .navigate_pointer("/buttons")?
597 .try_into_iter()?
598 .find_path("/musicPlayButtonRenderer")?
599 .take_value_pointer("/playNavigationEndpoint/watchEndpoint/playlistId")?;
600 Ok(GetPlaylistDetails {
601 id,
602 privacy,
603 title,
604 description,
605 author,
606 year,
607 duration,
608 track_count_text,
609 thumbnails,
610 views,
611 author_avatar_url,
612 })
613}
614
615#[cfg(test)]
616mod tests {
617 use crate::auth::BrowserToken;
618 use crate::common::{ApiOutcome, PlaylistID, VideoID, YoutubeID};
619 use crate::query::playlist::GetPlaylistDetailsQuery;
620 use crate::query::{
621 AddPlaylistItemsQuery, EditPlaylistQuery, GetPlaylistTracksQuery, GetWatchPlaylistQuery,
622 };
623 use crate::{Error, process_json};
624 use pretty_assertions::assert_eq;
625 use std::path::Path;
626
627 #[tokio::test]
628 async fn test_add_playlist_items_query_failure() {
629 let source_path = Path::new("./test_json/add_playlist_items_failure_20240626.json");
630 let source = tokio::fs::read_to_string(source_path)
631 .await
632 .expect("Expect file read to pass during tests");
633 let query = AddPlaylistItemsQuery::new_from_playlist(
635 PlaylistID::from_raw(""),
636 PlaylistID::from_raw(""),
637 );
638 let output = process_json::<_, BrowserToken>(source, query);
639 let err: crate::Result<()> = Err(Error::status_failed());
640 assert_eq!(format!("{:?}", err), format!("{:?}", output));
641 }
642 #[tokio::test]
643 async fn test_add_playlist_items_query() {
644 parse_test!(
645 "./test_json/add_playlist_items_20240626.json",
646 "./test_json/add_playlist_items_20240626_output.txt",
647 AddPlaylistItemsQuery::new_from_playlist(
648 PlaylistID::from_raw(""),
649 PlaylistID::from_raw(""),
650 ),
651 BrowserToken
652 );
653 }
654 #[tokio::test]
655 async fn test_edit_playlist_title_query() {
656 parse_test_value!(
657 "./test_json/edit_playlist_title_20240626.json",
658 ApiOutcome::Success,
659 EditPlaylistQuery::new_title(PlaylistID::from_raw(""), ""),
660 BrowserToken
661 );
662 }
663 #[tokio::test]
664 async fn test_get_playlist_details_query_2024() {
665 parse_test!(
666 "./test_json/get_playlist_20240624.json",
667 "./test_json/get_playlist_details_20240624_output.txt",
668 GetPlaylistDetailsQuery::new(PlaylistID::from_raw("")),
669 BrowserToken
670 );
671 }
672 #[tokio::test]
673 async fn test_get_playlist_details_query_2025() {
675 parse_test!(
676 "./test_json/get_playlist_20250604.json",
677 "./test_json/get_playlist_details_20250604_output.txt",
678 GetPlaylistDetailsQuery::new(PlaylistID::from_raw("")),
679 BrowserToken
680 );
681 }
682 #[tokio::test]
683 async fn test_get_playlist_details_query_2024_no_channel_thumbnail() {
684 parse_test!(
685 "./test_json/get_playlist_no_channel_thumbnail_20240818.json",
686 "./test_json/get_playlist_details_no_channel_thumbnail_20240818_output.txt",
687 GetPlaylistDetailsQuery::new(PlaylistID::from_raw("")),
688 BrowserToken
689 );
690 }
691 #[tokio::test]
692 async fn test_get_playlist_tracks_query() {
693 parse_with_matching_continuation_test!(
694 "./test_json/get_playlist_20250604.json",
695 "./test_json/get_playlist_continuation_20250604.json",
696 "./test_json/get_playlist_tracks_20250604_output.txt",
697 GetPlaylistTracksQuery::new(PlaylistID::from_raw("")),
698 BrowserToken
699 );
700 }
701 #[tokio::test]
702 async fn test_get_watch_playlist_query() {
703 parse_with_matching_continuation_test!(
704 "./test_json/get_watch_playlist_20250630.json",
705 "./test_json/get_watch_playlist_continuation_20250630.json",
706 "./test_json/get_watch_playlist_20250630_output.txt",
707 GetWatchPlaylistQuery::new_from_video_id(VideoID::from_raw("")),
708 BrowserToken
709 );
710 }
711}