Skip to content

Commit 54a3ef3

Browse files
author
Datanoise
committed
feat: support multiple AutoDJ instances with monitoring and progress tracking
1 parent 201efcd commit 54a3ef3

4 files changed

Lines changed: 265 additions & 68 deletions

File tree

config/config.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ type WebhookConfig struct {
4646
Enabled bool `json:"enabled"`
4747
}
4848

49+
type AutoDJConfig struct {
50+
Name string `json:"name"`
51+
Mount string `json:"mount"`
52+
MusicDir string `json:"music_dir"`
53+
Enabled bool `json:"enabled"`
54+
Loop bool `json:"loop"`
55+
}
56+
4957
type Config struct {
5058
BindHost string `json:"bind_host"`
5159
Port string `json:"port"`
@@ -91,9 +99,10 @@ type Config struct {
9199
DirectoryServer string `json:"directory_server"`
92100

93101
// Internal Streamer (AutoDJ)
94-
MPDEnabled bool `json:"mpd_enabled"`
95-
MPDPort string `json:"mpd_port"`
96-
MusicDir string `json:"music_dir"`
102+
MPDEnabled bool `json:"mpd_enabled"`
103+
MPDPort string `json:"mpd_port"`
104+
MusicDir string `json:"music_dir"`
105+
AutoDJs []*AutoDJConfig `json:"autodjs"`
97106

98107
// Multi-tenant
99108
Users map[string]*User `json:"users"`
@@ -193,6 +202,9 @@ func (config *Config) initMapsAndArrays() {
193202
if config.BannedIPs == nil {
194203
config.BannedIPs = make([]string, 0)
195204
}
205+
if config.AutoDJs == nil {
206+
config.AutoDJs = make([]*AutoDJConfig, 0)
207+
}
196208
}
197209

198210
func (config *Config) handleMigrations() {

relay/streamer.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ type Streamer struct {
2929
Playlist []string
3030
CurrentPos int
3131
State StreamerState
32-
33-
relay *Relay
34-
cancel context.CancelFunc
35-
mu sync.RWMutex
32+
Loop bool
33+
34+
relay *Relay
35+
cancel context.CancelFunc
36+
mu sync.RWMutex
3637

3738
// Stats
38-
BytesStreamed int64
39-
CurrentFile string
39+
BytesStreamed int64
40+
CurrentFile string
41+
CurrentFileTime time.Time
4042
}
4143

4244
type StreamerManager struct {
@@ -101,7 +103,7 @@ func (s *Streamer) ScanMusicDir() error {
101103
return nil
102104
}
103105

104-
func (sm *StreamerManager) StartStreamer(name, mount, musicDir string) (*Streamer, error) {
106+
func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool) (*Streamer, error) {
105107
sm.mu.Lock()
106108
defer sm.mu.Unlock()
107109

@@ -115,6 +117,7 @@ func (sm *StreamerManager) StartStreamer(name, mount, musicDir string) (*Streame
115117
OutputMount: mount,
116118
MusicDir: musicDir,
117119
State: StateStopped,
120+
Loop: loop,
118121
relay: sm.relay,
119122
cancel: cancel,
120123
}
@@ -124,6 +127,16 @@ func (sm *StreamerManager) StartStreamer(name, mount, musicDir string) (*Streame
124127
return s, nil
125128
}
126129

130+
func (sm *StreamerManager) StopStreamer(mount string) {
131+
sm.mu.Lock()
132+
defer sm.mu.Unlock()
133+
134+
if s, ok := sm.instances[mount]; ok {
135+
s.Stop()
136+
delete(sm.instances, mount)
137+
}
138+
}
139+
127140
func (sm *StreamerManager) GetStreamers() []*Streamer {
128141
sm.mu.RLock()
129142
defer sm.mu.RUnlock()
@@ -142,7 +155,7 @@ func (sm *StreamerManager) GetStreamer(mount string) *Streamer {
142155

143156
func (sm *StreamerManager) runStreamerLoop(ctx context.Context, s *Streamer) {
144157
logrus.Infof("Streamer %s starting for mount %s", s.Name, s.OutputMount)
145-
158+
146159
for {
147160
select {
148161
case <-ctx.Done():
@@ -159,9 +172,15 @@ func (sm *StreamerManager) runStreamerLoop(ctx context.Context, s *Streamer) {
159172
time.Sleep(1 * time.Second)
160173
continue
161174
}
162-
175+
163176
if s.CurrentPos >= len(s.Playlist) {
164-
s.CurrentPos = 0
177+
if s.Loop {
178+
s.CurrentPos = 0
179+
} else {
180+
s.State = StateStopped
181+
s.mu.RUnlock()
182+
continue
183+
}
165184
}
166185
filePath := s.Playlist[s.CurrentPos]
167186
s.mu.RUnlock()
@@ -188,6 +207,7 @@ func (sm *StreamerManager) streamFile(ctx context.Context, s *Streamer, path str
188207

189208
s.mu.Lock()
190209
s.CurrentFile = filepath.Base(path)
210+
s.CurrentFileTime = time.Now()
191211
s.mu.Unlock()
192212

193213
// Update stream metadata
@@ -198,7 +218,7 @@ func (sm *StreamerManager) streamFile(ctx context.Context, s *Streamer, path str
198218
// For now, we only support MP3 files and we assume they are compatible
199219
// with our output settings.
200220
// TODO: Add proper decoding/encoding if formats mismatch.
201-
221+
202222
buf := make([]byte, 8192)
203223
for {
204224
select {
@@ -213,7 +233,7 @@ func (sm *StreamerManager) streamFile(ctx context.Context, s *Streamer, path str
213233
if n > 0 {
214234
output.Broadcast(buf[:n], sm.relay)
215235
atomic.AddInt64(&s.BytesStreamed, int64(n))
216-
236+
217237
// Basic pacing for MP3 (very rough)
218238
// We should ideally use a proper pacer or decode to PCM to pace.
219239
// For now, let's just assume 128kbps for pacing estimation.

server/server.go

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ func (s *Server) setupRoutes() *http.ServeMux {
263263
mux.HandleFunc("/admin/delete-webhook", s.handleDeleteWebhook)
264264
mux.HandleFunc("/admin/player/toggle", s.handlePlayerToggle)
265265
mux.HandleFunc("/admin/player/scan", s.handlePlayerScan)
266+
mux.HandleFunc("/admin/autodj/add", s.handleAddAutoDJ)
267+
mux.HandleFunc("/admin/autodj/delete", s.handleDeleteAutoDJ)
268+
mux.HandleFunc("/admin/autodj/toggle", s.handleToggleAutoDJ)
266269
mux.HandleFunc("/admin/add-relay", s.handleAddRelay)
267270
mux.HandleFunc("/admin/toggle-relay", s.handleToggleRelay)
268271
mux.HandleFunc("/admin/restart-relay", s.handleRestartRelay)
@@ -383,9 +386,23 @@ func (s *Server) Start() error {
383386
}
384387
}
385388

386-
if s.Config.MusicDir != "" {
389+
// Start configured AutoDJs
390+
for _, adj := range s.Config.AutoDJs {
391+
if adj.Enabled {
392+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop)
393+
if err != nil {
394+
logrus.WithError(err).Errorf("Failed to start AutoDJ %s", adj.Name)
395+
} else {
396+
logrus.Infof("AutoDJ %s started on %s", adj.Name, adj.Mount)
397+
streamer.ScanMusicDir() // Initial scan
398+
}
399+
}
400+
}
401+
402+
// Legacy/Default AutoDJ (backward compatibility)
403+
if s.Config.MusicDir != "" && s.StreamerM.GetStreamer("/autodj") == nil {
387404
// Start a default streamer for /autodj
388-
streamer, err := s.StreamerM.StartStreamer("AutoDJ", "/autodj", s.Config.MusicDir)
405+
streamer, err := s.StreamerM.StartStreamer("AutoDJ", "/autodj", s.Config.MusicDir, true)
389406
if err == nil {
390407
if s.Config.MPDEnabled {
391408
port := s.Config.MPDPort
@@ -399,6 +416,7 @@ func (s *Server) Start() error {
399416
logrus.Infof("MPD Server listening on port %s", port)
400417
}
401418
}
419+
streamer.ScanMusicDir()
402420
} else {
403421
logrus.WithError(err).Error("Failed to start default streamer")
404422
}
@@ -2128,6 +2146,104 @@ func (s *Server) handlePlayerScan(w http.ResponseWriter, r *http.Request) {
21282146
http.Redirect(w, r, "/admin#tab-streamer", http.StatusSeeOther)
21292147
}
21302148

2149+
func (s *Server) handleAddAutoDJ(w http.ResponseWriter, r *http.Request) {
2150+
if !s.isCSRFSafe(r) {
2151+
http.Error(w, "Forbidden", http.StatusForbidden)
2152+
return
2153+
}
2154+
if _, ok := s.checkAuth(r); !ok {
2155+
return
2156+
}
2157+
2158+
name := r.FormValue("name")
2159+
mount := r.FormValue("mount")
2160+
musicDir := r.FormValue("music_dir")
2161+
loop := r.FormValue("loop") == "on"
2162+
2163+
if name == "" || mount == "" || musicDir == "" {
2164+
http.Error(w, "Name, mount, and music directory are required", http.StatusBadRequest)
2165+
return
2166+
}
2167+
2168+
if mount[0] != '/' {
2169+
mount = "/" + mount
2170+
}
2171+
2172+
adj := &config.AutoDJConfig{
2173+
Name: name,
2174+
Mount: mount,
2175+
MusicDir: musicDir,
2176+
Enabled: true,
2177+
Loop: loop,
2178+
}
2179+
2180+
s.Config.AutoDJs = append(s.Config.AutoDJs, adj)
2181+
s.Config.SaveConfig()
2182+
2183+
// Start it immediately
2184+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop)
2185+
if err == nil {
2186+
streamer.ScanMusicDir()
2187+
streamer.Play()
2188+
}
2189+
2190+
http.Redirect(w, r, "/admin#tab-streamer", http.StatusSeeOther)
2191+
}
2192+
2193+
func (s *Server) handleDeleteAutoDJ(w http.ResponseWriter, r *http.Request) {
2194+
if !s.isCSRFSafe(r) {
2195+
http.Error(w, "Forbidden", http.StatusForbidden)
2196+
return
2197+
}
2198+
if _, ok := s.checkAuth(r); !ok {
2199+
return
2200+
}
2201+
2202+
mount := r.FormValue("mount")
2203+
newADJs := []*config.AutoDJConfig{}
2204+
for _, adj := range s.Config.AutoDJs {
2205+
if adj.Mount != mount {
2206+
newADJs = append(newADJs, adj)
2207+
} else {
2208+
s.StreamerM.StopStreamer(mount)
2209+
}
2210+
}
2211+
s.Config.AutoDJs = newADJs
2212+
s.Config.SaveConfig()
2213+
2214+
http.Redirect(w, r, "/admin#tab-streamer", http.StatusSeeOther)
2215+
}
2216+
2217+
func (s *Server) handleToggleAutoDJ(w http.ResponseWriter, r *http.Request) {
2218+
if !s.isCSRFSafe(r) {
2219+
http.Error(w, "Forbidden", http.StatusForbidden)
2220+
return
2221+
}
2222+
if _, ok := s.checkAuth(r); !ok {
2223+
return
2224+
}
2225+
2226+
mount := r.FormValue("mount")
2227+
for _, adj := range s.Config.AutoDJs {
2228+
if adj.Mount == mount {
2229+
adj.Enabled = !adj.Enabled
2230+
if adj.Enabled {
2231+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop)
2232+
if err == nil {
2233+
streamer.ScanMusicDir()
2234+
streamer.Play()
2235+
}
2236+
} else {
2237+
s.StreamerM.StopStreamer(mount)
2238+
}
2239+
break
2240+
}
2241+
}
2242+
s.Config.SaveConfig()
2243+
2244+
http.Redirect(w, r, "/admin#tab-streamer", http.StatusSeeOther)
2245+
}
2246+
21312247
func (s *Server) handleGetStats(w http.ResponseWriter, r *http.Request) {
21322248
if _, ok := s.checkAuth(r); !ok {
21332249
http.Error(w, "Unauthorized", http.StatusUnauthorized)

0 commit comments

Comments
 (0)