@@ -3,6 +3,7 @@ package main
33import (
44 "bufio"
55 "context"
6+ "errors"
67 "fmt"
78 "io"
89 "log/slog"
@@ -16,80 +17,132 @@ type EventType int
1617const (
1718 EventTypeUnset EventType = iota
1819 EventTypePing
20+ EventTypeServer
1921 EventTypeMPD
2022)
2123
22- // the MPDIdler continually calls the `idle` MPD
23- // command and sends idle events on the returned
24- // string channel.
25- func startMPDIdler (mpd net.Conn ) chan string {
26- eventC := make (chan string )
27- go func () {
28- slog .Debug ("start MPD Idler" )
29- defer slog .Debug ("end MPD Idler" )
30- defer close (eventC )
31- sc := bufio .NewScanner (mpd )
32- // MPD Header
33- sc .Scan ()
24+ // An Event contains information about something
25+ // that happened on the proxy that may be of interest
26+ // to the frontend.
27+ type Event struct {
28+ Type EventType
29+ Payload string
30+ }
31+
32+ // SSEPayload formats the given Event
33+ // as a Server-Sent Event payload
34+ func (ev Event ) SSEPayload () string {
35+ var sb strings.Builder
36+ var typ string
37+ switch ev .Type {
38+ case EventTypePing :
39+ typ = "ping"
40+ case EventTypeMPD :
41+ typ = "mpd"
42+ case EventTypeServer :
43+ typ = "server"
44+ default :
45+ typ = ""
46+ }
47+ if typ != "" {
48+ fmt .Fprintf (& sb , "event: %s\n " , typ )
49+ }
50+ fmt .Fprintf (& sb , "data: %s\n " , ev .Payload )
51+ return sb .String () + "\n "
52+ }
3453
54+ // The MPPIdler opens a connection to the MPD
55+ // server and receives real-time updates using the
56+ // `idle` command. All updates returned to the idler
57+ // are published to the given topic as an `mpd:{type}`
58+ // event.
59+ //
60+ // If the "idle" loop fails due to a dropped connection
61+ // or otherwise, the idler logs a "server:mpd-connection-lost"
62+ // event and tries to re-create the connection. After re-
63+ // connecting, the idler sends a `server:mpd-connected`
64+ // event and continues idling.
65+ func MPDIdler (tpc * Topic [Event ]) {
66+ oneRound := func () error {
67+ slog .Info ("start idler" )
68+ defer slog .Info ("stop idler" )
69+ mpd , err := net .Dial ("tcp" , MpdAuthority )
70+ if err != nil {
71+ return err
72+ }
73+ defer mpd .Close ()
74+ tpc .Publish (Event {
75+ Type : EventTypeServer ,
76+ Payload : "mpd-connected" ,
77+ })
78+ return mpdIdle (mpd , tpc )
79+ }
80+ for {
81+ err := oneRound ()
82+ tpc .Publish (Event {
83+ Type : EventTypeServer ,
84+ Payload : "mpd-connection-lost" ,
85+ })
86+ slog .Error ("idler exited" , "error" , err )
87+ time .Sleep (2 * time .Second )
88+ }
89+ }
90+
91+ // the MPDIdler continually calls the `idle` MPD
92+ // command and sends idle events on the returned
93+ // string channel.
94+ func mpdIdle (mpd net.Conn , tpc * Topic [Event ]) error {
95+ rd := bufio .NewReader (mpd )
96+ _ , err := rd .ReadString ('\n' )
97+ if err != nil {
98+ return err
99+ }
100+ for {
101+ _ , err := io .WriteString (mpd , "idle\n " )
102+ if err != nil {
103+ return err
104+ }
35105 for {
36- io .WriteString (mpd , "idle\n " )
37- for sc .Scan () {
38- line := sc .Text ()
39- if line == "OK" {
40- break
41- }
42- parts := strings .SplitN (line , ":" , 2 )
43- if len (parts ) != 2 {
44- slog .Error (fmt .Sprintf ("unexpected string: %s\n " , line ))
45- return
46- }
47- ev := strings .TrimSpace (parts [1 ])
48- slog .Debug (fmt .Sprint ("sending event:" , ev ))
49- eventC <- ev
106+ line , err := rd .ReadString ('\n' )
107+ if err != nil {
108+ return err
50109 }
51- if err := sc .Err (); err != nil {
52- if _ , ok := err .(* net.OpError ); ! ok {
53- slog .Error (fmt .Sprintf ("unexpected error: %T %s\n " , err , err ))
54- }
55- return
110+ line = strings .TrimSpace (line )
111+ if line == "OK" {
112+ break
113+ }
114+ parts := strings .SplitN (line , ":" , 2 )
115+ if len (parts ) != 2 {
116+ return errors .New ("MPD returned strange response: " + line )
56117 }
118+ ev := strings .TrimSpace (parts [1 ])
119+ tpc .Publish (Event {
120+ Type : EventTypeMPD ,
121+ Payload : ev ,
122+ })
57123 }
58- }()
59- return eventC
60- }
61-
62- // An Event is any asynchronous event
63- // that should be sent to a listening client.
64- type Event struct {
65- Type EventType
66- Data string
124+ }
67125}
68126
69- // getEvents starts a goroutine which emits events
70- // to send to the client. Events include:
71- // - any string sent on the provided string channel is sent as an MPDEvent
72- // - the emitter produces a "ping" event that clients can use as a heartbeat.
73- // The emitter exits and closes the returned channel when the given context
74- // is cancelled.
75- func getEvents (ts chan string , ctx context.Context ) chan Event {
127+ // getEvents wraps the given Topic[Event] with a ticker
128+ // that sends a `ping` event every 5 seconds.
129+ func getEvents (tpc * Topic [Event ], ctx context.Context ) chan Event {
76130 ret := make (chan Event )
77- ticker := time .NewTicker (5 * time .Second )
78131 go func () {
79132 defer close (ret )
133+ ts := tpc .Subscribe ()
134+ defer tpc .Unsubscribe (ts )
135+ ticker := time .NewTicker (5 * time .Second )
80136 defer ticker .Stop ()
81137 ret <- Event {Type : EventTypePing }
82138 for {
83139 select {
140+ case <- ctx .Done ():
141+ return
84142 case <- ticker .C :
85143 ret <- Event {Type : EventTypePing }
86144 case t := <- ts :
87- if t == "" {
88- return
89- }
90- ret <- Event {Type : EventTypeMPD , Data : t }
91- case <- ctx .Done ():
92- return
145+ ret <- t
93146 }
94147 }
95148 }()
0 commit comments