140 lines
3.5 KiB
Go
140 lines
3.5 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"database/sql"
|
|
"strconv"
|
|
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/jukebox"
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/streaming"
|
|
)
|
|
|
|
func init() {
|
|
register("jukeboxControl", handleJukeboxControl)
|
|
}
|
|
|
|
// resolveSongPath maps a song ID to an absolute, path-traversal-checked
|
|
// filesystem path — the same validation every other media endpoint uses,
|
|
// so the jukebox can't be used to make the server play (or leak the
|
|
// existence of) an arbitrary file.
|
|
func resolveSongPath(db *sql.DB) jukebox.ResolveFunc {
|
|
return func(songID string) (string, error) {
|
|
song, err := database.GetSong(db, songID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
mf, err := database.GetMediaFile(db, song.MediaFileID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
folder, err := database.GetMusicFolder(db, mf.MusicFolderID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return streaming.ResolvePath(folder.Path, mf.RelativePath)
|
|
}
|
|
}
|
|
|
|
func jukeboxStatusModel(state jukebox.State) *JukeboxStatus {
|
|
return &JukeboxStatus{
|
|
CurrentIndex: state.CurrentIndex,
|
|
Playing: state.Playing,
|
|
Gain: state.Gain,
|
|
Position: state.PositionSecs,
|
|
}
|
|
}
|
|
|
|
func jukeboxPlaylistModel(rc *Context, state jukebox.State) *JukeboxPlaylist {
|
|
out := &JukeboxPlaylist{JukeboxStatus: *jukeboxStatusModel(state)}
|
|
db := rc.Server.DB
|
|
for _, id := range state.SongIDs {
|
|
song, err := database.GetSong(db, id)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
mf, err := database.GetMediaFile(db, song.MediaFileID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out.Entry = append(out.Entry, childFromSong(db, song, mf, albumOfSong(db, song), rc.User.ID))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func handleJukeboxControl(rc *Context) *apiError {
|
|
if !rc.User.Allows(rc.User.Permissions.Jukebox) {
|
|
return errUnauthorized("jukebox control")
|
|
}
|
|
jb := rc.Server.Jukebox
|
|
if jb == nil || !jb.Available {
|
|
return newError(ErrGeneric, "Jukebox is not available on this server (mpv is not installed)")
|
|
}
|
|
|
|
action, aerr := rc.Params.Required("action")
|
|
if aerr != nil {
|
|
return aerr
|
|
}
|
|
resolve := resolveSongPath(rc.Server.DB)
|
|
|
|
var state jukebox.State
|
|
var err error
|
|
|
|
switch action {
|
|
case "get":
|
|
state, err = jb.Status()
|
|
if err != nil {
|
|
return newError(ErrGeneric, err.Error())
|
|
}
|
|
rc.Response = newResponse()
|
|
rc.Response.JukeboxPlaylist = jukeboxPlaylistModel(rc, state)
|
|
return nil
|
|
|
|
case "status":
|
|
state, err = jb.Status()
|
|
case "set":
|
|
state, err = jb.Set(rc.Params.GetAll("id"), resolve)
|
|
case "add":
|
|
state, err = jb.Add(rc.Params.GetAll("id"), resolve)
|
|
case "clear":
|
|
state, err = jb.Clear()
|
|
case "remove":
|
|
idx, ierr := rc.Params.RequiredInt("index")
|
|
if ierr != nil {
|
|
return ierr
|
|
}
|
|
state, err = jb.Remove(idx)
|
|
case "shuffle":
|
|
state, err = jb.Shuffle(resolve)
|
|
case "skip":
|
|
idx, ierr := rc.Params.RequiredInt("index")
|
|
if ierr != nil {
|
|
return ierr
|
|
}
|
|
offset := float64(rc.Params.Int("offset", 0))
|
|
state, err = jb.SkipTo(idx, offset)
|
|
case "start":
|
|
state, err = jb.Start()
|
|
case "stop":
|
|
state, err = jb.Stop()
|
|
case "setGain":
|
|
gainStr, gerr := rc.Params.Required("gain")
|
|
if gerr != nil {
|
|
return gerr
|
|
}
|
|
gain, perr := strconv.ParseFloat(gainStr, 64)
|
|
if perr != nil {
|
|
return newError(ErrGeneric, "invalid gain value")
|
|
}
|
|
state, err = jb.SetGain(gain)
|
|
default:
|
|
return newError(ErrGeneric, "Unknown jukebox action: "+action)
|
|
}
|
|
|
|
if err != nil {
|
|
return newError(ErrGeneric, err.Error())
|
|
}
|
|
rc.Response = newResponse()
|
|
rc.Response.JukeboxStatus = jukeboxStatusModel(state)
|
|
return nil
|
|
}
|