Skip to main content

ytmapi_rs/query/
song.rs

1use super::{PostMethod, PostQuery, Query};
2use crate::Result;
3use crate::auth::AuthToken;
4use crate::common::{LyricsID, SongTrackingUrl, VideoID, YoutubeID};
5use crate::parse::Lyrics;
6use serde_json::json;
7use std::borrow::Cow;
8use std::time::SystemTime;
9
10pub struct GetLyricsIDQuery<'a> {
11    video_id: VideoID<'a>,
12}
13
14pub struct GetLyricsQuery<'a> {
15    id: LyricsID<'a>,
16}
17
18pub struct GetSongTrackingUrlQuery<'a> {
19    video_id: VideoID<'a>,
20    signature_timestamp: u64,
21}
22
23impl<'a> GetLyricsIDQuery<'a> {
24    pub fn new(video_id: VideoID<'a>) -> GetLyricsIDQuery<'a> {
25        GetLyricsIDQuery { video_id }
26    }
27}
28
29impl<'a> GetLyricsQuery<'a> {
30    pub fn new(id: LyricsID<'a>) -> GetLyricsQuery<'a> {
31        GetLyricsQuery { id }
32    }
33}
34
35impl GetSongTrackingUrlQuery<'_> {
36    /// # NOTE
37    /// A GetSongTrackingUrlQuery stores a timestamp, it's not recommended
38    /// to store these for a long period of time. The constructor can fail
39    /// due to a System Time error.
40    pub fn new(video_id: VideoID<'_>) -> Result<GetSongTrackingUrlQuery<'_>> {
41        let signature_timestamp = get_signature_timestamp()?;
42        Ok(GetSongTrackingUrlQuery {
43            video_id,
44            signature_timestamp,
45        })
46    }
47}
48
49impl<A: AuthToken> Query<A> for GetLyricsIDQuery<'_> {
50    type Output = LyricsID<'static>;
51    type Method = PostMethod;
52}
53impl PostQuery for GetLyricsIDQuery<'_> {
54    fn header(&self) -> serde_json::Map<String, serde_json::Value> {
55        let serde_json::Value::Object(map) = json!({
56            "enablePersistentPlaylistPanel": true,
57            "isAudioOnly": true,
58            "tunerSettingValue": "AUTOMIX_SETTING_NORMAL",
59            "playlistId" : format!("RDAMVM{}", self.video_id.get_raw()),
60            "videoId" : self.video_id.get_raw(),
61        }) else {
62            unreachable!()
63        };
64        map
65    }
66    fn path(&self) -> &str {
67        "next"
68    }
69    fn params(&self) -> Vec<(&str, Cow<'_, str>)> {
70        vec![]
71    }
72}
73
74impl<A: AuthToken> Query<A> for GetLyricsQuery<'_> {
75    type Output = Lyrics;
76    type Method = PostMethod;
77}
78impl PostQuery for GetLyricsQuery<'_> {
79    fn header(&self) -> serde_json::Map<String, serde_json::Value> {
80        let serde_json::Value::Object(map) = json!({
81            "browseId": self.id.get_raw(),
82        }) else {
83            unreachable!()
84        };
85        map
86    }
87    fn path(&self) -> &str {
88        "browse"
89    }
90    fn params(&self) -> std::vec::Vec<(&str, std::borrow::Cow<'_, str>)> {
91        vec![]
92    }
93}
94
95impl<A: AuthToken> Query<A> for GetSongTrackingUrlQuery<'_> {
96    type Output = SongTrackingUrl<'static>;
97    type Method = PostMethod;
98}
99impl PostQuery for GetSongTrackingUrlQuery<'_> {
100    fn header(&self) -> serde_json::Map<String, serde_json::Value> {
101        serde_json::Map::from_iter([
102            (
103                "playbackContext".to_string(),
104                json!(
105                    {
106                        "contentPlaybackContext": {
107                            "signatureTimestamp": self.signature_timestamp
108                        }
109                    }
110                ),
111            ),
112            ("video_id".to_string(), json!(self.video_id)),
113        ])
114    }
115    fn params(&self) -> Vec<(&str, Cow<'_, str>)> {
116        vec![]
117    }
118    fn path(&self) -> &str {
119        "player"
120    }
121}
122
123// Original: https://github.com/sigma67/ytmusicapi/blob/a15d90c4f356a530c6b2596277a9d70c0b117a0c/ytmusicapi/mixins/_utils.py#L42
124/// Approximation for google's signatureTimestamp which would normally be
125/// extracted from base.js.
126fn get_signature_timestamp() -> Result<u64> {
127    const SECONDS_IN_DAY: u64 = 60 * 60 * 24;
128    Ok(SystemTime::now()
129        .duration_since(SystemTime::UNIX_EPOCH)?
130        .as_secs()
131        // SAFETY: SECONDS_IN_DAY is nonzero.
132        .saturating_div(SECONDS_IN_DAY))
133}