openlibspot_core/
token.rs

1// Ported from librespot-java. Relicensed under MIT with permission.
2
3// Known scopes:
4//   ugc-image-upload, playlist-read-collaborative, playlist-modify-private,
5//   playlist-modify-public, playlist-read-private, user-read-playback-position,
6//   user-read-recently-played, user-top-read, user-modify-playback-state,
7//   user-read-currently-playing, user-read-playback-state, user-read-private, user-read-email,
8//   user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming,
9//   app-remote-control
10
11use std::time::{Duration, Instant};
12
13use serde::Deserialize;
14use thiserror::Error;
15
16use crate::Error;
17
18component! {
19    TokenProvider : TokenProviderInner {
20        tokens: Vec<Token> = vec![],
21    }
22}
23
24#[derive(Debug, Error)]
25pub enum TokenError {
26    #[error("no tokens available")]
27    Empty,
28}
29
30impl From<TokenError> for Error {
31    fn from(err: TokenError) -> Self {
32        Error::unavailable(err)
33    }
34}
35
36#[derive(Clone, Debug)]
37pub struct Token {
38    pub access_token: String,
39    pub expires_in: Duration,
40    pub token_type: String,
41    pub scopes: Vec<String>,
42    pub timestamp: Instant,
43}
44
45#[derive(Deserialize)]
46#[serde(rename_all = "camelCase")]
47struct TokenData {
48    access_token: String,
49    expires_in: u64,
50    token_type: String,
51    scope: Vec<String>,
52}
53
54impl TokenProvider {
55    fn find_token(&self, scopes: Vec<&str>) -> Option<usize> {
56        self.lock(|inner| {
57            (0..inner.tokens.len()).find(|&i| inner.tokens[i].in_scopes(scopes.clone()))
58        })
59    }
60
61    // scopes must be comma-separated
62    pub async fn get_token(&self, scopes: &str) -> Result<Token, Error> {
63        let client_id = self.session().client_id();
64        if client_id.is_empty() {
65            return Err(Error::invalid_argument("Client ID cannot be empty"));
66        }
67
68        if let Some(index) = self.find_token(scopes.split(',').collect()) {
69            let cached_token = self.lock(|inner| inner.tokens[index].clone());
70            if cached_token.is_expired() {
71                self.lock(|inner| inner.tokens.remove(index));
72            } else {
73                return Ok(cached_token);
74            }
75        }
76
77        trace!(
78            "Requested token in scopes {:?} unavailable or expired, requesting new token.",
79            scopes
80        );
81
82        let query_uri = format!(
83            "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}",
84            scopes,
85            client_id,
86            self.session().device_id(),
87        );
88        let request = self.session().mercury().get(query_uri)?;
89        let response = request.await?;
90        let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
91        let token = Token::from_json(String::from_utf8(data)?)?;
92        trace!("Got token: {:#?}", token);
93        self.lock(|inner| inner.tokens.push(token.clone()));
94        Ok(token)
95    }
96}
97
98impl Token {
99    const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);
100
101    pub fn from_json(body: String) -> Result<Self, Error> {
102        let data: TokenData = serde_json::from_slice(body.as_ref())?;
103        Ok(Self {
104            access_token: data.access_token,
105            expires_in: Duration::from_secs(data.expires_in),
106            token_type: data.token_type,
107            scopes: data.scope,
108            timestamp: Instant::now(),
109        })
110    }
111
112    pub fn is_expired(&self) -> bool {
113        self.timestamp + (self.expires_in.saturating_sub(Self::EXPIRY_THRESHOLD)) < Instant::now()
114    }
115
116    pub fn in_scope(&self, scope: &str) -> bool {
117        for s in &self.scopes {
118            if *s == scope {
119                return true;
120            }
121        }
122        false
123    }
124
125    pub fn in_scopes(&self, scopes: Vec<&str>) -> bool {
126        for s in scopes {
127            if !self.in_scope(s) {
128                return false;
129            }
130        }
131        true
132    }
133}