Improve playback controls#86
Conversation
Seek was completely unusable - rapid key presses would freeze the player, corrupt audio, cause looping glitches, prevent the app from quitting, and require Ctrl+C to kill. This made the < and > keys essentially broken. Native streaming fixes: - Throttle seeks to 50ms (max 20/sec) to prevent overwhelming librespot - Ignore position events for 500ms after seek to prevent UI jumpback - Queue pending seeks and flush on tick for smooth rapid pressing External devices (API) fixes: - Throttle seeks to 200ms (max 5/sec) to respect Spotify rate limits - Mark stale poll data after seek to prevent UI desync - Remove immediate playback refresh after seek (caused out-of-order responses) MPRIS shuffle/loop support: - Advertise shuffle and loop_status capabilities to D-Bus clients - Handle SetShuffle and SetLoopStatus events from playerctl/media keys - Update MPRIS state after commands so clients see new values - Sync UI state bidirectionally (spotatui <-> MPRIS) - Emit Seeked signal after native seeks Known limitations: - Rapid API seeking may briefly desync UI (recovers in ~1 second) - Shuffle/loop changes from Spotify app don't sync to spotatui UI
When resuming playback on external devices, the UI would briefly show 0:00 before the API returned the real position. Now we only reset progress when starting new content, not when resuming.
There was a problem hiding this comment.
Pull request overview
This PR improves playback control stability (especially seeking) and expands Linux MPRIS integration to support shuffle and loop/repeat state changes, while also preventing the UI progress bar from flashing 0:00 on resume for external devices.
Changes:
- Add seek throttling/queuing for both native streaming (librespot) and external-device (Spotify Web API) playback paths.
- Add MPRIS shuffle + loop status capability advertisement, event handling (SetShuffle/SetLoopStatus), and state syncing back to MPRIS clients.
- Avoid resetting progress to
0when resuming playback via the Spotify API.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/app.rs |
Adds seek throttling/queuing state and implements queued/throttled seek execution + MPRIS seek notifications. |
src/main.rs |
Flushes pending seeks on tick, ignores native position updates briefly after seeking, and handles new MPRIS shuffle/loop events. |
src/mpris.rs |
Adds shuffle + loop status support to the MPRIS server (events + commands + initial advertised properties). |
src/network.rs |
Avoids resetting progress on resume and removes immediate playback refresh after API seeks. |
src/player/streaming.rs |
Adds direct repeat-mode setter used by MPRIS loop-status handling. |
| // Mark current poll data as stale so it won't override our target after ignore window | ||
| // By setting this to now, when the 500ms ignore window expires, the poll data | ||
| // will be >500ms old (>300ms threshold), so resync won't happen | ||
| self.instant_since_last_current_playback_poll = Instant::now(); | ||
|
|
There was a problem hiding this comment.
queue_api_seek sets instant_since_last_current_playback_poll = Instant::now(), but update_on_tick treats ms_since_poll < 300 as fresh API data and will immediately resync song_progress_ms from the (stale) current_playback_context.progress. This is especially problematic when the seek is throttled/queued (pending_api_seek set) because last_api_seek isn’t updated yet, so the 500ms ignore window doesn’t apply and the UI can jump back right after a seek keypress.
Consider decoupling “delay next poll” from “data freshness”: e.g., track a separate ignore_api_progress_until / last_seek_requested used by update_on_tick, or mark the poll timestamp as old (not new) if the intent is to avoid the <300ms resync path while a seek is pending.
| const SEEK_IGNORE_MS: u128 = 500; // Ignore position events for 500ms after seeking | ||
| let recently_seeked = app | ||
| .last_native_seek | ||
| .is_some_and(|t| t.elapsed().as_millis() < SEEK_IGNORE_MS); |
There was a problem hiding this comment.
SEEK_IGNORE_MS (500ms) is duplicated in multiple places (App::update_on_tick and both start_ui variants). To avoid these drifting out of sync over time, consider defining a single constant (e.g., in app or player module) and reusing it from all call sites.
- Extract SEEK_POSITION_IGNORE_MS as a public constant to avoid duplication - Fix timing bug where queued API seeks didn't start the ignore window, which could cause the UI to jump back to old positions during rapid seeks - Collapse nested if statements to satisfy clippy
|
Fixed the issues! |
|
Btw also added an spotatui-git package to the AUR |
|
@all-contributors please add @El-Mundos for platform |
|
I couldn't determine any contributions to add, did you specify any contributions? |
|
@all-contributors please add @El-Mundos for platform |
|
I've put up a pull request to add @El-Mundos! 🎉 |
Adds @El-Mundos as a contributor for platform. This was requested by LargeModGames [in this comment](#86 (comment)) [skip ci]
fix(playback): fix unusable seek and add MPRIS shuffle/loop support
Seek was completely unusable - rapid key presses would freeze the player,
corrupt audio, cause looping glitches, prevent the app from quitting,
and require Ctrl+C to kill. This made the < and > keys essentially broken.
Native streaming fixes:
External devices (API) fixes:
MPRIS shuffle/loop support:
Known limitations:
fix(ui): don't reset progress to 0:00 when resuming playback
When resuming playback on external devices, the UI would briefly show
0:00 before the API returned the real position. Now we only reset
progress when starting new content, not when resuming.
Summary
Fix completely broken seek functionality and add full MPRIS shuffle/loop support.
Seek was unusable - rapid < and > key presses would freeze the player, corrupt audio, cause
looping glitches, and prevent the app from quitting (required Ctrl+C). Now works smoothly.
MPRIS shuffle/loop - Added support (previously wasn't implemented) and bidirectional sync between spotatui and MPRIS clients (playerctl, media keys, desktop widgets).
Also fixed UI briefly showing 0:00 when resuming playback on external devices.
Testing
Passed all
Manual testing:
Additional notes
Known limitations: