Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- [connect] Add method `transfer` to `Spirc` to automatically transfer the playback to ourselves
- [core] Add method `transfer` to `SpClient`
- [core] Add `SpotifyUri` type to represent more types of URI than `SpotifyId` can

### Changed
Expand Down
30 changes: 26 additions & 4 deletions connect/src/spirc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
authentication::Credentials,
dealer::{
manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply},
protocol::{Command, FallbackWrapper, Message, Request},
protocol::{Command, FallbackWrapper, Message, Request, TransferOptions},
},
session::UserAttributes,
},
Expand Down Expand Up @@ -73,6 +73,7 @@ struct SpircTask {

/// the state management object
connect_state: ConnectState,
connect_established: bool,

play_request_id: Option<u64>,
play_status: SpircPlayStatus,
Expand Down Expand Up @@ -128,6 +129,7 @@ enum SpircCommand {
SetPosition(u32),
SetVolume(u16),
Activate,
Transfer(Option<TransferOptions>),
Load(LoadRequest),
}

Expand Down Expand Up @@ -225,6 +227,7 @@ impl Spirc {
mixer,

connect_state,
connect_established: false,

play_request_id: None,
play_status: SpircPlayStatus::Stopped,
Expand Down Expand Up @@ -393,10 +396,19 @@ impl Spirc {

/// Acquires the control as active connect device.
///
/// Does nothing if we are not the active device.
/// Does not [Spirc::transfer] the playback. Does nothing if we are not the active device.
pub fn activate(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Activate)?)
}

/// Acquires the control as active connect device over the transfer flow.
///
/// Does nothing if we are not the active device.
pub fn transfer(&self, transfer_options: Option<TransferOptions>) -> Result<(), Error> {
Ok(self
.commands
.send(SpircCommand::Transfer(transfer_options))?)
}
}

impl SpircTask {
Expand Down Expand Up @@ -489,7 +501,7 @@ impl SpircTask {
session_update,
match |session_update| self.handle_session_update(session_update)
},
cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd {
cmd = async { commands?.recv().await }, if commands.is_some() && self.connect_established => if let Some(cmd) = cmd {
if let Err(e) = self.handle_command(cmd).await {
debug!("could not dispatch command: {e}");
}
Expand Down Expand Up @@ -622,12 +634,20 @@ impl SpircTask {
rx.close()
}
}
SpircCommand::Transfer(options) if !self.connect_state.is_active() => {
let device_id = self.session.device_id();
self.session
.spclient()
.transfer(device_id, device_id, options.as_ref())
.await?;
return Ok(());
}
SpircCommand::Activate if !self.connect_state.is_active() => {
trace!("Received SpircCommand::{cmd:?}");
self.handle_activate();
return self.notify().await;
}
SpircCommand::Activate => {
SpircCommand::Transfer(..) | SpircCommand::Activate => {
warn!("SpircCommand::{cmd:?} will be ignored while already active")
}
_ if !self.connect_state.is_active() => {
Expand Down Expand Up @@ -812,6 +832,8 @@ impl SpircTask {
self.session.device_id()
);

self.connect_established = true;

let same_session = cluster.player_state.session_id == self.session.session_id()
|| cluster.player_state.session_id.is_empty();
if !cluster.active_device_id.is_empty() || !same_session {
Expand Down
16 changes: 10 additions & 6 deletions core/src/dealer/protocol/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
transfer_state::TransferState,
},
};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::{Display, Formatter};

Expand Down Expand Up @@ -165,12 +165,16 @@ pub struct GenericCommand {
pub logging_params: LoggingParams,
}

#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct TransferOptions {
pub restore_paused: String,
pub restore_position: String,
pub restore_track: String,
pub retain_session: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub restore_paused: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub restore_position: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub restore_track: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retain_session: Option<String>,
}

#[derive(Clone, Debug, Deserialize)]
Expand Down
29 changes: 29 additions & 0 deletions core/src/spclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
Error, FileId, SpotifyId, SpotifyUri,
apresolve::SocketAddress,
config::SessionConfig,
dealer::protocol::TransferOptions,
error::ErrorKind,
protocol::{
autoplay_context_request::AutoplayContextRequest,
Expand Down Expand Up @@ -902,4 +903,32 @@ impl SpClient {

self.request(&Method::GET, &endpoint, None, None).await
}

/// Triggers the transfers of the playback from one device to another
///
/// Using the same `device_id` for `from_device_id` and `to_device_id`, initiates the transfer
/// from the currently active device.
pub async fn transfer(
&self,
from_device_id: &str,
to_device_id: &str,
transfer_options: Option<&TransferOptions>,
) -> SpClientResult {
let transfer_options = transfer_options
.as_ref()
.map(serde_json::to_string)
.transpose()?;
let body = transfer_options.map(|op| format!(r#"{{ "transfer_options": {op} }}"#));

let endpoint =
format!("/connect-state/v1/connect/transfer/from/{from_device_id}/to/{to_device_id}");
self.request_with_options(
&Method::POST,
&endpoint,
None,
body.as_deref().map(|s| s.as_bytes()),
&NO_METRICS_AND_SALT,
)
.await
}
}
Loading