Skip to content

feat: sync play queue with subsonic servers - #618

Open
FormalSnake wants to merge 1 commit into
Kopuz-org:masterfrom
FormalSnake:queue-sync
Open

feat: sync play queue with subsonic servers#618
FormalSnake wants to merge 1 commit into
Kopuz-org:masterfrom
FormalSnake:queue-sync

Conversation

@FormalSnake

Copy link
Copy Markdown
Contributor

Adds support for Navidrome's queue sync that is also supported by other clients but not Kopuz yet. It's not like spotify sync, it's like I play some music on my phone, I come back to the computer after pausing on my phone, and the same song and queue is ready. Pretty cool ngl :D
AI assistance has been used, and has been tested

Sanity Checking

  • I have read and followed the contribution guidelines.
  • My commits follow Kopuz's scoped commit convention and history hygiene
    rules.
  • I have disclosed any AI assistance as required by the AI policy in the
    contribution guidelines, or this pull request did not use AI assistance.
  • I have tested and self-reviewed my changes.

Style and Consistency

  • My changes are consistent with the existing crate boundaries and Dioxus
    style.
  • I ran cargo fmt --all --check or cargo fmt --all as appropriate.
  • I ran cargo clippy --workspace --all-targets -- -D warnings, or
    explained why it could not be run.
  • I kept generated assets, translations, and packaging files in sync when
    this change depends on them.

Testing

  • I ran the smallest relevant verifier for this change.
  • I documented any platform or verifier that I could not run.

Tested on platform(s):

  • x86_64-linux
  • aarch64-linux
  • x86_64-darwin
  • aarch64-darwin
  • Windows
  • Android
  • iOS

Adds savePlayQueue/getPlayQueue to the Subsonic client and a play_queue
capability on the source abstraction, so kopuz interoperates with other
Subsonic clients' play queue (Navidrome, Arpeggi). The queue pushes to
the server on pause, track change and app quit; on startup, if the
server's queue was last saved by another client, it's adopted over the
local one.

Verified savePlayQueue/getPlayQueue's request/response shape against a
live Navidrome instance.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds remote play-queue persistence for Subsonic sources. The player pushes eligible queue changes, restores queues changed by another client at startup, and flushes queue state during shutdown. Other source types explicitly disable this capability.

Changes

Play-queue synchronization

Layer / File(s) Summary
Remote queue contracts and transport
crates/server/src/source/types.rs, crates/server/src/source.rs, crates/server/src/subsonic.rs
Adds remote queue types, capability-gated source methods, Subsonic queue serialization, retrieval, clearing, and deserialization tests.
Source capability and Subsonic adapter
crates/server/src/source/subsonic.rs, crates/server/src/source/{jellyfin,local,offline,soundcloud,spotify,youtube_music}.rs
Enables play-queue support for Subsonic, maps queue data through MediaSource, and disables the capability for other sources.
Player synchronization and lifecycle integration
crates/hooks/src/lib.rs, crates/hooks/src/play_queue_sync.rs, crates/hooks/src/use_player_task.rs, crates/kopuz/src/main.rs
Pushes eligible queue changes on track changes and pauses, restores queues changed elsewhere, and saves queue state during shutdown.

Sequence Diagram(s)

sequenceDiagram
  participant PlayerTask
  participant play_queue_sync
  participant MediaSource
  participant SubsonicClient
  participant PlayerController
  PlayerTask->>play_queue_sync: push queue on track change or pause
  play_queue_sync->>MediaSource: save_play_queue(item IDs, current ID, position)
  MediaSource->>SubsonicClient: save_play_queue(parameters)
  PlayerController->>play_queue_sync: restore_if_changed_elsewhere at startup
  play_queue_sync->>MediaSource: get_play_queue()
  MediaSource->>SubsonicClient: get_play_queue()
  play_queue_sync->>PlayerController: restore changed remote queue
Loading

Possibly related PRs

  • Kopuz-org/kopuz#150: Modifies queue restoration and playback state handling used by startup synchronization.
  • Kopuz-org/kopuz#180: Adds tracks to the local queue that this change can synchronize remotely.
  • Kopuz-org/kopuz#185: Changes PlayerController queue handling used by remote synchronization.

Suggested reviewers: notashelf, temidaradev, umceko

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: play queue synchronization with Subsonic-compatible servers.
Description check ✅ Passed The description explains play queue synchronization with Navidrome and other Subsonic-compatible servers, including usage and testing details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/hooks/src/play_queue_sync.rs`:
- Around line 59-66: The playback-triggered save in the async block must use a
source-scoped single-flight, latest-wins writer so remote queue snapshots are
serialized and stale requests cannot overwrite newer state; update the
play-queue synchronization flow around save_play_queue accordingly. In
crates/kopuz/src/main.rs lines 624-630, route the close flush through that same
writer and await completion of its final snapshot. Ensure both sites share the
writer rather than issuing independent savePlayQueue requests.
- Around line 18-20: Update server_item_ids so an empty queue returns Some with
an empty Vec instead of None, allowing save_payload to call savePlayQueue
without a current item. Add a test verifying that an empty queue produces an
empty payload and reaches savePlayQueue.

In `@crates/hooks/src/use_player_task.rs`:
- Around line 1033-1051: Update the play-queue sync block in the task loop to
track a queue revision or fingerprint instead of only current_item_id, so
additions, removals, and reordering trigger a push even when the current track
is unchanged. Compare and update this queue-change value alongside the existing
track state, while preserving the just_paused trigger for position updates and
the existing service filtering.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8017b43-31a0-4a9c-8191-1bb4163f6ede

📥 Commits

Reviewing files that changed from the base of the PR and between 0f8b355 and 67fb371.

📒 Files selected for processing (14)
  • crates/hooks/src/lib.rs
  • crates/hooks/src/play_queue_sync.rs
  • crates/hooks/src/use_player_task.rs
  • crates/kopuz/src/main.rs
  • crates/server/src/source.rs
  • crates/server/src/source/jellyfin.rs
  • crates/server/src/source/local.rs
  • crates/server/src/source/offline.rs
  • crates/server/src/source/soundcloud.rs
  • crates/server/src/source/spotify.rs
  • crates/server/src/source/subsonic.rs
  • crates/server/src/source/types.rs
  • crates/server/src/source/youtube_music.rs
  • crates/server/src/subsonic.rs

Comment on lines +18 to +20
fn server_item_ids(queue: &[Track]) -> Option<Vec<String>> {
if queue.is_empty() {
return None;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Allow an empty queue to clear the remote queue.

Line 20 returns None for an empty queue. save_payload then skips both normal synchronization and the close flush. A locally cleared queue leaves the prior remote queue intact. A later startup can restore that stale queue.

Return Some(Vec::new()) for an empty queue. Add a test that verifies an empty payload reaches savePlayQueue without a current item.

Proposed fix
     if queue.is_empty() {
-        return None;
+        return Some(Vec::new());
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn server_item_ids(queue: &[Track]) -> Option<Vec<String>> {
if queue.is_empty() {
return None;
fn server_item_ids(queue: &[Track]) -> Option<Vec<String>> {
if queue.is_empty() {
return Some(Vec::new());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/hooks/src/play_queue_sync.rs` around lines 18 - 20, Update
server_item_ids so an empty queue returns Some with an empty Vec instead of
None, allowing save_payload to call savePlayQueue without a current item. Add a
test verifying that an empty queue produces an empty payload and reaches
savePlayQueue.

Comment on lines +59 to +66
spawn(async move {
if let Err(e) = source
.save_play_queue(&item_ids, current_id.as_deref(), position_ms)
.await
{
tracing::debug!(error = %e, "play queue push failed");
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialize all remote queue snapshot writes.

savePlayQueue writes a complete queue snapshot. Independent requests can finish out of order. An older playback-triggered request can therefore overwrite a newer queue or the shutdown snapshot.

  • crates/hooks/src/play_queue_sync.rs#L59-L66: send snapshots through a source-scoped single-flight, latest-wins writer.
  • crates/kopuz/src/main.rs#L624-L630: use the same writer for the close flush and wait for its final snapshot to complete.
📍 Affects 2 files
  • crates/hooks/src/play_queue_sync.rs#L59-L66 (this comment)
  • crates/kopuz/src/main.rs#L624-L630
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/hooks/src/play_queue_sync.rs` around lines 59 - 66, The
playback-triggered save in the async block must use a source-scoped
single-flight, latest-wins writer so remote queue snapshots are serialized and
stale requests cannot overwrite newer state; update the play-queue
synchronization flow around save_play_queue accordingly. In
crates/kopuz/src/main.rs lines 624-630, route the close flush through that same
writer and await completion of its final snapshot. Ensure both sites share the
writer rather than issuing independent savePlayQueue requests.

Comment on lines +1033 to +1051
// Play-queue sync (Subsonic savePlayQueue): push on track change
// and on pause. No-ops via capabilities() for every other backend.
{
let current_idx = *ctrl.current_queue_index.read();
let current_item_id = ctrl.get_track_at(current_idx).and_then(|t| {
matches!(
t.id.service(),
Some(MusicService::Subsonic) | Some(MusicService::Custom)
)
.then(|| t.id.key().into_owned())
});
let track_changed = last_queue_push_id != current_item_id;
let just_paused = is_playing != prev_playing && !is_playing;

if current_item_id.is_some() && (track_changed || just_paused) {
crate::play_queue_sync::push(ctrl);
}
last_queue_push_id = current_item_id;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Push when the queue changes, not only when the current item changes.

Line 1044 compares only the current item ID. Adding, removing, or reordering other items leaves that ID unchanged. Line 1048 then does not save the modified queue until pause or exit.

Track a queue revision or queue fingerprint. Push when that value changes. Keep the existing pause trigger for position updates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/hooks/src/use_player_task.rs` around lines 1033 - 1051, Update the
play-queue sync block in the task loop to track a queue revision or fingerprint
instead of only current_item_id, so additions, removals, and reordering trigger
a push even when the current track is unchanged. Compare and update this
queue-change value alongside the existing track state, while preserving the
just_paused trigger for position updates and the existing service filtering.

@temidaradev

temidaradev commented Aug 9, 2026

Copy link
Copy Markdown
Member

I will kirk morph vms wallahi

@FormalSnake

Copy link
Copy Markdown
Contributor Author

on wallahi idk what you just said 😭

@saradoesthings

Copy link
Copy Markdown

on wallahi

'on i swear to god' يحرق بيتك

@FormalSnake

Copy link
Copy Markdown
Contributor Author

on wallahi

'on i swear to god' يحرق بيتك

wallahi is deadass the only thing i understood idk what he meant with kirk vms

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants