Files
web_server/backend/handlers/handle_radio.go
Adam French d215333128
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m44s
Add admin UI for managing radio fallback music
Upload, list, and delete fallback music files from the admin page.
Backend handlers validate file type/size and prevent path traversal.
Nginx max body size increased to 50M to support large audio files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 16:19:10 +01:00

105 lines
2.6 KiB
Go

package handlers
import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
const fallbackMusicDir = "/backend/fallback_music"
var allowedAudioExtensions = map[string]bool{
".mp3": true, ".ogg": true, ".flac": true, ".wav": true, ".m4a": true, ".opus": true,
}
func (store *Store) UploadRadioSong(ctx *gin.Context) {
file, err := ctx.FormFile("file")
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "file is required"})
return
}
const maxSize = 50 << 20 // 50MB
if file.Size > maxSize {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "file too large (max 50MB)"})
return
}
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedAudioExtensions[ext] {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "file type not allowed (accepted: .mp3, .ogg, .flac, .wav, .m4a, .opus)"})
return
}
filename := filepath.Base(file.Filename)
dest := filepath.Join(fallbackMusicDir, filename)
if _, err := os.Stat(dest); err == nil {
ctx.JSON(http.StatusConflict, gin.H{"error": "file already exists"})
return
}
if err := ctx.SaveUploadedFile(file, dest); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save file"})
return
}
ctx.JSON(http.StatusOK, gin.H{"name": filename})
}
func (store *Store) ListRadioSongs(ctx *gin.Context) {
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
}
info, err := entry.Info()
if err != nil {
continue
}
songs = append(songs, songInfo{
Name: entry.Name(),
Size: info.Size(),
Modified: info.ModTime().Unix(),
})
}
ctx.JSON(http.StatusOK, gin.H{"songs": songs})
}
func (store *Store) DeleteRadioSong(ctx *gin.Context) {
filename := filepath.Base(ctx.Param("filename"))
if filename == "." || filename == "/" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"})
return
}
path := filepath.Join(fallbackMusicDir, filename)
if _, err := os.Stat(path); os.IsNotExist(err) {
ctx.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
return
}
if err := os.Remove(path); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete file"})
return
}
ctx.JSON(http.StatusOK, gin.H{"deleted": filename})
}