Skip to content

Commit 9a5eec5

Browse files
author
Datanoise
committed
feat: MPD password protection and support for more MPD commands (next, pause)
1 parent 4982334 commit 9a5eec5

5 files changed

Lines changed: 101 additions & 59 deletions

File tree

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type AutoDJConfig struct {
5858
Playlist []string `json:"playlist"`
5959
MPDEnabled bool `json:"mpd_enabled"`
6060
MPDPort string `json:"mpd_port"`
61+
MPDPassword string `json:"mpd_password"`
6162
}
6263

6364
type Config struct {

relay/mpd.go

Lines changed: 78 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import (
1212

1313
type MPDServer struct {
1414
Port string
15+
Password string
1516
streamer *Streamer
1617
listener net.Listener
1718
}
1819

19-
func NewMPDServer(port string, s *Streamer) *MPDServer {
20+
func NewMPDServer(port, password string, s *Streamer) *MPDServer {
2021
return &MPDServer{
2122
Port: port,
23+
Password: password,
2224
streamer: s,
2325
}
2426
}
@@ -59,57 +61,83 @@ func (m *MPDServer) handleConnection(conn net.Conn) {
5961
writer := bufio.NewWriter(conn)
6062
reader := bufio.NewReader(conn)
6163

62-
// Greeting
63-
writer.WriteString("OK MPD 0.23.5\n")
64-
writer.Flush()
65-
66-
for {
67-
line, err := reader.ReadString('\n')
68-
if err != nil {
69-
return
70-
}
71-
line = strings.TrimSpace(line)
72-
if line == "" {
73-
continue
74-
}
75-
76-
cmd := strings.Split(line, " ")[0]
77-
// args := strings.TrimPrefix(line, cmd+" ")
78-
79-
switch cmd { case "status":
80-
m.handleStatus(writer)
81-
case "currentsong":
82-
m.handleCurrentSong(writer)
83-
case "play":
84-
m.streamer.Play()
85-
writer.WriteString("OK\n")
86-
case "stop":
87-
m.streamer.Stop()
88-
writer.WriteString("OK\n")
89-
case "ping":
90-
writer.WriteString("OK\n")
91-
case "close":
92-
return
93-
case "listall", "listallinfo":
94-
m.handleListAll(writer)
95-
case "listplaylists":
96-
writer.WriteString("OK\n")
97-
case "lsinfo":
98-
m.handleListAll(writer)
99-
case "outputs":
100-
fmt.Fprintf(writer, "outputid: 0\noutputname: %s\noutputenabled: 1\n", m.streamer.OutputMount)
101-
writer.WriteString("OK\n")
102-
case "config":
103-
writer.WriteString("OK\n")
104-
case "stats":
105-
fmt.Fprintf(writer, "uptime: 0\nplaytime: 0\nartists: 0\nalbums: 0\nsongs: %d\n", len(m.streamer.Playlist))
64+
// Greeting
65+
writer.WriteString("OK MPD 0.23.5\n")
66+
writer.Flush()
67+
68+
authenticated := m.Password == ""
69+
70+
for {
71+
line, err := reader.ReadString('\n')
72+
if err != nil {
73+
return
74+
}
75+
line = strings.TrimSpace(line)
76+
if line == "" {
77+
continue
78+
}
79+
80+
cmd := strings.Split(line, " ")[0]
81+
args := strings.TrimPrefix(line, cmd+" ")
82+
83+
if !authenticated && cmd != "password" && cmd != "close" {
84+
writer.WriteString("ACK [5@0] {} permission denied\n")
85+
writer.Flush()
86+
continue
87+
}
88+
89+
switch cmd {
90+
case "password":
91+
if args == m.Password {
92+
authenticated = true
10693
writer.WriteString("OK\n")
107-
default:
108-
logrus.Debugf("MPD: Unknown command: %s", cmd)
109-
writer.WriteString("OK\n") // Be lenient for now
94+
} else {
95+
writer.WriteString("ACK [3@0] {password} incorrect password\n")
11096
}
111-
writer.Flush()
112-
}}
97+
case "status":
98+
m.handleStatus(writer)
99+
case "currentsong":
100+
m.handleCurrentSong(writer)
101+
case "play":
102+
m.streamer.Play()
103+
writer.WriteString("OK\n")
104+
case "stop":
105+
m.streamer.Stop()
106+
writer.WriteString("OK\n")
107+
case "pause":
108+
m.streamer.TogglePlay()
109+
writer.WriteString("OK\n")
110+
case "next":
111+
m.streamer.Next()
112+
writer.WriteString("OK\n")
113+
case "previous":
114+
// We don't have previous yet, but let's be polite
115+
writer.WriteString("OK\n")
116+
case "ping":
117+
writer.WriteString("OK\n")
118+
case "close":
119+
return
120+
case "listall", "listallinfo":
121+
m.handleListAll(writer)
122+
case "listplaylists":
123+
writer.WriteString("OK\n")
124+
case "lsinfo":
125+
m.handleListAll(writer)
126+
case "outputs":
127+
fmt.Fprintf(writer, "outputid: 0\noutputname: %s\noutputenabled: 1\n", m.streamer.OutputMount)
128+
writer.WriteString("OK\n")
129+
case "config":
130+
writer.WriteString("OK\n")
131+
case "stats":
132+
fmt.Fprintf(writer, "uptime: 0\nplaytime: 0\nartists: 0\nalbums: 0\nsongs: %d\n", len(m.streamer.Playlist))
133+
writer.WriteString("OK\n")
134+
default:
135+
logrus.Debugf("MPD: Unknown command: %s", cmd)
136+
writer.WriteString("OK\n") // Be lenient for now
137+
}
138+
writer.Flush()
139+
}
140+
}
113141

114142
func (m *MPDServer) handleStatus(w *bufio.Writer) {
115143
state := "stop"

relay/streamer.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Streamer struct {
3737
Loop bool
3838
Shuffle bool
3939
InjectMetadata bool
40+
MPDPassword string
4041

4142
relay *Relay
4243
cancel context.CancelFunc
@@ -200,7 +201,7 @@ func (s *Streamer) RemoveFromPlaylist(idx int) {
200201
}
201202
}
202203

203-
func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool, format string, bitrate int, injectMetadata bool, initialPlaylist []string, mpdEnabled bool, mpdPort string) (*Streamer, error) {
204+
func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool, format string, bitrate int, injectMetadata bool, initialPlaylist []string, mpdEnabled bool, mpdPort, mpdPassword string) (*Streamer, error) {
204205
sm.mu.Lock()
205206
defer sm.mu.Unlock()
206207

@@ -219,12 +220,13 @@ func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool
219220
State: StateStopped,
220221
Loop: loop,
221222
InjectMetadata: injectMetadata,
223+
MPDPassword: mpdPassword,
222224
relay: sm.relay,
223225
cancel: cancel,
224226
}
225227

226228
if mpdEnabled && mpdPort != "" {
227-
s.MPDServer = NewMPDServer(mpdPort, s)
229+
s.MPDServer = NewMPDServer(mpdPort, mpdPassword, s)
228230
if err := s.MPDServer.Start(); err != nil {
229231
logrus.WithError(err).Errorf("Failed to start MPD server for AutoDJ %s", name)
230232
} else {

server/server.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ func (s *Server) Start() error {
397397
// Start configured AutoDJs
398398
for _, adj := range s.Config.AutoDJs {
399399
if adj.Enabled {
400-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist, adj.MPDEnabled, adj.MPDPort)
400+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist, adj.MPDEnabled, adj.MPDPort, adj.MPDPassword)
401401
if err != nil {
402402
logrus.WithError(err).Errorf("Failed to start AutoDJ %s", adj.Name)
403403
} else {
@@ -2166,6 +2166,7 @@ func (s *Server) handleAddAutoDJ(w http.ResponseWriter, r *http.Request) {
21662166
injectMetadata := r.FormValue("inject_metadata") == "on"
21672167
mpdEnabled := r.FormValue("mpd_enabled") == "on"
21682168
mpdPort := r.FormValue("mpd_port")
2169+
mpdPassword := r.FormValue("mpd_password")
21692170

21702171
if name == "" || mount == "" || musicDir == "" {
21712172
http.Error(w, "Name, mount, and music directory are required", http.StatusBadRequest)
@@ -2195,13 +2196,14 @@ func (s *Server) handleAddAutoDJ(w http.ResponseWriter, r *http.Request) {
21952196
InjectMetadata: injectMetadata,
21962197
MPDEnabled: mpdEnabled,
21972198
MPDPort: mpdPort,
2199+
MPDPassword: mpdPassword,
21982200
}
21992201

22002202
s.Config.AutoDJs = append(s.Config.AutoDJs, adj)
22012203
s.Config.SaveConfig()
22022204

22032205
// Start it immediately
2204-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, nil, adj.MPDEnabled, adj.MPDPort)
2206+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, nil, adj.MPDEnabled, adj.MPDPort, adj.MPDPassword)
22052207
if err == nil {
22062208
if adj.InjectMetadata {
22072209
if st, ok := s.Relay.GetStream(adj.Mount); ok {
@@ -2264,7 +2266,7 @@ func (s *Server) handleToggleAutoDJ(w http.ResponseWriter, r *http.Request) {
22642266

22652267
if adj.Enabled {
22662268
if existing == nil {
2267-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist, adj.MPDEnabled, adj.MPDPort)
2269+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist, adj.MPDEnabled, adj.MPDPort, adj.MPDPassword)
22682270
if err == nil {
22692271
if adj.InjectMetadata {
22702272
if st, ok := s.Relay.GetStream(adj.Mount); ok {
@@ -2524,6 +2526,7 @@ func (s *Server) handleUpdateAutoDJ(w http.ResponseWriter, r *http.Request) {
25242526
injectMetadata := r.FormValue("inject_metadata") == "on"
25252527
mpdEnabled := r.FormValue("mpd_enabled") == "on"
25262528
mpdPort := r.FormValue("mpd_port")
2529+
mpdPassword := r.FormValue("mpd_password")
25272530

25282531
if name == "" || newMount == "" || musicDir == "" {
25292532
http.Error(w, "Name, mount, and music directory are required", http.StatusBadRequest)
@@ -2548,11 +2551,12 @@ func (s *Server) handleUpdateAutoDJ(w http.ResponseWriter, r *http.Request) {
25482551
adj.InjectMetadata = injectMetadata
25492552
adj.MPDEnabled = mpdEnabled
25502553
adj.MPDPort = mpdPort
2554+
adj.MPDPassword = mpdPassword
25512555

25522556
// Restart streamer
25532557
s.StreamerM.StopStreamer(oldMount)
25542558
if adj.Enabled {
2555-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist, adj.MPDEnabled, adj.MPDPort)
2559+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist, adj.MPDEnabled, adj.MPDPort, adj.MPDPassword)
25562560
if err == nil {
25572561
streamer.Play()
25582562
}

server/templates/admin.html

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,14 +374,17 @@ <h2>Provision AutoDJ</h2>
374374
<input type="checkbox" name="inject_metadata" id="adj-metadata" checked style="width: auto;">
375375
<label for="adj-metadata" style="margin-bottom: 0;">Inject Metadata (ICY)</label>
376376
</div>
377-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center;">
377+
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; align-items: center;">
378378
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0;">
379379
<input type="checkbox" name="mpd_enabled" id="adj-mpd" style="width: auto;">
380380
<label for="adj-mpd" style="margin-bottom: 0;">Expose MPD</label>
381381
</div>
382382
<div class="form-group" style="margin-bottom: 0;">
383383
<input type="text" name="mpd_port" placeholder="Port (e.g. 6600)" style="padding: 0.4rem;">
384384
</div>
385+
<div class="form-group" style="margin-bottom: 0;">
386+
<input type="password" name="mpd_password" placeholder="Password (Optional)" style="padding: 0.4rem;">
387+
</div>
385388
</div>
386389
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1.5rem;">Create AutoDJ</button>
387390
</form>
@@ -408,7 +411,7 @@ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
408411
Dir: <span style="font-family: monospace; font-size: 0.75rem;">{{.MusicDir}}</span>
409412
</div> </div>
410413
<div style="display: flex; gap: 0.5rem; align-items: center;">
411-
<button type="button" class="btn btn-outline btn-sm" title="Edit AutoDJ" onclick='showEditAutoDJ({ "Name": "{{.Name}}", "Mount": "{{.OutputMount}}", "MusicDir": "{{.MusicDir}}", "Format": "{{.Format}}", "Bitrate": {{.Bitrate}}, "Loop": {{.Loop}}, "InjectMetadata": {{.InjectMetadata}}, "MPDEnabled": {{if .MPDServer}}true{{else}}false{{end}}, "MPDPort": "{{if .MPDServer}}{{.MPDServer.Port}}{{end}}" })'>
414+
<button type="button" class="btn btn-outline btn-sm" title="Edit AutoDJ" onclick='showEditAutoDJ({ "Name": "{{.Name}}", "Mount": "{{.OutputMount}}", "MusicDir": "{{.MusicDir}}", "Format": "{{.Format}}", "Bitrate": {{.Bitrate}}, "Loop": {{.Loop}}, "InjectMetadata": {{.InjectMetadata}}, "MPDEnabled": {{if .MPDServer}}true{{else}}false{{end}}, "MPDPort": "{{if .MPDServer}}{{.MPDServer.Port}}{{end}}", "MPDPassword": "{{.MPDPassword}}" })'>
412415
<i data-lucide="edit-3" style="width: 14px; height: 14px;"></i>
413416
</button>
414417
<div class="btn-group" style="display: flex; background: rgba(0,0,0,0.2); border-radius: 8px; padding: 2px;">
@@ -621,14 +624,17 @@ <h2>Edit AutoDJ</h2>
621624
<input type="checkbox" name="inject_metadata" id="edit-metadata" style="width: auto;">
622625
<label for="edit-metadata" style="margin-bottom: 0;">Inject Metadata (ICY)</label>
623626
</div>
624-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center;">
627+
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; align-items: center;">
625628
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0;">
626629
<input type="checkbox" name="mpd_enabled" id="edit-mpd" style="width: auto;">
627630
<label for="edit-mpd" style="margin-bottom: 0;">Expose MPD</label>
628631
</div>
629632
<div class="form-group" style="margin-bottom: 0;">
630633
<input type="text" name="mpd_port" id="edit-mpd-port" placeholder="Port (e.g. 6600)" style="padding: 0.4rem;">
631634
</div>
635+
<div class="form-group" style="margin-bottom: 0;">
636+
<input type="password" name="mpd_password" id="edit-mpd-password" placeholder="Password (Optional)" style="padding: 0.4rem;">
637+
</div>
632638
</div>
633639
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
634640
<button type="submit" class="btn btn-primary" style="flex: 1;">Save Changes</button>
@@ -698,6 +704,7 @@ <h2>Edit AutoDJ</h2>
698704
document.getElementById('edit-metadata').checked = data.InjectMetadata;
699705
document.getElementById('edit-mpd').checked = data.MPDEnabled;
700706
document.getElementById('edit-mpd-port').value = data.MPDPort;
707+
document.getElementById('edit-mpd-password').value = data.MPDPassword || '';
701708
document.getElementById('edit-autodj-modal').style.display = 'block';
702709
}
703710

0 commit comments

Comments
 (0)