@@ -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
7496func (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.
147186func (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
157227func (d * daemon ) playCurrent () {
0 commit comments