diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 0068df3..23cc1f8 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -148,6 +148,25 @@ type SupportsRating interface { SetRating(params RatingFavoriteParameters, rating int) error } +type JukeboxProvider interface { + JukeboxStart() error + JukeboxStop() error + JukeboxSeek(idx, seconds int) error + JukeboxClear() error + JukeboxSet(trackID string) error + JukeboxAdd(trackID string) error + JukeboxRemove(idx int) error + JukeboxSetVolume(vol int) error + JukeboxGetStatus() (*JukeboxStatus, error) +} + +type JukeboxStatus struct { + Volume int + CurrentTrack int + Playing bool + PositionSeconds float64 +} + func genresMatch(filterGenres, albumGenres []string) bool { for _, g1 := range filterGenres { for _, g2 := range albumGenres { diff --git a/backend/mediaprovider/subsonic/jukebox.go b/backend/mediaprovider/subsonic/jukebox.go new file mode 100644 index 0000000..de95d93 --- /dev/null +++ b/backend/mediaprovider/subsonic/jukebox.go @@ -0,0 +1,67 @@ +package subsonic + +import ( + "strconv" + + "github.com/dweymouth/supersonic/backend/mediaprovider" +) + +var _ mediaprovider.JukeboxProvider = (*subsonicMediaProvider)(nil) + +func (s *subsonicMediaProvider) JukeboxStart() error { + _, err := s.client.JukeboxControl("start", nil) + return err +} + +func (s *subsonicMediaProvider) JukeboxStop() error { + _, err := s.client.JukeboxControl("stop", nil) + return err +} + +func (s *subsonicMediaProvider) JukeboxClear() error { + _, err := s.client.JukeboxControl("clear", nil) + return err +} + +func (s *subsonicMediaProvider) JukeboxSetVolume(vol int) error { + _, err := s.client.JukeboxControl("setGain", + map[string]string{"gain": strconv.Itoa(vol)}) + return err +} + +func (s *subsonicMediaProvider) JukeboxSeek(idx, seconds int) error { + _, err := s.client.JukeboxControl("skip", + map[string]string{"index": strconv.Itoa(idx), "offset": strconv.Itoa(seconds)}) + return err +} + +func (s *subsonicMediaProvider) JukeboxRemove(idx int) error { + _, err := s.client.JukeboxControl("remove", + map[string]string{"index": strconv.Itoa(idx)}) + return err +} + +func (s *subsonicMediaProvider) JukeboxSet(trackID string) error { + _, err := s.client.JukeboxControl("set", + map[string]string{"id": trackID}) + return err +} + +func (s *subsonicMediaProvider) JukeboxAdd(trackID string) error { + _, err := s.client.JukeboxControl("add", + map[string]string{"id": trackID}) + return err +} + +func (s *subsonicMediaProvider) JukeboxGetStatus() (*mediaprovider.JukeboxStatus, error) { + stat, err := s.client.JukeboxControl("status", nil) + if err != nil { + return nil, err + } + return &mediaprovider.JukeboxStatus{ + Volume: int(stat.Gain * 100), + CurrentTrack: stat.CurrentIndex, + Playing: stat.Playing, + PositionSeconds: float64(stat.Position), + }, nil +} diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 8a580c3..99fcf80 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -229,8 +229,8 @@ func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, appendToQueu return err } urlP.AppendFile(url) - } else if trP, ok := p.player.(player.TrackIDPlayer); ok { - trP.AppendTrack(tracks[i].ID) + } else if trP, ok := p.player.(player.TrackPlayer); ok { + trP.AppendTrack(tracks[i]) } else { panic("unsupported player type") } diff --git a/go.mod b/go.mod index 15a09b8..57fcb02 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee - github.com/dweymouth/go-subsonic v0.0.0-20231216234924-3f62122dc53a + github.com/dweymouth/go-subsonic v0.0.0-20231217175944-9b48c9ffc002 github.com/fsnotify/fsnotify v1.6.0 github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 97ad075..fd340a1 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc h1:wJy4U12Ys github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc/go.mod h1:BMwS4vdjEYf1gmjPGSKCzWP/I6YlI6fkefJ9nsjBjaU= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= -github.com/dweymouth/go-subsonic v0.0.0-20231216234924-3f62122dc53a h1:UJcwloSIzRpUOIwRE207JF2SHoTmOg7vJWzfQWpztSk= -github.com/dweymouth/go-subsonic v0.0.0-20231216234924-3f62122dc53a/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= +github.com/dweymouth/go-subsonic v0.0.0-20231217175944-9b48c9ffc002 h1:DhWQZJObkCUSFmHu/eEzHRqy0R33DcR266nPAvZJE34= +github.com/dweymouth/go-subsonic v0.0.0-20231217175944-9b48c9ffc002/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/player/jukebox/jukeboxplayer.go b/player/jukebox/jukeboxplayer.go new file mode 100644 index 0000000..5525cc9 --- /dev/null +++ b/player/jukebox/jukeboxplayer.go @@ -0,0 +1,132 @@ +package jukebox + +import ( + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/player" +) + +const ( + stopped = 0 + playing = 1 + paused = 2 +) + +type JukeboxPlayer struct { + provider mediaprovider.JukeboxProvider + + state int // stopped, playing, paused + volume int + seeking bool + numTracks int + + curTrack int + curTrackDuration float64 + startTrackTime float64 + startedAtUnixSecs float64 +} + +func (j *JukeboxPlayer) SetVolume(vol int) error { + go func() { + if err := j.provider.JukeboxSetVolume(vol); err == nil { + j.volume = vol + } + }() + return nil +} + +func (j *JukeboxPlayer) GetVolume() int { + return j.volume +} + +func (j *JukeboxPlayer) PlayTrackAt(idx int) error { + go func() { + if err := j.provider.JukeboxSeek(idx, 0); err == nil { + j.curTrack = idx + j.Continue() + } + }() + return nil +} + +func (j *JukeboxPlayer) Continue() error { + if j.state == playing { + return nil + } + go func() { + if err := j.provider.JukeboxStart(); err != nil { + return + } + j.state = playing + }() + return nil +} + +func (j *JukeboxPlayer) Pause() error { + if j.state != playing { + return nil + } + go func() { + if err := j.provider.JukeboxStop(); err != nil { + return + } + j.state = paused + }() + return nil +} + +func (j *JukeboxPlayer) Stop() error { + if j.state == stopped { + return nil + } + go func() { + if err := j.provider.JukeboxStop(); err != nil { + return + } + j.state = stopped + }() + return nil +} + +func (j *JukeboxPlayer) SeekPrevious() error { + track := j.curTrack + if track > 0 { + track = j.curTrack - 1 + } + return j.PlayTrackAt(track) +} + +func (j *JukeboxPlayer) SeekNext() error { + track := j.curTrack + if track >= j.numTracks { + return nil + } + return j.PlayTrackAt(track + 1) +} + +func (j *JukeboxPlayer) SeekSeconds(secs float64) error { + j.seeking = true + go func() { + j.provider.JukeboxSeek(j.curTrack, int(secs)) + j.seeking = false + }() + return nil +} + +func (j *JukeboxPlayer) IsSeeking() bool { + return j.seeking +} + +func (j *JukeboxPlayer) GetStatus() player.Status { + state := player.Stopped + if j.state == playing { + state = player.Playing + } else if j.state == paused { + state = player.Paused + } + + // TODO - the rest + + return player.Status{ + State: state, + } +} diff --git a/player/player.go b/player/player.go index a33a2eb..632fe1b 100644 --- a/player/player.go +++ b/player/player.go @@ -1,15 +1,15 @@ package player +import "github.com/dweymouth/supersonic/backend/mediaprovider" + type URLPlayer interface { BasePlayer AppendFile(url string) error - PlayFile(url string) error } -type TrackIDPlayer interface { +type TrackPlayer interface { BasePlayer - AppendTrack(trackID string) error - PlayTrack(trackID string) error + AppendTrack(track *mediaprovider.Track) error } type BasePlayer interface {