librespot_connect/
state.rs

1pub(super) mod context;
2mod handle;
3mod metadata;
4mod options;
5pub(super) mod provider;
6mod restrictions;
7mod tracks;
8mod transfer;
9
10use crate::{
11    core::{
12        Error, Session, config::DeviceType, date::Date, dealer::protocol::Request,
13        spclient::SpClientResult, version,
14    },
15    model::SpircPlayStatus,
16    protocol::{
17        connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest},
18        media::AudioQuality,
19        player::{
20            ContextIndex, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack,
21            Suppressions,
22        },
23    },
24    state::{
25        context::{ContextType, ResetContext, StateContext},
26        options::ShuffleState,
27        provider::{IsProvider, Provider},
28    },
29};
30use log::LevelFilter;
31use protobuf::{EnumOrUnknown, MessageField};
32use std::{
33    collections::hash_map::DefaultHasher,
34    hash::{Hash, Hasher},
35    time::{Duration, SystemTime, UNIX_EPOCH},
36};
37use thiserror::Error;
38
39// these limitations are essential, otherwise to many tracks will overload the web-player
40const SPOTIFY_MAX_PREV_TRACKS_SIZE: usize = 10;
41const SPOTIFY_MAX_NEXT_TRACKS_SIZE: usize = 80;
42
43#[derive(Debug, Error)]
44pub(super) enum StateError {
45    #[error("the current track couldn't be resolved from the transfer state")]
46    CouldNotResolveTrackFromTransfer,
47    #[error("context is not available. type: {0:?}")]
48    NoContext(ContextType),
49    #[error("could not find track {0:?} in context of {1}")]
50    CanNotFindTrackInContext(Option<usize>, usize),
51    #[error("currently {action} is not allowed because {reason}")]
52    CurrentlyDisallowed {
53        action: &'static str,
54        reason: String,
55    },
56    #[error("the provided context has no tracks")]
57    ContextHasNoTracks,
58    #[error("playback of local files is not supported")]
59    UnsupportedLocalPlayback,
60    #[error("track uri <{0:?}> contains invalid characters")]
61    InvalidTrackUri(Option<String>),
62}
63
64impl From<StateError> for Error {
65    fn from(err: StateError) -> Self {
66        use StateError::*;
67        match err {
68            CouldNotResolveTrackFromTransfer
69            | NoContext(_)
70            | CanNotFindTrackInContext(_, _)
71            | ContextHasNoTracks
72            | InvalidTrackUri(_) => Error::failed_precondition(err),
73            CurrentlyDisallowed { .. } | UnsupportedLocalPlayback => Error::unavailable(err),
74        }
75    }
76}
77
78/// Configuration of the connect device
79#[derive(Debug, Clone)]
80pub struct ConnectConfig {
81    /// The name of the connect device (default: librespot)
82    pub name: String,
83    /// The icon type of the connect device (default: [DeviceType::Speaker])
84    pub device_type: DeviceType,
85    /// Displays the [DeviceType] twice in the ui to show up as a group (default: false)
86    pub is_group: bool,
87    /// The volume with which the connect device will be initialized (default: 50%)
88    pub initial_volume: u16,
89    /// Disables the option to control the volume remotely (default: false)
90    pub disable_volume: bool,
91    /// Number of incremental steps (default: 64)
92    pub volume_steps: u16,
93}
94
95impl Default for ConnectConfig {
96    fn default() -> Self {
97        Self {
98            name: "librespot".to_string(),
99            device_type: DeviceType::Speaker,
100            is_group: false,
101            initial_volume: u16::MAX / 2,
102            disable_volume: false,
103            volume_steps: 64,
104        }
105    }
106}
107
108#[derive(Default, Debug)]
109pub(super) struct ConnectState {
110    /// the entire state that is updated to the remote server
111    request: PutStateRequest,
112
113    unavailable_uri: Vec<String>,
114
115    active_since: Option<SystemTime>,
116    queue_count: u64,
117
118    // separation is necessary because we could have already loaded
119    // the autoplay context but are still playing from the default context
120    /// to update the active context use [switch_active_context](ConnectState::set_active_context)
121    pub active_context: ContextType,
122    fill_up_context: ContextType,
123
124    /// the context from which we play, is used to top up prev and next tracks
125    context: Option<StateContext>,
126    /// seed extracted in [ConnectState::handle_initial_transfer] and used in [ConnectState::finish_transfer]
127    transfer_shuffle: Option<ShuffleState>,
128
129    /// a context to keep track of the autoplay context
130    autoplay_context: Option<StateContext>,
131
132    /// The volume adjustment per step when handling individual volume adjustments.
133    pub volume_step_size: u16,
134}
135
136impl ConnectState {
137    pub fn new(cfg: ConnectConfig, session: &Session) -> Self {
138        let volume_step_size = u16::MAX.checked_div(cfg.volume_steps).unwrap_or(1024);
139
140        let device_info = DeviceInfo {
141            can_play: true,
142            volume: cfg.initial_volume.into(),
143            name: cfg.name,
144            device_id: session.device_id().to_string(),
145            device_type: EnumOrUnknown::new(cfg.device_type.into()),
146            device_software_version: version::SEMVER.to_string(),
147            spirc_version: version::SPOTIFY_SPIRC_VERSION.to_string(),
148            client_id: session.client_id(),
149            is_group: cfg.is_group,
150            capabilities: MessageField::some(Capabilities {
151                volume_steps: cfg.volume_steps.into(),
152                disable_volume: cfg.disable_volume,
153
154                gaia_eq_connect_id: true,
155                can_be_player: true,
156                needs_full_player_state: true,
157                is_observable: true,
158                is_controllable: true,
159                hidden: false,
160
161                supports_gzip_pushes: true,
162                // todo: enable after logout handling is implemented, see spirc logout_request
163                supports_logout: false,
164                supported_types: vec![
165                    "audio/episode".into(),
166                    "audio/track".into(),
167                    "audio/local".into(),
168                ],
169                supports_playlist_v2: true,
170                supports_transfer_command: true,
171                supports_command_request: true,
172                supports_set_options_command: true,
173
174                is_voice_enabled: false,
175                restrict_to_local: false,
176                connect_disabled: false,
177                supports_rename: false,
178                supports_external_episodes: false,
179                supports_set_backend_metadata: false,
180                supports_hifi: MessageField::none(),
181                // that "AI" dj thingy only available to specific regions/users
182                supports_dj: false,
183                supports_rooms: false,
184                // AudioQuality::HIFI is available, further investigation necessary
185                supported_audio_quality: EnumOrUnknown::new(AudioQuality::VERY_HIGH),
186
187                command_acks: true,
188
189                ..Default::default()
190            }),
191            ..Default::default()
192        };
193
194        let mut state = Self {
195            request: PutStateRequest {
196                member_type: EnumOrUnknown::new(MemberType::CONNECT_STATE),
197                put_state_reason: EnumOrUnknown::new(PutStateReason::PLAYER_STATE_CHANGED),
198                device: MessageField::some(Device {
199                    device_info: MessageField::some(device_info),
200                    player_state: MessageField::some(PlayerState {
201                        session_id: session.session_id(),
202                        ..Default::default()
203                    }),
204                    ..Default::default()
205                }),
206                ..Default::default()
207            },
208            volume_step_size,
209            ..Default::default()
210        };
211        state.reset();
212        state
213    }
214
215    fn reset(&mut self) {
216        self.set_active(false);
217        self.queue_count = 0;
218
219        // preserve the session_id
220        let session_id = self.player().session_id.clone();
221
222        self.device_mut().player_state = MessageField::some(PlayerState {
223            session_id,
224            is_system_initiated: true,
225            playback_speed: 1.,
226            play_origin: MessageField::some(PlayOrigin::new()),
227            suppressions: MessageField::some(Suppressions::new()),
228            options: MessageField::some(ContextPlayerOptions::new()),
229            // + 1, so that we have a buffer where we can swap elements
230            prev_tracks: Vec::with_capacity(SPOTIFY_MAX_PREV_TRACKS_SIZE + 1),
231            next_tracks: Vec::with_capacity(SPOTIFY_MAX_NEXT_TRACKS_SIZE + 1),
232            ..Default::default()
233        });
234    }
235
236    fn device_mut(&mut self) -> &mut Device {
237        self.request
238            .device
239            .as_mut()
240            .expect("the request is always available")
241    }
242
243    fn player_mut(&mut self) -> &mut PlayerState {
244        self.device_mut()
245            .player_state
246            .as_mut()
247            .expect("the player_state has to be always given")
248    }
249
250    pub fn device_info(&self) -> &DeviceInfo {
251        &self.request.device.device_info
252    }
253
254    pub fn player(&self) -> &PlayerState {
255        &self.request.device.player_state
256    }
257
258    pub fn is_active(&self) -> bool {
259        self.request.is_active
260    }
261
262    /// Returns the `is_playing` value as perceived by other connect devices
263    ///
264    /// see [ConnectState::set_status]
265    pub fn is_playing(&self) -> bool {
266        let player = self.player();
267        player.is_playing && !player.is_paused
268    }
269
270    /// Returns the `is_paused` state value as perceived by other connect devices
271    ///
272    /// see [ConnectState::set_status]
273    pub fn is_pause(&self) -> bool {
274        let player = self.player();
275        player.is_playing && player.is_paused && player.is_buffering
276    }
277
278    pub fn set_volume(&mut self, volume: u32) {
279        self.device_mut()
280            .device_info
281            .as_mut()
282            .expect("the device_info has to be always given")
283            .volume = volume;
284    }
285
286    pub fn set_last_command(&mut self, command: Request) {
287        self.request.last_command_message_id = command.message_id;
288        self.request.last_command_sent_by_device_id = command.sent_by_device_id;
289    }
290
291    pub fn set_now(&mut self, now: u64) {
292        self.request.client_side_timestamp = now;
293
294        if let Some(active_since) = self.active_since {
295            if let Ok(active_since_duration) = active_since.duration_since(UNIX_EPOCH) {
296                match active_since_duration.as_millis().try_into() {
297                    Ok(active_since_ms) => self.request.started_playing_at = active_since_ms,
298                    Err(why) => warn!("couldn't update active since because {why}"),
299                }
300            }
301        }
302    }
303
304    pub fn set_active(&mut self, value: bool) {
305        if value {
306            if self.request.is_active {
307                return;
308            }
309
310            self.request.is_active = true;
311            self.active_since = Some(SystemTime::now())
312        } else {
313            self.request.is_active = false;
314            self.active_since = None
315        }
316    }
317
318    pub fn set_origin(&mut self, origin: PlayOrigin) {
319        self.player_mut().play_origin = MessageField::some(origin)
320    }
321
322    pub fn set_session_id(&mut self, session_id: String) {
323        self.player_mut().session_id = session_id;
324    }
325
326    pub(crate) fn set_status(&mut self, status: &SpircPlayStatus) {
327        let player = self.player_mut();
328        player.is_paused = matches!(
329            status,
330            SpircPlayStatus::LoadingPause { .. }
331                | SpircPlayStatus::Paused { .. }
332                | SpircPlayStatus::Stopped
333        );
334
335        if player.is_paused {
336            player.playback_speed = 0.;
337        } else {
338            player.playback_speed = 1.;
339        }
340
341        // desktop and mobile require all 'states' set to true, when we are paused,
342        // otherwise the play button (desktop) is grayed out or the preview (mobile) can't be opened
343        player.is_buffering = player.is_paused
344            || matches!(
345                status,
346                SpircPlayStatus::LoadingPause { .. } | SpircPlayStatus::LoadingPlay { .. }
347            );
348        player.is_playing = player.is_paused
349            || matches!(
350                status,
351                SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::Playing { .. }
352            );
353
354        debug!(
355            "updated connect play status playing: {}, paused: {}, buffering: {}",
356            player.is_playing, player.is_paused, player.is_buffering
357        );
358
359        self.update_restrictions()
360    }
361
362    /// index is 0 based, so the first track is index 0
363    pub fn update_current_index(&mut self, f: impl Fn(&mut ContextIndex)) {
364        match self.player_mut().index.as_mut() {
365            Some(player_index) => f(player_index),
366            None => {
367                let mut new_index = ContextIndex::new();
368                f(&mut new_index);
369                self.player_mut().index = MessageField::some(new_index)
370            }
371        }
372    }
373
374    pub fn update_position(&mut self, position_ms: u32, timestamp: i64) {
375        let player = self.player_mut();
376        player.position_as_of_timestamp = position_ms.into();
377        player.timestamp = timestamp;
378    }
379
380    pub fn update_duration(&mut self, duration: u32) {
381        self.player_mut().duration = duration.into()
382    }
383
384    pub fn update_queue_revision(&mut self) {
385        let mut state = DefaultHasher::new();
386        self.next_tracks()
387            .iter()
388            .for_each(|t| t.uri.hash(&mut state));
389        self.player_mut().queue_revision = state.finish().to_string()
390    }
391
392    pub fn reset_playback_to_position(&mut self, new_index: Option<usize>) -> Result<(), Error> {
393        debug!(
394            "reset_playback with active ctx <{:?}> fill_up ctx <{:?}>",
395            self.active_context, self.fill_up_context
396        );
397
398        let new_index = new_index.unwrap_or(0);
399        self.update_current_index(|i| i.track = new_index as u32);
400        self.update_context_index(self.active_context, new_index + 1)?;
401        self.fill_up_context = self.active_context;
402
403        if !self.current_track(|t| t.is_queue() || self.is_skip_track(t, None)) {
404            self.set_current_track(new_index)?;
405        }
406
407        self.clear_prev_track();
408
409        if new_index > 0 {
410            let context = self.get_context(self.active_context)?;
411
412            let before_new_track = context.tracks.len() - new_index;
413            self.player_mut().prev_tracks = context
414                .tracks
415                .iter()
416                .rev()
417                .skip(before_new_track)
418                .take(SPOTIFY_MAX_PREV_TRACKS_SIZE)
419                .rev()
420                .cloned()
421                .collect();
422            debug!("has {} prev tracks", self.prev_tracks().len())
423        }
424
425        self.clear_next_tracks();
426        self.fill_up_next_tracks()?;
427        self.update_restrictions();
428
429        Ok(())
430    }
431
432    fn mark_as_unavailable_for_match(track: &mut ProvidedTrack, uri: &str) {
433        if track.uri == uri {
434            debug!("Marked <{}:{}> as unavailable", track.provider, track.uri);
435            track.set_provider(Provider::Unavailable);
436        }
437    }
438
439    pub fn update_position_in_relation(&mut self, timestamp: i64) {
440        let player = self.player_mut();
441
442        let diff = timestamp - player.timestamp;
443        player.position_as_of_timestamp += diff;
444
445        if log::max_level() >= LevelFilter::Debug {
446            let pos = Duration::from_millis(player.position_as_of_timestamp as u64);
447            let time = Date::from_timestamp_ms(timestamp)
448                .map(|d| d.time().to_string())
449                .unwrap_or_else(|_| timestamp.to_string());
450
451            let sec = pos.as_secs();
452            let (min, sec) = (sec / 60, sec % 60);
453            debug!("update position to {min}:{sec:0>2} at {time}");
454        }
455
456        player.timestamp = timestamp;
457    }
458
459    pub async fn became_inactive(&mut self, session: &Session) -> SpClientResult {
460        self.reset();
461        self.reset_context(ResetContext::Completely);
462
463        session.spclient().put_connect_state_inactive(false).await
464    }
465
466    async fn send_with_reason(
467        &mut self,
468        session: &Session,
469        reason: PutStateReason,
470    ) -> SpClientResult {
471        let prev_reason = self.request.put_state_reason;
472
473        self.request.put_state_reason = EnumOrUnknown::new(reason);
474        let res = self.send_state(session).await;
475
476        self.request.put_state_reason = prev_reason;
477        res
478    }
479
480    /// Notifies the remote server about a new device
481    pub async fn notify_new_device_appeared(&mut self, session: &Session) -> SpClientResult {
482        self.send_with_reason(session, PutStateReason::NEW_DEVICE)
483            .await
484    }
485
486    /// Notifies the remote server about a new volume
487    pub async fn notify_volume_changed(&mut self, session: &Session) -> SpClientResult {
488        self.send_with_reason(session, PutStateReason::VOLUME_CHANGED)
489            .await
490    }
491
492    /// Sends the connect state for the connect session to the remote server
493    pub async fn send_state(&self, session: &Session) -> SpClientResult {
494        session
495            .spclient()
496            .put_connect_state_request(&self.request)
497            .await
498    }
499}