ytmapi_rs/auth/
browser.rs

1use super::private::Sealed;
2use super::AuthToken;
3use crate::client::Client;
4use crate::error::{Error, Result};
5use crate::parse::ProcessedResult;
6use crate::query::PostQuery;
7use crate::{client, utils};
8use crate::{
9    process::RawResult,
10    query::Query,
11    utils::constants::{USER_AGENT, YTM_API_URL, YTM_PARAMS, YTM_PARAMS_KEY, YTM_URL},
12};
13use serde::{Deserialize, Serialize};
14use serde_json::json;
15use std::fmt::Debug;
16use std::path::Path;
17
18#[derive(Clone, Serialize, Deserialize)]
19pub struct BrowserToken {
20    sapisid: String,
21    client_version: String,
22    cookies: String,
23}
24
25impl Sealed for BrowserToken {}
26impl AuthToken for BrowserToken {
27    async fn raw_query_post<'a, Q: PostQuery + Query<Self>>(
28        &self,
29        client: &client::Client,
30        query: &'a Q,
31    ) -> Result<RawResult<'a, Q, BrowserToken>> {
32        // TODO: Functionize - used for OAuth as well.
33        let url = format!("{YTM_API_URL}{}{YTM_PARAMS}{YTM_PARAMS_KEY}", query.path());
34        let mut body = json!({
35            "context" : {
36                "client" : {
37                    "clientName" : "WEB_REMIX",
38                    "clientVersion" : self.client_version,
39                },
40            },
41        });
42        if let Some(body) = body.as_object_mut() {
43            body.append(&mut query.header());
44        } else {
45            unreachable!("Body created in this function as an object")
46        };
47        let hash = utils::hash_sapisid(&self.sapisid);
48        let headers = [
49            ("X-Origin", YTM_URL.into()),
50            ("Content-Type", "application/json".into()),
51            ("Authorization", format!("SAPISIDHASH {hash}").into()),
52            ("Cookie", self.cookies.as_str().into()),
53        ];
54        let result = client
55            .post_query(url, headers, &body, &query.params())
56            .await?;
57        let result = RawResult::from_raw(result, query);
58        Ok(result)
59    }
60    async fn raw_query_get<'a, Q: crate::query::GetQuery + Query<Self>>(
61        &self,
62        client: &Client,
63        query: &'a Q,
64    ) -> Result<RawResult<'a, Q, Self>> {
65        // COPY AND PASTE OF ABOVE.
66        let hash = utils::hash_sapisid(&self.sapisid);
67        let headers = [
68            ("X-Origin", YTM_URL.into()),
69            ("Content-Type", "application/json".into()),
70            ("Authorization", format!("SAPISIDHASH {hash}").into()),
71            ("Cookie", self.cookies.as_str().into()),
72        ];
73        let result = client
74            .get_query(query.url(), headers, &query.params())
75            .await?;
76        let result = RawResult::from_raw(result, query);
77        Ok(result)
78    }
79    fn deserialize_json<Q: Query<Self>>(
80        raw: RawResult<Q, Self>,
81    ) -> Result<crate::parse::ProcessedResult<Q>> {
82        let processed = ProcessedResult::try_from(raw)?;
83        // Guard against error codes in json response.
84        // TODO: Add a test for this
85        if let Some(error) = processed.get_json().pointer("/error") {
86            let Some(code) = error.pointer("/code").and_then(|v| v.as_u64()) else {
87                // TODO: Better error.
88                return Err(Error::response("API reported an error but no code"));
89            };
90            let message = error
91                .pointer("/message")
92                .and_then(|s| s.as_str())
93                .map(|s| s.to_string())
94                .unwrap_or_default();
95            match code {
96                // Assuming Error:NotAuthenticated means browser token has expired.
97                // May be incorrect - browser token may be invalid?
98                // TODO: Investigate.
99                401 => return Err(Error::browser_authentication_failed()),
100                other => return Err(Error::other_code(other, message)),
101            }
102        }
103        Ok(processed)
104    }
105}
106
107impl BrowserToken {
108    pub async fn from_str(cookie_str: &str, client: &Client) -> Result<Self> {
109        let cookies = cookie_str.trim().to_string();
110        let user_agent = USER_AGENT;
111        let headers = [
112            ("User-Agent", user_agent.into()),
113            ("Cookie", cookies.as_str().into()),
114        ];
115        let response = client.get_query(YTM_URL, headers, &()).await?;
116        // parse for user agent issues here.
117        if response.contains("Sorry, YouTube Music is not optimised for your browser. Check for updates or try Google Chrome.") {
118            return Err(Error::invalid_user_agent(user_agent));
119        };
120        // TODO: Better error.
121        let client_version = response
122            .split_once("INNERTUBE_CLIENT_VERSION\":\"")
123            .ok_or(Error::header())?
124            .1
125            .split_once('\"')
126            .ok_or(Error::header())?
127            .0
128            .to_string();
129        let sapisid = cookies
130            .split_once("SAPISID=")
131            .ok_or(Error::header())?
132            .1
133            .split_once(';')
134            .ok_or(Error::header())?
135            .0
136            .to_string();
137        Ok(Self {
138            sapisid,
139            client_version,
140            cookies,
141        })
142    }
143    pub async fn from_cookie_file<P>(path: P, client: &Client) -> Result<Self>
144    where
145        P: AsRef<Path>,
146    {
147        let contents = tokio::fs::read_to_string(path).await?;
148        BrowserToken::from_str(&contents, client).await
149    }
150}
151
152// Don't use default Debug implementation for BrowserToken - contents are
153// private
154impl Debug for BrowserToken {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        write!(f, "Private BrowserToken")
157    }
158}