Skip to content
Draft
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
12 changes: 12 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,16 @@ pub struct CliConfig {
#[arg(long)]
pub no_daemon: bool,

/// Enable spectator mode (listen only, no playback control)
#[arg(
long,
default_missing_value("true"),
require_equals = true,
num_args(0..=1),
value_name = "BOOL"
)]
pub spectator: Option<bool>,

/// Path to PID file.
#[cfg(unix)]
#[arg(long, value_name = "PATH")]
Expand Down Expand Up @@ -635,6 +645,7 @@ pub(crate) struct SpotifydConfig {
pub(crate) discovery: bool,
pub(crate) zeroconf_port: Option<u16>,
pub(crate) device_type: LSDeviceType,
pub(crate) spectator: bool,
#[cfg(feature = "dbus_mpris")]
pub(crate) mpris: MprisConfig,
#[cfg(feature = "alsa_backend")]
Expand Down Expand Up @@ -758,6 +769,7 @@ pub(crate) fn get_internal_config(config: CliConfig) -> SpotifydConfig {
discovery: !config.shared_config.disable_discovery.unwrap_or(false),
zeroconf_port: config.shared_config.zeroconf_port,
device_type,
spectator: config.spectator.unwrap_or(false),
#[cfg(unix)]
pid,
#[cfg(feature = "dbus_mpris")]
Expand Down
58 changes: 43 additions & 15 deletions src/dbus_mpris.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,27 +502,51 @@ 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() {
if let Some(current) = cur_conn.as_mut() {
if current.conn_id.is_empty() {
current.conn_id = connection_id;
}
} else if let (Some(spirc), Some(session)) = (spirc.clone(), session.clone()) {
let mut cr = crossroads.lock().await;
let seeked_fn = register_player_interface(
&mut cr,
spirc,
session,
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 });
}
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) {
if cur_conn
.as_ref()
.is_some_and(|d| d.conn_id.is_empty() || d.conn_id == connection_id)
{
let mut cr = crossroads.lock().await;
conn.release_name(&mpris_name).await?;
cr.remove::<()>(&MPRIS_PATH.into());
cur_conn = None;
}
} else {
if cur_conn.is_none()
&& let (Some(spirc), Some(session)) = (spirc.clone(), session.clone())
{
let mut cr = crossroads.lock().await;
let seeked_fn = register_player_interface(
&mut cr,
spirc,
session,
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,
});
}

let (changed, seeked) = current_state
.write()
.expect("state has been poisoned")
Expand Down Expand Up @@ -572,8 +596,10 @@ async fn create_dbus_server(
}
ControlMessage::DropSession => {
let mut cr = crossroads.lock().await;
conn.release_name(&mpris_name).await?;
cr.remove::<()>(&MPRIS_PATH.into());
if cur_conn.is_some() {
conn.release_name(&mpris_name).await?;
cr.remove::<()>(&MPRIS_PATH.into());
}
cr.remove::<()>(&CONTROLS_PATH.into());
spirc = None;
session = None;
Expand All @@ -583,7 +609,9 @@ async fn create_dbus_server(
}
}
}
conn.release_name(&mpris_name).await?;
if cur_conn.is_some() {
conn.release_name(&mpris_name).await?;
}
conn.release_name(&spotifyd_name).await?;
Ok(())
}
Expand Down
131 changes: 126 additions & 5 deletions src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,27 @@ use futures::{
future::{self, Fuse, FusedFuture},
stream::Peekable,
};
use librespot_connect::{ConnectConfig, Spirc};
use librespot_connect::{ClusterUpdateInfo, ConnectConfig, Spirc};
use librespot_core::{
Error, SessionConfig, authentication::Credentials, cache::Cache, config::DeviceType,
session::Session,
Error, SessionConfig, SpotifyUri, authentication::Credentials, cache::Cache,
config::DeviceType, session::Session,
};
use librespot_discovery::Discovery;
use librespot_metadata::audio::AudioItem;
use librespot_playback::{
audio_backend::Sink,
config::{AudioFormat, PlayerConfig},
mixer::Mixer,
player::Player,
player::{Player, PlayerEvent},
};
use log::{error, info};
use std::pin::Pin;
use std::sync::Arc;

const SPECTATOR_CONNECTION_ID: &str = "spectator-remote";
const SPECTATOR_USER_NAME: &str = "spectator";
const SPECTATOR_PLAY_REQUEST_ID: u64 = 0;

#[cfg(not(feature = "dbus_mpris"))]
type DbusServer = Pending<()>;

Expand Down Expand Up @@ -87,6 +92,7 @@ pub(crate) struct MainLoop {
pub(crate) device_name: String,
pub(crate) player_event_program: Option<String>,
pub(crate) credentials_provider: CredentialsProvider,
pub(crate) spectator: bool,
#[cfg(feature = "dbus_mpris")]
pub(crate) mpris_config: MprisConfig,
}
Expand All @@ -99,6 +105,12 @@ struct ConnectionInfo<SpircTask: Future<Output = ()>> {
spirc_task: SpircTask,
}

#[cfg(feature = "dbus_mpris")]
async fn fetch_audio_item(session: &Session, track_uri: &str) -> Option<Box<AudioItem>> {
let uri = SpotifyUri::from_uri(track_uri).ok()?;
AudioItem::get_file(session, uri).await.ok().map(Box::new)
}

impl MainLoop {
async fn get_connection(
&mut self,
Expand Down Expand Up @@ -197,6 +209,8 @@ impl MainLoop {
tokio::pin!(spirc_task);

let shared_spirc = Arc::new(connection.spirc);
#[cfg(feature = "dbus_mpris")]
let spectator_session = connection.session.clone();

#[cfg(feature = "dbus_mpris")]
if let Either::Left(mut dbus_server) = Either::as_pin_mut(dbus_server.as_mut())
Expand All @@ -212,6 +226,64 @@ impl MainLoop {
let mut running_event_program = Box::pin(Fuse::terminated());

let mut event_channel = connection.player.get_player_event_channel();
let mut cluster_update_channel = self
.spectator
.then(|| shared_spirc.as_ref().get_cluster_update_channel());

let mut last_track_uri: Option<String> = None;
#[cfg(feature = "dbus_mpris")]
let mut spectator_connected = false;

let emit_spectator_state = |last_track_uri: &mut Option<String>,
track_uri: String,
is_playing: bool,
position_ms: u32,
play_request_id: u64| {
let track_changed = if let Some(ref last_uri) = *last_track_uri {
last_uri != &track_uri && !track_uri.is_empty()
} else {
!track_uri.is_empty()
};

if track_changed {
#[cfg(feature = "dbus_mpris")]
{
let session = spectator_session.clone();
let uri = track_uri.clone();
let tx_clone = mpris_event_tx.clone();
tokio::spawn(async move {
if let Some(audio_item) = fetch_audio_item(&session, &uri).await {
if let Some(ref tx) = tx_clone {
let _ = tx.send(PlayerEvent::TrackChanged { audio_item });
}
}
});
}
}

if let Ok(uri) = SpotifyUri::from_uri(&track_uri) {
let event = if is_playing {
PlayerEvent::Playing {
play_request_id,
track_id: uri,
position_ms,
}
} else {
PlayerEvent::Paused {
play_request_id,
track_id: uri,
position_ms,
}
};

#[cfg(feature = "dbus_mpris")]
if let Some(ref tx) = mpris_event_tx {
let _ = tx.send(event);
}
}

*last_track_uri = Some(track_uri);
};

loop {
tokio::select!(
Expand Down Expand Up @@ -243,8 +315,57 @@ impl MainLoop {
#[cfg(not(feature = "dbus_mpris"))]
result // unused variable
}
state = async {
match cluster_update_channel.as_mut() {
Some(channel) => channel.recv().await,
None => future::pending().await
}
}, if self.spectator => {
if let Ok(ClusterUpdateInfo { active_device_id, track_uri, is_playing, position_ms }) = state {
let has_active_remote = !active_device_id.is_empty();

#[cfg(feature = "dbus_mpris")]
if let Some(ref tx) = mpris_event_tx {
if has_active_remote && !spectator_connected {
let _ = tx.send(PlayerEvent::SessionConnected {
connection_id: SPECTATOR_CONNECTION_ID.to_string(),
user_name: SPECTATOR_USER_NAME.to_string(),
});
spectator_connected = true;
} else if !has_active_remote && spectator_connected {
if let Some(ref uri) = last_track_uri
&& let Ok(track_id) = SpotifyUri::from_uri(uri)
{
let _ = tx.send(PlayerEvent::Stopped {
play_request_id: SPECTATOR_PLAY_REQUEST_ID,
track_id,
});
}
let _ = tx.send(PlayerEvent::SessionDisconnected {
connection_id: SPECTATOR_CONNECTION_ID.to_string(),
user_name: SPECTATOR_USER_NAME.to_string(),
});
spectator_connected = false;
}
}

if !has_active_remote {
continue;
}

if let Some(track_uri) = track_uri {
emit_spectator_state(
&mut last_track_uri,
track_uri,
is_playing.unwrap_or(false),
position_ms.unwrap_or(0),
SPECTATOR_PLAY_REQUEST_ID,
);
}
}
}
// a new player event is available and no program is running
event = event_channel.recv(), if running_event_program.is_terminated() => {
event = event_channel.recv(), if running_event_program.is_terminated() && !self.spectator => {
let event = event.unwrap();
#[cfg(feature = "dbus_mpris")]
if let Some(ref tx) = mpris_event_tx {
Expand Down
1 change: 1 addition & 0 deletions src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ pub(crate) fn initial_state(
device_type: config.device_type,
device_name: config.device_name,
player_event_program: config.onevent,
spectator: config.spectator,
#[cfg(feature = "dbus_mpris")]
mpris_config: config.mpris,
})
Expand Down