From a44011bf0b8172c7f985a5d933c713660c00f398 Mon Sep 17 00:00:00 2001 From: Adam French Date: Mon, 30 Mar 2026 16:47:04 +0100 Subject: [PATCH] Add disable/enable toggle for radio fallback songs Co-Authored-By: Claude Opus 4.6 --- backend/handlers/handle_radio.go | 90 ++++++++++++++++++++++++++--- backend/main.go | 2 + vue/src/views/admin/ManageRadio.vue | 21 ++++++- 3 files changed, 103 insertions(+), 10 deletions(-) diff --git a/backend/handlers/handle_radio.go b/backend/handlers/handle_radio.go index e414f2d..6b0209c 100644 --- a/backend/handlers/handle_radio.go +++ b/backend/handlers/handle_radio.go @@ -51,19 +51,21 @@ func (store *Store) UploadRadioSong(ctx *gin.Context) { } func (store *Store) ListRadioSongs(ctx *gin.Context) { + type songInfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + Modified int64 `json:"modified"` + Disabled bool `json:"disabled"` + } + + songs := []songInfo{} + + // Read enabled songs entries, err := os.ReadDir(fallbackMusicDir) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read music directory"}) return } - - type songInfo struct { - Name string `json:"name"` - Size int64 `json:"size"` - Modified int64 `json:"modified"` - } - - songs := []songInfo{} for _, entry := range entries { if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { continue @@ -76,12 +78,84 @@ func (store *Store) ListRadioSongs(ctx *gin.Context) { Name: entry.Name(), Size: info.Size(), Modified: info.ModTime().Unix(), + Disabled: false, }) } + // Read disabled songs + disabledDir := filepath.Join(fallbackMusicDir, "disabled") + disabledEntries, err := os.ReadDir(disabledDir) + if err == nil { + for _, entry := range disabledEntries { + if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { + continue + } + info, err := entry.Info() + if err != nil { + continue + } + songs = append(songs, songInfo{ + Name: entry.Name(), + Size: info.Size(), + Modified: info.ModTime().Unix(), + Disabled: true, + }) + } + } + ctx.JSON(http.StatusOK, gin.H{"songs": songs}) } +func (store *Store) DisableRadioSong(ctx *gin.Context) { + filename := filepath.Base(ctx.Param("filename")) + if filename == "." || filename == "/" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"}) + return + } + + src := filepath.Join(fallbackMusicDir, filename) + if _, err := os.Stat(src); os.IsNotExist(err) { + ctx.JSON(http.StatusNotFound, gin.H{"error": "file not found"}) + return + } + + disabledDir := filepath.Join(fallbackMusicDir, "disabled") + if err := os.MkdirAll(disabledDir, 0o755); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create disabled directory"}) + return + } + + dst := filepath.Join(disabledDir, filename) + if err := os.Rename(src, dst); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to disable song"}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"disabled": filename}) +} + +func (store *Store) EnableRadioSong(ctx *gin.Context) { + filename := filepath.Base(ctx.Param("filename")) + if filename == "." || filename == "/" { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"}) + return + } + + src := filepath.Join(fallbackMusicDir, "disabled", filename) + if _, err := os.Stat(src); os.IsNotExist(err) { + ctx.JSON(http.StatusNotFound, gin.H{"error": "file not found in disabled directory"}) + return + } + + dst := filepath.Join(fallbackMusicDir, filename) + if err := os.Rename(src, dst); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to enable song"}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"enabled": filename}) +} + func (store *Store) DeleteRadioSong(ctx *gin.Context) { filename := filepath.Base(ctx.Param("filename")) if filename == "." || filename == "/" { diff --git a/backend/main.go b/backend/main.go index 0e73dfe..95cef24 100644 --- a/backend/main.go +++ b/backend/main.go @@ -129,6 +129,8 @@ func main() { admin.POST("/radio/upload", store.UploadRadioSong) admin.GET("/radio/songs", store.ListRadioSongs) admin.DELETE("/radio/songs/:filename", store.DeleteRadioSong) + admin.PATCH("/radio/songs/:filename/disable", store.DisableRadioSong) + admin.PATCH("/radio/songs/:filename/enable", store.EnableRadioSong) // MESSAGES r.GET("/ws", store.ConnectWebSocket) diff --git a/vue/src/views/admin/ManageRadio.vue b/vue/src/views/admin/ManageRadio.vue index a689e93..27a4148 100644 --- a/vue/src/views/admin/ManageRadio.vue +++ b/vue/src/views/admin/ManageRadio.vue @@ -58,6 +58,16 @@ async function deleteSong(name) { } } +async function toggleSong(song) { + const action = song.disabled ? "enable" : "disable"; + try { + await axios.patch(`/api/radio/songs/${encodeURIComponent(song.name)}/${action}`); + await fetchSongs(); + } catch (err) { + console.error(err); + } +} + function formatSize(bytes) { if (bytes < 1024) return bytes + " B"; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; @@ -76,9 +86,16 @@ onMounted(fetchSongs); {{ r.name }}: {{ r.status }} -
- {{ song.name }} +
+ {{ song.name }} {{ formatSize(song.size) }} + disabled +