openlibspot_core/
session.rs

1use std::{
2    collections::HashMap,
3    future::Future,
4    io,
5    pin::Pin,
6    sync::{Arc, Weak},
7    task::{Context, Poll},
8    time::{Duration, SystemTime, UNIX_EPOCH},
9};
10
11use byteorder::{BigEndian, ByteOrder};
12use bytes::Bytes;
13use futures_core::TryStream;
14use futures_util::{future, ready, StreamExt, TryStreamExt};
15use num_traits::FromPrimitive;
16use once_cell::sync::OnceCell;
17use parking_lot::RwLock;
18use quick_xml::events::Event;
19use thiserror::Error;
20use tokio::{sync::mpsc, time::Instant};
21use tokio_stream::wrappers::UnboundedReceiverStream;
22
23use crate::{
24    apresolve::ApResolver,
25    audio_key::AudioKeyManager,
26    authentication::Credentials,
27    cache::Cache,
28    channel::ChannelManager,
29    config::SessionConfig,
30    connection::{self, AuthenticationError},
31    http_client::HttpClient,
32    mercury::MercuryManager,
33    packet::PacketType,
34    protocol::keyexchange::ErrorCode,
35    spclient::SpClient,
36    token::TokenProvider,
37    Error,
38};
39
40#[derive(Debug, Error)]
41pub enum SessionError {
42    #[error(transparent)]
43    AuthenticationError(#[from] AuthenticationError),
44    #[error("Cannot create session: {0}")]
45    IoError(#[from] io::Error),
46    #[error("Session is not connected")]
47    NotConnected,
48    #[error("packet {0} unknown")]
49    Packet(u8),
50}
51
52impl From<SessionError> for Error {
53    fn from(err: SessionError) -> Self {
54        match err {
55            SessionError::AuthenticationError(_) => Error::unauthenticated(err),
56            SessionError::IoError(_) => Error::unavailable(err),
57            SessionError::NotConnected => Error::unavailable(err),
58            SessionError::Packet(_) => Error::unimplemented(err),
59        }
60    }
61}
62
63pub type UserAttributes = HashMap<String, String>;
64
65#[derive(Debug, Clone, Default)]
66pub struct UserData {
67    pub country: String,
68    pub canonical_username: String,
69    pub attributes: UserAttributes,
70}
71
72#[derive(Debug, Clone, Default)]
73struct SessionData {
74    client_id: String,
75    client_name: String,
76    client_brand_name: String,
77    client_model_name: String,
78    connection_id: String,
79    time_delta: i64,
80    invalid: bool,
81    user_data: UserData,
82    last_ping: Option<Instant>,
83}
84
85struct SessionInternal {
86    config: SessionConfig,
87    data: RwLock<SessionData>,
88
89    http_client: HttpClient,
90    tx_connection: OnceCell<mpsc::UnboundedSender<(u8, Vec<u8>)>>,
91
92    apresolver: OnceCell<ApResolver>,
93    audio_key: OnceCell<AudioKeyManager>,
94    channel: OnceCell<ChannelManager>,
95    mercury: OnceCell<MercuryManager>,
96    spclient: OnceCell<SpClient>,
97    token_provider: OnceCell<TokenProvider>,
98    cache: Option<Arc<Cache>>,
99
100    handle: tokio::runtime::Handle,
101}
102
103/// A shared reference to a Spotify session.
104///
105/// After instantiating, you need to login via [Session::connect].
106/// You can either implement the whole playback logic yourself by using
107/// this structs interface directly or hand it to a
108/// `Player`.
109///
110/// *Note*: [Session] instances cannot yet be reused once invalidated. After
111/// an unexpectedly closed connection, you'll need to create a new [Session].
112#[derive(Clone)]
113pub struct Session(Arc<SessionInternal>);
114
115impl Session {
116    pub fn new(config: SessionConfig, cache: Option<Cache>) -> Self {
117        let http_client = HttpClient::new(config.proxy.as_ref());
118
119        debug!("new Session");
120
121        let session_data = SessionData {
122            client_id: config.client_id.clone(),
123            ..SessionData::default()
124        };
125
126        Self(Arc::new(SessionInternal {
127            config,
128            data: RwLock::new(session_data),
129            http_client,
130            tx_connection: OnceCell::new(),
131            cache: cache.map(Arc::new),
132            apresolver: OnceCell::new(),
133            audio_key: OnceCell::new(),
134            channel: OnceCell::new(),
135            mercury: OnceCell::new(),
136            spclient: OnceCell::new(),
137            token_provider: OnceCell::new(),
138            handle: tokio::runtime::Handle::current(),
139        }))
140    }
141
142    pub async fn connect(
143        &self,
144        credentials: Credentials,
145        store_credentials: bool,
146    ) -> Result<(), Error> {
147        let (reusable_credentials, transport) = loop {
148            let ap = self.apresolver().resolve("accesspoint").await?;
149            info!("Connecting to AP \"{}:{}\"", ap.0, ap.1);
150            let mut transport =
151                connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?;
152
153            match connection::authenticate(
154                &mut transport,
155                credentials.clone(),
156                &self.config().device_id,
157            )
158            .await
159            {
160                Ok(creds) => break (creds, transport),
161                Err(e) => {
162                    if let Some(AuthenticationError::LoginFailed(ErrorCode::TryAnotherAP)) =
163                        e.error.downcast_ref::<AuthenticationError>()
164                    {
165                        warn!("Instructed to try another access point...");
166                        continue;
167                    } else {
168                        return Err(e);
169                    }
170                }
171            }
172        };
173
174        info!("Authenticated as \"{}\" !", reusable_credentials.username);
175        self.set_username(&reusable_credentials.username);
176        if let Some(cache) = self.cache() {
177            if store_credentials {
178                let cred_changed = cache
179                    .credentials()
180                    .map(|c| c != reusable_credentials)
181                    .unwrap_or(true);
182                if cred_changed {
183                    cache.save_credentials(&reusable_credentials);
184                }
185            }
186        }
187
188        let (tx_connection, rx_connection) = mpsc::unbounded_channel();
189        self.0
190            .tx_connection
191            .set(tx_connection)
192            .map_err(|_| SessionError::NotConnected)?;
193
194        let (sink, stream) = transport.split();
195        let sender_task = UnboundedReceiverStream::new(rx_connection)
196            .map(Ok)
197            .forward(sink);
198        let receiver_task = DispatchTask(stream, self.weak());
199        let timeout_task = Session::session_timeout(self.weak());
200
201        tokio::spawn(async move {
202            let result = future::try_join3(sender_task, receiver_task, timeout_task).await;
203
204            if let Err(e) = result {
205                error!("{}", e);
206            }
207        });
208
209        Ok(())
210    }
211
212    pub fn apresolver(&self) -> &ApResolver {
213        self.0
214            .apresolver
215            .get_or_init(|| ApResolver::new(self.weak()))
216    }
217
218    pub fn audio_key(&self) -> &AudioKeyManager {
219        self.0
220            .audio_key
221            .get_or_init(|| AudioKeyManager::new(self.weak()))
222    }
223
224    pub fn channel(&self) -> &ChannelManager {
225        self.0
226            .channel
227            .get_or_init(|| ChannelManager::new(self.weak()))
228    }
229
230    pub fn http_client(&self) -> &HttpClient {
231        &self.0.http_client
232    }
233
234    pub fn mercury(&self) -> &MercuryManager {
235        self.0
236            .mercury
237            .get_or_init(|| MercuryManager::new(self.weak()))
238    }
239
240    pub fn spclient(&self) -> &SpClient {
241        self.0.spclient.get_or_init(|| SpClient::new(self.weak()))
242    }
243
244    pub fn token_provider(&self) -> &TokenProvider {
245        self.0
246            .token_provider
247            .get_or_init(|| TokenProvider::new(self.weak()))
248    }
249
250    /// Returns an error, when we haven't received a ping for too long (2 minutes),
251    /// which means that we silently lost connection to Spotify servers.
252    async fn session_timeout(session: SessionWeak) -> io::Result<()> {
253        // pings are sent every 2 minutes and a 5 second margin should be fine
254        const SESSION_TIMEOUT: Duration = Duration::from_secs(125);
255
256        while let Some(session) = session.try_upgrade() {
257            if session.is_invalid() {
258                break;
259            }
260            let last_ping = session.0.data.read().last_ping.unwrap_or_else(Instant::now);
261            if last_ping.elapsed() >= SESSION_TIMEOUT {
262                session.shutdown();
263                // TODO: Optionally reconnect (with cached/last credentials?)
264                return Err(io::Error::new(
265                    io::ErrorKind::TimedOut,
266                    "session lost connection to server",
267                ));
268            }
269            // drop the strong reference before sleeping
270            drop(session);
271            // a potential timeout cannot occur at least until SESSION_TIMEOUT after the last_ping
272            tokio::time::sleep_until(last_ping + SESSION_TIMEOUT).await;
273        }
274        Ok(())
275    }
276
277    pub fn time_delta(&self) -> i64 {
278        self.0.data.read().time_delta
279    }
280
281    pub fn spawn<T>(&self, task: T)
282    where
283        T: Future + Send + 'static,
284        T::Output: Send + 'static,
285    {
286        self.0.handle.spawn(task);
287    }
288
289    fn debug_info(&self) {
290        debug!(
291            "Session strong={} weak={}",
292            Arc::strong_count(&self.0),
293            Arc::weak_count(&self.0)
294        );
295    }
296
297    fn check_catalogue(attributes: &UserAttributes) {
298        if let Some(account_type) = attributes.get("type") {
299            if account_type != "premium" {
300                // error!("openlibspot does not support {:?} accounts.", account_type);
301                info!("Please support Spotify and your artists and sign up for a premium account.");
302
303                // TODO: logout instead of exiting
304                // exit(1);
305            }
306        }
307    }
308
309    fn dispatch(&self, cmd: u8, data: Bytes) -> Result<(), Error> {
310        use PacketType::*;
311
312        let packet_type = FromPrimitive::from_u8(cmd);
313        let cmd = match packet_type {
314            Some(cmd) => cmd,
315            None => {
316                trace!("Ignoring unknown packet {:x}", cmd);
317                return Err(SessionError::Packet(cmd).into());
318            }
319        };
320
321        match packet_type {
322            Some(Ping) => {
323                let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64;
324                let timestamp = SystemTime::now()
325                    .duration_since(UNIX_EPOCH)
326                    .unwrap_or(Duration::ZERO)
327                    .as_secs() as i64;
328
329                {
330                    let mut data = self.0.data.write();
331                    data.time_delta = server_timestamp.saturating_sub(timestamp);
332                    data.last_ping = Some(Instant::now());
333                }
334
335                self.debug_info();
336                self.send_packet(Pong, vec![0, 0, 0, 0])
337            }
338            Some(CountryCode) => {
339                let country = String::from_utf8(data.as_ref().to_owned())?;
340                info!("Country: {:?}", country);
341                self.0.data.write().user_data.country = country;
342                Ok(())
343            }
344            Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data),
345            Some(AesKey) | Some(AesKeyError) => self.audio_key().dispatch(cmd, data),
346            Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => {
347                self.mercury().dispatch(cmd, data)
348            }
349            Some(ProductInfo) => {
350                let data = std::str::from_utf8(&data)?;
351                let mut reader = quick_xml::Reader::from_str(data);
352
353                let mut buf = Vec::new();
354                let mut current_element = String::new();
355                let mut user_attributes: UserAttributes = HashMap::new();
356
357                loop {
358                    match reader.read_event_into(&mut buf) {
359                        Ok(Event::Start(ref element)) => {
360                            current_element = std::str::from_utf8(element)?.to_owned()
361                        }
362                        Ok(Event::End(_)) => {
363                            current_element = String::new();
364                        }
365                        Ok(Event::Text(ref value)) => {
366                            if !current_element.is_empty() {
367                                let _ = user_attributes
368                                    .insert(current_element.clone(), value.unescape()?.to_string());
369                            }
370                        }
371                        Ok(Event::Eof) => break,
372                        Ok(_) => (),
373                        Err(e) => warn!(
374                            "Error parsing XML at position {}: {:?}",
375                            reader.buffer_position(),
376                            e
377                        ),
378                    }
379                }
380
381                trace!("Received product info: {:#?}", user_attributes);
382                Self::check_catalogue(&user_attributes);
383
384                self.0.data.write().user_data.attributes = user_attributes;
385                Ok(())
386            }
387            Some(PongAck)
388            | Some(SecretBlock)
389            | Some(LegacyWelcome)
390            | Some(UnknownDataAllZeros)
391            | Some(LicenseVersion) => Ok(()),
392            _ => {
393                trace!("Ignoring {:?} packet with data {:#?}", cmd, data);
394                Err(SessionError::Packet(cmd as u8).into())
395            }
396        }
397    }
398
399    pub fn send_packet(&self, cmd: PacketType, data: Vec<u8>) -> Result<(), Error> {
400        match self.0.tx_connection.get() {
401            Some(tx) => Ok(tx.send((cmd as u8, data))?),
402            None => Err(SessionError::NotConnected.into()),
403        }
404    }
405
406    pub fn cache(&self) -> Option<&Arc<Cache>> {
407        self.0.cache.as_ref()
408    }
409
410    pub fn config(&self) -> &SessionConfig {
411        &self.0.config
412    }
413
414    // This clones a fairly large struct, so use a specific getter or setter unless
415    // you need more fields at once, in which case this can spare multiple `read`
416    // locks.
417    pub fn user_data(&self) -> UserData {
418        self.0.data.read().user_data.clone()
419    }
420
421    pub fn device_id(&self) -> &str {
422        &self.config().device_id
423    }
424
425    pub fn client_id(&self) -> String {
426        self.0.data.read().client_id.clone()
427    }
428
429    pub fn set_client_id(&self, client_id: &str) {
430        self.0.data.write().client_id = client_id.to_owned();
431    }
432
433    pub fn client_name(&self) -> String {
434        self.0.data.read().client_name.clone()
435    }
436
437    pub fn set_client_name(&self, client_name: &str) {
438        self.0.data.write().client_name = client_name.to_owned();
439    }
440
441    pub fn client_brand_name(&self) -> String {
442        self.0.data.read().client_brand_name.clone()
443    }
444
445    pub fn set_client_brand_name(&self, client_brand_name: &str) {
446        self.0.data.write().client_brand_name = client_brand_name.to_owned();
447    }
448
449    pub fn client_model_name(&self) -> String {
450        self.0.data.read().client_model_name.clone()
451    }
452
453    pub fn set_client_model_name(&self, client_model_name: &str) {
454        self.0.data.write().client_model_name = client_model_name.to_owned();
455    }
456
457    pub fn connection_id(&self) -> String {
458        self.0.data.read().connection_id.clone()
459    }
460
461    pub fn set_connection_id(&self, connection_id: &str) {
462        self.0.data.write().connection_id = connection_id.to_owned();
463    }
464
465    pub fn username(&self) -> String {
466        self.0.data.read().user_data.canonical_username.clone()
467    }
468
469    pub fn set_username(&self, username: &str) {
470        self.0.data.write().user_data.canonical_username = username.to_owned();
471    }
472
473    pub fn country(&self) -> String {
474        self.0.data.read().user_data.country.clone()
475    }
476
477    pub fn filter_explicit_content(&self) -> bool {
478        match self.get_user_attribute("filter-explicit-content") {
479            Some(value) => matches!(&*value, "1"),
480            None => false,
481        }
482    }
483
484    pub fn autoplay(&self) -> bool {
485        if let Some(overide) = self.config().autoplay {
486            return overide;
487        }
488
489        match self.get_user_attribute("autoplay") {
490            Some(value) => matches!(&*value, "1"),
491            None => false,
492        }
493    }
494
495    pub fn set_user_attribute(&self, key: &str, value: &str) -> Option<String> {
496        let mut dummy_attributes = UserAttributes::new();
497        dummy_attributes.insert(key.to_owned(), value.to_owned());
498        Self::check_catalogue(&dummy_attributes);
499
500        self.0
501            .data
502            .write()
503            .user_data
504            .attributes
505            .insert(key.to_owned(), value.to_owned())
506    }
507
508    pub fn set_user_attributes(&self, attributes: UserAttributes) {
509        Self::check_catalogue(&attributes);
510
511        self.0.data.write().user_data.attributes.extend(attributes)
512    }
513
514    pub fn get_user_attribute(&self, key: &str) -> Option<String> {
515        self.0
516            .data
517            .read()
518            .user_data
519            .attributes
520            .get(key)
521            .map(Clone::clone)
522    }
523
524    fn weak(&self) -> SessionWeak {
525        SessionWeak(Arc::downgrade(&self.0))
526    }
527
528    pub fn shutdown(&self) {
529        debug!("Invalidating session");
530        self.0.data.write().invalid = true;
531        self.mercury().shutdown();
532        self.channel().shutdown();
533    }
534
535    pub fn is_invalid(&self) -> bool {
536        self.0.data.read().invalid
537    }
538}
539
540#[derive(Clone)]
541pub struct SessionWeak(Weak<SessionInternal>);
542
543impl SessionWeak {
544    fn try_upgrade(&self) -> Option<Session> {
545        self.0.upgrade().map(Session)
546    }
547
548    pub(crate) fn upgrade(&self) -> Session {
549        self.try_upgrade()
550            .expect("session was dropped and so should have this component")
551    }
552}
553
554impl Drop for SessionInternal {
555    fn drop(&mut self) {
556        debug!("drop Session");
557    }
558}
559
560struct DispatchTask<S>(S, SessionWeak)
561where
562    S: TryStream<Ok = (u8, Bytes)> + Unpin;
563
564impl<S> Future for DispatchTask<S>
565where
566    S: TryStream<Ok = (u8, Bytes)> + Unpin,
567    <S as TryStream>::Ok: std::fmt::Debug,
568{
569    type Output = Result<(), S::Error>;
570
571    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
572        let session = match self.1.try_upgrade() {
573            Some(session) => session,
574            None => return Poll::Ready(Ok(())),
575        };
576
577        loop {
578            let (cmd, data) = match ready!(self.0.try_poll_next_unpin(cx)) {
579                Some(Ok(t)) => t,
580                None => {
581                    warn!("Connection to server closed.");
582                    session.shutdown();
583                    return Poll::Ready(Ok(()));
584                }
585                Some(Err(e)) => {
586                    session.shutdown();
587                    return Poll::Ready(Err(e));
588                }
589            };
590
591            if let Err(e) = session.dispatch(cmd, data) {
592                debug!("could not dispatch command: {}", e);
593            }
594        }
595    }
596}
597
598impl<S> Drop for DispatchTask<S>
599where
600    S: TryStream<Ok = (u8, Bytes)> + Unpin,
601{
602    fn drop(&mut self) {
603        debug!("drop Dispatch");
604    }
605}