ytmapi_rs/
query.rs

1//! Type safe queries to pass to the API, and the traits to allow you to
2//! implement new ones.
3//! # Implementation example
4//! Note, to implement Query, you must also meet the trait bounds for
5//! QueryMethod. In practice, this means you must implement both Query and
6//! PostQuery when using PostMethod, and Query and GetQuery when using
7//! GetMethod.
8//! In addition, note that your output type will need to implement ParseFrom -
9//! see [`crate::parse`] for implementation notes.
10//! ```no_run
11//! # #[derive(Debug)]
12//! # struct Date;
13//! # impl ytmapi_rs::parse::ParseFrom<GetDateQuery> for Date {
14//! #     fn parse_from(_: ytmapi_rs::parse::ProcessedResult<GetDateQuery>) -> ytmapi_rs::Result<Self> {todo!()}
15//! # }
16//! struct GetDateQuery;
17//! impl ytmapi_rs::query::Query<ytmapi_rs::auth::BrowserToken> for GetDateQuery {
18//!     type Output = Date;
19//!     type Method = ytmapi_rs::query::PostMethod;
20//! }
21//! // Note that this is not a real Innertube endpoint - example for reference only!
22//! impl ytmapi_rs::query::PostQuery for GetDateQuery {
23//!     fn header(&self) -> serde_json::Map<String, serde_json::Value> {
24//!         serde_json::Map::from_iter([("get_date".to_string(), serde_json::json!("YYYYMMDD"))])
25//!     }
26//!     fn params(&self) -> Vec<(&str, std::borrow::Cow<str>)> {
27//!         vec![]
28//!     }
29//!     fn path(&self) -> &str {
30//!         "date"
31//!     }
32//! }
33//! ```
34use crate::auth::AuthToken;
35use crate::parse::ParseFrom;
36use crate::{RawResult, Result};
37use std::borrow::Cow;
38use std::future::Future;
39
40use private::Sealed;
41
42pub use album::*;
43pub use artist::*;
44pub use continuations::*;
45pub use history::*;
46pub use library::*;
47pub use playlist::*;
48pub use podcasts::*;
49pub use recommendations::*;
50pub use search::*;
51pub use upload::*;
52
53mod artist;
54mod continuations;
55mod history;
56mod library;
57mod playlist;
58mod podcasts;
59mod recommendations;
60mod search;
61mod upload;
62
63mod private {
64    pub trait Sealed {}
65}
66
67/// Represents a query that can be passed to Innertube.
68/// The Output associated type describes how to parse a result from the query,
69/// and the Method associated type describes how to call the query.
70pub trait Query<A: AuthToken>: Sized {
71    type Output: ParseFrom<Self>;
72    type Method: QueryMethod<Self, A, Self::Output>;
73}
74
75/// Represents a plain POST query that can be sent to Innertube.
76pub trait PostQuery {
77    fn header(&self) -> serde_json::Map<String, serde_json::Value>;
78    fn params(&self) -> Vec<(&str, Cow<str>)>;
79    fn path(&self) -> &str;
80}
81/// Represents a plain GET query that can be sent to Innertube.
82pub trait GetQuery {
83    fn url(&self) -> &str;
84    fn params(&self) -> Vec<(&str, Cow<str>)>;
85}
86
87/// The GET query method
88pub struct GetMethod;
89/// The POST query method
90pub struct PostMethod;
91
92/// Represents a method of calling an query, using a query, client and auth
93/// token. Not intended to be implemented by api users, the pre-implemented
94/// GetMethod and PostMethod structs should be sufficient, and in addition,
95/// async methods are required currently.
96// Allow async_fn_in_trait required, as trait currently sealed.
97#[allow(async_fn_in_trait)]
98pub trait QueryMethod<Q, A, O>: Sealed
99where
100    Q: Query<A>,
101    A: AuthToken,
102{
103    async fn call<'a>(
104        query: &'a Q,
105        client: &crate::client::Client,
106        tok: &A,
107    ) -> Result<RawResult<'a, Q, A>>;
108}
109
110impl Sealed for GetMethod {}
111impl<Q, A, O> QueryMethod<Q, A, O> for GetMethod
112where
113    Q: GetQuery + Query<A, Output = O>,
114    A: AuthToken,
115{
116    fn call<'a>(
117        query: &'a Q,
118        client: &crate::client::Client,
119        tok: &A,
120    ) -> impl Future<Output = Result<RawResult<'a, Q, A>>>
121    where
122        Self: Sized,
123    {
124        tok.raw_query_get(client, query)
125    }
126}
127
128impl Sealed for PostMethod {}
129impl<Q, A, O> QueryMethod<Q, A, O> for PostMethod
130where
131    Q: PostQuery + Query<A, Output = O>,
132    A: AuthToken,
133{
134    fn call<'a>(
135        query: &'a Q,
136        client: &crate::client::Client,
137        tok: &A,
138    ) -> impl Future<Output = Result<RawResult<'a, Q, A>>>
139    where
140        Self: Sized,
141    {
142        tok.raw_query_post(client, query)
143    }
144}
145
146pub mod album {
147    use super::{PostMethod, PostQuery, Query};
148    use crate::{
149        auth::AuthToken,
150        common::{AlbumID, YoutubeID},
151        parse::GetAlbum,
152    };
153    use serde_json::json;
154
155    #[derive(Clone)]
156    pub struct GetAlbumQuery<'a> {
157        browse_id: AlbumID<'a>,
158    }
159    impl<A: AuthToken> Query<A> for GetAlbumQuery<'_> {
160        type Output = GetAlbum;
161        type Method = PostMethod;
162    }
163    impl PostQuery for GetAlbumQuery<'_> {
164        fn header(&self) -> serde_json::Map<String, serde_json::Value> {
165            let serde_json::Value::Object(map) = json!({
166                 "browseId" : self.browse_id.get_raw(),
167            }) else {
168                unreachable!("Created a map");
169            };
170            map
171        }
172        fn path(&self) -> &str {
173            "browse"
174        }
175        fn params(&self) -> std::vec::Vec<(&str, std::borrow::Cow<'_, str>)> {
176            vec![]
177        }
178    }
179    impl<'a> GetAlbumQuery<'_> {
180        pub fn new<T: Into<AlbumID<'a>>>(browse_id: T) -> GetAlbumQuery<'a> {
181            GetAlbumQuery {
182                browse_id: browse_id.into(),
183            }
184        }
185    }
186}
187
188pub mod lyrics {
189    use super::{PostMethod, PostQuery, Query};
190    use crate::{
191        auth::AuthToken,
192        common::{LyricsID, YoutubeID},
193        parse::Lyrics,
194    };
195    use serde_json::json;
196
197    pub struct GetLyricsQuery<'a> {
198        id: LyricsID<'a>,
199    }
200    impl<A: AuthToken> Query<A> for GetLyricsQuery<'_> {
201        type Output = Lyrics;
202        type Method = PostMethod;
203    }
204    impl PostQuery for GetLyricsQuery<'_> {
205        fn header(&self) -> serde_json::Map<String, serde_json::Value> {
206            let serde_json::Value::Object(map) = json!({
207                "browseId": self.id.get_raw(),
208            }) else {
209                unreachable!()
210            };
211            map
212        }
213        fn path(&self) -> &str {
214            "browse"
215        }
216        fn params(&self) -> std::vec::Vec<(&str, std::borrow::Cow<'_, str>)> {
217            vec![]
218        }
219    }
220    impl<'a> GetLyricsQuery<'a> {
221        pub fn new(id: LyricsID<'a>) -> GetLyricsQuery<'a> {
222            GetLyricsQuery { id }
223        }
224    }
225}
226
227pub mod watch {
228    use super::{PostMethod, PostQuery, Query};
229    use crate::{
230        auth::AuthToken,
231        common::{PlaylistID, VideoID, YoutubeID},
232    };
233    use serde_json::json;
234    use std::borrow::Cow;
235
236    pub trait GetWatchPlaylistQueryID {
237        fn get_video_id(&self) -> Option<Cow<str>>;
238        fn get_playlist_id(&self) -> Cow<str>;
239    }
240
241    pub struct GetWatchPlaylistQuery<T: GetWatchPlaylistQueryID> {
242        id: T,
243    }
244    pub struct VideoAndPlaylistID<'a> {
245        video_id: VideoID<'a>,
246        playlist_id: PlaylistID<'a>,
247    }
248
249    impl GetWatchPlaylistQueryID for VideoAndPlaylistID<'_> {
250        fn get_video_id(&self) -> Option<Cow<str>> {
251            Some(self.video_id.get_raw().into())
252        }
253
254        fn get_playlist_id(&self) -> Cow<str> {
255            self.playlist_id.get_raw().into()
256        }
257    }
258    impl GetWatchPlaylistQueryID for VideoID<'_> {
259        fn get_video_id(&self) -> Option<Cow<str>> {
260            Some(self.get_raw().into())
261        }
262
263        fn get_playlist_id(&self) -> Cow<str> {
264            format!("RDAMVM{}", self.get_raw()).into()
265        }
266    }
267    impl GetWatchPlaylistQueryID for PlaylistID<'_> {
268        fn get_video_id(&self) -> Option<Cow<str>> {
269            None
270        }
271        fn get_playlist_id(&self) -> Cow<str> {
272            self.get_raw().into()
273        }
274    }
275
276    impl<T: GetWatchPlaylistQueryID, A: AuthToken> Query<A> for GetWatchPlaylistQuery<T> {
277        type Output = crate::parse::WatchPlaylist;
278        type Method = PostMethod;
279    }
280    impl<T: GetWatchPlaylistQueryID> PostQuery for GetWatchPlaylistQuery<T> {
281        fn header(&self) -> serde_json::Map<String, serde_json::Value> {
282            let serde_json::Value::Object(mut map) = json!({
283                "enablePersistentPlaylistPanel": true,
284                "isAudioOnly": true,
285                "tunerSettingValue": "AUTOMIX_SETTING_NORMAL",
286                "playlistId" : self.id.get_playlist_id(),
287            }) else {
288                unreachable!()
289            };
290            if let Some(video_id) = self.id.get_video_id() {
291                map.insert("videoId".to_string(), json!(video_id));
292            };
293            map
294        }
295        fn path(&self) -> &str {
296            "next"
297        }
298        fn params(&self) -> Vec<(&str, Cow<str>)> {
299            vec![]
300        }
301    }
302    impl<'a> GetWatchPlaylistQuery<VideoID<'a>> {
303        pub fn new_from_video_id<T: Into<VideoID<'a>>>(
304            id: T,
305        ) -> GetWatchPlaylistQuery<VideoID<'a>> {
306            GetWatchPlaylistQuery { id: id.into() }
307        }
308        pub fn with_playlist_id(
309            self,
310            playlist_id: PlaylistID<'a>,
311        ) -> GetWatchPlaylistQuery<VideoAndPlaylistID<'a>> {
312            GetWatchPlaylistQuery {
313                id: VideoAndPlaylistID {
314                    video_id: self.id,
315                    playlist_id,
316                },
317            }
318        }
319    }
320    impl<'a> GetWatchPlaylistQuery<PlaylistID<'a>> {
321        pub fn new_from_playlist_id(id: PlaylistID<'a>) -> GetWatchPlaylistQuery<PlaylistID<'a>> {
322            GetWatchPlaylistQuery { id }
323        }
324        pub fn with_video_id(
325            self,
326            video_id: VideoID<'a>,
327        ) -> GetWatchPlaylistQuery<VideoAndPlaylistID<'a>> {
328            GetWatchPlaylistQuery {
329                id: VideoAndPlaylistID {
330                    video_id,
331                    playlist_id: self.id,
332                },
333            }
334        }
335    }
336}
337
338pub mod rate {
339    use std::borrow::Cow;
340
341    use super::{PostMethod, PostQuery, Query};
342    use crate::{
343        auth::AuthToken,
344        common::{LikeStatus, PlaylistID, VideoID, YoutubeID},
345    };
346    use serde_json::json;
347
348    pub struct RateSongQuery<'a> {
349        video_id: VideoID<'a>,
350        rating: LikeStatus,
351    }
352    impl<'a> RateSongQuery<'a> {
353        pub fn new(video_id: VideoID<'a>, rating: LikeStatus) -> Self {
354            Self { video_id, rating }
355        }
356    }
357    pub struct RatePlaylistQuery<'a> {
358        playlist_id: PlaylistID<'a>,
359        rating: LikeStatus,
360    }
361    impl<'a> RatePlaylistQuery<'a> {
362        pub fn new(playlist_id: PlaylistID<'a>, rating: LikeStatus) -> Self {
363            Self {
364                playlist_id,
365                rating,
366            }
367        }
368    }
369
370    // AUTH REQUIRED
371    impl<A: AuthToken> Query<A> for RateSongQuery<'_> {
372        type Output = ();
373        type Method = PostMethod;
374    }
375    impl PostQuery for RateSongQuery<'_> {
376        fn header(&self) -> serde_json::Map<String, serde_json::Value> {
377            serde_json::Map::from_iter([(
378                "target".to_string(),
379                json!({"videoId" : self.video_id.get_raw()} ),
380            )])
381        }
382        fn params(&self) -> Vec<(&str, Cow<str>)> {
383            vec![]
384        }
385        fn path(&self) -> &str {
386            like_endpoint(&self.rating)
387        }
388    }
389
390    // AUTH REQUIRED
391    impl<A: AuthToken> Query<A> for RatePlaylistQuery<'_> {
392        type Output = ();
393        type Method = PostMethod;
394    }
395
396    impl PostQuery for RatePlaylistQuery<'_> {
397        fn header(&self) -> serde_json::Map<String, serde_json::Value> {
398            serde_json::Map::from_iter([(
399                "target".to_string(),
400                json!({"playlistId" : self.playlist_id.get_raw()} ),
401            )])
402        }
403        fn params(&self) -> Vec<(&str, Cow<str>)> {
404            vec![]
405        }
406        fn path(&self) -> &str {
407            like_endpoint(&self.rating)
408        }
409    }
410
411    fn like_endpoint(rating: &LikeStatus) -> &'static str {
412        match *rating {
413            LikeStatus::Liked => "like/like",
414            LikeStatus::Disliked => "like/dislike",
415            LikeStatus::Indifferent => "like/removelike",
416        }
417    }
418}
419
420// Potentially better belongs within another module.
421pub mod song {
422    use super::{PostMethod, PostQuery, Query};
423    use crate::common::VideoID;
424    use crate::{auth::AuthToken, common::SongTrackingUrl, Result};
425    use serde_json::json;
426    use std::borrow::Cow;
427    use std::time::SystemTime;
428
429    pub struct GetSongTrackingUrlQuery<'a> {
430        video_id: VideoID<'a>,
431        signature_timestamp: u64,
432    }
433
434    impl GetSongTrackingUrlQuery<'_> {
435        /// # NOTE
436        /// A GetSongTrackingUrlQuery stores a timestamp, it's not recommended
437        /// to store these for a long period of time. The constructor can fail
438        /// due to a System Time error.
439        pub fn new(video_id: VideoID) -> Result<GetSongTrackingUrlQuery<'_>> {
440            let signature_timestamp = get_signature_timestamp()?;
441            Ok(GetSongTrackingUrlQuery {
442                video_id,
443                signature_timestamp,
444            })
445        }
446    }
447
448    impl<A: AuthToken> Query<A> for GetSongTrackingUrlQuery<'_> {
449        type Output = SongTrackingUrl<'static>;
450        type Method = PostMethod;
451    }
452    impl PostQuery for GetSongTrackingUrlQuery<'_> {
453        fn header(&self) -> serde_json::Map<String, serde_json::Value> {
454            serde_json::Map::from_iter([
455                (
456                    "playbackContext".to_string(),
457                    json!(
458                        {
459                            "contentPlaybackContext": {
460                                "signatureTimestamp": self.signature_timestamp
461                            }
462                        }
463                    ),
464                ),
465                ("video_id".to_string(), json!(self.video_id)),
466            ])
467        }
468        fn params(&self) -> Vec<(&str, Cow<str>)> {
469            vec![]
470        }
471        fn path(&self) -> &str {
472            "player"
473        }
474    }
475
476    // Original: https://github.com/sigma67/ytmusicapi/blob/a15d90c4f356a530c6b2596277a9d70c0b117a0c/ytmusicapi/mixins/_utils.py#L42
477    /// Approximation for google's signatureTimestamp which would normally be
478    /// extracted from base.js.
479    fn get_signature_timestamp() -> Result<u64> {
480        const SECONDS_IN_DAY: u64 = 60 * 60 * 24;
481        Ok(SystemTime::now()
482            .duration_since(SystemTime::UNIX_EPOCH)?
483            .as_secs()
484            // SAFETY: SECONDS_IN_DAY is nonzero.
485            .saturating_div(SECONDS_IN_DAY))
486    }
487}