subsonic-tui/internal/playback/controller.go
Sagi Dayan 92e3c4ea7d
All checks were successful
ci / check for spelling errors (pull_request) Successful in 21s
ci / code quality (lint/tests) (pull_request) Successful in 2m29s
ci / Make sure build does not fail (pull_request) Successful in 2m25s
ci / notify-fail (pull_request) Has been skipped
ci / check for spelling errors (push) Successful in 22s
ci / code quality (lint/tests) (push) Successful in 1m53s
ci / Make sure build does not fail (push) Successful in 2m55s
ci / notify-fail (push) Has been skipped
Added ci workflow + fixed linting issues
Signed-off-by: Sagi Dayan <sagidayan@gmail.com>
2024-12-18 18:02:01 +02:00

238 lines
4.8 KiB
Go

package playback
import (
"io"
"time"
"git.dayanhub.com/sagi/subsonic-tui/internal/client"
"github.com/delucks/go-subsonic"
"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/mp3"
"github.com/gopxl/beep/v2/speaker"
)
type PlaybackState int
const (
PlaybackStateStopped = iota
PlaybackStatePaused
PlaybackStatePlaying
)
type Controller struct {
client *client.Client
stream beep.StreamSeekCloser
song *subsonic.Child
songElapsedFunc func(song *subsonic.Child, elapsed time.Duration)
initialFormat *beep.Format
currentFormat *beep.Format
position float64
closeChan chan bool
songEndedChan chan bool
songEndedFunc func(song *subsonic.Child)
ctrl *beep.Ctrl
desktopPlayback DesktopPlayback
queue *queue
playbackState PlaybackState
}
func NewController(client *client.Client) *Controller {
controller := &Controller{
client: client,
closeChan: make(chan bool),
songEndedChan: make(chan bool),
playbackState: PlaybackStateStopped,
queue: newQueue(),
}
controller.desktopPlayback = desktopPlayer(controller)
controller.desktopPlayback.Start()
go controller.playbackTicker()
return controller
}
func (c *Controller) State() PlaybackState {
return c.playbackState
}
func (c *Controller) Play(song *subsonic.Child) {
if song == nil {
return
}
r, err := c.client.Stream(song.ID)
if err != nil {
c.Stop()
return
}
c.Stream(r, song)
}
func (c *Controller) Next() {
song := c.queue.Next()
if song != nil {
c.Play(song)
}
}
func (c *Controller) AddToQueue(songs []*subsonic.Child) {
shouldPlay := c.queue.Add(songs...)
if shouldPlay {
c.Play(c.queue.GetCurrentSong())
}
c.desktopPlayback.OnPlaylistChanged()
}
func (c *Controller) GetQueuePosition() int {
return c.queue.GetPosition()
}
func (c *Controller) GetCurrentSong() *subsonic.Child {
return c.queue.GetCurrentSong()
}
func (c *Controller) SetQueue(songs []*subsonic.Child) {
c.queue.Clear()
c.Stop()
c.queue.Set(songs)
c.Play(c.queue.GetCurrentSong())
c.desktopPlayback.OnPlaylistChanged()
}
func (c *Controller) SetQueuePosition(position int) {
s := c.queue.SetPosition(position)
c.Play(s)
}
func (c *Controller) Prev() {
song := c.queue.Prev()
if song != nil {
c.Play(song)
}
}
func (c *Controller) GetQueue() []*subsonic.Child {
return c.queue.Get()
}
func (c *Controller) ClearQueue() {
c.Stop()
c.queue.Clear()
c.desktopPlayback.OnPlayPause()
c.desktopPlayback.OnSongChanged()
c.desktopPlayback.OnPlaylistChanged()
}
func (c *Controller) SetSongElapsedFunc(f func(sing *subsonic.Child, elapsed time.Duration)) {
c.songElapsedFunc = f
}
func (c *Controller) SetSongEndedFunc(f func(song *subsonic.Child)) {
c.songEndedFunc = f
}
func (c *Controller) Close() error {
c.Stop()
c.closeChan <- true
return nil
}
func (c *Controller) TogglePlayPause() {
if c.playbackState != PlaybackStateStopped {
c.ctrl.Paused = !c.ctrl.Paused
if c.ctrl.Paused {
c.playbackState = PlaybackStatePaused
} else {
c.playbackState = PlaybackStatePlaying
}
}
c.desktopPlayback.OnPlayPause()
}
func (c *Controller) Stop() {
if c.ctrl == nil {
return
}
speaker.Clear()
c.ctrl.Paused = true
c.playbackState = PlaybackStateStopped
c.ctrl = nil
c.stream = nil
c.songElapsedFunc(c.song, time.Duration(0))
c.song = nil
c.position = 0
c.desktopPlayback.OnPlayPause()
c.desktopPlayback.OnPlaylistChanged()
}
func (c *Controller) playbackTicker() {
for {
select {
case <-c.closeChan:
return
case <-c.songEndedChan:
c.Stop()
c.Next()
default:
if c.playbackState == PlaybackStatePlaying && c.song != nil {
if c.stream != nil {
pos := c.stream.Position()
elapsed := c.currentFormat.SampleRate.D(pos).Round(time.Second)
c.position = elapsed.Seconds()
c.songElapsedFunc(c.song, elapsed)
c.desktopPlayback.OnPositionChanged(int(c.position))
}
}
}
time.Sleep(time.Second)
}
}
func (c *Controller) Stream(reader io.Reader, song *subsonic.Child) {
c.Stop()
// Ensure artwork cache...
_, _ = c.client.GetCoverArt(song.CoverArt)
readerCloser := io.NopCloser(reader)
decodedMp3, format, err := mp3.Decode(readerCloser)
decodedMp3.Position()
if err != nil {
panic("mp3.NewDecoder failed: " + err.Error())
}
if c.initialFormat == nil {
c.initialFormat = &format
}
c.currentFormat = &format
var stream beep.Streamer = decodedMp3
if err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)); err != nil {
stream = beep.Resample(3, format.SampleRate, c.initialFormat.SampleRate, decodedMp3)
}
c.stream = decodedMp3
c.song = song
ctrl := &beep.Ctrl{Streamer: stream}
c.ctrl = ctrl
c.playbackState = PlaybackStatePlaying
speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
c.songEndedFunc(song)
c.songEndedChan <- true
})))
c.desktopPlayback.OnSongChanged()
c.desktopPlayback.OnPlayPause()
}