Skip to content

Improve playback controls#86

Merged
LargeModGames merged 3 commits intoLargeModGames:mainfrom
El-Mundos:improve-playback-controls
Feb 8, 2026
Merged

Improve playback controls#86
LargeModGames merged 3 commits intoLargeModGames:mainfrom
El-Mundos:improve-playback-controls

Conversation

@El-Mundos
Copy link
Copy Markdown
Contributor

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:

  • 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

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

  • cargo ftm --all
  • cargo clippy --locked --features=streaming,mpris -- -D -warnings
  • cargo build --release --features=streaming,mpris

Passed all

Manual testing:

  • Rapid seek with < and > keys (native streaming + external devices)
  • playerctl -p spotatui shuffle toggle
  • playerctl -p spotatui loop Playlist/Track/None
  • Toggle shuffle/loop from spotatui UI -> verified MPRIS updates
  • Resume playback on external device -> no 0:00 flash

Additional notes

Known limitations:

  • Rapid API seeking may briefly desync UI, lasts for about a second
  • Shuffle/loop changes from spotify mobile/desktop app don't sync to spotatui (would require reworking stale-state protection)

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.
Copilot AI review requested due to automatic review settings February 7, 2026 18:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 0 when 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.

Comment thread src/app.rs Outdated
Comment on lines +932 to +936
// 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();

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/main.rs Outdated
Comment on lines +1824 to +1827
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);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
- 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
@El-Mundos
Copy link
Copy Markdown
Contributor Author

Fixed the issues!

@El-Mundos
Copy link
Copy Markdown
Contributor Author

Btw also added an spotatui-git package to the AUR

@LargeModGames LargeModGames merged commit 803b82e into LargeModGames:main Feb 8, 2026
5 checks passed
@LargeModGames
Copy link
Copy Markdown
Owner

LargeModGames commented Feb 8, 2026

@all-contributors please add @El-Mundos for platform

@allcontributors
Copy link
Copy Markdown
Contributor

@LargeModGames

I couldn't determine any contributions to add, did you specify any contributions?
Please make sure to use valid contribution names.

@LargeModGames
Copy link
Copy Markdown
Owner

@all-contributors please add @El-Mundos for platform

@allcontributors
Copy link
Copy Markdown
Contributor

@LargeModGames

I've put up a pull request to add @El-Mundos! 🎉

LargeModGames added a commit that referenced this pull request Feb 8, 2026
Adds @El-Mundos as a contributor for platform.

This was requested by LargeModGames [in this
comment](#86 (comment))

[skip ci]
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