feat(jukebox): use mpv over ipc as a player backend
This commit is contained in:
@@ -2,8 +2,11 @@ package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unicode"
|
||||
@@ -270,80 +273,135 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeJukebox(r *http.Request) *spec.Response {
|
||||
func (c *Controller) ServeJukebox(r *http.Request) *spec.Response { // nolint:gocyclo
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
getTracks := func() []*db.Track {
|
||||
var tracks []*db.Track
|
||||
ids, err := params.GetIDList("id")
|
||||
if err != nil {
|
||||
return tracks
|
||||
}
|
||||
trackPaths := func(ids []specid.ID) ([]string, error) {
|
||||
var paths []string
|
||||
for _, id := range ids {
|
||||
track := &db.Track{}
|
||||
c.DB.
|
||||
Preload("Album").
|
||||
Preload("TrackStar", "user_id=?", user.ID).
|
||||
Preload("TrackRating", "user_id=?", user.ID).
|
||||
First(track, id.Value)
|
||||
if track.ID != 0 {
|
||||
tracks = append(tracks, track)
|
||||
var track db.Track
|
||||
if err := c.DB.Preload("Album").Preload("TrackStar", "user_id=?", user.ID).Preload("TrackRating", "user_id=?", user.ID).First(&track, id.Value).Error; err != nil {
|
||||
return nil, fmt.Errorf("find track by id: %w", err)
|
||||
}
|
||||
paths = append(paths, track.AbsPath())
|
||||
}
|
||||
return tracks
|
||||
return paths, nil
|
||||
}
|
||||
getStatus := func() spec.JukeboxStatus {
|
||||
status := c.Jukebox.GetStatus()
|
||||
return spec.JukeboxStatus{
|
||||
getSpecStatus := func() (*spec.JukeboxStatus, error) {
|
||||
status, err := c.Jukebox.GetStatus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get status: %w", err)
|
||||
}
|
||||
return &spec.JukeboxStatus{
|
||||
CurrentIndex: status.CurrentIndex,
|
||||
Playing: status.Playing,
|
||||
Gain: status.Gain,
|
||||
Gain: float64(status.GainPct) / 100.0,
|
||||
Position: status.Position,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
getStatusTracks := func() []*spec.TrackChild {
|
||||
tracks := c.Jukebox.GetTracks()
|
||||
ret := make([]*spec.TrackChild, len(tracks))
|
||||
for i, track := range tracks {
|
||||
ret[i] = spec.NewTrackByTags(track, track.Album)
|
||||
getSpecPlaylist := func() ([]*spec.TrackChild, error) {
|
||||
var ret []*spec.TrackChild
|
||||
playlist, err := c.Jukebox.GetPlaylist()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get playlist: %w", err)
|
||||
}
|
||||
return ret
|
||||
for _, path := range playlist {
|
||||
cwd, _ := os.Getwd()
|
||||
path, _ = filepath.Rel(cwd, path)
|
||||
var track db.Track
|
||||
err := c.DB.
|
||||
Preload("Album").
|
||||
Where(`(albums.root_dir || ? || albums.left_path || albums.right_path || ? || tracks.filename)=?`,
|
||||
string(filepath.Separator), string(filepath.Separator), path).
|
||||
Joins(`JOIN albums ON tracks.album_id=albums.id`).
|
||||
First(&track).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch track: %w", err)
|
||||
}
|
||||
ret = append(ret, spec.NewTrackByTags(&track, track.Album))
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
switch act, _ := params.Get("action"); act {
|
||||
case "set":
|
||||
c.Jukebox.SetTracks(getTracks())
|
||||
ids := params.GetOrIDList("id", nil)
|
||||
paths, err := trackPaths(ids)
|
||||
if err != nil {
|
||||
return spec.NewError(0, "error creating playlist items: %v", err)
|
||||
}
|
||||
if err := c.Jukebox.SetPlaylist(paths); err != nil {
|
||||
return spec.NewError(0, "error setting playlist: %v", err)
|
||||
}
|
||||
case "add":
|
||||
c.Jukebox.AddTracks(getTracks())
|
||||
ids := params.GetOrIDList("id", nil)
|
||||
paths, err := trackPaths(ids)
|
||||
if err != nil {
|
||||
return spec.NewError(10, "error creating playlist items: %v", err)
|
||||
}
|
||||
if err := c.Jukebox.AppendToPlaylist(paths); err != nil {
|
||||
return spec.NewError(0, "error appending to playlist: %v", err)
|
||||
}
|
||||
case "clear":
|
||||
c.Jukebox.ClearTracks()
|
||||
if err := c.Jukebox.ClearPlaylist(); err != nil {
|
||||
return spec.NewError(0, "error clearing playlist: %v", err)
|
||||
}
|
||||
case "remove":
|
||||
index, err := params.GetInt("index")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an id for remove actions")
|
||||
}
|
||||
c.Jukebox.RemoveTrack(index)
|
||||
if err := c.Jukebox.RemovePlaylistIndex(index); err != nil {
|
||||
return spec.NewError(0, "error removing: %v", err)
|
||||
}
|
||||
case "stop":
|
||||
c.Jukebox.Stop()
|
||||
if err := c.Jukebox.Pause(); err != nil {
|
||||
return spec.NewError(0, "error stopping: %v", err)
|
||||
}
|
||||
case "start":
|
||||
c.Jukebox.Start()
|
||||
if err := c.Jukebox.Play(); err != nil {
|
||||
return spec.NewError(0, "error starting: %v", err)
|
||||
}
|
||||
case "skip":
|
||||
index, err := params.GetInt("index")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an index for skip actions")
|
||||
}
|
||||
offset, _ := params.GetInt("offset")
|
||||
c.Jukebox.Skip(index, offset)
|
||||
if err := c.Jukebox.SkipToPlaylistIndex(index, offset); err != nil {
|
||||
return spec.NewError(0, "error skipping: %v", err)
|
||||
}
|
||||
case "get":
|
||||
specPlaylist, err := getSpecPlaylist()
|
||||
if err != nil {
|
||||
return spec.NewError(10, "error getting status tracks: %v", err)
|
||||
}
|
||||
status, err := getSpecStatus()
|
||||
if err != nil {
|
||||
return spec.NewError(10, "error getting status: %v", err)
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
sub.JukeboxPlaylist = &spec.JukeboxPlaylist{
|
||||
JukeboxStatus: getStatus(),
|
||||
List: getStatusTracks(),
|
||||
JukeboxStatus: status,
|
||||
List: specPlaylist,
|
||||
}
|
||||
return sub
|
||||
case "setGain":
|
||||
gain, err := params.GetFloat("gain")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide a valid gain param")
|
||||
}
|
||||
if err := c.Jukebox.SetVolumePct(int(math.Min(gain, 1) * 100)); err != nil {
|
||||
return spec.NewError(0, "error setting gain: %v", err)
|
||||
}
|
||||
}
|
||||
// all actions except get are expected to return a status
|
||||
status, err := getSpecStatus()
|
||||
if err != nil {
|
||||
return spec.NewError(10, "error getting status: %v", err)
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
status := getStatus()
|
||||
sub.JukeboxStatus = &status
|
||||
sub.JukeboxStatus = status
|
||||
return sub
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ type JukeboxStatus struct {
|
||||
|
||||
type JukeboxPlaylist struct {
|
||||
List []*TrackChild `xml:"entry,omitempty" json:"entry,omitempty"`
|
||||
JukeboxStatus
|
||||
*JukeboxStatus
|
||||
}
|
||||
|
||||
type Podcasts struct {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@@ -100,7 +101,6 @@ func New(opts Options) (*Server, error) {
|
||||
CoverCachePath: opts.CoverCachePath,
|
||||
PodcastsPath: opts.PodcastPath,
|
||||
MusicPaths: opts.MusicPaths,
|
||||
Jukebox: &jukebox.Jukebox{},
|
||||
Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, &listenbrainz.Scrobbler{}},
|
||||
Podcasts: podcast,
|
||||
Transcoder: cacheTranscoder,
|
||||
@@ -360,13 +360,29 @@ func (s *Server) StartScanWatcher() (FuncExecute, FuncInterrupt) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) StartJukebox() (FuncExecute, FuncInterrupt) {
|
||||
func (s *Server) StartJukebox(mpvExtraArgs []string) (FuncExecute, FuncInterrupt) {
|
||||
var sockFile *os.File
|
||||
return func() error {
|
||||
log.Printf("starting job 'jukebox'\n")
|
||||
return s.jukebox.Listen()
|
||||
var err error
|
||||
sockFile, err = os.CreateTemp("", "gonic-jukebox-*.sock")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create tmp sock file: %w", err)
|
||||
}
|
||||
if err := s.jukebox.Start(sockFile.Name(), mpvExtraArgs); err != nil {
|
||||
return fmt.Errorf("start jukebox: %w", err)
|
||||
}
|
||||
if err := s.jukebox.Wait(); err != nil {
|
||||
return fmt.Errorf("start jukebox: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, func(_ error) {
|
||||
// stop job
|
||||
s.jukebox.Quit()
|
||||
if err := s.jukebox.Quit(); err != nil {
|
||||
log.Printf("error quitting jukebox: %v", err)
|
||||
}
|
||||
_ = sockFile.Close()
|
||||
_ = os.Remove(sockFile.Name())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user