Skip to content
Closed
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
279 changes: 163 additions & 116 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ sha-1 = "0.10"
tokio = {version = "1.26.0", features = ["signal", "rt-multi-thread", "process", "io-std"] }
tokio-stream = "0.1.7"
url = "2.2.2"
librespot-audio = { version = "0.6", default-features = false }
librespot-playback = { version = "0.6", default-features = false }
librespot-core = "0.6"
librespot-discovery = "0.6"
librespot-connect = "0.6"
librespot-metadata = "0.6"
librespot-protocol = "0.6"
librespot-oauth = "0.6"
librespot-audio = { git = 'https://github.com/librespot-org/librespot.git', rev = 'ba3d501', default-features = false }
librespot-playback = { git = 'https://github.com/librespot-org/librespot.git', rev = 'ba3d501', default-features = false }
librespot-core = { git = 'https://github.com/librespot-org/librespot.git', rev = 'ba3d501' }
librespot-discovery = { git = 'https://github.com/librespot-org/librespot.git', rev = 'ba3d501' }
librespot-connect = { git = 'https://github.com/librespot-org/librespot.git', rev = 'ba3d501' }
librespot-metadata = { git = 'https://github.com/librespot-org/librespot.git', rev = 'ba3d501' }
librespot-protocol = { git = 'https://github.com/librespot-org/librespot.git', rev = 'ba3d501' }
librespot-oauth = { git = 'https://github.com/librespot-org/librespot.git', rev = 'ba3d501' }
toml = "0.8.19"
color-eyre = "0.6"
directories = "6.0.0"
Expand Down
6 changes: 3 additions & 3 deletions src/alsa_mixer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ impl AlsaMixer {
}

impl Mixer for AlsaMixer {
fn open(_: MixerConfig) -> AlsaMixer {
AlsaMixer {
fn open(_: MixerConfig) -> Result<Self, librespot_core::Error> {
Ok(AlsaMixer {
device: "default".to_string(),
mixer: "Master".to_string(),
linear_scaling: false,
}
})
}

fn volume(&self) -> u16 {
Expand Down
4 changes: 1 addition & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ pub enum DeviceType {
UnknownSpotify,
CarThing,
Observer,
HomeThing,
}

impl From<DeviceType> for LSDeviceType {
Expand All @@ -93,7 +92,6 @@ impl From<DeviceType> for LSDeviceType {
DeviceType::UnknownSpotify => LSDeviceType::UnknownSpotify,
DeviceType::CarThing => LSDeviceType::CarThing,
DeviceType::Observer => LSDeviceType::Observer,
DeviceType::HomeThing => LSDeviceType::HomeThing,
}
}
}
Expand Down Expand Up @@ -596,7 +594,7 @@ impl SharedConfigValues {
}

pub(crate) fn get_config_file() -> Option<PathBuf> {
let etc_conf = format!("/etc/{}", CONFIG_FILE_NAME);
let etc_conf = format!("/etc/{CONFIG_FILE_NAME}");
let dirs = directories::ProjectDirs::from("", "", "spotifyd")?;
let mut path = dirs.config_dir().to_path_buf();
path.push(CONFIG_FILE_NAME);
Expand Down
166 changes: 74 additions & 92 deletions src/dbus_mpris.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ use futures::{
task::{Context, Poll},
Future,
};
use librespot_connect::spirc::{Spirc, SpircLoadCommand};
use librespot_core::{spotify_id::SpotifyItemType, Session, SpotifyId};
use librespot_connect::{
LoadContextOptions, LoadRequest, LoadRequestOptions, Options, PlayingTrack, Spirc,
};
use librespot_core::{session::Session, spotify_id::SpotifyItemType, SpotifyId};
use librespot_metadata::audio::AudioItem;
use librespot_playback::player::PlayerEvent;
use librespot_protocol::spirc::TrackRef;
use log::{debug, error, warn};
use log::{debug, warn};
use std::convert::TryFrom;
use std::{
collections::HashMap,
Expand All @@ -27,12 +28,9 @@ use std::{
};
use thiserror::Error;
use time::format_description::well_known::Iso8601;
use tokio::{
runtime::Handle,
sync::{
mpsc::{UnboundedReceiver, UnboundedSender},
Mutex,
},
use tokio::sync::{
mpsc::{UnboundedReceiver, UnboundedSender},
Mutex,
};

type DbusMap = HashMap<String, Variant<Box<dyn RefArg>>>;
Expand Down Expand Up @@ -269,12 +267,16 @@ impl CurrentStateInner {
self.update_position(Duration::milliseconds(position_ms as i64));
seeked = true;
}
PlayerEvent::PositionChanged { position_ms, .. } => {
// Update internal position; no Seeked signal for periodic updates
self.update_position(Duration::milliseconds(position_ms as i64));
}
PlayerEvent::ShuffleChanged { shuffle } => {
self.shuffle = shuffle;
insert_attr(&mut changed, "Shuffle", self.shuffle);
}
PlayerEvent::RepeatChanged { repeat } => {
self.repeat = repeat.into();
PlayerEvent::RepeatChanged { context, .. } => {
self.repeat = context.into();
insert_attr(
&mut changed,
"LoopStatus",
Expand Down Expand Up @@ -472,18 +474,20 @@ async fn create_dbus_server(
let event = event.expect("event channel was unexpectedly closed");

if let PlayerEvent::SessionConnected { connection_id, .. } = event {
let mut cr = crossroads.lock().await;
let seeked_fn = register_player_interface(
&mut cr,
spirc.clone().unwrap(),
session.clone().unwrap(),
current_state.clone(),
quit_tx.clone(),
);
if cur_conn.is_none() {
let mut cr = crossroads.lock().await;
let seeked_fn = register_player_interface(
&mut cr,
spirc.clone().unwrap(),
session.clone().unwrap(),
current_state.clone(),
quit_tx.clone(),
);
conn.request_name(&mpris_name, true, true, true).await?;
cur_conn = Some(ConnectionData { conn_id: connection_id, seeked_fn });
} else if let Some(cur) = cur_conn.as_mut() {
cur.conn_id = connection_id;
}
cur_conn = Some(ConnectionData { conn_id: connection_id, seeked_fn });
} else if let PlayerEvent::SessionDisconnected { connection_id, .. } = event {
// if this message isn't outdated yet, we vanish from the bus
if cur_conn.as_ref().is_some_and(|d| d.conn_id == connection_id) {
Expand Down Expand Up @@ -537,6 +541,18 @@ async fn create_dbus_server(
ControlMessage::SetSession(new_spirc, new_session) => {
let mut cr = crossroads.lock().await;
register_controls_interface(&mut cr, new_spirc.clone());
// If MPRIS is not yet registered, register it now so tools like playerctl can discover us
if cur_conn.is_none() {
let seeked_fn = register_player_interface(
&mut cr,
new_spirc.clone(),
new_session.clone(),
current_state.clone(),
quit_tx.clone(),
);
conn.request_name(&mpris_name, true, true, true).await?;
cur_conn = Some(ConnectionData { conn_id: String::new(), seeked_fn });
}
spirc = Some(new_spirc);
session = Some(new_session);
}
Expand All @@ -547,6 +563,7 @@ async fn create_dbus_server(
cr.remove::<()>(&CONTROLS_PATH.into());
spirc = None;
session = None;
cur_conn = None;
}
}
}
Expand All @@ -562,7 +579,7 @@ type SeekedSignal = Box<dyn Fn(&dbus::Path, &(i64,)) -> dbus::Message + Send + S
fn register_player_interface(
cr: &mut Crossroads,
spirc: Arc<Spirc>,
session: Session,
_session: Session,
current_state: Arc<CurrentState>,
quit_tx: tokio::sync::mpsc::UnboundedSender<()>,
) -> SeekedSignal {
Expand Down Expand Up @@ -640,7 +657,10 @@ fn register_player_interface(
});
let local_spirc = spirc.clone();
b.method("Stop", (), (), move |_, _, (): ()| {
local_spirc.disconnect().map_err(|e| MethodErr::failed(&e))
// Pause playback when disconnecting from Spotify Connect
local_spirc
.disconnect(true)
.map_err(|e| MethodErr::failed(&e))
});

let local_spirc = spirc.clone();
Expand Down Expand Up @@ -708,75 +728,36 @@ fn register_player_interface(
shuffle, repeat, ..
} = *local_state.read()?;

fn id_to_trackref(id: &SpotifyId) -> TrackRef {
let mut trackref = TrackRef::new();
if let Ok(uri) = id.to_uri() {
trackref.set_uri(uri);
} else {
trackref.set_gid(id.to_raw().to_vec());
// Build LoadRequestOptions based on current state
let mut request_options = LoadRequestOptions::default();
request_options.start_playing = true;
request_options.seek_to = 0;
request_options.context_options = Some(LoadContextOptions::Options(Options {
shuffle,
repeat: bool::from(repeat),
repeat_track: false,
}));

// Choose whether to start a context or play specific tracks
let request = match id.item_type {
SpotifyItemType::Album
| SpotifyItemType::Artist
| SpotifyItemType::Playlist
| SpotifyItemType::Show => {
// Start the given context and play from the beginning
request_options.playing_track = Some(PlayingTrack::Index(0));
LoadRequest::from_context_uri(uri, request_options)
}
trackref
}

let session = session.clone();

let (playing_track_index, context_uri, tracks) = Handle::current()
.block_on(async move {
use librespot_metadata::*;
Ok::<_, librespot_core::Error>(match id.item_type {
SpotifyItemType::Album => {
let album = Album::get(&session, &id).await?;
(0, uri, album.tracks().map(id_to_trackref).collect())
}
SpotifyItemType::Artist => {
let artist = Artist::get(&session, &id).await?;
(
0,
uri,
artist
.top_tracks
.for_country(&session.country())
.iter()
.map(id_to_trackref)
.collect(),
)
}
SpotifyItemType::Playlist => {
let playlist = Playlist::get(&session, &id).await?;
(0, uri, playlist.tracks().map(id_to_trackref).collect())
}
SpotifyItemType::Track => {
let track = Track::get(&session, &id).await?;
(
track.number as u32,
track.album.id.to_uri()?,
vec![id_to_trackref(&track.id)],
)
}
SpotifyItemType::Episode => (0, uri, vec![id_to_trackref(&id)]),
SpotifyItemType::Show => {
let show = Show::get(&session, &id).await?;
(0, uri, show.episodes.iter().map(id_to_trackref).collect())
}
SpotifyItemType::Local | SpotifyItemType::Unknown => {
return Err(librespot_core::Error::unimplemented(
"this type of uri is not supported",
));
}
})
})
.map_err(|e| MethodErr::failed(&e))?;
SpotifyItemType::Track | SpotifyItemType::Episode => {
// Play the single item directly
LoadRequest::from_tracks(vec![uri], request_options)
}
SpotifyItemType::Local | SpotifyItemType::Unknown => {
return Err(dbus::MethodErr::failed("this type of uri is not supported"));
}
};

local_spirc
.load(SpircLoadCommand {
context_uri,
start_playing: true,
shuffle,
repeat: repeat.into(),
playing_track_index,
tracks,
})
.map_err(|e| MethodErr::failed(&e))
local_spirc.load(request).map_err(|e| MethodErr::failed(&e))
});

let local_state = current_state.clone();
Expand Down Expand Up @@ -829,17 +810,18 @@ fn register_player_interface(
Ok(repeat.to_mpris().to_string())
})
.set(move |_, _, value| {
let new_repeat = match value.as_str() {
let context = match value.as_str() {
"None" => false,
"Playlist" => true,
// We don't support Track via MPRIS yet
mode => {
return Err(dbus::MethodErr::failed(&format!(
"unsupported repeat mode: {mode}"
)))
}
};
local_spirc
.repeat(new_repeat)
.repeat(context)
.map_err(|e| MethodErr::failed(&e))?;
// TODO: remove, once librespot sends us updates here
Ok(Some(value))
Expand Down
18 changes: 7 additions & 11 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,13 @@ impl Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErrorKind::Subprocess { cmd, msg, shell } => match msg {
Message::None => write!(f, "Failed to execute {:?} using {:?}.", cmd, shell),
Message::Error(ref e) => write!(
f,
"Failed to execute {:?} using {:?}. Error: {}",
cmd, shell, e
),
Message::String(ref s) => write!(
f,
"Failed to execute {:?} using {:?}. Error: {}",
cmd, shell, s
),
Message::None => write!(f, "Failed to execute {cmd:?} using {shell:?}."),
Message::Error(ref e) => {
write!(f, "Failed to execute {cmd:?} using {shell:?}. Error: {e}")
}
Message::String(ref s) => {
write!(f, "Failed to execute {cmd:?} using {shell:?}. Error: {s}")
}
},
ErrorKind::NormalisationPregainInvalid => write!(
f,
Expand Down
7 changes: 4 additions & 3 deletions src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use futures::{
stream::Peekable,
Future, FutureExt, StreamExt,
};
use librespot_connect::{config::ConnectConfig, spirc::Spirc};
use librespot_connect::{ConnectConfig, Spirc};
use librespot_core::{
authentication::Credentials, cache::Cache, config::DeviceType, session::Session, Error,
SessionConfig,
Expand Down Expand Up @@ -125,8 +125,9 @@ impl MainLoop {
name: self.device_name.clone(),
device_type: self.device_type,
is_group: false,
initial_volume: self.initial_volume,
has_volume_ctrl: self.has_volume_ctrl,
initial_volume: self.initial_volume.unwrap_or(50),
disable_volume: !self.has_volume_ctrl,
volume_steps: 64,
},
session.clone(),
creds.clone(),
Expand Down
4 changes: 2 additions & 2 deletions src/no_mixer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use librespot_playback::mixer::{Mixer, MixerConfig};
pub struct NoMixer;

impl Mixer for NoMixer {
fn open(_: MixerConfig) -> NoMixer {
NoMixer {}
fn open(_: MixerConfig) -> Result<NoMixer, librespot_core::Error> {
Ok(NoMixer {})
}

fn volume(&self) -> u16 {
Expand Down
Loading