1use super::search::SearchResultVideo;
2use super::{
3 ParseFrom, ParsedSongAlbum, ParsedSongArtist, ProcessedResult, Thumbnail,
4 parse_flex_column_item, parse_song_album, parse_song_artists,
5};
6use crate::Result;
7use crate::common::{
8 AlbumID, AlbumType, ArtistChannelID, BrowseParams, Explicit, LibraryManager, LibraryStatus,
9 LikeStatus, PlaylistID, VideoID,
10};
11use crate::nav_consts::*;
12use crate::query::*;
13use const_format::concatcp;
14use json_crawler::{JsonCrawler, JsonCrawlerIterator, JsonCrawlerOwned};
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18#[non_exhaustive]
19pub struct GetArtist {
20 pub description: Option<String>,
21 pub views: Option<String>,
22 pub name: String,
23 pub channel_id: ArtistChannelID<'static>,
24 pub shuffle_id: Option<String>,
25 pub radio_id: Option<String>,
26 pub subscribers: Option<String>,
27 pub subscribed: bool,
28 pub thumbnails: Vec<Thumbnail>,
29 pub top_releases: GetArtistTopReleases,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[non_exhaustive]
34pub struct GetArtistAlbumsAlbum {
35 pub title: String,
36 pub playlist_id: Option<String>,
38 pub browse_id: AlbumID<'static>,
40 pub category: Option<String>, pub thumbnails: Vec<Thumbnail>,
42 pub year: Option<String>,
43}
44
45impl<'a> ParseFrom<GetArtistQuery<'a>> for GetArtist {
46 fn parse_from(p: ProcessedResult<GetArtistQuery<'a>>) -> crate::Result<Self> {
47 let mut json_crawler: JsonCrawlerOwned = p.into();
48 let mut results =
49 json_crawler.borrow_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST))?;
50 let mut maybe_description_shelf = results.try_iter_mut()?.find_path(DESCRIPTION_SHELF).ok();
51 let description = maybe_description_shelf
52 .as_mut()
53 .map(|description_shelf| description_shelf.take_value_pointer(DESCRIPTION))
54 .transpose()?;
55 let views = maybe_description_shelf.and_then(|mut description_shelf| {
56 description_shelf
57 .take_value_pointer(concatcp!("/subheader", RUN_TEXT))
58 .ok()
59 });
60 let top_releases = parse_artist_top_releases_from_section_list_contents(results)?;
61 let mut header = json_crawler.navigate_pointer("/header/musicImmersiveHeaderRenderer")?;
62 let name = header.take_value_pointer(TITLE_TEXT)?;
63 let shuffle_id = header
64 .take_value_pointer(concatcp!(
65 "/playButton/buttonRenderer",
66 NAVIGATION_PLAYLIST_ID
67 ))
68 .ok();
69 let radio_id = header
70 .take_value_pointer(concatcp!(
71 "/startRadioButton/buttonRenderer",
72 NAVIGATION_PLAYLIST_ID
73 ))
74 .ok();
75 let thumbnails = header.take_value_pointer(THUMBNAILS)?;
76 let mut subscription_button =
77 header.navigate_pointer("/subscriptionButton/subscribeButtonRenderer")?;
78 let channel_id = subscription_button.take_value_pointer("/channelId")?;
79 let subscribers = subscription_button
80 .take_value_pointer("/subscriberCountText/runs/0/text")
81 .ok();
82 let subscribed = subscription_button.take_value_pointer("/subscribed")?;
83 Ok(GetArtist {
84 views,
85 description,
86 name,
87 top_releases,
88 thumbnails,
89 subscribed,
90 radio_id,
91 channel_id,
92 shuffle_id,
93 subscribers,
94 })
95 }
96}
97
98impl ParseFrom<SubscribeArtistQuery<'_>> for () {
99 fn parse_from(p: ProcessedResult<SubscribeArtistQuery<'_>>) -> crate::Result<Self> {
100 let json_crawler: JsonCrawlerOwned = p.into();
101 json_crawler
103 .navigate_pointer("/actions")?
104 .try_into_iter()?
105 .find_path("/addToToastAction")?
106 .navigate_pointer("/item/notificationTextRenderer/successResponseText")?;
107 Ok(())
108 }
109}
110impl ParseFrom<UnsubscribeArtistsQuery<'_>> for () {
111 fn parse_from(p: ProcessedResult<UnsubscribeArtistsQuery<'_>>) -> crate::Result<Self> {
112 let json_crawler: JsonCrawlerOwned = p.into();
113 json_crawler
115 .navigate_pointer("/actions")?
116 .try_into_iter()?
117 .find_path("/updateSubscribeButtonAction")?
118 .navigate_pointer("/subscribed")?;
119 Ok(())
120 }
121}
122
123#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
124#[non_exhaustive]
125pub struct GetArtistTopReleases {
126 pub songs: Option<GetArtistSongs>,
127 pub albums: Option<GetArtistAlbums>,
128 pub singles: Option<GetArtistAlbums>,
129 pub videos: Option<GetArtistVideos>,
130 pub related: Option<GetArtistRelated>,
131}
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
133#[non_exhaustive]
134pub struct GetArtistRelated {
135 pub results: Vec<RelatedResult>,
136}
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138#[non_exhaustive]
139pub struct GetArtistSongs {
140 pub results: Vec<ArtistSong>,
141 pub browse_id: PlaylistID<'static>,
142}
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
144#[non_exhaustive]
145pub struct ArtistSong {
146 pub video_id: VideoID<'static>,
147 pub plays: String,
148 pub album: ParsedSongAlbum,
149 pub artists: Vec<ParsedSongArtist>,
150 pub library_management: Option<LibraryManager>,
154 pub title: String,
155 pub like_status: LikeStatus,
156 pub explicit: Explicit,
157}
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
159#[non_exhaustive]
160pub struct GetArtistVideos {
161 pub results: Vec<SearchResultVideo>,
162 pub browse_id: PlaylistID<'static>,
163}
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169#[non_exhaustive]
170pub struct GetArtistAlbums {
171 pub results: Vec<AlbumResult>,
172 pub browse_id: Option<ArtistChannelID<'static>>,
174 pub params: Option<BrowseParams<'static>>,
175}
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177#[non_exhaustive]
178pub struct RelatedResult {
179 pub browse_id: ArtistChannelID<'static>,
180 pub title: String,
181 pub subscribers: String,
182}
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184#[non_exhaustive]
185pub struct AlbumResult {
186 pub title: String,
187 #[deprecated = "Future deprecation see https://github.com/nick42d/youtui/issues/211"]
188 pub album_type: Option<AlbumType>,
189 pub year: String,
190 pub album_id: AlbumID<'static>,
191 pub library_status: LibraryStatus,
192 pub thumbnails: Vec<Thumbnail>,
193 pub explicit: Explicit,
194}
195
196#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
197#[non_exhaustive]
198pub struct TableListSong {
202 pub video_id: VideoID<'static>,
203 pub album: ParsedSongAlbum,
204 pub duration: String,
205 pub library_management: Option<LibraryManager>,
208 pub title: String,
209 pub artists: Vec<super::ParsedSongArtist>,
210 pub like_status: LikeStatus,
212 pub thumbnails: Vec<Thumbnail>,
213 pub explicit: Explicit,
214 pub is_available: bool,
215 pub playlist_id: PlaylistID<'static>,
217}
218
219#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
221enum ArtistTopReleaseCategory {
222 #[serde(alias = "albums")]
223 Albums,
224 #[serde(alias = "singles")]
225 Singles,
226 #[serde(alias = "videos")]
227 Videos,
228 #[serde(alias = "playlists")]
229 Playlists,
230 #[serde(alias = "fans might also like")]
231 Related,
232 #[serde(other)]
233 None,
234}
235
236fn parse_artist_song(mut json: impl JsonCrawler) -> Result<ArtistSong> {
237 let mut data = json.borrow_pointer(MRLIR)?;
238 let title = parse_flex_column_item(&mut data, 0, 0)?;
239 let plays = parse_flex_column_item(&mut data, 2, 0)?;
240 let artists = parse_song_artists(&mut data, 1)?;
241 let album = parse_song_album(&mut data, 3)?;
242 let video_id = data.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
243 let explicit = if data.path_exists(BADGE_LABEL) {
244 Explicit::IsExplicit
245 } else {
246 Explicit::NotExplicit
247 };
248 let like_status = data.take_value_pointer(MENU_LIKE_STATUS)?;
249 let library_management =
250 parse_library_management_items_from_menu(data.borrow_pointer(MENU_ITEMS)?)?;
251 Ok(ArtistSong {
252 video_id,
253 plays,
254 album,
255 artists,
256 library_management,
257 title,
258 like_status,
259 explicit,
260 })
261}
262fn parse_artist_songs(mut json: impl JsonCrawler) -> Result<GetArtistSongs> {
263 let browse_id = json.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
265 let results = json
266 .borrow_pointer("/contents")?
267 .try_into_iter()?
268 .map(parse_artist_song)
269 .collect::<Result<Vec<ArtistSong>>>()?;
270 Ok(GetArtistSongs { results, browse_id })
271}
272#[allow(clippy::field_reassign_with_default)]
275fn parse_artist_top_releases_from_section_list_contents(
276 mut contents: impl JsonCrawler,
277) -> Result<GetArtistTopReleases> {
278 let mut top_releases = GetArtistTopReleases::default();
279 top_releases.songs = contents
280 .borrow_pointer(concatcp!("/0", MUSIC_SHELF))
281 .ok()
282 .map(parse_artist_songs)
283 .transpose()?;
284 for mut r in contents
290 .try_iter_mut()
291 .into_iter()
292 .flatten()
293 .filter_map(|r| r.navigate_pointer("/musicCarouselShelfRenderer").ok())
294 {
295 let category = r.take_value_pointer(concatcp!(CAROUSEL_TITLE, "/text"))?;
297 let browse_id: Option<ArtistChannelID> = r
300 .take_value_pointer(concatcp!(CAROUSEL_TITLE, NAVIGATION_BROWSE_ID))
301 .ok();
302 let params = r
305 .take_value_pointer(concatcp!(
306 CAROUSEL_TITLE,
307 "/navigationEndpoint/browseEndpoint/params"
308 ))
309 .ok();
310 match category {
312 ArtistTopReleaseCategory::Related => (),
313 ArtistTopReleaseCategory::Videos => (),
314 ArtistTopReleaseCategory::Singles => (),
315 ArtistTopReleaseCategory::Albums => {
316 let mut results = Vec::new();
317 for i in r.navigate_pointer("/contents")?.try_iter_mut()? {
318 results.push(parse_album_from_mtrir(i.navigate_pointer(MTRIR)?)?);
319 }
320 let albums = GetArtistAlbums {
321 browse_id,
322 params,
323 results,
324 };
325 top_releases.albums = Some(albums);
326 }
327 ArtistTopReleaseCategory::Playlists => (),
328 ArtistTopReleaseCategory::None => (),
329 }
330 }
331 Ok(top_releases)
332}
333
334pub(crate) fn parse_album_from_mtrir(mut navigator: impl JsonCrawler) -> Result<AlbumResult> {
336 let title = navigator.take_value_pointer(TITLE_TEXT)?;
337
338 let (year, album_type) = match navigator.take_value_pointer(SUBTITLE2) {
339 Ok(subtitle2) => {
340 ab_warn!();
342 (subtitle2, navigator.take_value_pointer(SUBTITLE)?)
343 }
344 Err(_) => (navigator.take_value_pointer(SUBTITLE)?, None),
345 };
346
347 let album_id = navigator.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
348 let thumbnails = navigator.take_value_pointer(THUMBNAIL_RENDERER)?;
349 let explicit = if navigator.path_exists(concatcp!(SUBTITLE_BADGE_LABEL)) {
350 Explicit::IsExplicit
351 } else {
352 Explicit::NotExplicit
353 };
354 let mut library_menu = navigator
355 .borrow_pointer(MENU_ITEMS)?
356 .try_into_iter()?
357 .find_path("/toggleMenuServiceItemRenderer")?;
358 let library_status = library_menu.take_value_pointer("/defaultIcon/iconType")?;
359 Ok(AlbumResult {
360 title,
361 album_type,
362 year,
363 album_id,
364 library_status,
365 thumbnails,
366 explicit,
367 })
368}
369
370pub(crate) fn parse_library_management_items_from_menu(
371 menu: impl JsonCrawler,
372) -> Result<Option<LibraryManager>> {
373 let Some((status, add_to_library_token, remove_from_library_token)) = menu
374 .try_into_iter()?
375 .filter_map(|menu_item| {
376 menu_item
377 .navigate_pointer("/toggleMenuServiceItemRenderer")
378 .ok()
379 })
380 .filter_map(|mut toggle_menu| {
381 let Ok(status) = toggle_menu.take_value_pointer("/defaultIcon/iconType") else {
382 return None;
387 };
388 if let Ok("Sign in") = toggle_menu
389 .take_value_pointer::<String>(DEFAULT_ENDPOINT_MODAL_TEXT)
390 .as_deref()
391 {
392 return None;
397 }
398 let (add_to_library_token, remove_from_library_token) = match status {
399 LibraryStatus::InLibrary => (
400 toggle_menu.take_value_pointer(TOGGLED_ENDPOINT),
401 toggle_menu.take_value_pointer(DEFAULT_ENDPOINT),
402 ),
403 LibraryStatus::NotInLibrary => (
404 toggle_menu.take_value_pointer(DEFAULT_ENDPOINT),
405 toggle_menu.take_value_pointer(TOGGLED_ENDPOINT),
406 ),
407 };
408 Some((status, add_to_library_token, remove_from_library_token))
409 })
410 .next()
411 else {
412 return Ok(None);
414 };
415 Ok(Some(LibraryManager {
416 status,
417 add_to_library_token: add_to_library_token?,
418 remove_from_library_token: remove_from_library_token?,
419 }))
420}
421
422impl<'a> ParseFrom<GetArtistAlbumsQuery<'a>> for Vec<GetArtistAlbumsAlbum> {
423 fn parse_from(p: ProcessedResult<GetArtistAlbumsQuery<'a>>) -> crate::Result<Self> {
424 let json_crawler: JsonCrawlerOwned = p.into();
425 let mut albums = Vec::new();
426 let mut json_crawler = json_crawler.navigate_pointer(concatcp!(
427 SINGLE_COLUMN_TAB,
428 SECTION_LIST_ITEM,
429 GRID_ITEMS
430 ))?;
431 for mut r in json_crawler
432 .borrow_mut()
433 .try_into_iter()?
434 .flat_map(|i| i.navigate_pointer(MTRIR))
435 {
436 let browse_id = r.take_value_pointer(concatcp!(TITLE, NAVIGATION_BROWSE_ID))?;
437 let playlist_id = r.take_value_pointer(MENU_PLAYLIST_ID).ok();
438 let title = r.take_value_pointer(TITLE_TEXT)?;
439 let thumbnails = r.take_value_pointer(THUMBNAIL_RENDERER)?;
440 let category = r.take_value_pointer(SUBTITLE).ok();
442 albums.push(GetArtistAlbumsAlbum {
443 browse_id,
444 year: None,
445 title,
446 category,
447 thumbnails,
448 playlist_id,
449 });
450 }
451 Ok(albums)
452 }
453}
454#[cfg(test)]
455mod tests {
456 use crate::auth::BrowserToken;
457 use crate::common::{ArtistChannelID, BrowseParams, YoutubeID};
458 use crate::query::GetArtistAlbumsQuery;
459
460 #[tokio::test]
461 async fn test_get_artist_albums_query() {
462 parse_test!(
463 "./test_json/browse_artist_albums.json",
465 "./test_json/browse_artist_albums_output.txt",
466 GetArtistAlbumsQuery::new(ArtistChannelID::from_raw(""), BrowseParams::from_raw("")),
467 BrowserToken
468 );
469 }
470
471 #[tokio::test]
473 async fn test_get_artist_old_1() {
474 parse_test!(
475 "./test_json/get_artist_20240705.json",
476 "./test_json/get_artist_20240705_output.txt",
477 crate::query::GetArtistQuery::new(ArtistChannelID::from_raw("")),
478 BrowserToken
479 );
480 }
481
482 #[tokio::test]
483 async fn test_get_artist() {
484 parse_test!(
485 "./test_json/get_artist_20250310.json",
486 "./test_json/get_artist_20250310_output.txt",
487 crate::query::GetArtistQuery::new(ArtistChannelID::from_raw("")),
488 BrowserToken
489 );
490 }
491 #[tokio::test]
492 async fn test_subscribe_artists() {
493 parse_test_value!(
494 "./test_json/subscribe_artist_20250704.json",
495 (),
496 crate::query::SubscribeArtistQuery::new(ArtistChannelID::from_raw("")),
497 BrowserToken
498 );
499 }
500 #[tokio::test]
501 async fn test_unsubscribe_artists() {
502 parse_test_value!(
503 "./test_json/unsubscribe_artists_20250704.json",
504 (),
505 crate::query::UnsubscribeArtistsQuery::new([]),
506 BrowserToken
507 );
508 }
509}