Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 26 additions & 3 deletions connect/src/spirc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
protocol::{Command, FallbackWrapper, Message, Request},
},
session::UserAttributes,
spclient::TransferRequest,
},
model::{LoadRequest, PlayingTrack, SpircPlayStatus},
playback::{
Expand Down Expand Up @@ -73,6 +74,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 +130,7 @@ enum SpircCommand {
SetPosition(u32),
SetVolume(u16),
Activate,
Transfer(Option<TransferRequest>),
Load(LoadRequest),
}

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

connect_state,
connect_established: false,

play_request_id: None,
play_status: SpircPlayStatus::Stopped,
Expand Down Expand Up @@ -393,10 +397,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_request: Option<TransferRequest>) -> Result<(), Error> {
Ok(self
.commands
.send(SpircCommand::Transfer(transfer_request))?)
}
}

impl SpircTask {
Expand Down Expand Up @@ -489,7 +502,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 +635,20 @@ impl SpircTask {
rx.close()
}
}
SpircCommand::Transfer(request) if !self.connect_state.is_active() => {
let device_id = self.session.device_id();
self.session
.spclient()
.transfer(device_id, device_id, request.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 +833,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
31 changes: 31 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 All @@ -34,6 +35,7 @@ use hyper::{
use hyper_util::client::legacy::ResponseFuture;
use protobuf::{Enum, Message, MessageFull};
use rand::RngCore;
use serde::Serialize;
use sysinfo::System;
use thiserror::Error;

Expand Down Expand Up @@ -106,6 +108,11 @@ impl Default for RequestOptions {
}
}

#[derive(Debug, Serialize)]
pub struct TransferRequest {
pub transfer_options: TransferOptions,
}

impl SpClient {
pub fn set_strategy(&self, strategy: RequestStrategy) {
self.lock(|inner| inner.strategy = strategy)
Expand Down Expand Up @@ -902,4 +909,28 @@ 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_request: Option<&TransferRequest>,
) -> SpClientResult {
let body = transfer_request.map(serde_json::to_string).transpose()?;

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