All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m27s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
4.7 KiB
Go
179 lines
4.7 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) {
|
|
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
|
|
}
|
|
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(),
|
|
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 == "/" {
|
|
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})
|
|
}
|