Skip to content

Commit bacf2c2

Browse files
author
Datanoise
committed
feat: per-instance MPD servers and accurate song progress tracking
1 parent 2bcff70 commit bacf2c2

4 files changed

Lines changed: 70 additions & 69 deletions

File tree

config/config.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,17 @@ type WebhookConfig struct {
4747
}
4848

4949
type AutoDJConfig struct {
50-
Name string `json:"name"`
51-
Mount string `json:"mount"`
52-
MusicDir string `json:"music_dir"`
50+
Name string `json:"name"`
51+
Mount string `json:"mount"`
52+
MusicDir string `json:"music_dir"`
5353
Format string `json:"format"` // "mp3" or "opus"
5454
Bitrate int `json:"bitrate"`
5555
Enabled bool `json:"enabled"`
5656
Loop bool `json:"loop"`
5757
InjectMetadata bool `json:"inject_metadata"`
5858
Playlist []string `json:"playlist"`
59+
MPDEnabled bool `json:"mpd_enabled"`
60+
MPDPort string `json:"mpd_port"`
5961
}
6062

6163
type Config struct {
@@ -103,10 +105,7 @@ type Config struct {
103105
DirectoryServer string `json:"directory_server"`
104106

105107
// Internal Streamer (AutoDJ)
106-
MPDEnabled bool `json:"mpd_enabled"`
107-
MPDPort string `json:"mpd_port"`
108-
MusicDir string `json:"music_dir"`
109-
AutoDJs []*AutoDJConfig `json:"autodjs"`
108+
AutoDJs []*AutoDJConfig `json:"autodjs"`
110109

111110
// Multi-tenant
112111
Users map[string]*User `json:"users"`

relay/streamer.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ type Streamer struct {
4242
mu sync.RWMutex
4343

4444
// Stats
45-
BytesStreamed int64
46-
CurrentFile string
47-
CurrentFileTime time.Time
45+
BytesStreamed int64
46+
CurrentFile string
47+
CurrentFileTime time.Time
48+
CurrentFileDuration time.Duration
49+
MPDServer *MPDServer
4850
}
4951

5052
type StreamerManager struct {
@@ -185,7 +187,7 @@ func (s *Streamer) RemoveFromPlaylist(idx int) {
185187
}
186188
}
187189

188-
func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool, format string, bitrate int, injectMetadata bool, initialPlaylist []string) (*Streamer, error) {
190+
func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool, format string, bitrate int, injectMetadata bool, initialPlaylist []string, mpdEnabled bool, mpdPort string) (*Streamer, error) {
189191
sm.mu.Lock()
190192
defer sm.mu.Unlock()
191193

@@ -207,6 +209,16 @@ func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool
207209
relay: sm.relay,
208210
cancel: cancel,
209211
}
212+
213+
if mpdEnabled && mpdPort != "" {
214+
s.MPDServer = NewMPDServer(mpdPort, s)
215+
if err := s.MPDServer.Start(); err != nil {
216+
logrus.WithError(err).Errorf("Failed to start MPD server for AutoDJ %s", name)
217+
} else {
218+
logrus.Infof("MPD Server for %s listening on port %s", name, mpdPort)
219+
}
220+
}
221+
210222
sm.instances[mount] = s
211223

212224
go sm.runStreamerLoop(ctx, s)
@@ -218,6 +230,9 @@ func (sm *StreamerManager) StopStreamer(mount string) {
218230
defer sm.mu.Unlock()
219231

220232
if s, ok := sm.instances[mount]; ok {
233+
if s.MPDServer != nil {
234+
s.MPDServer.Stop()
235+
}
221236
s.Stop()
222237
delete(sm.instances, mount)
223238
}
@@ -327,6 +342,7 @@ func (sm *StreamerManager) streamFile(ctx context.Context, s *Streamer, path str
327342
s.mu.Lock()
328343
s.CurrentFile = filepath.Base(path)
329344
s.CurrentFileTime = time.Now()
345+
s.CurrentFileDuration = time.Duration(decoder.Length()) * time.Second / (44100 * 2 * 2) // Rough estimate for 44.1kHz 16bit stereo
330346
s.mu.Unlock()
331347

332348
// Update stream metadata

server/server.go

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ func (s *Server) Start() error {
395395
// Start configured AutoDJs
396396
for _, adj := range s.Config.AutoDJs {
397397
if adj.Enabled {
398-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist)
398+
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)
399399
if err != nil {
400400
logrus.WithError(err).Errorf("Failed to start AutoDJ %s", adj.Name)
401401
} else {
@@ -407,29 +407,6 @@ func (s *Server) Start() error {
407407
}
408408
}
409409

410-
// Legacy/Default AutoDJ (backward compatibility)
411-
if s.Config.MusicDir != "" && s.StreamerM.GetStreamer("/autodj") == nil {
412-
// Start a default streamer for /autodj
413-
streamer, err := s.StreamerM.StartStreamer("AutoDJ", "/autodj", s.Config.MusicDir, true, "mp3", 128, true, nil)
414-
if err == nil {
415-
if s.Config.MPDEnabled {
416-
port := s.Config.MPDPort
417-
if port == "" {
418-
port = "6600"
419-
}
420-
s.mpdServer = relay.NewMPDServer(port, streamer)
421-
if err := s.mpdServer.Start(); err != nil {
422-
logrus.WithError(err).Error("Failed to start MPD server")
423-
} else {
424-
logrus.Infof("MPD Server listening on port %s", port)
425-
}
426-
}
427-
streamer.ScanMusicDir()
428-
} else {
429-
logrus.WithError(err).Error("Failed to start default streamer")
430-
}
431-
}
432-
433410
if !s.Config.UseHTTPS {
434411
logrus.Infof("Starting TinyIce on %s (HTTP)", addr)
435412
srv := &http.Server{
@@ -2180,6 +2157,8 @@ func (s *Server) handleAddAutoDJ(w http.ResponseWriter, r *http.Request) {
21802157
bitrateStr := r.FormValue("bitrate")
21812158
loop := r.FormValue("loop") == "on"
21822159
injectMetadata := r.FormValue("inject_metadata") == "on"
2160+
mpdEnabled := r.FormValue("mpd_enabled") == "on"
2161+
mpdPort := r.FormValue("mpd_port")
21832162

21842163
if name == "" || mount == "" || musicDir == "" {
21852164
http.Error(w, "Name, mount, and music directory are required", http.StatusBadRequest)
@@ -2207,13 +2186,15 @@ func (s *Server) handleAddAutoDJ(w http.ResponseWriter, r *http.Request) {
22072186
Enabled: true,
22082187
Loop: loop,
22092188
InjectMetadata: injectMetadata,
2189+
MPDEnabled: mpdEnabled,
2190+
MPDPort: mpdPort,
22102191
}
22112192

22122193
s.Config.AutoDJs = append(s.Config.AutoDJs, adj)
22132194
s.Config.SaveConfig()
22142195

22152196
// Start it immediately
2216-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, nil)
2197+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, nil, adj.MPDEnabled, adj.MPDPort)
22172198
if err == nil {
22182199
streamer.ScanMusicDir()
22192200
streamer.Play()
@@ -2261,7 +2242,7 @@ func (s *Server) handleToggleAutoDJ(w http.ResponseWriter, r *http.Request) {
22612242
if adj.Mount == mount {
22622243
adj.Enabled = !adj.Enabled
22632244
if adj.Enabled {
2264-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist)
2245+
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)
22652246
if err == nil {
22662247
if len(adj.Playlist) == 0 {
22672248
streamer.ScanMusicDir()
@@ -2277,19 +2258,10 @@ func (s *Server) handleToggleAutoDJ(w http.ResponseWriter, r *http.Request) {
22772258
}
22782259

22792260
if !found {
2280-
// Might be a legacy or manual streamer
2261+
// Might be a manual streamer
22812262
streamer := s.StreamerM.GetStreamer(mount)
22822263
if streamer != nil {
22832264
s.StreamerM.StopStreamer(mount)
2284-
} else {
2285-
// If it was the legacy one, we can try to restart it
2286-
if mount == "/autodj" && s.Config.MusicDir != "" {
2287-
streamer, err := s.StreamerM.StartStreamer("AutoDJ", "/autodj", s.Config.MusicDir, true, "mp3", 128, true, nil)
2288-
if err == nil {
2289-
streamer.ScanMusicDir()
2290-
streamer.Play()
2291-
}
2292-
}
22932265
}
22942266
}
22952267

server/templates/admin.html

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,16 @@ <h2>Provision AutoDJ</h2>
372372
<input type="checkbox" name="inject_metadata" id="adj-metadata" checked style="width: auto;">
373373
<label for="adj-metadata" style="margin-bottom: 0;">Inject Metadata (ICY)</label>
374374
</div>
375-
<button type="submit" class="btn btn-primary" style="width: 100%;">Create AutoDJ</button>
375+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center;">
376+
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0;">
377+
<input type="checkbox" name="mpd_enabled" id="adj-mpd" style="width: auto;">
378+
<label for="adj-mpd" style="margin-bottom: 0;">Expose MPD</label>
379+
</div>
380+
<div class="form-group" style="margin-bottom: 0;">
381+
<input type="text" name="mpd_port" placeholder="Port (e.g. 6600)" style="padding: 0.4rem;">
382+
</div>
383+
</div>
384+
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1.5rem;">Create AutoDJ</button>
376385
</form>
377386
</div>
378387
</section>
@@ -390,12 +399,12 @@ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
390399
<div class="dot"></div> {{if eq .State 1}}LIVE{{else}}IDLE{{end}}
391400
</span>
392401
</h3>
393-
<div style="font-size: 0.8rem; color: var(--text-dim); margin-top: 0.2rem;">
394-
Mount: <span class="mount-code">{{.OutputMount}}</span> |
395-
Format: <span style="font-weight: bold; color: var(--primary);">{{.Format}} {{.Bitrate}}K</span> |
396-
Dir: <span style="font-family: monospace; font-size: 0.75rem;">{{.MusicDir}}</span>
397-
</div>
398-
</div>
402+
<div style="font-size: 0.8rem; color: var(--text-dim); margin-top: 0.2rem;">
403+
Mount: <span class="mount-code">{{.OutputMount}}</span> |
404+
Format: <span style="font-weight: bold; color: var(--primary);">{{.Format}} {{.Bitrate}}K</span> |
405+
{{if .MPDServer}}MPD: <span style="color: var(--success); font-weight: bold;">PORT {{.MPDServer.Port}}</span> |{{end}}
406+
Dir: <span style="font-family: monospace; font-size: 0.75rem;">{{.MusicDir}}</span>
407+
</div> </div>
399408
<div style="display: flex; gap: 0.5rem;">
400409
<form action="/admin/player/shuffle" method="POST">
401410
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
@@ -433,14 +442,13 @@ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
433442
<div class="song-title" style="font-size: 1rem; color: var(--text); margin-bottom: 0.75rem;">
434443
{{if .CurrentFile}}{{.CurrentFile}}{{else}}Preparing...{{end}}
435444
</div>
436-
<div style="height: 4px; background: var(--bg); border-radius: 2px; overflow: hidden; position: relative;">
437-
<div class="progress-bar-adj" data-start="{{.CurrentFileTime.Unix}}" style="height: 100%; background: var(--primary); width: 0%; transition: width 1s linear;"></div>
438-
</div>
439-
<div style="display: flex; justify-content: space-between; margin-top: 0.4rem; font-size: 0.7rem; color: var(--text-dim); font-family: monospace;">
440-
<span class="progress-time-current">00:00</span>
441-
<span class="progress-time-total">--:--</span>
442-
</div>
443-
</div>
445+
<div style="height: 4px; background: var(--bg); border-radius: 2px; overflow: hidden; position: relative;">
446+
<div class="progress-bar-adj" data-start="{{.CurrentFileTime.Unix}}" data-duration="{{.CurrentFileDuration.Seconds}}" style="height: 100%; background: var(--primary); width: 0%; transition: width 1s linear;"></div>
447+
</div>
448+
<div style="display: flex; justify-content: space-between; margin-top: 0.4rem; font-size: 0.7rem; color: var(--text-dim); font-family: monospace;">
449+
<span class="progress-time-current">00:00</span>
450+
<span class="progress-time-total" id="duration-{{.OutputMount}}">00:00</span>
451+
</div> </div>
444452
{{end}}
445453

446454
<div style="margin-top: 1.5rem;">
@@ -581,21 +589,27 @@ <h2>Top Connection Scanners (404s)</h2>
581589

582590
bars.forEach(bar => {
583591
const startTime = parseInt(bar.dataset.start);
584-
if (!startTime) return;
585-
586-
const elapsed = now - startTime;
587-
const card = bar.closest('.card');
592+
const duration = parseFloat(bar.dataset.duration);
593+
if (!startTime || !duration) return;
588594

589-
// For now, since we don't have exact duration from the backend easily
590-
// we'll use a placeholder logic or just show elapsed time.
591-
// In a real scenario, we'd fetch the current song duration.
595+
const elapsed = Math.max(0, now - startTime);
596+
const progress = Math.min(100, (elapsed / duration) * 100);
597+
bar.style.width = progress + '%';
592598

599+
const card = bar.closest('.card');
593600
const currentEl = card.querySelector('.progress-time-current');
601+
const totalEl = card.querySelector('.progress-time-total');
602+
594603
if (currentEl) {
595604
const m = Math.floor(elapsed / 60).toString().padStart(2, '0');
596605
const s = Math.floor(elapsed % 60).toString().padStart(2, '0');
597606
currentEl.textContent = `${m}:${s}`;
598607
}
608+
if (totalEl) {
609+
const m = Math.floor(duration / 60).toString().padStart(2, '0');
610+
const s = Math.floor(Math.floor(duration) % 60).toString().padStart(2, '0');
611+
totalEl.textContent = `${m}:${s}`;
612+
}
599613
});
600614
}
601615
setInterval(updateAutoDJProgress, 1000);

0 commit comments

Comments
 (0)