Skip to content

Conversation

@llehouerou
Copy link
Owner

Summary

Closes #3

Adds MPRIS (Media Player Remote Interfacing Specification) support, enabling desktop media controls, media keys, and tools like playerctl to control Waves playback.

What's Implemented

  • Playback controls: Play, Pause, Stop, Next, Previous via media keys or playerctl
  • Seek support: Position tracking and seeking
  • Shuffle/Loop modes: Toggle shuffle, set loop mode (None/Track/Playlist)
  • Track metadata: Title, Artist, Album, Duration, Track number
  • Album art: Automatic discovery of cover images (cover.jpg, folder.jpg, etc.)

Architecture

  • New internal/mpris/ package with Linux-only implementation using build tags
  • Uses go-mpris-server library for D-Bus handling
  • No-op stub for non-Linux platforms (macOS/Windows compile cleanly)
  • Non-fatal initialization - app works without D-Bus

Usage

# List players
playerctl -l

# Control playback
playerctl -p waves play-pause
playerctl -p waves next
playerctl -p waves previous

# View metadata
playerctl -p waves metadata

# Seek
playerctl -p waves position 30

# Shuffle/Loop
playerctl -p waves shuffle on
playerctl -p waves loop Track

Test Plan

  • make check passes (lint + tests)
  • playerctl -l shows "waves"
  • Play/Pause/Next/Previous work
  • Metadata displays correctly
  • Seek works
  • Shuffle/Loop modes work
  • Album art URL returned when cover exists

🤖 Generated with Claude Code

llehouerou and others added 26 commits December 28, 2025 17:40
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Track type for representing queue tracks (independent of playlist.Track)
and event types (StateChange, TrackChange, QueueChange, ModeChange,
PositionChange) for the subscription system.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Subscription type with buffered channels for event delivery.
Subscribers receive events without blocking the service through
non-blocking send patterns using select with default.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement serviceImpl struct that wraps player.Interface and
playlist.PlayingQueue, exposing them through the Service interface.
State query methods are fully implemented; playback control methods
are stubs for now.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add playback control methods with proper state management and event emission:
- Add ErrEmptyQueue and ErrNoCurrentTrack error types
- Add emitStateChange helper for notifying subscribers
- Implement Play() with queue/track validation
- Implement Pause() with no-op when not playing
- Implement Stop() with no-op when already stopped
- Implement Toggle() for play/pause cycling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add navigation methods to the playback service:
- Next(): advances to the next track, auto-plays if already playing
- Previous(): goes back to previous track (no-op at start)
- JumpTo(index): jumps to specific index with bounds checking
- emitTrackChange(): helper to notify subscribers of track changes

Includes comprehensive tests for all navigation scenarios.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add seek and mode control functionality to the playback service:
- Seek(delta) adjusts position by delta and emits PositionChanged
- SeekTo(position) calculates delta from current position and seeks
- SetRepeatMode, CycleRepeatMode for repeat mode control
- SetShuffle, ToggleShuffle for shuffle control
- emitPositionChange and emitModeChange helper methods for events

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add goroutine to watch for track completion signals and automatically
advance to the next track in the queue. When the queue ends, playback
stops and a StateChanged event is emitted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add tests to verify thread safety of PlaybackService:
- TestService_ConcurrentAccess_NoRace: runs 300 concurrent operations
  (50 iterations x 6 goroutines) to verify no race conditions
- TestService_MultipleSubscribers_AllReceiveEvents: verifies all
  subscribers receive state change events

Also make player.Mock thread-safe with mutex protection to fix race
conditions detected by the race detector in existing tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wire up the playback service to handle playback control through the
app's event-driven architecture:

- Add PlaybackService field to app.Model initialized in New()
- Add service event messages (ServiceStateChangedMsg, ServiceTrackChangedMsg,
  ServiceClosedMsg) for async notifications
- Add WatchServiceEvents command to listen for service events
- Refactor handlers to use PlaybackService methods:
  - HandleSpaceAction uses service.Toggle()
  - StartQueuePlayback uses service.Play()
  - PlayTrackAtIndex uses service.Play()
  - handleSeek uses service.Seek()
  - Stop actions use service.Stop()
  - Shuffle/repeat modes use service methods
- Handle scrobble reset and radio fill in handleServiceStateChanged
  when transitioning from stopped to playing
- Update all test helpers to include PlaybackService

The service emits events which are received via WatchServiceEvents
and converted to tea.Msg for the Bubble Tea update cycle. This enables
external control (e.g., MPRIS) while keeping the UI reactive.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The PlaybackService was created during app.New() with an empty queue,
but the actual queue was restored from saved state during handleInitResult
and only updated in PlaybackManager. This caused "queue is empty" errors
when trying to play.

Fix by recreating the PlaybackService with the restored queue and
starting WatchServiceEvents after initialization completes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The refactoring to use PlaybackService bypassed PlayTrack which
contained the triggerRadioFill() call. The handlers were only calling
checkRadioFillNearEnd() which triggers near the end of a track.

Fix by calling triggerRadioFill() in service event handlers:
- handleServiceStateChanged: when starting from stopped
- handleServiceTrackChanged: when track changes (auto-advance)

This restores the pre-fetch behavior when starting the last track.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When skipping tracks with pgdown/pgup while playing, the queue moves
immediately for UI feedback, then PlayTrackAtIndex is called after
debounce. Since the queue was already moved, service.Play() doesn't
emit TrackChange (index unchanged), so handleServiceTrackChanged
never runs and radio fill isn't triggered.

Fix by calling resetScrobbleState and triggerRadioFill directly in
PlayTrackAtIndex to handle track changes from debounced navigation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
DisplayMode is a UI layout concern, not playback state.
- Add playerDisplayMode field to LayoutManager
- Add PlayerDisplayMode() and SetPlayerDisplayMode() methods
- Update playback.go, view.go, layout.go to use Layout methods

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add methods to PlaybackService for complete queue management:
- AddTracks, ReplaceTracks, ClearQueue for manipulation
- QueueLen, QueueIsEmpty, QueueHasNext for queries
- Undo, Redo for history
- Rename Queue() to QueueTracks(), QueueIndex() to QueueCurrentIndex()

Add track conversion helpers between playback.Track and playlist.Track.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add convenience methods to PlaybackService:
- IsPlaying(), IsStopped(), IsPaused() for state checks
- TrackInfo() to access player's track metadata

These allow callers to use the service directly without
accessing the underlying player.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Complete migration from PlaybackManager to PlaybackService as the single
abstraction for all playback operations:

- Add PlayPath, QueueAdvance, QueueMoveTo, Player methods to service
- Migrate all app code to use PlaybackService instead of Playback
- Update test files to use service methods
- Remove PlaybackManager and PlaybackController interface
- Clean up unused imports

All queue and player operations now go through PlaybackService,
eliminating the redundant manager layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Critical fix:
- Remove deprecated WatchTrackFinished and TrackFinishedMsg
- Service now handles track finished internally via watchTrackFinished goroutine
- App uses ServiceTrackChangedMsg for UI updates

Important fixes:
- Close old service before recreating with restored queue
- Implement QueueChange events for AddTracks, ReplaceTracks, ClearQueue
- Add ErrorEvent type and error handling for playback failures
- App shows error popup when service encounters playback errors

Test fixes:
- Add playbackSub to test model helpers
- Update tests to use service messages instead of TrackFinishedMsg

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add PreviousIndex to TrackChange event for accurate tracking
- Emit QueueChange events on Undo/Redo operations
- Handle all subscription channels (QueueChanged, ModeChanged,
  PositionChanged) to prevent buffer fill-up
- Add nolint comment for intentionally ignored Stop() error on shutdown
- Use TrackFromPlaylist helper consistently to reduce duplication

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add MPRIS adapter that bridges PlaybackService to D-Bus, enabling media
key support and integration with desktop media controls (playerctl, etc).

The adapter implements go-mpris-server interfaces:
- OrgMprisMediaPlayer2Adapter for root properties
- OrgMprisMediaPlayer2PlayerAdapter for playback control
- OrgMprisMediaPlayer2PlayerAdapterLoopStatus for repeat modes
- OrgMprisMediaPlayer2PlayerAdapterShuffle for shuffle mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@llehouerou llehouerou merged commit cb7b2a9 into main Dec 29, 2025
1 check passed
@llehouerou llehouerou deleted the feature/playback-service branch December 29, 2025 10:47
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.

MPRIS support?

2 participants