Skip to content

Commit 57a5fbb

Browse files
authored
feat: Initiate transfer playback via spclient api (#1530)
* feat: provide an easy transfer option to the spirc * fix: execute spirc commands after connection establishment
1 parent c0477c9 commit 57a5fbb

File tree

4 files changed

+69
-9
lines changed

4 files changed

+69
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- [connect] Add method `transfer` to `Spirc` to automatically transfer the playback to ourselves
13+
- [core] Add method `transfer` to `SpClient`
1214
- [core] Add `SpotifyUri` type to represent more types of URI than `SpotifyId` can
1315
- [discovery] Add support for [device aliases](https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf#device-aliases)
1416

connect/src/spirc.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::{
99
protocol::{Command, FallbackWrapper, Message, Request},
1010
},
1111
session::UserAttributes,
12+
spclient::TransferRequest,
1213
},
1314
model::{LoadRequest, PlayingTrack, SpircPlayStatus},
1415
playback::{
@@ -73,6 +74,7 @@ struct SpircTask {
7374

7475
/// the state management object
7576
connect_state: ConnectState,
77+
connect_established: bool,
7678

7779
play_request_id: Option<u64>,
7880
play_status: SpircPlayStatus,
@@ -128,6 +130,7 @@ enum SpircCommand {
128130
SetPosition(u32),
129131
SetVolume(u16),
130132
Activate,
133+
Transfer(Option<TransferRequest>),
131134
Load(LoadRequest),
132135
}
133136

@@ -225,6 +228,7 @@ impl Spirc {
225228
mixer,
226229

227230
connect_state,
231+
connect_established: false,
228232

229233
play_request_id: None,
230234
play_status: SpircPlayStatus::Stopped,
@@ -393,10 +397,19 @@ impl Spirc {
393397

394398
/// Acquires the control as active connect device.
395399
///
396-
/// Does nothing if we are not the active device.
400+
/// Does not [Spirc::transfer] the playback. Does nothing if we are not the active device.
397401
pub fn activate(&self) -> Result<(), Error> {
398402
Ok(self.commands.send(SpircCommand::Activate)?)
399403
}
404+
405+
/// Acquires the control as active connect device over the transfer flow.
406+
///
407+
/// Does nothing if we are not the active device.
408+
pub fn transfer(&self, transfer_request: Option<TransferRequest>) -> Result<(), Error> {
409+
Ok(self
410+
.commands
411+
.send(SpircCommand::Transfer(transfer_request))?)
412+
}
400413
}
401414

402415
impl SpircTask {
@@ -489,7 +502,7 @@ impl SpircTask {
489502
session_update,
490503
match |session_update| self.handle_session_update(session_update)
491504
},
492-
cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd {
505+
cmd = async { commands?.recv().await }, if commands.is_some() && self.connect_established => if let Some(cmd) = cmd {
493506
if let Err(e) = self.handle_command(cmd).await {
494507
debug!("could not dispatch command: {e}");
495508
}
@@ -622,12 +635,20 @@ impl SpircTask {
622635
rx.close()
623636
}
624637
}
638+
SpircCommand::Transfer(request) if !self.connect_state.is_active() => {
639+
let device_id = self.session.device_id();
640+
self.session
641+
.spclient()
642+
.transfer(device_id, device_id, request.as_ref())
643+
.await?;
644+
return Ok(());
645+
}
625646
SpircCommand::Activate if !self.connect_state.is_active() => {
626647
trace!("Received SpircCommand::{cmd:?}");
627648
self.handle_activate();
628649
return self.notify().await;
629650
}
630-
SpircCommand::Activate => {
651+
SpircCommand::Transfer(..) | SpircCommand::Activate => {
631652
warn!("SpircCommand::{cmd:?} will be ignored while already active")
632653
}
633654
_ if !self.connect_state.is_active() => {
@@ -812,6 +833,8 @@ impl SpircTask {
812833
self.session.device_id()
813834
);
814835

836+
self.connect_established = true;
837+
815838
let same_session = cluster.player_state.session_id == self.session.session_id()
816839
|| cluster.player_state.session_id.is_empty();
817840
if !cluster.active_device_id.is_empty() || !same_session {

core/src/dealer/protocol/request.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
transfer_state::TransferState,
88
},
99
};
10-
use serde::Deserialize;
10+
use serde::{Deserialize, Serialize};
1111
use serde_json::Value;
1212
use std::fmt::{Display, Formatter};
1313

@@ -165,12 +165,16 @@ pub struct GenericCommand {
165165
pub logging_params: LoggingParams,
166166
}
167167

168-
#[derive(Clone, Debug, Deserialize)]
168+
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
169169
pub struct TransferOptions {
170-
pub restore_paused: String,
171-
pub restore_position: String,
172-
pub restore_track: String,
173-
pub retain_session: String,
170+
#[serde(skip_serializing_if = "Option::is_none")]
171+
pub restore_paused: Option<String>,
172+
#[serde(skip_serializing_if = "Option::is_none")]
173+
pub restore_position: Option<String>,
174+
#[serde(skip_serializing_if = "Option::is_none")]
175+
pub restore_track: Option<String>,
176+
#[serde(skip_serializing_if = "Option::is_none")]
177+
pub retain_session: Option<String>,
174178
}
175179

176180
#[derive(Clone, Debug, Deserialize)]

core/src/spclient.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::{
88
Error, FileId, SpotifyId, SpotifyUri,
99
apresolve::SocketAddress,
1010
config::SessionConfig,
11+
dealer::protocol::TransferOptions,
1112
error::ErrorKind,
1213
protocol::{
1314
autoplay_context_request::AutoplayContextRequest,
@@ -34,6 +35,7 @@ use hyper::{
3435
use hyper_util::client::legacy::ResponseFuture;
3536
use protobuf::{Enum, Message, MessageFull};
3637
use rand::RngCore;
38+
use serde::Serialize;
3739
use sysinfo::System;
3840
use thiserror::Error;
3941

@@ -106,6 +108,11 @@ impl Default for RequestOptions {
106108
}
107109
}
108110

111+
#[derive(Debug, Serialize)]
112+
pub struct TransferRequest {
113+
pub transfer_options: TransferOptions,
114+
}
115+
109116
impl SpClient {
110117
pub fn set_strategy(&self, strategy: RequestStrategy) {
111118
self.lock(|inner| inner.strategy = strategy)
@@ -902,4 +909,28 @@ impl SpClient {
902909

903910
self.request(&Method::GET, &endpoint, None, None).await
904911
}
912+
913+
/// Triggers the transfers of the playback from one device to another
914+
///
915+
/// Using the same `device_id` for `from_device_id` and `to_device_id`, initiates the transfer
916+
/// from the currently active device.
917+
pub async fn transfer(
918+
&self,
919+
from_device_id: &str,
920+
to_device_id: &str,
921+
transfer_request: Option<&TransferRequest>,
922+
) -> SpClientResult {
923+
let body = transfer_request.map(serde_json::to_string).transpose()?;
924+
925+
let endpoint =
926+
format!("/connect-state/v1/connect/transfer/from/{from_device_id}/to/{to_device_id}");
927+
self.request_with_options(
928+
&Method::POST,
929+
&endpoint,
930+
None,
931+
body.as_deref().map(|s| s.as_bytes()),
932+
&NO_METRICS_AND_SALT,
933+
)
934+
.await
935+
}
905936
}

0 commit comments

Comments
 (0)