librespot_core/
token.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Ported from librespot-java. Relicensed under MIT with permission.

// Known scopes:
//   ugc-image-upload, playlist-read-collaborative, playlist-modify-private,
//   playlist-modify-public, playlist-read-private, user-read-playback-position,
//   user-read-recently-played, user-top-read, user-modify-playback-state,
//   user-read-currently-playing, user-read-playback-state, user-read-private, user-read-email,
//   user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming,
//   app-remote-control

use std::time::{Duration, Instant};

use serde::Deserialize;
use thiserror::Error;

use crate::Error;

component! {
    TokenProvider : TokenProviderInner {
        tokens: Vec<Token> = vec![],
    }
}

#[derive(Debug, Error)]
pub enum TokenError {
    #[error("no tokens available")]
    Empty,
}

impl From<TokenError> for Error {
    fn from(err: TokenError) -> Self {
        Error::unavailable(err)
    }
}

#[derive(Clone, Debug)]
pub struct Token {
    pub access_token: String,
    pub expires_in: Duration,
    pub token_type: String,
    pub scopes: Vec<String>,
    pub timestamp: Instant,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TokenData {
    access_token: String,
    expires_in: u64,
    token_type: String,
    scope: Vec<String>,
}

impl TokenProvider {
    fn find_token(&self, scopes: Vec<&str>) -> Option<usize> {
        self.lock(|inner| {
            (0..inner.tokens.len()).find(|&i| inner.tokens[i].in_scopes(scopes.clone()))
        })
    }

    // scopes must be comma-separated
    pub async fn get_token(&self, scopes: &str) -> Result<Token, Error> {
        let client_id = self.session().client_id();
        if client_id.is_empty() {
            return Err(Error::invalid_argument("Client ID cannot be empty"));
        }

        if let Some(index) = self.find_token(scopes.split(',').collect()) {
            let cached_token = self.lock(|inner| inner.tokens[index].clone());
            if cached_token.is_expired() {
                self.lock(|inner| inner.tokens.remove(index));
            } else {
                return Ok(cached_token);
            }
        }

        trace!(
            "Requested token in scopes {:?} unavailable or expired, requesting new token.",
            scopes
        );

        let query_uri = format!(
            "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}",
            scopes,
            client_id,
            self.session().device_id(),
        );
        let request = self.session().mercury().get(query_uri)?;
        let response = request.await?;
        let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
        let token = Token::from_json(String::from_utf8(data)?)?;
        trace!("Got token: {:#?}", token);
        self.lock(|inner| inner.tokens.push(token.clone()));
        Ok(token)
    }
}

impl Token {
    const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);

    pub fn from_json(body: String) -> Result<Self, Error> {
        let data: TokenData = serde_json::from_slice(body.as_ref())?;
        Ok(Self {
            access_token: data.access_token,
            expires_in: Duration::from_secs(data.expires_in),
            token_type: data.token_type,
            scopes: data.scope,
            timestamp: Instant::now(),
        })
    }

    pub fn is_expired(&self) -> bool {
        self.timestamp + (self.expires_in.saturating_sub(Self::EXPIRY_THRESHOLD)) < Instant::now()
    }

    pub fn in_scope(&self, scope: &str) -> bool {
        for s in &self.scopes {
            if *s == scope {
                return true;
            }
        }
        false
    }

    pub fn in_scopes(&self, scopes: Vec<&str>) -> bool {
        for s in scopes {
            if !self.in_scope(s) {
                return false;
            }
        }
        true
    }
}