Skip to main content

ytmapi_rs/
auth.rs

1//! Available authorisation tokens.
2use crate::Error;
3use crate::client::{Client, QueryResponse};
4use crate::error::Result;
5use crate::parse::ProcessedResult;
6use crate::query::{GetQuery, PostQuery};
7use crate::utils::constants::{YTM_API_URL, YTM_PARAMS, YTM_PARAMS_KEY};
8pub use browser::BrowserToken;
9use chrono::Utc;
10pub use oauth::{OAuthToken, OAuthTokenGenerator};
11use reqwest::Url;
12use serde_json::json;
13use std::borrow::Cow;
14use std::marker::PhantomData;
15
16pub mod browser;
17pub mod noauth;
18pub mod oauth;
19
20/// An AuthToken is required to use the API.
21/// AuthToken is reponsible for HTTP request headers, client_version and
22/// performing the initial error checking and processing prior to parsing.
23pub trait AuthToken: Sized {
24    fn headers(&self) -> Result<impl IntoIterator<Item = (&str, Cow<'_, str>)>>;
25    fn client_version(&self) -> Cow<'_, str>;
26    fn deserialize_response<Q>(raw: RawResult<Q, Self>) -> Result<ProcessedResult<Q>>;
27}
28
29/// The raw result of a query to the API.
30// NOTE: The reason this is exposed in the public API, is that it is required to implement
31// AuthToken.
32#[derive(PartialEq, Debug)]
33pub struct RawResult<'a, Q, A>
34where
35    A: AuthToken,
36{
37    // A PhantomData is held to ensure token is processed correctly depending on the AuthToken that
38    // generated it.
39    token: PhantomData<A>,
40    /// The query that generated this RawResult.
41    pub query: &'a Q,
42    /// The raw string output returned from the web request to YouTube.
43    pub json: String,
44}
45
46impl<'a, Q, A: AuthToken> RawResult<'a, Q, A> {
47    pub(crate) fn from_raw(json: String, query: &'a Q) -> Self {
48        Self {
49            query,
50            token: PhantomData,
51            json,
52        }
53    }
54    pub fn destructure_json(self) -> String {
55        self.json
56    }
57    pub fn process(self) -> Result<ProcessedResult<'a, Q>> {
58        A::deserialize_response(self)
59    }
60}
61
62pub(crate) async fn raw_query_post<'a, A: AuthToken, Q: PostQuery>(
63    q: &'a Q,
64    tok: &A,
65    c: &Client,
66) -> Result<RawResult<'a, Q, A>> {
67    let url = format!("{YTM_API_URL}{}{YTM_PARAMS}{YTM_PARAMS_KEY}", q.path());
68    let mut body = json!({
69        "context" : {
70            "client" : {
71                "clientName" : "WEB_REMIX",
72                "clientVersion" : tok.client_version(),
73                "user" : {},
74            },
75        },
76    });
77    if let Some(body) = body.as_object_mut() {
78        body.append(&mut q.header());
79    } else {
80        unreachable!("Body created in this function as an object")
81    };
82    let QueryResponse { text, .. } = c
83        .post_json_query(url, tok.headers()?, &body, &q.params())
84        .await?;
85    Ok(RawResult::from_raw(text, q))
86}
87
88pub(crate) async fn raw_query_get<'a, Q: GetQuery, A: AuthToken>(
89    tok: &A,
90    client: &Client,
91    query: &'a Q,
92) -> Result<RawResult<'a, Q, A>> {
93    let url = Url::parse_with_params(query.url(), query.params())
94        .map_err(|e| Error::web(format!("{e}")))?;
95    let result = client
96        .get_query(url, tok.headers()?, &query.params())
97        .await?;
98    let result = RawResult::from_raw(result.text, query);
99    Ok(result)
100}
101
102/// Marker trait to mark an AuthToken as LoggedIn
103/// To allow Query implementors to write like
104/// `impl<A: LoggedIn> Query<A> for AddSongToPlaylistQuery`
105/// Since AuthToken is sealed, no-one else can implement this.
106pub trait LoggedIn: AuthToken {}
107
108impl LoggedIn for BrowserToken {}
109impl LoggedIn for OAuthToken {}
110
111/// Generate a dummy client version at the provided time.
112/// Original implementation: https://github.com/sigma67/ytmusicapi/blob/459bc40e4ce31584f9d87cf75838a1f404aa472d/ytmusicapi/helpers.py#L35C18-L35C31
113fn fallback_client_version(time: &chrono::DateTime<Utc>) -> String {
114    format!("1.{}.01.00", time.format("%Y%m%d"))
115}