@@ -301,20 +301,24 @@ func (s *Server) setupRoutes() *http.ServeMux {
301301 mux .HandleFunc ("/admin/add-webhook" , s .handleAddWebhook )
302302 mux .HandleFunc ("/admin/delete-webhook" , s .handleDeleteWebhook )
303303 mux .HandleFunc ("/admin/player/toggle" , s .handlePlayerToggle )
304+ mux .HandleFunc ("/admin/player/restart" , s .handlePlayerRestart )
304305 mux .HandleFunc ("/admin/player/scan" , s .handlePlayerScan )
305306 mux .HandleFunc ("/admin/player/clear-playlist" , s .handlePlayerClearPlaylist )
306307 mux .HandleFunc ("/admin/player/save-playlist" , s .handlePlayerSavePlaylist )
308+ mux .HandleFunc ("/admin/player/playlist-info" , s .handlePlayerPlaylistInfo )
307309 mux .HandleFunc ("/admin/player/load-playlist" , s .handlePlayerLoadPlaylist )
308310 mux .HandleFunc ("/admin/player/reorder" , s .handlePlayerReorder )
309311 mux .HandleFunc ("/admin/player/queue" , s .handlePlayerQueue )
310312 mux .HandleFunc ("/admin/player/shuffle" , s .handlePlayerShuffle )
311313 mux .HandleFunc ("/admin/player/loop" , s .handlePlayerLoop )
314+ mux .HandleFunc ("/admin/player/metadata" , s .handlePlayerMetadata )
312315 mux .HandleFunc ("/admin/player/next" , s .handlePlayerNext )
313316 mux .HandleFunc ("/admin/player/files" , s .handlePlayerFiles )
314317 mux .HandleFunc ("/admin/player/playlist-action" , s .handlePlayerPlaylistAction )
315318 mux .HandleFunc ("/admin/autodj/add" , s .handleAddAutoDJ )
316319 mux .HandleFunc ("/admin/autodj/delete" , s .handleDeleteAutoDJ )
317320 mux .HandleFunc ("/admin/autodj/toggle" , s .handleToggleAutoDJ )
321+ mux .HandleFunc ("/admin/autodj/studio" , s .handleAutoDJStudio )
318322 mux .HandleFunc ("/admin/autodj/update" , s .handleUpdateAutoDJ )
319323 mux .HandleFunc ("/admin/add-relay" , s .handleAddRelay )
320324 mux .HandleFunc ("/admin/toggle-relay" , s .handleToggleRelay )
@@ -590,8 +594,10 @@ func (s *Server) safeJoin(base, rel string) (string, error) {
590594 return "" , err
591595 }
592596
597+ logrus .Debugf ("safeJoin: base=%s rel=%s joined=%s relPath=%s" , absBase , rel , joined , relPath )
598+
593599 if strings .HasPrefix (relPath , ".." ) || strings .HasPrefix (relPath , "/" ) {
594- return "" , fmt .Errorf ("security: path traversal attempt detected" )
600+ return "" , fmt .Errorf ("security: path traversal attempt detected: %s" , relPath )
595601 }
596602
597603 return joined , nil
@@ -2681,13 +2687,14 @@ func (s *Server) handlePlayerQueue(w http.ResponseWriter, r *http.Request) {
26812687 musicDir := streamer .GetMusicDir ()
26822688 rel , err := filepath .Rel (musicDir , path )
26832689 if err != nil {
2690+ logrus .WithError (err ).Warnf ("Security: Blocked queue addition: filepath.Rel failed for base=%s path=%s" , musicDir , path )
26842691 http .Error (w , "Forbidden" , http .StatusForbidden )
26852692 return
26862693 }
26872694
26882695 fullPath , err := s .safeJoin (musicDir , rel )
26892696 if err != nil {
2690- logrus .WithError (err ).Warnf ("Security: Blocked queue addition of %s for mount %s" , path , mount )
2697+ logrus .WithError (err ).Warnf ("Security: Blocked queue addition of %s (relative to %s) for mount %s. Full path attempted: %s " , rel , musicDir , mount , fullPath )
26912698 http .Error (w , "Forbidden" , http .StatusForbidden )
26922699 return
26932700 }
@@ -2774,6 +2781,110 @@ func (s *Server) handlePlayerLoop(w http.ResponseWriter, r *http.Request) {
27742781 w .WriteHeader (http .StatusOK )
27752782}
27762783
2784+ func (s * Server ) handleAutoDJStudio (w http.ResponseWriter , r * http.Request ) {
2785+ user , ok := s .checkAuth (r )
2786+ if ! ok {
2787+ return
2788+ }
2789+
2790+ mount := r .URL .Query ().Get ("mount" )
2791+ logrus .Infof ("Studio: Requested mount: %s" , mount )
2792+
2793+ streamer := s .StreamerM .GetStreamer (mount )
2794+ if streamer == nil {
2795+ logrus .Warnf ("Studio: Streamer not found for mount: %s" , mount )
2796+ http .Redirect (w , r , "/admin#tab-streamer" , http .StatusSeeOther )
2797+ return
2798+ }
2799+
2800+ s .sessionsMu .RLock ()
2801+ sess := s .sessions [user .Username ]
2802+ csrf := ""
2803+ if sess != nil {
2804+ csrf = sess .CSRFToken
2805+ }
2806+ s .sessionsMu .RUnlock ()
2807+
2808+ data := map [string ]interface {}{
2809+ "Streamer" : streamer .GetStats (),
2810+ "CSRFToken" : csrf ,
2811+ "Config" : s .Config ,
2812+ "User" : user ,
2813+ }
2814+
2815+ w .Header ().Set ("Content-Type" , "text/html" )
2816+ if err := s .tmpl .ExecuteTemplate (w , "autodj_studio.html" , data ); err != nil {
2817+ logrus .WithError (err ).Error ("Studio template error" )
2818+ }
2819+ }
2820+
2821+ func (s * Server ) handlePlayerPlaylistInfo (w http.ResponseWriter , r * http.Request ) {
2822+ if _ , ok := s .checkAuth (r ); ! ok {
2823+ return
2824+ }
2825+
2826+ mount := r .URL .Query ().Get ("mount" )
2827+ streamer := s .StreamerM .GetStreamer (mount )
2828+ if streamer == nil {
2829+ http .Error (w , "Streamer not found" , http .StatusNotFound )
2830+ return
2831+ }
2832+
2833+ w .Header ().Set ("Content-Type" , "application/json" )
2834+ json .NewEncoder (w ).Encode (streamer .GetPlaylistInfo ())
2835+ }
2836+
2837+ func (s * Server ) handlePlayerMetadata (w http.ResponseWriter , r * http.Request ) {
2838+ if ! s .isCSRFSafe (r ) {
2839+ http .Error (w , "Forbidden" , http .StatusForbidden )
2840+ return
2841+ }
2842+ if _ , ok := s .checkAuth (r ); ! ok {
2843+ return
2844+ }
2845+
2846+ mount := r .FormValue ("mount" )
2847+ streamer := s .StreamerM .GetStreamer (mount )
2848+ if streamer == nil {
2849+ http .Error (w , "Streamer not found" , http .StatusNotFound )
2850+ return
2851+ }
2852+
2853+ streamer .ToggleInjectMetadata ()
2854+ metaState := streamer .GetStats ().InjectMetadata
2855+
2856+ // Persist
2857+ for _ , adj := range s .Config .AutoDJs {
2858+ if adj .Mount == mount {
2859+ adj .InjectMetadata = metaState
2860+ s .Config .SaveConfig ()
2861+ break
2862+ }
2863+ }
2864+
2865+ w .WriteHeader (http .StatusOK )
2866+ }
2867+
2868+ func (s * Server ) handlePlayerRestart (w http.ResponseWriter , r * http.Request ) {
2869+ if ! s .isCSRFSafe (r ) {
2870+ http .Error (w , "Forbidden" , http .StatusForbidden )
2871+ return
2872+ }
2873+ if _ , ok := s .checkAuth (r ); ! ok {
2874+ return
2875+ }
2876+
2877+ mount := r .FormValue ("mount" )
2878+ streamer := s .StreamerM .GetStreamer (mount )
2879+ if streamer == nil {
2880+ http .Error (w , "Streamer not found" , http .StatusNotFound )
2881+ return
2882+ }
2883+
2884+ streamer .Restart ()
2885+ w .WriteHeader (http .StatusOK )
2886+ }
2887+
27772888func (s * Server ) handlePlayerNext (w http.ResponseWriter , r * http.Request ) {
27782889 if ! s .isCSRFSafe (r ) {
27792890 http .Error (w , "Forbidden" , http .StatusForbidden )
@@ -2809,7 +2920,9 @@ func (s *Server) handlePlayerFiles(w http.ResponseWriter, r *http.Request) {
28092920 }
28102921
28112922 musicDir := streamer .GetMusicDir ()
2812- fullPath , err := s .safeJoin (musicDir , subDir )
2923+
2924+ // Ensure fullPath is a valid path within musicDir
2925+ fullPath , err := s .validatePathInMusicDir (musicDir , filepath .Join (musicDir , subDir ))
28132926 if err != nil {
28142927 logrus .WithError (err ).Warnf ("Security: Blocked file browser access to %s for mount %s" , subDir , mount )
28152928 http .Error (w , "Forbidden" , http .StatusForbidden )
@@ -2913,11 +3026,12 @@ func (s *Server) handlePlayerPlaylistAction(w http.ResponseWriter, r *http.Reque
29133026
29143027 fullPath , err := s .safeJoin (musicDir , rel )
29153028 if err != nil {
2916- logrus .WithError (err ).Warnf ("Security: Blocked playlist addition of %s for mount %s" , relPath , mount )
3029+ logrus .WithError (err ).Warnf ("Security: Blocked playlist addition of %s (relative to %s) for mount %s. Full path attempted: %s " , rel , musicDir , mount , fullPath )
29173030 http .Error (w , "Forbidden" , http .StatusForbidden )
29183031 return
29193032 }
29203033
3034+
29213035 logrus .Infof ("AutoDJ %s: Attempting to add %s" , mount , fullPath )
29223036
29233037 info , err := os .Stat (fullPath )
0 commit comments