1use super::{
2 ParseFrom, CATEGORY_TITLE, GRID, RUN_TEXT, TASTE_ITEM_CONTENTS, TASTE_PROFILE_ARTIST,
3 TASTE_PROFILE_IMPRESSION, TASTE_PROFILE_ITEMS, TASTE_PROFILE_SELECTION,
4};
5use crate::{
6 common::{MoodCategoryParams, PlaylistID, TasteToken, Thumbnail},
7 nav_consts::{
8 CAROUSEL, CAROUSEL_TITLE, CATEGORY_PARAMS, MTRIR, NAVIGATION_BROWSE_ID, SECTION_LIST,
9 SINGLE_COLUMN_TAB, SUBTITLE_RUNS, THUMBNAIL_RENDERER, TITLE_TEXT,
10 },
11 query::{
12 GetMoodCategoriesQuery, GetMoodPlaylistsQuery, GetTasteProfileQuery, SetTasteProfileQuery,
13 },
14 Result,
15};
16use const_format::concatcp;
17use itertools::Itertools;
18use json_crawler::{CrawlerResult, JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerOwned};
19use serde::{Deserialize, Serialize};
20
21#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
22pub struct TasteProfileArtist {
23 pub artist: String,
24 pub taste_tokens: TasteToken<'static>,
25}
26#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
27pub struct MoodCategorySection {
28 pub section_name: String,
29 pub mood_categories: Vec<MoodCategory>,
30}
31
32#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
33pub struct MoodCategory {
34 pub title: String,
35 pub params: MoodCategoryParams<'static>,
36}
37
38#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
39pub struct MoodPlaylistCategory {
40 pub category_name: String,
41 pub playlists: Vec<MoodPlaylist>,
42}
43
44#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
45pub struct MoodPlaylist {
46 pub playlist_id: PlaylistID<'static>,
47 pub title: String,
48 pub thumbnails: Vec<Thumbnail>,
49 pub author: String,
50}
51
52impl<'a, I> ParseFrom<SetTasteProfileQuery<'a, I>> for ()
53where
54 I: Iterator<Item = TasteToken<'a>> + Clone,
55{
56 fn parse_from(_: super::ProcessedResult<SetTasteProfileQuery<'a, I>>) -> Result<Self> {
57 Ok(())
60 }
61}
62
63impl ParseFrom<GetTasteProfileQuery> for Vec<TasteProfileArtist> {
64 fn parse_from(p: super::ProcessedResult<GetTasteProfileQuery>) -> Result<Self> {
65 let crawler = JsonCrawlerOwned::from(p);
66 crawler
67 .navigate_pointer(TASTE_PROFILE_ITEMS)?
68 .try_into_iter()?
69 .map(|item| -> Result<_> {
70 Ok(item
71 .navigate_pointer(TASTE_ITEM_CONTENTS)?
72 .try_into_iter()?
73 .map(get_taste_profile_artist))
74 })
75 .process_results(|nested_iter| nested_iter.flatten().collect::<Result<_>>())?
76 }
77}
78
79impl ParseFrom<GetMoodCategoriesQuery> for Vec<MoodCategorySection> {
80 fn parse_from(p: super::ProcessedResult<GetMoodCategoriesQuery>) -> crate::Result<Self> {
81 let crawler = JsonCrawlerOwned::from(p);
82 crawler
83 .navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST))?
84 .try_into_iter()?
85 .map(parse_mood_category_sections)
86 .collect()
87 }
88}
89impl<'a> ParseFrom<GetMoodPlaylistsQuery<'a>> for Vec<MoodPlaylistCategory> {
90 fn parse_from(p: super::ProcessedResult<GetMoodPlaylistsQuery<'a>>) -> Result<Self> {
91 fn parse_mood_playlist_category(
92 mut crawler: JsonCrawlerOwned,
93 ) -> Result<MoodPlaylistCategory> {
94 let array = vec![
95 |s: &mut JsonCrawlerOwned| -> std::result::Result<_, json_crawler::CrawlerError> {
96 parse_mood_playlist_category_grid(s.borrow_pointer(GRID)?)
97 },
98 |s: &mut JsonCrawlerOwned| -> std::result::Result<_, json_crawler::CrawlerError> {
99 parse_mood_playlist_category_carousel(s.borrow_pointer(CAROUSEL)?)
100 },
101 ];
102 crawler.try_functions(array).map_err(Into::into)
103 }
104 fn parse_mood_playlist_category_grid(
105 mut crawler: JsonCrawlerBorrowed,
106 ) -> json_crawler::CrawlerResult<MoodPlaylistCategory> {
107 let category_name =
108 crawler.take_value_pointer(concatcp!("/header/gridHeaderRenderer", TITLE_TEXT))?;
109 let playlists = crawler
110 .navigate_pointer("/items")?
111 .try_iter_mut()?
112 .map(parse_mood_playlist)
113 .collect::<CrawlerResult<_>>()?;
114 Ok(MoodPlaylistCategory {
115 category_name,
116 playlists,
117 })
118 }
119 fn parse_mood_playlist_category_carousel(
120 mut crawler: JsonCrawlerBorrowed,
121 ) -> json_crawler::CrawlerResult<MoodPlaylistCategory> {
122 let category_name = crawler.take_value_pointer(concatcp!(CAROUSEL_TITLE, "/text"))?;
123 let playlists = crawler
124 .navigate_pointer("/contents")?
125 .try_iter_mut()?
126 .map(parse_mood_playlist)
127 .collect::<CrawlerResult<_>>()?;
128 Ok(MoodPlaylistCategory {
129 category_name,
130 playlists,
131 })
132 }
133 fn parse_mood_playlist(
134 crawler: JsonCrawlerBorrowed,
135 ) -> json_crawler::CrawlerResult<MoodPlaylist> {
136 let mut item = crawler.navigate_pointer(MTRIR)?;
137 let playlist_id = item.take_value_pointer(NAVIGATION_BROWSE_ID)?;
138 let title = item.take_value_pointer(TITLE_TEXT)?;
139 let thumbnails = item.take_value_pointer(THUMBNAIL_RENDERER)?;
140
141 let author = item.borrow_pointer(SUBTITLE_RUNS)?.try_expect(
142 "Subtitle runs should contain at least 1 item",
143 |subtitle_runs| {
144 subtitle_runs
145 .try_iter_mut()?
146 .take(3)
147 .next_back()
148 .map(|mut run| run.take_value_pointer("/text"))
149 .transpose()
150 },
151 )?;
152
153 Ok(MoodPlaylist {
154 playlist_id,
155 title,
156 thumbnails,
157 author,
158 })
159 }
160 let json_crawler: JsonCrawlerOwned = p.into();
161 json_crawler
162 .navigate_pointer(concatcp!(SINGLE_COLUMN_TAB, SECTION_LIST))?
163 .try_into_iter()?
164 .map(parse_mood_playlist_category)
165 .collect()
166 }
167}
168
169fn parse_mood_category_sections(crawler: JsonCrawlerOwned) -> Result<MoodCategorySection> {
170 let mut crawler = crawler.navigate_pointer(GRID)?;
171 let section_name =
172 crawler.take_value_pointer(concatcp!("/header/gridHeaderRenderer/title", RUN_TEXT))?;
173 let mood_categories = crawler
174 .navigate_pointer("/items")?
175 .try_into_iter()?
176 .map(parse_mood_categories)
177 .collect::<Result<Vec<_>>>()?;
178 Ok(MoodCategorySection {
179 section_name,
180 mood_categories,
181 })
182}
183fn parse_mood_categories(crawler: JsonCrawlerOwned) -> Result<MoodCategory> {
184 let mut crawler = crawler.navigate_pointer("/musicNavigationButtonRenderer")?;
185 let title = crawler.take_value_pointer(concatcp!(CATEGORY_TITLE))?;
186 let params = crawler.take_value_pointer(concatcp!(CATEGORY_PARAMS))?;
187 Ok(MoodCategory { title, params })
188}
189
190fn get_taste_profile_artist(mut crawler: JsonCrawlerOwned) -> Result<TasteProfileArtist> {
191 let artist = crawler.take_value_pointer(TASTE_PROFILE_ARTIST)?;
192 let impression_value = crawler.take_value_pointer(TASTE_PROFILE_IMPRESSION)?;
193 let selection_value = crawler.take_value_pointer(TASTE_PROFILE_SELECTION)?;
194 let taste_tokens = TasteToken {
195 impression_value,
196 selection_value,
197 };
198 Ok(TasteProfileArtist {
199 artist,
200 taste_tokens,
201 })
202}
203
204#[cfg(test)]
205mod tests {
206 use crate::{
207 auth::BrowserToken,
208 common::{
209 MoodCategoryParams, TasteToken, TasteTokenImpression, TasteTokenSelection, YoutubeID,
210 },
211 query::{
212 GetMoodCategoriesQuery, GetMoodPlaylistsQuery, GetTasteProfileQuery,
213 SetTasteProfileQuery,
214 },
215 };
216
217 #[tokio::test]
218 async fn test_get_mood_categories() {
219 parse_test!(
220 "./test_json/get_mood_categories_20240723.json",
221 "./test_json/get_mood_categories_20240723_output.txt",
222 GetMoodCategoriesQuery,
223 BrowserToken
224 );
225 }
226 #[tokio::test]
227 async fn test_get_mood_playlists() {
228 parse_test!(
229 "./test_json/get_mood_playlists_20240723.json",
230 "./test_json/get_mood_playlists_20240723_output.txt",
231 GetMoodPlaylistsQuery::new(MoodCategoryParams::from_raw("")),
232 BrowserToken
233 );
234 }
235 #[tokio::test]
236 async fn test_get_taste_profile() {
237 parse_test!(
238 "./test_json/get_taste_profile_20240722.json",
239 "./test_json/get_taste_profile_20240722_output.txt",
240 GetTasteProfileQuery,
241 BrowserToken
242 );
243 }
244 #[tokio::test]
245 async fn test_set_taste_profile() {
246 parse_test_value!(
247 "./test_json/set_taste_profile_20240723.json",
248 (),
249 SetTasteProfileQuery::new([TasteToken {
250 impression_value: TasteTokenImpression::from_raw(""),
251 selection_value: TasteTokenSelection::from_raw("")
252 }]),
253 BrowserToken
254 );
255 }
256}