Add disable/enable toggle for radio fallback songs
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m27s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m27s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,19 +51,21 @@ func (store *Store) UploadRadioSong(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) ListRadioSongs(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)
|
entries, err := os.ReadDir(fallbackMusicDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read music directory"})
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read music directory"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type songInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Modified int64 `json:"modified"`
|
|
||||||
}
|
|
||||||
|
|
||||||
songs := []songInfo{}
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
||||||
continue
|
continue
|
||||||
@@ -76,12 +78,84 @@ func (store *Store) ListRadioSongs(ctx *gin.Context) {
|
|||||||
Name: entry.Name(),
|
Name: entry.Name(),
|
||||||
Size: info.Size(),
|
Size: info.Size(),
|
||||||
Modified: info.ModTime().Unix(),
|
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})
|
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) {
|
func (store *Store) DeleteRadioSong(ctx *gin.Context) {
|
||||||
filename := filepath.Base(ctx.Param("filename"))
|
filename := filepath.Base(ctx.Param("filename"))
|
||||||
if filename == "." || filename == "/" {
|
if filename == "." || filename == "/" {
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ func main() {
|
|||||||
admin.POST("/radio/upload", store.UploadRadioSong)
|
admin.POST("/radio/upload", store.UploadRadioSong)
|
||||||
admin.GET("/radio/songs", store.ListRadioSongs)
|
admin.GET("/radio/songs", store.ListRadioSongs)
|
||||||
admin.DELETE("/radio/songs/:filename", store.DeleteRadioSong)
|
admin.DELETE("/radio/songs/:filename", store.DeleteRadioSong)
|
||||||
|
admin.PATCH("/radio/songs/:filename/disable", store.DisableRadioSong)
|
||||||
|
admin.PATCH("/radio/songs/:filename/enable", store.EnableRadioSong)
|
||||||
|
|
||||||
// MESSAGES
|
// MESSAGES
|
||||||
r.GET("/ws", store.ConnectWebSocket)
|
r.GET("/ws", store.ConnectWebSocket)
|
||||||
|
|||||||
@@ -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) {
|
function formatSize(bytes) {
|
||||||
if (bytes < 1024) return bytes + " B";
|
if (bytes < 1024) return bytes + " B";
|
||||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||||
@@ -76,9 +86,16 @@ onMounted(fetchSongs);
|
|||||||
<span class="text-primary">{{ r.name }}: </span>
|
<span class="text-primary">{{ r.name }}: </span>
|
||||||
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
<span :class="r.ok ? 'text-secondary' : 'text-red-500'">{{ r.status }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="song in songs" :key="song.name" class="flex flex-row items-center gap-2">
|
<div
|
||||||
<span>{{ song.name }}</span>
|
v-for="song in songs"
|
||||||
|
:key="song.name"
|
||||||
|
class="flex flex-row items-center gap-2"
|
||||||
|
:class="{ 'opacity-50': song.disabled }"
|
||||||
|
>
|
||||||
|
<span :class="{ 'line-through': song.disabled }">{{ song.name }}</span>
|
||||||
<span class="text-secondary text-sm">{{ formatSize(song.size) }}</span>
|
<span class="text-secondary text-sm">{{ formatSize(song.size) }}</span>
|
||||||
|
<span v-if="song.disabled" class="text-red-400 text-xs">disabled</span>
|
||||||
|
<Button @click="toggleSong(song)">{{ song.disabled ? "Enable" : "Disable" }}</Button>
|
||||||
<Button @click="deleteSong(song.name)">Delete</Button>
|
<Button @click="deleteSong(song.name)">Delete</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user