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 }