1use crate::{
24 auth::AuthToken,
25 common::{AlbumID, ArtistChannelID, Thumbnail},
26 error,
27 json::Json,
28 nav_consts::*,
29 process::{fixed_column_item_pointer, flex_column_item_pointer},
30 query::Query,
31};
32use crate::{RawResult, Result};
33use json_crawler::{JsonCrawler, JsonCrawlerOwned};
34use serde::de::DeserializeOwned;
35use serde::{Deserialize, Serialize};
36use std::fmt::Debug;
37
38pub use album::*;
39pub use artist::*;
40pub use history::*;
41pub use library::*;
42pub use lyrics::*;
43pub use playlists::*;
44pub use podcasts::*;
45pub use recommendations::*;
46pub use search::*;
47pub use upload::*;
48pub use watch::*;
49
50mod album;
51mod artist;
52mod history;
53mod library;
54mod playlists;
55mod podcasts;
56mod rate;
57mod recommendations;
58mod search;
59mod upload;
60
61pub trait ParseFrom<Q>: Debug + Sized {
66 fn parse_from(p: ProcessedResult<Q>) -> crate::Result<Self>;
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub enum EpisodeDate {
71 Live,
72 Recorded { date: String },
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub enum EpisodeDuration {
77 Live,
78 Recorded { duration: String },
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct ParsedSongArtist {
84 pub name: String,
85 pub id: Option<ArtistChannelID<'static>>,
86}
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct ParsedSongAlbum {
90 pub name: String,
91 pub id: AlbumID<'static>,
92}
93
94pub struct ProcessedResult<'a, Q> {
97 pub query: &'a Q,
98 pub source: String,
100 pub json: Json,
103}
104
105impl<'a, Q: Query<A>, A: AuthToken> TryFrom<RawResult<'a, Q, A>> for ProcessedResult<'a, Q> {
106 type Error = crate::Error;
107 fn try_from(value: RawResult<'a, Q, A>) -> Result<Self> {
108 let RawResult {
109 json: source,
110 query,
111 ..
112 } = value;
113 let json = match source.as_str() {
114 "" => serde_json::Value::Null,
116 other => serde_json::from_str(other)
117 .map_err(|e| error::Error::response(format!("{:?}", e)))?,
118 };
119 let json = Json::new(json);
120 Ok(Self {
121 query,
122 source,
123 json,
124 })
125 }
126}
127
128impl<'a, Q> ProcessedResult<'a, Q> {
129 pub(crate) fn destructure(self) -> (&'a Q, String, serde_json::Value) {
130 let ProcessedResult {
131 query,
132 source,
133 json,
134 } = self;
135 (query, source, json.inner)
136 }
137 pub(crate) fn clone_json(self) -> String {
138 serde_json::to_string_pretty(&self.json)
139 .expect("Serialization of serde_json::value should not fail")
140 }
141 pub(crate) fn get_json(&self) -> &serde_json::Value {
142 &self.json.inner
143 }
144}
145
146impl<Q> ProcessedResult<'_, Q> {
147 pub fn parse_into<O: ParseFrom<Q>>(self) -> Result<O> {
148 O::parse_from(self)
149 }
150}
151
152impl<Q> From<ProcessedResult<'_, Q>> for JsonCrawlerOwned {
153 fn from(value: ProcessedResult<Q>) -> Self {
154 let (_, source, crawler) = value.destructure();
155 JsonCrawlerOwned::new(source, crawler)
156 }
157}
158
159fn parse_song_artists(
163 data: &mut impl JsonCrawler,
164 col_idx: usize,
165) -> Result<Vec<ParsedSongArtist>> {
166 data.borrow_pointer(format!("{}/text/runs", flex_column_item_pointer(col_idx)))?
167 .try_into_iter()?
168 .step_by(2)
169 .map(|mut item| parse_song_artist(&mut item))
170 .collect()
171}
172
173fn parse_song_artist(data: &mut impl JsonCrawler) -> Result<ParsedSongArtist> {
174 Ok(ParsedSongArtist {
175 name: data.take_value_pointer("/text")?,
176 id: data.take_value_pointer(NAVIGATION_BROWSE_ID).ok(),
177 })
178}
179
180fn parse_song_album(data: &mut impl JsonCrawler, col_idx: usize) -> Result<ParsedSongAlbum> {
181 Ok(ParsedSongAlbum {
182 name: parse_flex_column_item(data, col_idx, 0)?,
183 id: data.take_value_pointer(format!(
184 "{}/text/runs/0{}",
185 flex_column_item_pointer(col_idx),
186 NAVIGATION_BROWSE_ID
187 ))?,
188 })
189}
190
191fn parse_flex_column_item<T: DeserializeOwned>(
192 item: &mut impl JsonCrawler,
193 col_idx: usize,
194 run_idx: usize,
195) -> Result<T> {
196 let pointer = format!(
197 "{}/text/runs/{run_idx}/text",
198 flex_column_item_pointer(col_idx)
199 );
200 Ok(item.take_value_pointer(pointer)?)
201}
202
203fn parse_fixed_column_item<T: DeserializeOwned>(
204 item: &mut impl JsonCrawler,
205 col_idx: usize,
206) -> Result<T> {
207 let pointer = format!("{}/text/runs/0/text", fixed_column_item_pointer(col_idx));
208 Ok(item.take_value_pointer(pointer)?)
209}
210
211mod lyrics {
212 use super::{ParseFrom, ProcessedResult};
213 use crate::nav_consts::{DESCRIPTION, DESCRIPTION_SHELF, RUN_TEXT, SECTION_LIST_ITEM};
214 use crate::query::lyrics::GetLyricsQuery;
215 use const_format::concatcp;
216 use json_crawler::{JsonCrawler, JsonCrawlerOwned};
217 use serde::{Deserialize, Serialize};
218
219 #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
220 #[non_exhaustive]
221 pub struct Lyrics {
222 pub lyrics: String,
223 pub source: String,
224 }
225
226 impl<'a> ParseFrom<GetLyricsQuery<'a>> for Lyrics {
227 fn parse_from(p: ProcessedResult<GetLyricsQuery<'a>>) -> crate::Result<Self> {
228 let json_crawler: JsonCrawlerOwned = p.into();
229 let mut description_shelf = json_crawler.navigate_pointer(concatcp!(
230 "/contents",
231 SECTION_LIST_ITEM,
232 DESCRIPTION_SHELF
233 ))?;
234 Ok(Lyrics {
235 lyrics: description_shelf.take_value_pointer(DESCRIPTION)?,
236 source: description_shelf.take_value_pointer(concatcp!("/footer", RUN_TEXT))?,
237 })
238 }
239 }
240
241 #[cfg(test)]
242 mod tests {
243 use crate::{
244 auth::BrowserToken,
245 common::{LyricsID, YoutubeID},
246 parse::lyrics::Lyrics,
247 process_json,
248 query::lyrics::GetLyricsQuery,
249 };
250
251 #[tokio::test]
252 async fn test_get_lyrics_query() {
253 let path = std::path::Path::new("./test_json/get_lyrics_20231219.json");
255 let file = tokio::fs::read_to_string(path)
256 .await
257 .expect("Expect file read to pass during tests");
258 let query = GetLyricsQuery::new(LyricsID::from_raw(""));
260 let output = process_json::<_, BrowserToken>(file, query).unwrap();
261 assert_eq!(
262 output,
263 Lyrics {
264 lyrics: "Push \r\nCome on, she almost there push, come on\r\nCome on, come on, push, it's almost there \r\nOne more time, come one\r\nCome on, push, baby, one more time \r\nHarder, harder, push it harder \r\nPush, push, come on \r\nOne more time, here it goes \r\nI see the head\r\nYeah, come on\r\nYeah, yeah\r\nYou did it, baby, yeah\r\n\r\nBut if you lose, don't ask no questions why\r\nThe only game you know is do or die\r\nAh-ha-ha\r\nHard to understand what a hell of a man\r\n\r\nHip hop the hippie the hippie\r\nTp the hip hop and you don't stop \r\nRock it out, baby bubba, to the boogie, the bang-bang\r\nThe boogie to the boogie that be\r\nNow what you hear is not a test, I'm rappin', to the beat \r\n\r\nGoddamn it, Voletta, what the fuck are you doin'?\r\nYou can't control that goddamn boy? (What?)\r\nI just saw Mr. Johnson, he told me he caught the motherfucking boy shoplifting \r\nWhat the fuck are you doing? (Kiss my black ass, motherfucker)\r\nYou can't control that god-, I don't know what the fuck to do with that boy\r\n(What the fuck do you want me to do?)\r\nIf if you can't fucking control that boy, I'ma send him\r\n(All you fucking do is bitch at me)\r\nBitch, bitch, I'ma send his motherfuckin' ass to a group home goddamnit, what?\r\nI'll smack the shit outta you bitch, what, what the fuck?\r\n(Kiss my black ass, motherfucker)\r\nYou're fuckin' up\r\n(Comin' in here smelling like sour socks you, dumb motherfucker) \r\n\r\nWhen I'm bustin' up a party I feel no guilt\r\nGizmo's cuttin' up for thee \r\nSuckers that's down with nei-\r\n\r\nWhat, nigga, you wanna rob them motherfuckin' trains, you crazy? \r\nYes, yes, motherfucker, motherfuckin' right, nigga, yes \r\nNigga, what the fuck, nigga? We gonna get-\r\nNigga, it's eighty-seven nigga, is you dead broke? \r\nYeah, nigga, but, but\r\nMotherfucker, is you broke, motherfucker? \r\nWe need to get some motherfuckin' paper, nigga \r\nNigga it's a train, ain't nobody never robbed no motherfuckin' train \r\nJust listen, man, is your mother givin' you money, nigga? \r\nMy moms don't give me shit nigga, it's time to get paid, nigga \r\nIs you with me? Motherfucker, is you with me? \r\nYeah, I'm with you, nigga, come on \r\nAlright then, nigga, lets make it happen then \r\nAll you motherfuckers get on the fuckin' floor \r\nGet on the motherfuckin' floor\r\nChill, give me all your motherfuckin' money \r\nAnd don't move, nigga\r\nI want the fuckin' jewelry \r\nGive me every fuckin' thing \r\nNigga, I'd shut the fuck up or I'ma blow your motherfuckin' brains out \r\nShut the fuck up, bitch, give me your fuckin' money, motherfucker\r\nFuck you, bitch, get up off that shit \r\nWhat the fuck you holdin' on to that shit for, bitch? \r\n\r\nI get money, money I got\r\nStunts call me honey if they feel real hot\r\n\r\nOpen C-74, Smalls \r\nMr. Smalls, let me walk you to the door \r\nSo how does it feel leavin' us? \r\nCome on, man, what kind of fuckin' question is that, man? \r\nTryin' to get the fuck up out this joint, dog \r\nYeah, yeah, you'll be back \r\nYou niggas always are \r\nGo ahead, man, what the fuck is you hollerin' about? \r\nYou won't see me up in this motherfucker no more \r\nWe'll see \r\nI got big plans nigga, big plans, hahaha".to_string(),
265 source: "Source: LyricFind".to_string()
266 }
267 );
268 }
269 }
270}
271mod watch {
272 use super::{ParseFrom, ProcessedResult};
273 use crate::{
274 common::{LyricsID, PlaylistID},
275 nav_consts::{NAVIGATION_PLAYLIST_ID, TAB_CONTENT},
276 query::watch::{GetWatchPlaylistQuery, GetWatchPlaylistQueryID},
277 Result,
278 };
279 use const_format::concatcp;
280 use json_crawler::{JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerOwned};
281 use serde::{Deserialize, Serialize};
282
283 #[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
284 #[non_exhaustive]
285 pub struct WatchPlaylist {
286 pub _tracks: Vec<()>,
289 pub playlist_id: Option<PlaylistID<'static>>,
290 pub lyrics_id: LyricsID<'static>,
291 }
292
293 impl<T: GetWatchPlaylistQueryID> ParseFrom<GetWatchPlaylistQuery<T>> for WatchPlaylist {
294 fn parse_from(p: ProcessedResult<GetWatchPlaylistQuery<T>>) -> crate::Result<Self> {
295 let json_crawler: JsonCrawlerOwned = p.into();
297 let mut watch_next_renderer = json_crawler.navigate_pointer("/contents/singleColumnMusicWatchNextResultsRenderer/tabbedRenderer/watchNextTabbedResultsRenderer")?;
298 let lyrics_id =
299 get_tab_browse_id(&mut watch_next_renderer.borrow_mut(), 1)?.take_value()?;
300 let mut results = watch_next_renderer.navigate_pointer(concatcp!(
301 TAB_CONTENT,
302 "/musicQueueRenderer/content/playlistPanelRenderer/contents"
303 ))?;
304 let playlist_id = results.try_iter_mut()?.find_map(|mut v| {
305 v.take_value_pointer(concatcp!(
306 "/playlistPanelVideoRenderer",
307 NAVIGATION_PLAYLIST_ID
308 ))
309 .ok()
310 });
311 Ok(WatchPlaylist {
312 _tracks: Vec::new(),
313 playlist_id,
314 lyrics_id,
315 })
316 }
317 }
318
319 fn get_tab_browse_id<'a>(
322 watch_next_renderer: &'a mut JsonCrawlerBorrowed,
323 tab_id: usize,
324 ) -> Result<JsonCrawlerBorrowed<'a>> {
325 let path = format!("/tabs/{tab_id}/tabRenderer/endpoint/browseEndpoint/browseId");
327 watch_next_renderer.borrow_pointer(path).map_err(Into::into)
328 }
329}
330mod song {
331 use super::ParseFrom;
332 use crate::{common::SongTrackingUrl, query::song::GetSongTrackingUrlQuery};
333 use json_crawler::{JsonCrawler, JsonCrawlerOwned};
334
335 impl<'a> ParseFrom<GetSongTrackingUrlQuery<'a>> for SongTrackingUrl<'static> {
336 fn parse_from(
337 p: super::ProcessedResult<GetSongTrackingUrlQuery<'a>>,
338 ) -> crate::Result<Self> {
339 let mut crawler = JsonCrawlerOwned::from(p);
340 crawler
341 .take_value_pointer("/playbackTracking/videostatsPlaybackUrl/baseUrl")
342 .map_err(Into::into)
343 }
344 }
345
346 #[cfg(test)]
347 mod tests {
348 use crate::{
349 auth::BrowserToken,
350 common::{SongTrackingUrl, VideoID, YoutubeID},
351 query::song::GetSongTrackingUrlQuery,
352 };
353
354 #[tokio::test]
355 async fn test_get_song_tracking_url_query() {
356 let output = SongTrackingUrl::from_raw("https://s.youtube.com/api/stats/playback?cl=655300395&docid=FZ8BxMU3BYc&ei=JSimZqHaNeyB9fwP9oqh0Ak&fexp=&ns=yt&plid=AAYeTNocW-liNkl6&el=detailpage&len=193&of=URbTjA0hNUiM-oZxeU_KzQ&osid=AAAAAYfxXtM%3AAOeUNAZhCDiglWHfELd4I0ksz0dyuGtLVg&uga=m32&vm=CAMQARgBOjJBSHFpSlRJMDQteFk3b0Z2MUZXblN3NTlza3ZKcEhkcXpWeVhhMXl4RGQyZXVFR2twZ2JiQU9BckJGdG4zbDdCcElKTGJHNkt3dlJVX2ZzZGdKMndGR1ZZdk92MVItWWYtUTBOYmdFQnYxd3J6cGJBNzdrZUJXMlQ0QWR4MVo4S1Rza1JTM0hvWGRTd2llYk5xZFd6Nne4AQE");
357 parse_test_value!(
358 "./test_json/get_song_tracking_url_20240728.json",
359 output,
360 GetSongTrackingUrlQuery::new(VideoID::from_raw("")).unwrap(),
361 BrowserToken
362 );
363 }
364 }
365}