Security - Serve character images through os.Root so names like ../ can't escape the characters folder - Sync start and /sync/reset are POST only (legacy and /api/v1) - /api/v1/token/cleanup requires a token, password compared in constant time, tokens no longer logged - Echo's rate limiter replaces the custom one; TRUSTED_PROXIES enables X-Forwarded-For for reverse proxies Crashes and races - Sync state lives in SyncService with atomics and a mutex instead of unsynchronised globals (fixes concurrent map writes during full sync) - Only one sync can run; /api/v1 sync and music routes are locked during sync like the legacy ones - No log.Fatal outside startup: errors become 4xx/5xx responses - Search uses an in-order character match instead of unescaped regexes - Empty queue / no songs return 404 instead of panicking or looping Behaviour - Sync result is computed once and stays the same between polls - Swagger and /openapi serve the embedded spec - Shutdown stops HTTP before closing the database - Expired tokens are deleted with a context that outlives the request - Files are served with Range support and correct content types; audio has no write deadline Performance - Migration 000006 adds soundtrack.fingerprint (names, sizes, mtimes) so unchanged folders are skipped without reading any files - Songs are reconciled per soundtrack with one query plus needed writes; renames keep play counts - Moved MUSIC_PATH updates soundtrack and song paths - .id cleanup only runs for changed soundtracks or a full sync - Classic random uses one SQL query; Gitea release lookups are cached with a timeout Structure - internal/config reads all environment variables once - sqlc emits a Querier interface; backend globals become services injected into handlers - One route registration shared by legacy and /api/v1 - Removed dead code (dbHelper.go, unused middleware, duplicate Gitea functions, embedded Swagger UI copy) and lib/pq - templ and swag pinned as go tools
241 lines
7.4 KiB
Go
241 lines
7.4 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"music-server/internal/backend"
|
|
"music-server/internal/logging"
|
|
|
|
"github.com/labstack/echo/v5"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type MusicHandler struct {
|
|
music *backend.MusicService
|
|
}
|
|
|
|
func NewMusicHandler(music *backend.MusicService) *MusicHandler {
|
|
return &MusicHandler{music: music}
|
|
}
|
|
|
|
// GetSong godoc
|
|
// @Summary Get a specific song
|
|
// @Description Returns the song at the given position in the queue
|
|
// @Tags music
|
|
// @Produce audio/mpeg
|
|
// @Param song query int true "Song number in the queue"
|
|
// @Success 200 {file} file
|
|
// @Success 206 {file} file
|
|
// @Failure 400 {string} string "song must be a number"
|
|
// @Failure 404 {string} string "Not Found"
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music [get]
|
|
func (m *MusicHandler) GetSong(ctx *echo.Context) error {
|
|
song, err := strconv.Atoi(ctx.QueryParam("song"))
|
|
if err != nil {
|
|
return ctx.String(http.StatusBadRequest, "song must be a number")
|
|
}
|
|
path, err := m.music.Song(song)
|
|
return serveSong(ctx, path, err)
|
|
}
|
|
|
|
// GetSoundCheckSong godoc
|
|
// @Summary Get sound check song
|
|
// @Description Returns the sound check song
|
|
// @Tags music
|
|
// @Produce audio/mpeg
|
|
// @Success 200 {file} file
|
|
// @Failure 404 {string} string "Not Found"
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/soundTest [get]
|
|
func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
|
|
path, err := m.music.SoundCheckSong()
|
|
return serveSong(ctx, path, err)
|
|
}
|
|
|
|
// ResetMusic godoc
|
|
// @Summary Reset music state
|
|
// @Description Resets the music state
|
|
// @Tags music
|
|
// @Success 200
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/reset [get]
|
|
func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
|
|
if err := m.music.Reset(ctx.Request().Context()); err != nil {
|
|
return songError(err)
|
|
}
|
|
return ctx.NoContent(http.StatusOK)
|
|
}
|
|
|
|
// GetRandomSong godoc
|
|
// @Summary Get random song
|
|
// @Description Returns a random song
|
|
// @Tags music
|
|
// @Produce audio/mpeg
|
|
// @Success 200 {file} file
|
|
// @Failure 404 {string} string "Not Found"
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/rand [get]
|
|
func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
|
|
path, err := m.music.RandomSong(ctx.Request().Context())
|
|
return serveSong(ctx, path, err)
|
|
}
|
|
|
|
// GetRandomSongLowChance godoc
|
|
// @Summary Get random song with low chance
|
|
// @Description Returns a random song, favouring soundtracks that have been played less
|
|
// @Tags music
|
|
// @Produce audio/mpeg
|
|
// @Success 200 {file} file
|
|
// @Failure 404 {string} string "Not Found"
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/rand/low [get]
|
|
func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
|
|
path, err := m.music.RandomSongLowChance(ctx.Request().Context())
|
|
return serveSong(ctx, path, err)
|
|
}
|
|
|
|
// GetRandomSongClassic godoc
|
|
// @Summary Get random classic song
|
|
// @Description Returns a random song where every song has the same chance
|
|
// @Tags music
|
|
// @Produce audio/mpeg
|
|
// @Success 200 {file} file
|
|
// @Failure 404 {string} string "Not Found"
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/rand/classic [get]
|
|
func (m *MusicHandler) GetRandomSongClassic(ctx *echo.Context) error {
|
|
path, err := m.music.RandomSongClassic(ctx.Request().Context())
|
|
return serveSong(ctx, path, err)
|
|
}
|
|
|
|
// GetSongInfo godoc
|
|
// @Summary Get current song info
|
|
// @Description Returns information about the current song
|
|
// @Tags music
|
|
// @Produce json
|
|
// @Success 200 {object} backend.SongInfo
|
|
// @Router /music/info [get]
|
|
func (m *MusicHandler) GetSongInfo(ctx *echo.Context) error {
|
|
return ctx.JSON(http.StatusOK, m.music.SongInfo(ctx.Request().Context()))
|
|
}
|
|
|
|
// GetPlayedSongs godoc
|
|
// @Summary Get played songs list
|
|
// @Description Returns a list of played songs
|
|
// @Tags music
|
|
// @Produce json
|
|
// @Success 200 {array} backend.SongInfo
|
|
// @Router /music/list [get]
|
|
func (m *MusicHandler) GetPlayedSongs(ctx *echo.Context) error {
|
|
return ctx.JSON(http.StatusOK, m.music.PlayedSongs(ctx.Request().Context()))
|
|
}
|
|
|
|
// GetNextSong godoc
|
|
// @Summary Get next song
|
|
// @Description Returns the next song in the queue
|
|
// @Tags music
|
|
// @Produce audio/mpeg
|
|
// @Success 200 {file} file
|
|
// @Failure 404 {string} string "Not Found"
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/next [get]
|
|
func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
|
|
path, err := m.music.NextSong()
|
|
return serveSong(ctx, path, err)
|
|
}
|
|
|
|
// GetPreviousSong godoc
|
|
// @Summary Get previous song
|
|
// @Description Returns the previous song in the queue
|
|
// @Tags music
|
|
// @Produce audio/mpeg
|
|
// @Success 200 {file} file
|
|
// @Failure 404 {string} string "Not Found"
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/previous [get]
|
|
func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
|
path, err := m.music.PreviousSong()
|
|
return serveSong(ctx, path, err)
|
|
}
|
|
|
|
// GetAllSoundtracks godoc
|
|
// @Summary Get all soundtracks
|
|
// @Description Returns a list of all soundtracks in order
|
|
// @Tags music
|
|
// @Produce json
|
|
// @Success 200 {array} string
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/all/order [get]
|
|
func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
|
|
names, err := m.music.SoundtrackNames(ctx.Request().Context())
|
|
if err != nil {
|
|
return songError(err)
|
|
}
|
|
return ctx.JSON(http.StatusOK, names)
|
|
}
|
|
|
|
// GetAllSoundtracksRandom godoc
|
|
// @Summary Get all soundtracks random
|
|
// @Description Returns a list of all soundtracks in random order
|
|
// @Tags music
|
|
// @Produce json
|
|
// @Success 200 {array} string
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/all/random [get]
|
|
func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
|
names, err := m.music.SoundtrackNamesRandom(ctx.Request().Context())
|
|
if err != nil {
|
|
return songError(err)
|
|
}
|
|
return ctx.JSON(http.StatusOK, names)
|
|
}
|
|
|
|
// PutPlayed godoc
|
|
// @Summary Mark song as played
|
|
// @Description Marks the song at the given queue position as played
|
|
// @Tags music
|
|
// @Param song query int true "Song number in the queue"
|
|
// @Success 200
|
|
// @Failure 400 {string} string "Bad Request"
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/played [put]
|
|
func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
|
|
song, err := strconv.Atoi(ctx.QueryParam("song"))
|
|
if err != nil {
|
|
return ctx.JSON(http.StatusBadRequest, err.Error())
|
|
}
|
|
logging.GetLogger().Info("Marking song as played", zap.Int("song_id", song))
|
|
if err := m.music.SetPlayed(ctx.Request().Context(), song); err != nil {
|
|
return songError(err)
|
|
}
|
|
return ctx.NoContent(http.StatusOK)
|
|
}
|
|
|
|
// AddLatestToQue godoc
|
|
// @Summary Add latest to queue
|
|
// @Description Adds the latest song to the queue
|
|
// @Tags music
|
|
// @Success 200
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/addQue [get]
|
|
func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
|
|
m.music.AddLatestToQueue()
|
|
return ctx.NoContent(http.StatusOK)
|
|
}
|
|
|
|
// AddLatestPlayed godoc
|
|
// @Summary Add latest to played
|
|
// @Description Marks the current song as played
|
|
// @Tags music
|
|
// @Success 200
|
|
// @Failure 423 {string} string "Syncing is in progress"
|
|
// @Router /music/addPlayed [get]
|
|
func (m *MusicHandler) AddLatestPlayed(ctx *echo.Context) error {
|
|
if err := m.music.AddLatestPlayed(ctx.Request().Context()); err != nil {
|
|
return songError(err)
|
|
}
|
|
return ctx.NoContent(http.StatusOK)
|
|
}
|