Skip to main content

ytmapi_rs/parse/
user.rs

1use super::ParseFrom;
2use crate::Result;
3use crate::common::{PlaylistID, Thumbnail, UserPlaylistsParams, UserVideosParams, VideoID};
4use crate::nav_consts::{
5    CAROUSEL, CAROUSEL_TITLE, FOREGROUND_THUMBNAIL_RENDERER, GRID_ITEMS, MTRIR, NAVIGATION_BROWSE,
6    NAVIGATION_BROWSE_ID, NAVIGATION_VIDEO_ID, SECTION_LIST, SECTION_LIST_ITEM, SINGLE_COLUMN_TAB,
7    SUBTITLE2, SUBTITLE3, THUMBNAIL_RENDERER, TITLE_TEXT, VISUAL_HEADER,
8};
9use crate::query::{GetUserPlaylistsQuery, GetUserQuery, GetUserVideosQuery};
10use const_format::concatcp;
11use json_crawler::{JsonCrawler, JsonCrawlerOwned};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct GetUser {
18    pub name: String,
19    pub videos: Vec<UserVideo>,
20    pub thumbnails: Vec<Thumbnail>,
21    pub all_videos_params: Option<UserVideosParams<'static>>,
22    pub playlists: Vec<UserPlaylist>,
23    pub all_playlists_params: Option<UserPlaylistsParams<'static>>,
24}
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[non_exhaustive]
27pub struct UserVideo {
28    pub title: String,
29    pub views: String,
30    pub thumbnails: Vec<Thumbnail>,
31    pub id: VideoID<'static>,
32}
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct UserPlaylist {
36    pub title: String,
37    pub views: String,
38    pub thumbnails: Vec<Thumbnail>,
39    pub id: PlaylistID<'static>,
40}
41
42impl ParseFrom<GetUserQuery<'_>> for GetUser {
43    fn parse_from(p: super::ProcessedResult<GetUserQuery>) -> Result<Self> {
44        let mut json_crawler: JsonCrawlerOwned = p.into();
45        let mut header = json_crawler.borrow_pointer(VISUAL_HEADER)?;
46        let name = header.take_value_pointer(TITLE_TEXT)?;
47        let thumbnails = header.take_value_pointer(FOREGROUND_THUMBNAIL_RENDERER)?;
48        let contents = json_crawler.navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST))?;
49        // TODO: i18n
50        let mut carousels: HashMap<String, _> = contents
51            .try_into_iter()?
52            .map(|crawler| {
53                let mut carousel = crawler.navigate_pointer(CAROUSEL)?;
54                let title = carousel.take_value_pointer(concatcp!(CAROUSEL_TITLE, "/text"))?;
55                Ok((title, carousel))
56            })
57            .collect::<Result<_>>()?;
58        let mut maybe_playlists_carousel = carousels.get_mut("Playlists");
59        let all_playlists_params =
60            maybe_playlists_carousel
61                .as_mut()
62                .and_then(|playlists_carousel| {
63                    playlists_carousel
64                        .take_value_pointer(concatcp!(CAROUSEL_TITLE, NAVIGATION_BROWSE, "/params"))
65                        .ok()
66                });
67        let playlists = match maybe_playlists_carousel {
68            Some(playlists_carousel) => playlists_carousel
69                .borrow_pointer("/contents")?
70                .try_into_iter()?
71                .map(parse_user_playlist)
72                .collect::<Result<_>>()?,
73            None => vec![],
74        };
75        let mut maybe_videos_carousel = carousels.get_mut("Videos");
76        let all_videos_params = maybe_videos_carousel.as_mut().and_then(|videos_carousel| {
77            videos_carousel
78                .take_value_pointer(concatcp!(CAROUSEL_TITLE, NAVIGATION_BROWSE, "/params"))
79                .ok()
80        });
81        let videos = match maybe_videos_carousel {
82            Some(videos_carousel) => videos_carousel
83                .borrow_pointer("/contents")?
84                .try_into_iter()?
85                .map(parse_user_video)
86                .collect::<Result<_>>()?,
87            None => vec![],
88        };
89
90        Ok(Self {
91            name,
92            thumbnails,
93            all_videos_params,
94            playlists,
95            videos,
96            all_playlists_params,
97        })
98    }
99}
100impl ParseFrom<GetUserPlaylistsQuery<'_>> for Vec<UserPlaylist> {
101    fn parse_from(p: super::ProcessedResult<GetUserPlaylistsQuery>) -> Result<Self> {
102        let json_crawler: JsonCrawlerOwned = p.into();
103        let results = json_crawler
104            .navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST_ITEM, GRID_ITEMS))?
105            .try_into_iter()?
106            .map(parse_user_playlist)
107            .collect::<Result<_>>()?;
108        Ok(results)
109    }
110}
111impl ParseFrom<GetUserVideosQuery<'_>> for Vec<UserVideo> {
112    fn parse_from(p: super::ProcessedResult<GetUserVideosQuery>) -> Result<Self> {
113        let json_crawler: JsonCrawlerOwned = p.into();
114        let results = json_crawler
115            .navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST_ITEM, GRID_ITEMS))?
116            .try_into_iter()?
117            .map(parse_user_video)
118            .collect::<Result<_>>()?;
119        Ok(results)
120    }
121}
122fn parse_user_video(c: impl JsonCrawler) -> Result<UserVideo> {
123    let mut item = c.navigate_pointer(MTRIR)?;
124    let title = item.take_value_pointer(TITLE_TEXT)?;
125    let views = item.take_value_pointer(SUBTITLE2)?;
126    let id = item.take_value_pointer(NAVIGATION_VIDEO_ID)?;
127    let thumbnails = item.take_value_pointer(THUMBNAIL_RENDERER)?;
128    Ok(UserVideo {
129        title,
130        views,
131        thumbnails,
132        id,
133    })
134}
135fn parse_user_playlist(c: impl JsonCrawler) -> Result<UserPlaylist> {
136    let mut item = c.navigate_pointer(MTRIR)?;
137    let title = item.take_value_pointer(TITLE_TEXT)?;
138    let views = item.take_value_pointer(SUBTITLE3)?;
139    let id = item.take_value_pointer(NAVIGATION_BROWSE_ID)?;
140    let thumbnails = item.take_value_pointer(THUMBNAIL_RENDERER)?;
141    Ok(UserPlaylist {
142        title,
143        views,
144        thumbnails,
145        id,
146    })
147}
148
149#[cfg(test)]
150mod tests {
151    use crate::auth::BrowserToken;
152    use crate::common::{UserChannelID, UserPlaylistsParams, UserVideosParams, YoutubeID};
153
154    #[tokio::test]
155    async fn test_get_user() {
156        parse_test!(
157            "./test_json/get_user_20250707.json",
158            "./test_json/get_user_20250707_output.txt",
159            crate::query::GetUserQuery::new(UserChannelID::from_raw("")),
160            BrowserToken
161        );
162    }
163    #[tokio::test]
164    async fn test_get_user_playlists() {
165        parse_test!(
166            "./test_json/get_user_playlists_20250707.json",
167            "./test_json/get_user_playlists_20250707_output.txt",
168            crate::query::GetUserPlaylistsQuery::new(
169                UserChannelID::from_raw(""),
170                UserPlaylistsParams::from_raw("")
171            ),
172            BrowserToken
173        );
174    }
175    #[tokio::test]
176    async fn test_get_user_videos() {
177        parse_test!(
178            "./test_json/get_user_videos_20250707.json",
179            "./test_json/get_user_videos_20250707_output.txt",
180            crate::query::GetUserVideosQuery::new(
181                UserChannelID::from_raw(""),
182                UserVideosParams::from_raw("")
183            ),
184            BrowserToken
185        );
186    }
187}