diff --git a/backend/handlers/handle_radio.go b/backend/handlers/handle_radio.go new file mode 100644 index 0000000..e414f2d --- /dev/null +++ b/backend/handlers/handle_radio.go @@ -0,0 +1,104 @@ +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}) +} diff --git a/backend/main.go b/backend/main.go index 55db1cd..0e73dfe 100644 --- a/backend/main.go +++ b/backend/main.go @@ -125,6 +125,11 @@ func main() { r.GET("/spotify/recent", store.RecentlyPlayed) // r.POST("/spotify", store.SendSong) + // RADIO + admin.POST("/radio/upload", store.UploadRadioSong) + admin.GET("/radio/songs", store.ListRadioSongs) + admin.DELETE("/radio/songs/:filename", store.DeleteRadioSong) + // MESSAGES r.GET("/ws", store.ConnectWebSocket) protected.POST("/messages/upload", store.UploadMessageFile) diff --git a/docker-compose.yml b/docker-compose.yml index e541bce..30e80ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,6 +72,7 @@ services: - ${OBSIDIAN_DIR}:/backend/notes - ./logs:/backend/logs - uploads:/backend/uploads + - ./icecast2/fallback_music:/backend/fallback_music db: image: postgres:16 diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index ec093b9..5ab5332 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -9,7 +9,7 @@ http { server_tokens off; charset utf-8; - client_max_body_size 10M; + client_max_body_size 50M; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; diff --git a/nginx/nginx_dev.conf.template b/nginx/nginx_dev.conf.template index 170b537..61f4f3d 100644 --- a/nginx/nginx_dev.conf.template +++ b/nginx/nginx_dev.conf.template @@ -9,7 +9,7 @@ http { server_tokens off; charset utf-8; - client_max_body_size 10M; + client_max_body_size 50M; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; diff --git a/vue/src/views/admin/Admin.vue b/vue/src/views/admin/Admin.vue index e026d52..cf9ae2a 100644 --- a/vue/src/views/admin/Admin.vue +++ b/vue/src/views/admin/Admin.vue @@ -9,6 +9,7 @@ import CreateFavorite from "./CreateFavorite.vue"; import CreateActivity from "./CreateActivity.vue"; import CreateRowing from "./CreateRowing.vue"; import ManageUsers from "./ManageUsers.vue"; +import ManageRadio from "./ManageRadio.vue"; const auth = useAuthStore(); @@ -23,6 +24,7 @@ const auth = useAuthStore(); + diff --git a/vue/src/views/admin/ManageRadio.vue b/vue/src/views/admin/ManageRadio.vue new file mode 100644 index 0000000..a689e93 --- /dev/null +++ b/vue/src/views/admin/ManageRadio.vue @@ -0,0 +1,85 @@ + + + + + Manage Radio + + Upload + + {{ r.name }}: + {{ r.status }} + + + {{ song.name }} + {{ formatSize(song.size) }} + Delete + + +