ytmapi_rs/parse/
upload.rs

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)]
25// Intentionally not marked non_exhaustive - not expecting this to change.
26pub struct ParsedUploadArtist {
27    pub name: String,
28    pub id: Option<UploadArtistID<'static>>,
29}
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31// Intentionally not marked non_exhaustive - not expecting this to change.
32pub struct ParsedUploadSongAlbum {
33    pub name: String,
34    pub id: UploadAlbumID<'static>,
35}
36
37#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
38#[non_exhaustive]
39// May need to be enum to track 'Not Available' case.
40pub 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    // Year appears to be optional.
57    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]
87// May need to be enum to track 'Not Available' case.
88pub 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        // Passing an invalid entity ID with will throw a 400 error which
283        // is caught by AuthToken.
284        // NOTE: Passing the same entity id for deletion multiple times
285        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}