Skip to content

Commit 94ba80c

Browse files
committed
Fix daemon mode autoplay and MPRIS registration (#232)
Daemon mode skipped pending URL resolution (feeds, M3U, yt-dlp/SoundCloud/ YouTube), so --auto-play with a remote URL left the playlist empty and the autoplay guard a no-op. It also never instantiated mediactl, so the daemon was invisible to playerctl and OS media keys. - Resolve resolved.Pending synchronously before runDaemon when --daemon is set - Wire mediactl.New in runDaemon; publish playback.State on each tick - Handle playback.{Play,Pause,Seek,SetPosition,SetVolume,Quit}Msg so MPRIS controls drive the daemon
1 parent a1ba57c commit 94ba80c

2 files changed

Lines changed: 90 additions & 8 deletions

File tree

daemon.go

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import (
99
"syscall"
1010
"time"
1111

12+
tea "charm.land/bubbletea/v2"
13+
1214
"cliamp/applog"
1315
"cliamp/external/local"
1416
"cliamp/internal/playback"
1517
"cliamp/internal/resume"
1618
"cliamp/ipc"
19+
"cliamp/mediactl"
1720
"cliamp/player"
1821
"cliamp/playlist"
1922
"cliamp/ui/model"
@@ -29,6 +32,19 @@ func runDaemon(p *player.Player, pl *playlist.Playlist, localProv *local.Provide
2932
player: p,
3033
playlist: pl,
3134
localProv: localProv,
35+
quit: make(chan struct{}, 1),
36+
}
37+
38+
// Wire MPRIS (Linux) / NowPlaying (macOS) so playerctl and OS media
39+
// keys see the daemon. mediactl callbacks dispatch back through d.Send.
40+
svc, mcErr := mediactl.New(func(msg tea.Msg) { d.Send(msg) })
41+
if mcErr != nil {
42+
fmt.Fprintf(os.Stderr, "media controls: %v\n", mcErr)
43+
applog.Warn("daemon: media controls unavailable: %v", mcErr)
44+
}
45+
if svc != nil {
46+
defer svc.Close()
47+
d.notifier = svc
3248
}
3349

3450
if autoPlay && pl.Len() > 0 {
@@ -55,6 +71,10 @@ func runDaemon(p *player.Player, pl *playlist.Playlist, localProv *local.Provide
5571
applog.Info("daemon: signal received, shutting down")
5672
d.saveResume()
5773
return nil
74+
case <-d.quit:
75+
applog.Info("daemon: quit requested via media control, shutting down")
76+
d.saveResume()
77+
return nil
5878
case <-ticker.C:
5979
d.tick()
6080
}
@@ -69,21 +89,23 @@ type daemon struct {
6989
player *player.Player
7090
playlist *playlist.Playlist
7191
localProv *local.Provider
92+
notifier playback.Notifier
93+
quit chan struct{}
7294
}
7395

7496
func (d *daemon) Send(msg any) {
7597
d.mu.Lock()
7698
defer d.mu.Unlock()
7799

78100
switch m := msg.(type) {
79-
case ipc.PlayMsg:
101+
case ipc.PlayMsg, playback.PlayMsg:
80102
if d.player.IsPaused() {
81103
d.player.TogglePause()
82104
} else if !d.player.IsPlaying() && d.playlist.Len() > 0 {
83105
d.playCurrent()
84106
}
85107

86-
case ipc.PauseMsg:
108+
case ipc.PauseMsg, playback.PauseMsg:
87109
if d.player.IsPlaying() && !d.player.IsPaused() {
88110
d.player.TogglePause()
89111
}
@@ -100,12 +122,28 @@ func (d *daemon) Send(msg any) {
100122
case playback.PrevMsg:
101123
d.prevTrack()
102124

125+
case playback.QuitMsg:
126+
select {
127+
case d.quit <- struct{}{}:
128+
default:
129+
}
130+
103131
case ipc.VolumeMsg:
104132
d.player.SetVolume(m.DB)
105133

134+
case playback.SetVolumeMsg:
135+
d.player.SetVolume(m.VolumeDB)
136+
106137
case ipc.SeekMsg:
107138
_ = d.player.Seek(m.Offset)
108139

140+
case playback.SeekMsg:
141+
_ = d.player.Seek(m.Offset)
142+
143+
case playback.SetPositionMsg:
144+
cur := d.player.Position()
145+
_ = d.player.Seek(m.Position - cur)
146+
109147
case ipc.LoadMsg:
110148
d.handleLoad(m)
111149

@@ -142,16 +180,48 @@ func (d *daemon) Send(msg any) {
142180
}
143181
}
144182

145-
// tick advances to the next track when the current one has drained.
146-
// Daemon mode skips gapless preloading; small inter-track gaps are fine.
183+
// tick advances to the next track when the current one has drained, and
184+
// republishes playback state to the media-control notifier. Daemon mode
185+
// skips gapless preloading; small inter-track gaps are fine.
147186
func (d *daemon) tick() {
148187
d.mu.Lock()
149-
if !d.player.IsPlaying() || d.player.IsPaused() || !d.player.Drained() {
150-
d.mu.Unlock()
151-
return
188+
if d.player.IsPlaying() && !d.player.IsPaused() && d.player.Drained() {
189+
d.nextTrack()
152190
}
153-
d.nextTrack()
191+
state := d.snapshotState()
154192
d.mu.Unlock()
193+
if d.notifier != nil {
194+
d.notifier.Update(state)
195+
}
196+
}
197+
198+
// snapshotState builds a playback.State for OS media-control notifiers.
199+
// Caller must hold d.mu.
200+
func (d *daemon) snapshotState() playback.State {
201+
status := playback.StatusStopped
202+
if d.player.IsPlaying() {
203+
if d.player.IsPaused() {
204+
status = playback.StatusPaused
205+
} else {
206+
status = playback.StatusPlaying
207+
}
208+
}
209+
track, _ := d.playlist.Current()
210+
return playback.State{
211+
Status: status,
212+
Track: playback.Track{
213+
Title: track.Title,
214+
Artist: track.Artist,
215+
Album: track.Album,
216+
Genre: track.Genre,
217+
TrackNumber: track.TrackNumber,
218+
URL: track.Path,
219+
Duration: d.player.Duration(),
220+
},
221+
VolumeDB: d.player.Volume(),
222+
Position: d.player.Position(),
223+
Seekable: d.player.Seekable(),
224+
}
155225
}
156226

157227
func (d *daemon) playCurrent() {

main.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@ func run(overrides config.Overrides, positional []string, daemon bool) error {
208208
}
209209
pl.Add(resolved.Tracks...)
210210

211+
// Daemon mode has no UI loop to drain pending URLs (feeds, M3U, yt-dlp),
212+
// so resolve them synchronously here. The TUI path does this in the
213+
// background via m.SetPendingURLs.
214+
if daemon && len(resolved.Pending) > 0 {
215+
fmt.Fprintf(os.Stderr, "cliamp: resolving %d remote URL(s)...\n", len(resolved.Pending))
216+
remote, err := resolve.Remote(resolved.Pending)
217+
if err != nil {
218+
return fmt.Errorf("resolve remote: %w", err)
219+
}
220+
pl.Add(remote...)
221+
}
222+
211223
if cfg.AudioDevice != "" {
212224
cleanup := player.PrepareAudioDevice(cfg.AudioDevice)
213225
defer cleanup()

0 commit comments

Comments
 (0)