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
39const 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#[derive(Debug, Clone)]
80pub struct ConnectConfig {
81 pub name: String,
83 pub device_type: DeviceType,
85 pub is_group: bool,
87 pub initial_volume: u16,
89 pub disable_volume: bool,
91 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 request: PutStateRequest,
112
113 unavailable_uri: Vec<String>,
114
115 active_since: Option<SystemTime>,
116 queue_count: u64,
117
118 pub active_context: ContextType,
122 fill_up_context: ContextType,
123
124 context: Option<StateContext>,
126 transfer_shuffle: Option<ShuffleState>,
128
129 autoplay_context: Option<StateContext>,
131
132 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 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 supports_dj: false,
183 supports_rooms: false,
184 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 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 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 pub fn is_playing(&self) -> bool {
266 let player = self.player();
267 player.is_playing && !player.is_paused
268 }
269
270 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 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 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 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 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 pub async fn send_state(&self, session: &Session) -> SpClientResult {
494 session
495 .spclient()
496 .put_connect_state_request(&self.request)
497 .await
498 }
499}