Skip to content

Commit 9a65963

Browse files
author
Datanoise
committed
feat: professional AutoDJ features (metadata injection, offline support, refined UI)
1 parent 048c32b commit 9a65963

5 files changed

Lines changed: 93 additions & 57 deletions

File tree

config/config.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ 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"`
53-
Format string `json:"format"` // "mp3" or "opus"
54-
Bitrate int `json:"bitrate"`
55-
Enabled bool `json:"enabled"`
56-
Loop bool `json:"loop"`
50+
Name string `json:"name"`
51+
Mount string `json:"mount"`
52+
MusicDir string `json:"music_dir"`
53+
Format string `json:"format"` // "mp3" or "opus"
54+
Bitrate int `json:"bitrate"`
55+
Enabled bool `json:"enabled"`
56+
Loop bool `json:"loop"`
57+
InjectMetadata bool `json:"inject_metadata"`
5758
}
5859

5960
type Config struct {

relay/streamer.go

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@ const (
2323
)
2424

2525
type Streamer struct {
26-
Name string
27-
OutputMount string
28-
MusicDir string
29-
Format string
30-
Bitrate int
31-
Playlist []string
32-
Queue []string
33-
CurrentPos int
34-
State StreamerState
35-
Loop bool
36-
Shuffle bool
26+
Name string
27+
OutputMount string
28+
MusicDir string
29+
Format string
30+
Bitrate int
31+
Playlist []string
32+
Queue []string
33+
CurrentPos int
34+
State StreamerState
35+
Loop bool
36+
Shuffle bool
37+
InjectMetadata bool
3738

3839
relay *Relay
3940
cancel context.CancelFunc
@@ -106,6 +107,26 @@ func (s *Streamer) RemoveFromQueue(index int) {
106107
s.Queue = append(s.Queue[:index], s.Queue[index+1:]...)
107108
}
108109

110+
func (s *Streamer) GetPlaylistNames() []string {
111+
s.mu.RLock()
112+
defer s.mu.RUnlock()
113+
res := make([]string, len(s.Playlist))
114+
for i, p := range s.Playlist {
115+
res[i] = filepath.Base(p)
116+
}
117+
return res
118+
}
119+
120+
func (s *Streamer) GetQueueNames() []string {
121+
s.mu.RLock()
122+
defer s.mu.RUnlock()
123+
res := make([]string, len(s.Queue))
124+
for i, p := range s.Queue {
125+
res[i] = filepath.Base(p)
126+
}
127+
return res
128+
}
129+
109130
func (s *Streamer) ScanMusicDir() error {
110131
s.mu.Lock()
111132
defer s.mu.Unlock()
@@ -128,7 +149,7 @@ func (s *Streamer) ScanMusicDir() error {
128149
return nil
129150
}
130151

131-
func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool, format string, bitrate int) (*Streamer, error) {
152+
func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool, format string, bitrate int, injectMetadata bool) (*Streamer, error) {
132153
sm.mu.Lock()
133154
defer sm.mu.Unlock()
134155

@@ -138,15 +159,16 @@ func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool
138159

139160
ctx, cancel := context.WithCancel(context.Background())
140161
s := &Streamer{
141-
Name: name,
142-
OutputMount: mount,
143-
MusicDir: musicDir,
144-
Format: format,
145-
Bitrate: bitrate,
146-
State: StateStopped,
147-
Loop: loop,
148-
relay: sm.relay,
149-
cancel: cancel,
162+
Name: name,
163+
OutputMount: mount,
164+
MusicDir: musicDir,
165+
Format: format,
166+
Bitrate: bitrate,
167+
State: StateStopped,
168+
Loop: loop,
169+
InjectMetadata: injectMetadata,
170+
relay: sm.relay,
171+
cancel: cancel,
150172
}
151173
sm.instances[mount] = s
152174

@@ -272,8 +294,10 @@ func (sm *StreamerManager) streamFile(ctx context.Context, s *Streamer, path str
272294

273295
// Update stream metadata
274296
output := sm.relay.GetOrCreateStream(s.OutputMount)
275-
output.CurrentSong = s.CurrentFile
276-
output.Name = s.Name
297+
if s.InjectMetadata {
298+
output.CurrentSong = s.CurrentFile
299+
output.Name = s.Name
300+
}
277301
output.Bitrate = fmt.Sprintf("%d", s.Bitrate)
278302

279303
if s.Format == "opus" {

server/assets/js/Sortable.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/server.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ func (s *Server) Start() error {
392392
// Start configured AutoDJs
393393
for _, adj := range s.Config.AutoDJs {
394394
if adj.Enabled {
395-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate)
395+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata)
396396
if err != nil {
397397
logrus.WithError(err).Errorf("Failed to start AutoDJ %s", adj.Name)
398398
} else {
@@ -405,7 +405,7 @@ func (s *Server) Start() error {
405405
// Legacy/Default AutoDJ (backward compatibility)
406406
if s.Config.MusicDir != "" && s.StreamerM.GetStreamer("/autodj") == nil {
407407
// Start a default streamer for /autodj
408-
streamer, err := s.StreamerM.StartStreamer("AutoDJ", "/autodj", s.Config.MusicDir, true, "mp3", 128)
408+
streamer, err := s.StreamerM.StartStreamer("AutoDJ", "/autodj", s.Config.MusicDir, true, "mp3", 128, true)
409409
if err == nil {
410410
if s.Config.MPDEnabled {
411411
port := s.Config.MPDPort
@@ -2164,6 +2164,7 @@ func (s *Server) handleAddAutoDJ(w http.ResponseWriter, r *http.Request) {
21642164
format := r.FormValue("format")
21652165
bitrateStr := r.FormValue("bitrate")
21662166
loop := r.FormValue("loop") == "on"
2167+
injectMetadata := r.FormValue("inject_metadata") == "on"
21672168

21682169
if name == "" || mount == "" || musicDir == "" {
21692170
http.Error(w, "Name, mount, and music directory are required", http.StatusBadRequest)
@@ -2183,20 +2184,21 @@ func (s *Server) handleAddAutoDJ(w http.ResponseWriter, r *http.Request) {
21832184
}
21842185

21852186
adj := &config.AutoDJConfig{
2186-
Name: name,
2187-
Mount: mount,
2188-
MusicDir: musicDir,
2189-
Format: format,
2190-
Bitrate: bitrate,
2191-
Enabled: true,
2192-
Loop: loop,
2187+
Name: name,
2188+
Mount: mount,
2189+
MusicDir: musicDir,
2190+
Format: format,
2191+
Bitrate: bitrate,
2192+
Enabled: true,
2193+
Loop: loop,
2194+
InjectMetadata: injectMetadata,
21932195
}
21942196

21952197
s.Config.AutoDJs = append(s.Config.AutoDJs, adj)
21962198
s.Config.SaveConfig()
21972199

21982200
// Start it immediately
2199-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate)
2201+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata)
22002202
if err == nil {
22012203
streamer.ScanMusicDir()
22022204
streamer.Play()
@@ -2243,7 +2245,7 @@ func (s *Server) handleToggleAutoDJ(w http.ResponseWriter, r *http.Request) {
22432245
if adj.Mount == mount {
22442246
adj.Enabled = !adj.Enabled
22452247
if adj.Enabled {
2246-
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate)
2248+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, adj.MusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata)
22472249
if err == nil {
22482250
streamer.ScanMusicDir()
22492251
streamer.Play()

server/templates/admin.html

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>TinyIce Admin</title>
77
<script src="/assets/js/lucide.min.js"></script>
8-
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
8+
<script src="/assets/js/Sortable.min.js"></script>
99
<style>
1010
:root {
1111
--bg: #0b0f1a; --surface: #161b2c; --surface-hover: #1e253a;
@@ -368,6 +368,10 @@ <h2>Provision AutoDJ</h2>
368368
<input type="checkbox" name="loop" id="adj-loop" checked style="width: auto;">
369369
<label for="adj-loop" style="margin-bottom: 0;">Loop Playlist</label>
370370
</div>
371+
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem;">
372+
<input type="checkbox" name="inject_metadata" id="adj-metadata" checked style="width: auto;">
373+
<label for="adj-metadata" style="margin-bottom: 0;">Inject Metadata (ICY)</label>
374+
</div>
371375
<button type="submit" class="btn btn-primary" style="width: 100%;">Create AutoDJ</button>
372376
</form>
373377
</div>
@@ -391,17 +395,18 @@ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
391395
Dir: <span style="font-family: monospace; font-size: 0.75rem;">{{.MusicDir}}</span>
392396
</div>
393397
</div>
394-
<div style="display: flex; gap: 0.5rem;">
395-
<form action="/admin/player/shuffle" method="POST">
396-
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
397-
<input type="hidden" name="mount" value="{{.OutputMount}}">
398-
<button type="submit" class="btn {{if .Shuffle}}btn-primary{{else}}btn-outline{{end}} btn-sm" title="Toggle Shuffle">
399-
<i data-lucide="shuffle" style="width: 14px; height: 14px;"></i>
400-
</button>
401-
</form>
402-
<a href="/player{{.OutputMount}}" target="_blank" class="btn btn-outline btn-sm"><i data-lucide="headphones" style="width: 14px; height: 14px; margin-right: 4px;"></i> MONITOR</a>
403-
<form action="/admin/autodj/toggle" method="POST">
404-
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
398+
<div style="display: flex; gap: 0.5rem;">
399+
<form action="/admin/player/shuffle" method="POST">
400+
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
401+
<input type="hidden" name="mount" value="{{.OutputMount}}">
402+
<button type="submit" class="btn {{if .Shuffle}}btn-primary{{else}}btn-outline{{end}} btn-sm" title="Toggle Shuffle">
403+
<i data-lucide="shuffle" style="width: 14px; height: 14px;"></i>
404+
</button>
405+
</form>
406+
{{if eq .State 1}}
407+
<a href="/player{{.OutputMount}}" target="_blank" class="btn btn-outline btn-sm"><i data-lucide="headphones" style="width: 14px; height: 14px; margin-right: 4px;"></i> MONITOR</a>
408+
{{end}}
409+
<form action="/admin/autodj/toggle" method="POST"> <input type="hidden" name="csrf" value="{{$.CSRFToken}}">
405410
<input type="hidden" name="mount" value="{{.OutputMount}}">
406411
<button type="submit" class="btn {{if eq .State 1}}btn-warning{{else}}btn-primary{{end}} btn-sm">
407412
{{if eq .State 1}}STOP{{else}}START{{end}}
@@ -441,7 +446,7 @@ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
441446
<div style="margin-bottom: 1rem;">
442447
<div class="label" style="font-size: 0.65rem; color: var(--primary); margin-bottom: 0.5rem; letter-spacing: 1px;">UP NEXT (QUEUE)</div>
443448
<div style="display: flex; flex-direction: column; gap: 0.4rem;">
444-
{{range $qidx, $qfile := .Queue}}
449+
{{range $qidx, $qfile := .GetQueueNames}}
445450
<div style="display: flex; justify-content: space-between; align-items: center; background: rgba(56, 189, 248, 0.05); padding: 0.5rem; border-radius: 4px; border: 1px dashed var(--primary-glow);">
446451
<span style="font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{$qfile}}</span>
447452
<form action="/admin/player/queue" method="POST">
@@ -460,18 +465,20 @@ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
460465
<div class="label" style="font-size: 0.65rem; color: var(--text-dim); margin-bottom: 0.5rem; letter-spacing: 1px;">PLAYLIST (DRAG TO REORDER)</div>
461466
<div class="playlist-sortable" data-mount="{{.OutputMount}}" style="max-height: 250px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px;">
462467
{{$current := .CurrentPos}}
463-
{{range $idx, $file := .Playlist}}
468+
{{range $idx, $file := .GetPlaylistNames}}
464469
<div class="playlist-item {{if and (not $.Shuffle) (eq $idx $current)}}active{{end}}" data-index="{{$idx}}" style="display: flex; justify-content: space-between; align-items: center; background: var(--bg); padding: 0.6rem; border-radius: 4px; cursor: grab; border: 1px solid transparent;">
465470
<div style="display: flex; align-items: center; gap: 0.75rem; overflow: hidden;">
466471
<i data-lucide="grip-vertical" style="width: 14px; height: 14px; color: var(--border);"></i>
467-
<span style="font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{$file}}</span>
472+
<span style="font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="{{$file}}">
473+
{{$file}}
474+
</span>
468475
</div>
469476
<div style="display: flex; gap: 0.5rem;">
470477
<form action="/admin/player/queue" method="POST">
471478
<input type="hidden" name="csrf" value="{{$.CSRFToken}}">
472479
<input type="hidden" name="mount" value="{{$.OutputMount}}">
473480
<input type="hidden" name="action" value="add">
474-
<input type="hidden" name="path" value="{{$file}}">
481+
<input type="hidden" name="path" value="{{index $.Playlist $idx}}">
475482
<button type="submit" class="btn btn-outline btn-sm" style="padding: 0.2rem 0.4rem;" title="Add to Queue"><i data-lucide="list-plus" style="width: 14px; height: 14px;"></i></button>
476483
</form>
477484
</div>

0 commit comments

Comments
 (0)