Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
861ef53
Allow cloning SPIRC
wisp3rwind Sep 17, 2024
a41e331
add release date to AudioItem
wisp3rwind Sep 17, 2024
6df285e
add Spirc.seek_offset command
wisp3rwind Sep 17, 2024
cd3f3a3
add initial MPRIS support using zbus
wisp3rwind Oct 1, 2024
fd91138
feat(mpris): serve identity based on configured name
paulfariello Sep 23, 2025
710826b
feat(mpris): Add set_volume handler
paulfariello Sep 23, 2025
0f5a0a3
feat(mpris): Retry with pid specific name on NameTaken error
paulfariello Sep 23, 2025
23e7635
feat(player): Send current state of player for all new player listeners
paulfariello Sep 23, 2025
e6da469
feat(mpris): Notify when volume changed
paulfariello Sep 23, 2025
ddb5f4b
fix(mpris): Remove done todo
paulfariello Sep 23, 2025
5e6807c
fix(mpris): Remove duplicated and commented function
paulfariello Sep 23, 2025
08fd96e
fix(mpris): Add comment concerning non-support of setting playback rate
paulfariello Sep 23, 2025
6f12e88
feat(mpris): Store metadata unserialized
paulfariello Sep 25, 2025
65cc265
feat(mpris): Add debug logging
paulfariello Sep 25, 2025
2f73b03
feat(mpris): Send biggest art url
paulfariello Sep 25, 2025
e7f0c8b
feat(mpris): Update track id on EndOfTrack
paulfariello Sep 25, 2025
a8ffe20
feat(player): Add position update option
paulfariello Sep 30, 2025
bb6d4bf
feat(mpris): Get position from player and provide it to MPRIS
paulfariello Sep 30, 2025
3a3ea76
feat(mpris): Check track_id when setting position
paulfariello Sep 30, 2025
11519ed
chore(mpris): Remove useless comment
paulfariello Sep 30, 2025
e8601c9
feat(mpris): Add support for desktop entry
paulfariello Sep 30, 2025
15bc920
feat(mpris): Return error when trying to play/pause in wrong context
paulfariello Sep 30, 2025
15280de
feat(mpris): Signal when position changed
paulfariello Sep 30, 2025
69112e3
chore(mpris): alias zbus::fdo::{Error, Result} for readability
paulfariello Oct 1, 2025
9996b4c
feat(player): Allow for stopped event without track_id
paulfariello Oct 1, 2025
cc8302b
feat(player): Rename position update interval option
paulfariello Oct 1, 2025
1c418ad
feat(mpris): Upgrade to zbus 5
paulfariello Oct 2, 2025
dfe09f0
feat(player): Add position_ms in Loading event
paulfariello Oct 2, 2025
fa4cc94
feat(changelog): Add mpris changelog
paulfariello Oct 8, 2025
c19f8b7
fixup! add initial MPRIS support using zbus
paulfariello Nov 13, 2025
953fafc
feat(player): Replace position update interval option with sensible d…
paulfariello Nov 13, 2025
ce54cfa
fixup! feat(mpris): Check track_id when setting position
paulfariello May 6, 2026
ea324eb
fixup! add initial MPRIS support using zbus
paulfariello May 6, 2026
d6a83b7
fixup! feat(player): Allow for stopped event without track_id
paulfariello May 6, 2026
a021864
fixup! feat(mpris): Store metadata unserialized
paulfariello May 6, 2026
46755ec
fixup! feat(mpris): Store metadata unserialized
paulfariello May 6, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- [connect] Add method `add_to_queue` to `Spirc` to add tracks, episodes, albums and playlists to the queue
- [playback] Add `SetQueue` player event, emitting when the queue changes (context loaded, track added to queue, or queue set via Spotify Connect). Gated behind `ConnectConfig::emit_set_queue_events`
- [dbus/mpris] Add dbus/mpris support to allow controlling player (breaking)

### Changed

Expand Down
41 changes: 22 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ repository = "https://github.com/librespot-org/librespot"
edition = "2024"

[features]
default = ["native-tls", "rodio-backend", "with-libmdns"]
default = ["native-tls", "rodio-backend", "with-libmdns", "with-mpris"]

# TLS backends (mutually exclusive - compile-time checks in oauth/src/lib.rs)
# Note: Feature validation is in oauth crate since it's compiled first in the dependency tree.
Expand Down Expand Up @@ -132,6 +132,10 @@ with-dns-sd = ["librespot-discovery/with-dns-sd"]
# data.
passthrough-decoder = ["librespot-playback/passthrough-decoder"]

# MPRIS: Allow external tool to have access to playback
# status, metadata and to control the player.
with-mpris = ["dep:zbus", "dep:zvariant", "dep:time"]

[lib]
name = "librespot"
path = "src/lib.rs"
Expand Down Expand Up @@ -180,7 +184,10 @@ tokio = { version = "1", features = [
"sync",
"process",
] }
time = { version = "0.3", features = ["formatting"], optional = true }
url = "2.2"
zbus = { version = "5", default-features = false, features = ["tokio"], optional = true }
zvariant = { version = "5", default-features = false, optional = true }

[package.metadata.deb]
maintainer = "Librespot Organization <noreply@github.com>"
Expand Down
29 changes: 29 additions & 0 deletions connect/src/spirc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ enum SpircCommand {
RepeatTrack(bool),
Disconnect { pause: bool },
SetPosition(u32),
SeekOffset(i32),
SetVolume(u16),
Activate,
Transfer(Option<TransferRequest>),
Expand All @@ -146,6 +147,7 @@ const VOLUME_UPDATE_DELAY: Duration = Duration::from_millis(500);
const UPDATE_STATE_DELAY: Duration = Duration::from_millis(200);

/// The spotify connect handle
#[derive(Clone)]
pub struct Spirc {
commands: mpsc::UnboundedSender<SpircCommand>,
}
Expand Down Expand Up @@ -395,6 +397,13 @@ impl Spirc {
Ok(self.commands.send(SpircCommand::Load(command))?)
}

/// Seek to given offset.
///
/// Does nothing if we are not the active device.
pub fn seek_offset(&self, offset_ms: i32) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::SeekOffset(offset_ms))?)
}

/// Adds a track, episode, album or playlist to the queue.
///
/// Does nothing if we are not the active device.
Expand Down Expand Up @@ -744,6 +753,7 @@ impl SpircTask {
SpircCommand::Repeat(repeat) => self.handle_repeat_context(repeat)?,
SpircCommand::RepeatTrack(repeat) => self.handle_repeat_track(repeat),
SpircCommand::SetPosition(position) => self.handle_seek(position),
SpircCommand::SeekOffset(offset) => self.handle_seek_offset(offset),
SpircCommand::SetVolume(volume) => self.set_volume(volume),
SpircCommand::Load(command) => self.handle_load(command, None, None).await?,
SpircCommand::AddToQueue(uri) => self.handle_add_to_queue(uri).await,
Expand Down Expand Up @@ -1604,6 +1614,25 @@ impl SpircTask {
};
}

fn handle_seek_offset(&mut self, offset_ms: i32) {
let position_ms = match self.play_status {
SpircPlayStatus::Stopped => return,
SpircPlayStatus::LoadingPause { position_ms }
| SpircPlayStatus::LoadingPlay { position_ms }
| SpircPlayStatus::Paused { position_ms, .. } => position_ms,
SpircPlayStatus::Playing {
nominal_start_time, ..
} => {
let now = self.now_ms();
(now - nominal_start_time) as u32
}
};

let position_ms = ((position_ms as i32) + offset_ms).max(0) as u32;

self.handle_seek(position_ms);
}

fn handle_shuffle(&mut self, shuffle: bool) -> Result<(), Error> {
self.player.emit_shuffle_changed_event(shuffle);
self.connect_state.handle_shuffle(shuffle)
Expand Down
4 changes: 4 additions & 0 deletions metadata/src/audio/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub enum UniqueFields {
Track {
artists: ArtistsWithRole,
album: String,
album_date: Date,
album_artists: Vec<String>,
popularity: u8,
number: u32,
Expand Down Expand Up @@ -90,6 +91,8 @@ impl AudioItem {
let uri_string = uri.to_uri();
let album = track.album.name;

let album_date = track.album.date;

let album_artists = track
.album
.artists
Expand Down Expand Up @@ -123,6 +126,7 @@ impl AudioItem {
let unique_fields = UniqueFields::Track {
artists: track.artists_with_role,
album,
album_date,
album_artists,
popularity,
number,
Expand Down
66 changes: 60 additions & 6 deletions playback/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ pub enum PlayerEvent {
},
// Fired when the player is stopped (e.g. by issuing a "stop" command to the player).
Stopped {
play_request_id: u64,
track_id: SpotifyUri,
play_request_id: Option<u64>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please tag in the changelog that this is a breaking change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

track_id: Option<SpotifyUri>,
},
// The player is delayed by loading a track.
Loading {
Expand Down Expand Up @@ -293,7 +293,8 @@ impl PlayerEvent {
play_request_id, ..
}
| Stopped {
play_request_id, ..
play_request_id: Some(play_request_id),
..
}
| PositionCorrection {
play_request_id, ..
Expand Down Expand Up @@ -728,6 +729,7 @@ enum PlayerState {
play_request_id: u64,
start_playback: bool,
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
position_ms: u32,
},
Paused {
track_id: SpotifyUri,
Expand Down Expand Up @@ -1381,6 +1383,7 @@ impl Future for PlayerInternal {
ref track_id,
start_playback,
play_request_id,
..
} = self.state
{
// The loader may be terminated if we are trying to load the same track
Expand Down Expand Up @@ -1696,8 +1699,8 @@ impl PlayerInternal {

self.ensure_sink_stopped(false);
self.send_event(PlayerEvent::Stopped {
track_id,
play_request_id,
track_id: Some(track_id),
play_request_id: Some(play_request_id),
});
self.state = PlayerState::Stopped;
}
Expand Down Expand Up @@ -2175,6 +2178,7 @@ impl PlayerInternal {
play_request_id,
start_playback: play,
loader,
position_ms,
};

Ok(())
Expand Down Expand Up @@ -2318,7 +2322,57 @@ impl PlayerInternal {

PlayerCommand::SetSession(session) => self.session = session,

PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender),
PlayerCommand::AddEventSender(sender) => {
// Send current player state to new event listener
Comment thread
paulfariello marked this conversation as resolved.
match self.state {
PlayerState::Loading {
ref track_id,
play_request_id,
position_ms,
..
} => {
let _ = sender.send(PlayerEvent::Loading {
play_request_id,
track_id: track_id.clone(),
position_ms,
});
}
PlayerState::Paused {
ref track_id,
play_request_id,
stream_position_ms,
..
} => {
let _ = sender.send(PlayerEvent::Paused {
play_request_id,
track_id: track_id.clone(),
position_ms: stream_position_ms,
});
}
PlayerState::Playing { ref audio_item, .. } => {
let audio_item = Box::new(audio_item.clone());
let _ = sender.send(PlayerEvent::TrackChanged { audio_item });
}
PlayerState::EndOfTrack {
play_request_id,
ref track_id,
..
} => {
let _ = sender.send(PlayerEvent::EndOfTrack {
play_request_id,
track_id: track_id.clone(),
});
}
PlayerState::Invalid | PlayerState::Stopped => {
let _ = sender.send(PlayerEvent::Stopped {
play_request_id: None,
track_id: None,
});
}
}

self.event_senders.push(sender);
}

PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback,

Expand Down
Loading
Loading