spotify_client/client/
spotify.rs

1use anyhow::{anyhow, Result};
2use librespot_core::session::Session;
3use maybe_async::maybe_async;
4use rspotify::{
5    clients::{BaseClient, OAuthClient},
6    http::HttpClient,
7    sync::Mutex,
8    ClientResult, Config, Credentials, OAuth, Token,
9};
10use std::{fmt, sync::Arc};
11
12use crate::token;
13
14#[derive(Clone, Default)]
15/// A Spotify client to interact with Spotify API server
16pub struct Spotify {
17    creds: Credentials,
18    oauth: OAuth,
19    config: Config,
20    token: Arc<Mutex<Option<Token>>>,
21    client_id: String,
22    http: HttpClient,
23    // session should always be non-empty, but `Option` is used to implement `Default`,
24    // which is required to implement `rspotify::BaseClient` trait
25    pub(crate) session: Arc<tokio::sync::Mutex<Option<Session>>>,
26}
27
28impl fmt::Debug for Spotify {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        f.debug_struct("Spotify")
31            .field("creds", &self.creds)
32            .field("oauth", &self.oauth)
33            .field("config", &self.config)
34            .field("token", &self.token)
35            .field("client_id", &self.client_id)
36            .finish()
37    }
38}
39
40impl Spotify {
41    /// creates a new Spotify client
42    pub fn new(session: Session, client_id: String) -> Spotify {
43        Self {
44            creds: Credentials::default(),
45            oauth: OAuth::default(),
46            config: Config {
47                token_refreshing: true,
48                ..Default::default()
49            },
50            token: Arc::new(Mutex::new(None)),
51            http: HttpClient::default(),
52            session: Arc::new(tokio::sync::Mutex::new(Some(session))),
53            client_id,
54        }
55    }
56
57    pub async fn session(&self) -> Session {
58        self.session
59            .lock()
60            .await
61            .clone()
62            .expect("non-empty Spotify session")
63    }
64
65    /// gets a Spotify access token.
66    /// The function may retrieve a new token and update the current token
67    /// stored inside the client if the old one is expired.
68    pub async fn access_token(&self) -> Result<String> {
69        let should_update = match self.token.lock().await.unwrap().as_ref() {
70            Some(token) => token.is_expired(),
71            None => true,
72        };
73        if should_update {
74            self.refresh_token().await?;
75        }
76
77        match self.token.lock().await.unwrap().as_ref() {
78            Some(token) => Ok(token.access_token.clone()),
79            None => Err(anyhow!(
80                "failed to get the authentication token stored inside the client."
81            )),
82        }
83    }
84}
85
86// TODO: remove the below uses of `maybe_async` crate once
87// async trait is fully supported in stable Rust.
88
89#[maybe_async]
90impl BaseClient for Spotify {
91    fn get_http(&self) -> &HttpClient {
92        &self.http
93    }
94
95    fn get_token(&self) -> Arc<Mutex<Option<Token>>> {
96        Arc::clone(&self.token)
97    }
98
99    fn get_creds(&self) -> &Credentials {
100        &self.creds
101    }
102
103    fn get_config(&self) -> &Config {
104        &self.config
105    }
106
107    async fn refetch_token(&self) -> ClientResult<Option<Token>> {
108        let session = self.session().await;
109        let old_token = self.token.lock().await.unwrap().clone();
110
111        if session.is_invalid() {
112            tracing::error!("Failed to get a new token: invalid session");
113            return Ok(old_token);
114        }
115
116        match token::get_token(&session, &self.client_id).await {
117            Ok(token) => Ok(Some(token)),
118            Err(err) => {
119                tracing::error!("Failed to get a new token: {err:#}");
120                Ok(old_token)
121            }
122        }
123    }
124}
125
126/// Implement `OAuthClient` trait for `Spotify` struct
127/// to allow calling methods that get/modify user's data such as
128/// `current_user_playlists`, `playlist_add_items`, etc.
129///
130/// Because the `Spotify` client interacts with Spotify APIs
131/// using an access token that is manually retrieved by
132/// the `librespot::get_token` function, implementing
133/// `OAuthClient::get_oauth` and `OAuthClient::request_token` is unnecessary
134#[maybe_async]
135impl OAuthClient for Spotify {
136    fn get_oauth(&self) -> &OAuth {
137        panic!("`OAuthClient::get_oauth` should never be called!")
138    }
139
140    async fn request_token(&self, _code: &str) -> ClientResult<()> {
141        panic!("`OAuthClient::request_token` should never be called!")
142    }
143}