Skip to content

Commit 82862fa

Browse files
author
Datanoise
committed
feat: AutoDJ stability, AJAX UI, and security refinements (cleanup music from history)
1 parent 296edc6 commit 82862fa

5 files changed

Lines changed: 119 additions & 38 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ tinyice.json
44
.DS_Store
55
history.db
66
MEDIUM_POST.md
7+
music/

relay/streamer.go

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,12 @@ type PlaylistItem struct {
131131

132132
func (s *Streamer) GetPlaylistInfo() []PlaylistItem {
133133
s.mu.RLock()
134-
defer s.mu.RUnlock()
135-
res := make([]PlaylistItem, len(s.Playlist))
136-
for i, p := range s.Playlist {
134+
playlist := make([]string, len(s.Playlist))
135+
copy(playlist, s.Playlist)
136+
s.mu.RUnlock()
137+
138+
res := make([]PlaylistItem, len(playlist))
139+
for i, p := range playlist {
137140
res[i] = PlaylistItem{
138141
Title: s.GetSongTitle(p),
139142
Path: p,
@@ -144,39 +147,41 @@ func (s *Streamer) GetPlaylistInfo() []PlaylistItem {
144147

145148
func (s *Streamer) GetSongTitle(path string) string {
146149
s.mu.RLock()
150+
defer s.mu.RUnlock()
147151
if title, ok := s.titleCache[path]; ok {
148-
s.mu.RUnlock()
149152
return title
150153
}
151-
s.mu.RUnlock()
152-
153-
f, err := os.Open(path)
154-
if err != nil {
155-
return filepath.Base(path)
156-
}
157-
defer f.Close()
154+
return filepath.Base(path)
155+
}
158156

157+
func (s *Streamer) fetchTitleAndCache(path string) string {
159158
title := filepath.Base(path)
160-
if m, err := tag.ReadFrom(f); err == nil {
161-
if m.Artist() != "" && m.Title() != "" {
162-
title = fmt.Sprintf("%s - %s", m.Artist(), m.Title())
163-
} else if m.Title() != "" {
164-
title = m.Title()
159+
f, err := os.Open(path)
160+
if err == nil {
161+
if m, err := tag.ReadFrom(f); err == nil {
162+
if m.Artist() != "" && m.Title() != "" {
163+
title = fmt.Sprintf("%s - %s", m.Artist(), m.Title())
164+
} else if m.Title() != "" {
165+
title = m.Title()
166+
}
165167
}
168+
f.Close()
166169
}
167170

168171
s.mu.Lock()
169172
s.titleCache[path] = title
170173
s.mu.Unlock()
171-
172174
return title
173175
}
174176

175177
func (s *Streamer) GetQueueInfo() []PlaylistItem {
176178
s.mu.RLock()
177-
defer s.mu.RUnlock()
178-
res := make([]PlaylistItem, len(s.Queue))
179-
for i, p := range s.Queue {
179+
queue := make([]string, len(s.Queue))
180+
copy(queue, s.Queue)
181+
s.mu.RUnlock()
182+
183+
res := make([]PlaylistItem, len(queue))
184+
for i, p := range queue {
180185
res[i] = PlaylistItem{
181186
Title: s.GetSongTitle(p),
182187
Path: p,
@@ -228,6 +233,17 @@ func (s *Streamer) ScanMusicDir() error {
228233
}
229234
return nil
230235
})
236+
237+
if err == nil {
238+
// Populate cache in background
239+
playlistCopy := make([]string, len(s.Playlist))
240+
copy(playlistCopy, s.Playlist)
241+
go func() {
242+
for _, p := range playlistCopy {
243+
s.fetchTitleAndCache(p)
244+
}
245+
}()
246+
}
231247
return err
232248
}
233249

@@ -322,10 +338,11 @@ func (sm *StreamerManager) StartStreamer(name, mount, musicDir string, loop bool
322338
}
323339

324340
ctx, cancel := context.WithCancel(context.Background())
341+
absMusicDir, _ := filepath.Abs(musicDir)
325342
s := &Streamer{
326343
Name: name,
327344
OutputMount: mount,
328-
MusicDir: musicDir,
345+
MusicDir: absMusicDir,
329346
Format: format,
330347
Bitrate: bitrate,
331348
Playlist: initialPlaylist,

server/server.go

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,25 @@ func (p *protocolSniffer) sniff() {
162162
reader: io.MultiReader(bytes.NewReader(buf), c),
163163
}
164164

165-
if buf[0] == 0x16 { // TLS Handshake record type
166-
p.tlsChan <- wrapped
167-
} else {
168-
p.httpChan <- wrapped
165+
target := p.httpChan
166+
if buf[0] == 0x16 {
167+
target = p.tlsChan
168+
}
169+
170+
select {
171+
case target <- wrapped:
172+
default:
173+
// Channel full, drop the oldest to make room for new (likely asset) requests
174+
select {
175+
case old := <-target:
176+
old.Close()
177+
default:
178+
}
179+
select {
180+
case target <- wrapped:
181+
default:
182+
c.Close() // Really full
183+
}
169184
}
170185
}(conn)
171186
}
@@ -341,7 +356,7 @@ func (s *Server) listenWithReuse(network, address string) (net.Listener, error)
341356
if err != nil {
342357
return nil, err
343358
}
344-
return &BannedListener{ln, s}, nil
359+
return ln, nil
345360
}
346361

347362
func (s *Server) Shutdown(ctx context.Context) error {
@@ -416,9 +431,11 @@ func (s *Server) Start() error {
416431
s.TranscoderM.StartTranscoder(tc)
417432
}
418433
}
434+
// Start configured AutoDJs
419435
for _, adj := range s.Config.AutoDJs {
420436
if adj.Enabled {
421-
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, adj.Visible)
437+
absMusicDir, _ := filepath.Abs(adj.MusicDir)
438+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, absMusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist, adj.MPDEnabled, adj.MPDPort, adj.MPDPassword, adj.Visible)
422439
if err != nil {
423440
logrus.WithError(err).Errorf("Failed to start AutoDJ %s", adj.Name)
424441
} else {
@@ -659,8 +676,8 @@ func (s *Server) startHTTPS(mux *http.ServeMux, addr string) error {
659676

660677
sniffer := &protocolSniffer{
661678
Listener: rawLn,
662-
tlsChan: make(chan net.Conn, 1024),
663-
httpChan: make(chan net.Conn, 1024),
679+
tlsChan: make(chan net.Conn, 4096),
680+
httpChan: make(chan net.Conn, 4096),
664681
}
665682
go sniffer.sniff()
666683

@@ -2354,14 +2371,16 @@ func (s *Server) handleAddAutoDJ(w http.ResponseWriter, r *http.Request) {
23542371
format = "mp3"
23552372
}
23562373

2374+
absMusicDir, _ := filepath.Abs(musicDir)
2375+
23572376
if mount[0] != '/' {
23582377
mount = "/" + mount
23592378
}
23602379

23612380
adj := &config.AutoDJConfig{
23622381
Name: name,
23632382
Mount: mount,
2364-
MusicDir: musicDir,
2383+
MusicDir: absMusicDir,
23652384
Format: format,
23662385
Bitrate: bitrate,
23672386
Enabled: true,
@@ -2440,7 +2459,8 @@ func (s *Server) handleToggleAutoDJ(w http.ResponseWriter, r *http.Request) {
24402459

24412460
if adj.Enabled {
24422461
if existing == nil {
2443-
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, adj.Visible)
2462+
absMusicDir, _ := filepath.Abs(adj.MusicDir)
2463+
streamer, err := s.StreamerM.StartStreamer(adj.Name, adj.Mount, absMusicDir, adj.Loop, adj.Format, adj.Bitrate, adj.InjectMetadata, adj.Playlist, adj.MPDEnabled, adj.MPDPort, adj.MPDPassword, adj.Visible)
24442464
if err == nil {
24452465
if adj.InjectMetadata {
24462466
if st, ok := s.Relay.GetStream(adj.Mount); ok {
@@ -2531,14 +2551,25 @@ func (s *Server) handlePlayerQueue(w http.ResponseWriter, r *http.Request) {
25312551
}
25322552

25332553
if action == "add" {
2534-
streamer.PushToQueue(path)
2554+
musicDir := streamer.GetMusicDir()
2555+
fullPath := path
2556+
if !filepath.IsAbs(path) {
2557+
fullPath = filepath.Join(musicDir, path)
2558+
}
2559+
// Security: Ensure fullPath is still within musicDir
2560+
if !strings.HasPrefix(fullPath, musicDir) {
2561+
http.Error(w, "Forbidden", http.StatusForbidden)
2562+
return
2563+
}
2564+
abs, _ := filepath.Abs(fullPath)
2565+
streamer.PushToQueue(abs)
25352566
} else if action == "remove" {
25362567
var index int
25372568
fmt.Sscanf(r.FormValue("index"), "%d", &index)
25382569
streamer.RemoveFromQueue(index)
25392570
}
25402571

2541-
http.Redirect(w, r, "/admin#tab-streamer", http.StatusSeeOther)
2572+
w.WriteHeader(http.StatusOK)
25422573
}
25432574

25442575
func (s *Server) handlePlayerShuffle(w http.ResponseWriter, r *http.Request) {
@@ -2598,8 +2629,22 @@ func (s *Server) handlePlayerFiles(w http.ResponseWriter, r *http.Request) {
25982629
musicDir := streamer.GetMusicDir()
25992630
fullPath := filepath.Join(musicDir, subDir)
26002631

2632+
// Security: Ensure fullPath is still within musicDir
2633+
if !strings.HasPrefix(fullPath, musicDir) {
2634+
http.Error(w, "Forbidden", http.StatusForbidden)
2635+
return
2636+
}
2637+
2638+
logrus.WithFields(logrus.Fields{
2639+
"mount": mount,
2640+
"musicDir": musicDir,
2641+
"subDir": subDir,
2642+
"fullPath": fullPath,
2643+
}).Debug("AutoDJ File Browser: Reading directory")
2644+
26012645
entries, err := os.ReadDir(fullPath)
26022646
if err != nil {
2647+
logrus.WithError(err).Errorf("AutoDJ File Browser: Failed to read directory: %s", fullPath)
26032648
http.Error(w, err.Error(), http.StatusInternalServerError)
26042649
return
26052650
}
@@ -2651,7 +2696,15 @@ func (s *Server) handlePlayerPlaylistAction(w http.ResponseWriter, r *http.Reque
26512696
}
26522697

26532698
if action == "add" {
2654-
fullPath := filepath.Join(streamer.GetMusicDir(), relPath)
2699+
musicDir := streamer.GetMusicDir()
2700+
fullPath := filepath.Join(musicDir, relPath)
2701+
2702+
// Security: Ensure fullPath is still within musicDir
2703+
if !strings.HasPrefix(fullPath, musicDir) {
2704+
http.Error(w, "Forbidden", http.StatusForbidden)
2705+
return
2706+
}
2707+
26552708
info, err := os.Stat(fullPath)
26562709
if err == nil {
26572710
if info.IsDir() {
@@ -2721,11 +2774,13 @@ func (s *Server) handleUpdateAutoDJ(w http.ResponseWriter, r *http.Request) {
27212774
bitrate := 128
27222775
fmt.Sscanf(bitrateStr, "%d", &bitrate)
27232776

2777+
absMusicDir, _ := filepath.Abs(musicDir)
2778+
27242779
for _, adj := range s.Config.AutoDJs {
27252780
if adj.Mount == oldMount {
27262781
adj.Name = name
27272782
adj.Mount = newMount
2728-
adj.MusicDir = musicDir
2783+
adj.MusicDir = absMusicDir
27292784
adj.Format = format
27302785
adj.Bitrate = bitrate
27312786
adj.Loop = loop

server/templates/admin.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ <h2>Edit AutoDJ</h2>
702702
if (progressBox) progressBox.style.opacity = '0.3';
703703
}
704704
return;
705-
};
705+
}
706706

707707
const progressBox = card.querySelector('.adj-progress-box');
708708
if (progressBox) progressBox.style.opacity = '1';
@@ -761,7 +761,12 @@ <h2>Edit AutoDJ</h2>
761761
container.dataset.loaded = 'true';
762762

763763
fetch('/admin/player/files?mount=' + encodeURIComponent(mount) + '&path=' + encodeURIComponent(subPath))
764-
.then(r => r.json())
764+
.then(r => {
765+
if (!r.ok) {
766+
return r.text().then(err => { throw new Error(err); });
767+
}
768+
return r.json();
769+
})
765770
.then(entries => {
766771
if (!entries || entries.length === 0) {
767772
container.innerHTML = '<div style="text-align:center; padding: 1rem; color: var(--text-dim); font-size: 0.8rem;">No entries found.</div>';
@@ -811,6 +816,9 @@ <h2>Edit AutoDJ</h2>
811816
html += '</div>';
812817
container.innerHTML = html;
813818
lucide.createIcons();
819+
})
820+
.catch(err => {
821+
container.innerHTML = `<div style="text-align:center; padding: 1rem; color: var(--danger); font-size: 0.8rem;">Error: ${err.message}</div>`;
814822
});
815823
}
816824

tinyice.pid

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
33149
1+
34201

0 commit comments

Comments
 (0)