feat(jukebox): use mpv over ipc as a player backend

This commit is contained in:
sentriz
2022-11-16 18:28:31 +00:00
committed by Senan Kelly
parent ec97289d45
commit e1488b0d18
26 changed files with 695 additions and 269 deletions

View File

@@ -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
}

View File

@@ -313,7 +313,7 @@ type JukeboxStatus struct {
type JukeboxPlaylist struct {
List []*TrackChild `xml:"entry,omitempty" json:"entry,omitempty"`
JukeboxStatus
*JukeboxStatus
}
type Podcasts struct {

View File

@@ -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())
}
}