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