Skip to main content

ytmapi_rs/auth/
browser.rs

1use super::{AuthToken, RawResult};
2use crate::client::Client;
3use crate::error::{Error, Result};
4use crate::parse::ProcessedResult;
5use crate::utils;
6use crate::utils::constants::{USER_AGENT, YTM_URL};
7use serde::{Deserialize, Serialize};
8use std::borrow::Cow;
9use std::fmt::Debug;
10use std::path::Path;
11
12#[derive(Clone, Serialize, Deserialize)]
13pub struct BrowserToken {
14    sapisid: String,
15    client_version: String,
16    cookies: String,
17}
18
19impl AuthToken for BrowserToken {
20    fn client_version(&self) -> Cow<'_, str> {
21        (&self.client_version).into()
22    }
23    fn deserialize_response<Q>(
24        raw: RawResult<Q, Self>,
25    ) -> Result<crate::parse::ProcessedResult<Q>> {
26        let processed = ProcessedResult::try_from(raw)?;
27        // Guard against error codes in json response.
28        // TODO: Check for a response the reflects an expired Headers token
29        // TODO: Add a test for this
30        if let Some(error) = processed.get_json().pointer("/error") {
31            let Some(code) = error.pointer("/code").and_then(|v| v.as_u64()) else {
32                // TODO: Better error.
33                return Err(Error::response("API reported an error but no code"));
34            };
35            let message = error
36                .pointer("/message")
37                .and_then(|s| s.as_str())
38                .map(|s| s.to_string())
39                .unwrap_or_default();
40            return Err(Error::other_code(code, message));
41        }
42        Ok(processed)
43    }
44    fn headers(&self) -> Result<impl IntoIterator<Item = (&str, Cow<'_, str>)>> {
45        let hash = utils::hash_sapisid(&self.sapisid);
46        Ok([
47            ("X-Origin", YTM_URL.into()),
48            ("Origin", YTM_URL.into()),
49            ("Content-Type", "application/json".into()),
50            ("Authorization", format!("SAPISIDHASH {hash}").into()),
51            ("Cookie", self.cookies.as_str().into()),
52            ("Accept", "*/*".into()),
53            ("Accept-Encoding", "gzip, deflate".into()),
54        ])
55    }
56}
57
58impl BrowserToken {
59    pub async fn from_str(cookie_str: &str, client: &Client) -> Result<Self> {
60        let cookies = cookie_str.trim().to_string();
61        let user_agent = USER_AGENT;
62        // TODO: Confirm if parsing for expired user agent also relevant here.
63        let initial_headers = [
64            ("User-Agent", user_agent.into()),
65            ("Cookie", cookies.as_str().into()),
66        ];
67        let response_text = client.get_query(YTM_URL, initial_headers, &()).await?.text;
68        // parse for user agent issues here.
69        if response_text.contains("Sorry, YouTube Music is not optimised for your browser. Check for updates or try Google Chrome.") {
70            return Err(Error::invalid_user_agent(user_agent));
71        };
72        // TODO: Better error.
73        let client_version = response_text
74            .split_once("INNERTUBE_CLIENT_VERSION\":\"")
75            .ok_or(Error::header())?
76            .1
77            .split_once('\"')
78            .ok_or(Error::header())?
79            .0
80            .to_string();
81        let sapisid = cookies
82            .split_once("SAPISID=")
83            .ok_or(Error::header())?
84            .1
85            .split_once(';')
86            .ok_or(Error::header())?
87            .0
88            .to_string();
89        Ok(Self {
90            sapisid,
91            client_version,
92            cookies,
93        })
94    }
95    pub async fn from_cookie_file<P>(path: P, client: &Client) -> Result<Self>
96    where
97        P: AsRef<Path>,
98    {
99        let contents = tokio::fs::read_to_string(path).await?;
100        BrowserToken::from_str(&contents, client).await
101    }
102}
103
104// Don't use default Debug implementation for BrowserToken - contents are
105// private
106impl Debug for BrowserToken {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "Private BrowserToken")
109    }
110}