-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add MPRIS support for desktop media controls #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
🤖 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>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
Closes #3
Adds MPRIS (Media Player Remote Interfacing Specification) support, enabling desktop media controls, media keys, and tools like
playerctlto control Waves playback.What's Implemented
playerctlArchitecture
internal/mpris/package with Linux-only implementation using build tagsgo-mpris-serverlibrary for D-Bus handlingUsage
Test Plan
make checkpasses (lint + tests)playerctl -lshows "waves"🤖 Generated with Claude Code