Merge pull request #815 from Tim-Kaiser/feature/shuffle
Shuffle play queue
This commit is contained in:
+65
-13
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/dweymouth/supersonic/backend/player/mpv"
|
||||
"github.com/dweymouth/supersonic/backend/util"
|
||||
"github.com/dweymouth/supersonic/backend/windows"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/20after4/configdir"
|
||||
@@ -28,11 +29,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
configFile = "config.toml"
|
||||
portableDir = "supersonic_portable"
|
||||
savedQueueFile = "saved_queue.json"
|
||||
themesDir = "themes"
|
||||
audioCacheSubdir = "audio"
|
||||
configFile = "config.toml"
|
||||
portableDir = "supersonic_portable"
|
||||
savedQueueFile = "saved_queue.json"
|
||||
savedUnshuffledQueueFile = "saved_unshuffled_queue.json"
|
||||
savedShuffledQueueFile = "saved_shuffled_queue.json"
|
||||
themesDir = "themes"
|
||||
audioCacheSubdir = "audio"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -613,31 +616,80 @@ func (a *App) SavePlayQueueIfEnabled() {
|
||||
queueServer = qs
|
||||
}
|
||||
}
|
||||
SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, path.Join(a.configDir, savedQueueFile), queueServer)
|
||||
SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetActivePlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedQueueFile), queueServer)
|
||||
if a.Config.Playback.Shuffle {
|
||||
// if shuffle
|
||||
// save the unshuffled queue to enable unshuffling on restarting supersonic
|
||||
// save the shuffled queue again to enable checking if the playQueue was changed server side on start up
|
||||
|
||||
// both files are just saved locally
|
||||
SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetPlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedUnshuffledQueueFile), nil)
|
||||
SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetShuffledPlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedShuffledQueueFile), nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) LoadSavedPlayQueue() error {
|
||||
queueFilePath := path.Join(a.configDir, savedQueueFile)
|
||||
queue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer)
|
||||
playQueue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(queue.Tracks) == 0 {
|
||||
|
||||
var unshuffledPlayQueue *SavedPlayQueue
|
||||
var shuffledPlayQueue *SavedPlayQueue
|
||||
|
||||
isShuffle := a.Config.Playback.Shuffle
|
||||
if isShuffle {
|
||||
unshuffledQueueFilePath := path.Join(a.configDir, savedUnshuffledQueueFile)
|
||||
unshuffledPlayQueue, err = LoadPlayQueue(unshuffledQueueFilePath, a.ServerManager, false)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shuffledQueueFilePath := path.Join(a.configDir, savedShuffledQueueFile)
|
||||
shuffledPlayQueue, err = LoadPlayQueue(shuffledQueueFilePath, a.ServerManager, false)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(playQueue.Tracks) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(a.PlaybackManager.GetPlayQueue()) > 0 {
|
||||
if len(a.PlaybackManager.GetActivePlayQueue()) > 0 {
|
||||
// don't restore play queue if the user has already queued new tracks
|
||||
return nil
|
||||
}
|
||||
|
||||
a.PlaybackManager.LoadTracks(queue.Tracks, Replace, false)
|
||||
if queue.TrackIndex >= 0 && queue.TrackIndex < len(queue.Tracks) {
|
||||
if isShuffle {
|
||||
serverStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(playQueue.Tracks)
|
||||
clientStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(shuffledPlayQueue.Tracks)
|
||||
|
||||
// Compare items by ID. This fails if any 2 elements don't match up. Two queues with the same items but different order will thus not count as same
|
||||
if slices.EqualFunc(serverStatePlayQueue, clientStatePlayQueue, func(a, b mediaprovider.MediaItem) bool {
|
||||
return (a.Metadata().ID == b.Metadata().ID)
|
||||
}) {
|
||||
a.PlaybackManager.SetQueueState(playQueue.Tracks, ShuffledPlayQueue)
|
||||
a.PlaybackManager.SetQueueState(unshuffledPlayQueue.Tracks, PlayQueue)
|
||||
} else {
|
||||
a.PlaybackManager.SetShuffle(false)
|
||||
a.PlaybackManager.SetQueueState(playQueue.Tracks, PlayQueue)
|
||||
}
|
||||
|
||||
} else {
|
||||
a.PlaybackManager.SetQueueState(playQueue.Tracks, PlayQueue)
|
||||
}
|
||||
|
||||
if playQueue.TrackIndex >= 0 && playQueue.TrackIndex < len(playQueue.Tracks) {
|
||||
// TODO: This isn't ideal but doesn't seem to cause an audible play-for-a-split-second artifact
|
||||
a.PlaybackManager.PlayTrackAt(queue.TrackIndex)
|
||||
a.PlaybackManager.PlayTrackAt(playQueue.TrackIndex)
|
||||
a.PlaybackManager.Pause()
|
||||
time.Sleep(100 * time.Millisecond) // MPV seek fails if run quickly after
|
||||
a.PlaybackManager.SeekSeconds(queue.TimePos)
|
||||
a.PlaybackManager.SeekSeconds(playQueue.TimePos)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ type NowPlayingPageConfig struct {
|
||||
|
||||
type PlaybackConfig struct {
|
||||
Autoplay bool
|
||||
Shuffle bool
|
||||
RepeatMode string
|
||||
SkipOneStarWhenShuffling bool
|
||||
SkipKeywordWhenShuffling string
|
||||
@@ -263,6 +264,7 @@ func DefaultConfig(appVersionTag string) *Config {
|
||||
},
|
||||
Playback: PlaybackConfig{
|
||||
Autoplay: false,
|
||||
Shuffle: false,
|
||||
RepeatMode: "None",
|
||||
UseWaveformSeekbar: false,
|
||||
},
|
||||
|
||||
@@ -25,6 +25,8 @@ const (
|
||||
// arg2: InsertMode
|
||||
// arg3: bool (shuffle)
|
||||
cmdLoadItems
|
||||
cmdLoadItemsAndPlayAtIdx
|
||||
cmdSetQueueState
|
||||
cmdLoadRadioStation // arg: *mediaprovider.RadioStation, arg2: InsertQueueMode
|
||||
|
||||
cmdForceRestartPlayback
|
||||
@@ -160,6 +162,29 @@ func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insert
|
||||
c.cmdAvailable.Signal()
|
||||
}
|
||||
|
||||
func (c *playbackCommandQueue) LoadItemsAndPlayAtIdx(items []mediaprovider.MediaItem, shuffle bool, idx int) {
|
||||
c.mutex.Lock()
|
||||
c.queue = append(c.queue, playbackCommand{
|
||||
Type: cmdLoadItemsAndPlayAtIdx,
|
||||
Arg: items,
|
||||
Arg2: shuffle,
|
||||
Arg3: idx,
|
||||
})
|
||||
c.mutex.Unlock()
|
||||
c.cmdAvailable.Signal()
|
||||
}
|
||||
|
||||
func (c *playbackCommandQueue) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) {
|
||||
c.mutex.Lock()
|
||||
c.queue = append(c.queue, playbackCommand{
|
||||
Type: cmdSetQueueState,
|
||||
Arg: tracks,
|
||||
Arg2: queueType,
|
||||
})
|
||||
c.mutex.Unlock()
|
||||
c.cmdAvailable.Signal()
|
||||
}
|
||||
|
||||
func (c *playbackCommandQueue) addCommand(command playbackCommand) {
|
||||
c.mutex.Lock()
|
||||
c.queue = append(c.queue, command)
|
||||
@@ -194,7 +219,7 @@ func (c *playbackCommandQueue) seekBackOrFwd(direction int) {
|
||||
switch cmd.Type {
|
||||
case cmdSeekFwdBackN:
|
||||
lastIdx = i
|
||||
case cmdRemoveTracksFromQueue, cmdLoadItems, cmdPlayTrackAt,
|
||||
case cmdRemoveTracksFromQueue, cmdLoadItems, cmdSetQueueState, cmdPlayTrackAt,
|
||||
cmdLoadRadioStation, cmdUpdatePlayQueue, cmdStopAndClearPlayQueue:
|
||||
// any queue-modifying command means we can't coalesce any
|
||||
// more seekFwdBackN commands before here
|
||||
|
||||
+298
-87
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -33,6 +34,14 @@ const (
|
||||
Append
|
||||
)
|
||||
|
||||
type QueueType int
|
||||
|
||||
const (
|
||||
PlayQueue QueueType = iota
|
||||
ShuffledPlayQueue
|
||||
Both
|
||||
)
|
||||
|
||||
// The playback loop mode (LoopNone, LoopAll, LoopOne).
|
||||
type LoopMode int
|
||||
|
||||
@@ -62,10 +71,13 @@ type playbackEngine struct {
|
||||
latestTrackPosition float64 // cleared by checkScrobble
|
||||
callbacksDisabled bool
|
||||
|
||||
playQueue []mediaprovider.MediaItem
|
||||
playQueue []mediaprovider.MediaItem
|
||||
shuffledPlayQueue []mediaprovider.MediaItem
|
||||
|
||||
nowPlayingIdx int
|
||||
isRadio bool
|
||||
loopMode LoopMode
|
||||
shuffle bool
|
||||
|
||||
pauseAfterCurrent bool // flag to pause playback after current track ends
|
||||
|
||||
@@ -98,6 +110,7 @@ type playbackEngine struct {
|
||||
onSongChange []func(nowPlaying mediaprovider.MediaItem, justScrobbledIfAny *mediaprovider.Track)
|
||||
onPlayTimeUpdate []func(float64, float64, bool)
|
||||
onLoopModeChange []func(LoopMode)
|
||||
onShuffleChange []func(bool)
|
||||
onVolumeChange []func(int)
|
||||
onSeek []func()
|
||||
onPaused []func()
|
||||
@@ -137,6 +150,8 @@ func NewPlaybackEngine(
|
||||
pm.loopMode = LoopOne
|
||||
}
|
||||
|
||||
pm.shuffle = playbackCfg.Shuffle
|
||||
|
||||
pm.registerPlayerCallbacks(p)
|
||||
s.OnLogout(func() {
|
||||
pm.StopAndClearPlayQueue()
|
||||
@@ -216,12 +231,78 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ======================= START PLAY QUEUE FUNCS ===========================
|
||||
|
||||
func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem {
|
||||
return p.playQueue
|
||||
}
|
||||
|
||||
func (p *playbackEngine) getShuffledPlayQueue() []mediaprovider.MediaItem {
|
||||
return p.shuffledPlayQueue
|
||||
}
|
||||
|
||||
func (p *playbackEngine) getActivePlayQueue() []mediaprovider.MediaItem {
|
||||
if p.shuffle {
|
||||
return p.shuffledPlayQueue
|
||||
}
|
||||
return p.playQueue
|
||||
}
|
||||
|
||||
func (p *playbackEngine) getPlayQueueLength() int {
|
||||
return len(p.getActivePlayQueue())
|
||||
}
|
||||
|
||||
func (p *playbackEngine) clearPlayQueue() {
|
||||
p.player.Stop(false)
|
||||
p.nowPlayingIdx = -1
|
||||
p.playQueue = nil
|
||||
p.shuffledPlayQueue = nil
|
||||
}
|
||||
|
||||
func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) {
|
||||
p.playQueue = items
|
||||
}
|
||||
|
||||
func (p *playbackEngine) setShuffledPlayQueue(items []mediaprovider.MediaItem) {
|
||||
p.shuffledPlayQueue = items
|
||||
}
|
||||
|
||||
func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem {
|
||||
return p.getActivePlayQueue()[idx]
|
||||
}
|
||||
|
||||
func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaItem, idx int, queueType QueueType) {
|
||||
switch queueType {
|
||||
case Both:
|
||||
p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...)
|
||||
p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...)
|
||||
case PlayQueue:
|
||||
p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...)
|
||||
case ShuffledPlayQueue:
|
||||
p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem {
|
||||
return deepCopyMediaItemSlice(p.getPlayQueue())
|
||||
}
|
||||
|
||||
func (p *playbackEngine) GetShuffledPlayQueueDeepCopy() []mediaprovider.MediaItem {
|
||||
return deepCopyMediaItemSlice(p.getShuffledPlayQueue())
|
||||
}
|
||||
|
||||
func (p *playbackEngine) GetActivePlayQueueDeepCopy() []mediaprovider.MediaItem {
|
||||
return deepCopyMediaItemSlice(p.getActivePlayQueue())
|
||||
}
|
||||
|
||||
// ======================== END PLAY QUEUE FUNCS =============================
|
||||
|
||||
func (p *playbackEngine) PlayTrackAt(idx int) error {
|
||||
return p.playTrackAt(idx, 0)
|
||||
}
|
||||
|
||||
func (p *playbackEngine) playTrackAt(idx int, startTime float64) error {
|
||||
if l := len(p.playQueue); idx < 0 || idx >= l {
|
||||
if l := p.getPlayQueueLength(); idx < 0 || idx >= l {
|
||||
return fmt.Errorf("track index (%d) out of range (0-%d)", idx, l)
|
||||
}
|
||||
// scrobble current track if needed
|
||||
@@ -234,10 +315,10 @@ func (p *playbackEngine) playTrackAt(idx int, startTime float64) error {
|
||||
|
||||
// Gets the curently playing media item, if any.
|
||||
func (p *playbackEngine) NowPlaying() mediaprovider.MediaItem {
|
||||
if p.nowPlayingIdx < 0 || len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped {
|
||||
if p.nowPlayingIdx < 0 || p.getPlayQueueLength() == 0 || p.player.GetStatus().State == player.Stopped {
|
||||
return nil
|
||||
}
|
||||
return p.playQueue[p.nowPlayingIdx]
|
||||
return p.getPlayQueueItemAt(p.nowPlayingIdx)
|
||||
}
|
||||
|
||||
func (p *playbackEngine) NowPlayingIndex() int {
|
||||
@@ -261,6 +342,58 @@ func (p *playbackEngine) GetLoopMode() LoopMode {
|
||||
return p.loopMode
|
||||
}
|
||||
|
||||
func (p *playbackEngine) GetTrackIdxByIdFrom(items []mediaprovider.MediaItem, id string) int {
|
||||
foundIdx := -1
|
||||
for i, tr := range items {
|
||||
if tr.Metadata().ID == id {
|
||||
foundIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return foundIdx
|
||||
}
|
||||
|
||||
func (p *playbackEngine) SetShuffle(shuffle bool) {
|
||||
if p.shuffle == shuffle {
|
||||
return
|
||||
}
|
||||
|
||||
for _, cb := range p.onShuffleChange {
|
||||
cb(shuffle)
|
||||
}
|
||||
p.shuffle = shuffle
|
||||
|
||||
// guard against changing shuffle with an empty queue
|
||||
if p.getPlayQueue() == nil || len(p.getPlayQueue()) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
newNowPlayingIdx := 0
|
||||
if shuffle {
|
||||
shuffledQueue := deepCopyMediaItemSlice(p.playQueue)
|
||||
rand.Shuffle(len(shuffledQueue), func(i, j int) {
|
||||
shuffledQueue[i], shuffledQueue[j] = shuffledQueue[j], shuffledQueue[i]
|
||||
})
|
||||
if p.nowPlayingIdx >= 0 && len(p.getPlayQueue()) > p.nowPlayingIdx {
|
||||
nowPlayingID := p.getPlayQueue()[p.nowPlayingIdx].Metadata().ID
|
||||
p.setShuffledPlayQueue(sharedutil.ReorderItems(shuffledQueue, []int{p.GetTrackIdxByIdFrom(shuffledQueue, nowPlayingID)}, 0))
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if p.nowPlayingIdx >= 0 && len(p.getShuffledPlayQueue()) > p.nowPlayingIdx {
|
||||
nowPlayingID := p.getShuffledPlayQueue()[p.nowPlayingIdx].Metadata().ID
|
||||
newNowPlayingIdx = p.GetTrackIdxByIdFrom(p.playQueue, nowPlayingID)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.nowPlayingIdx = newNowPlayingIdx
|
||||
p.handleNextTrackUpdated()
|
||||
p.invokeNoArgCallbacks(p.onQueueChange)
|
||||
}
|
||||
|
||||
func (p *playbackEngine) PlaybackStatus() PlaybackStatus {
|
||||
stat := p.pendingPlayerChangeStatus
|
||||
if !p.pendingPlayerChange {
|
||||
@@ -311,7 +444,7 @@ func (p *playbackEngine) SeekFwdBackN(n int) error {
|
||||
return p.player.SeekSeconds(0) // seek back in current song
|
||||
}
|
||||
|
||||
lastIdx := len(p.playQueue) - 1
|
||||
lastIdx := p.getPlayQueueLength() - 1
|
||||
newIdx := min(lastIdx, max(0, idx+n))
|
||||
|
||||
if idx == lastIdx && n > 0 {
|
||||
@@ -356,6 +489,23 @@ func (p *playbackEngine) Continue() error {
|
||||
return p.player.Continue()
|
||||
}
|
||||
|
||||
// Load items into the specified queue(s). This overrides the queue. For standard inserts or replaces use p.LoadItems
|
||||
// This is used on program startup to populate both the playQueue and shuffledPlayQueue with the previously saved client state.
|
||||
func (p *playbackEngine) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) error {
|
||||
newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks)
|
||||
switch queueType {
|
||||
case PlayQueue:
|
||||
p.setPlayQueue(newTracks)
|
||||
case ShuffledPlayQueue:
|
||||
p.setShuffledPlayQueue(newTracks)
|
||||
case Both:
|
||||
p.setPlayQueue(newTracks)
|
||||
p.setShuffledPlayQueue(newTracks)
|
||||
}
|
||||
p.invokeNoArgCallbacks(p.onQueueChange)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load items into the play queue.
|
||||
// If replacing the current queue (!appendToQueue), playback will be stopped.
|
||||
func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error {
|
||||
@@ -363,32 +513,73 @@ func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueM
|
||||
return p.doLoaditems(newItems, insertQueueMode, shuffle)
|
||||
}
|
||||
|
||||
// Load items into the play queue.
|
||||
// If replacing the current queue (!appendToQueue), playback will be stopped.
|
||||
func (p *playbackEngine) LoadItemsAndPlayAtIdx(items []mediaprovider.MediaItem, shuffle bool, idx int) error {
|
||||
newItems := deepCopyMediaItemSlice(items)
|
||||
|
||||
if p.shuffle || shuffle {
|
||||
rand.Shuffle(len(newItems), func(i, j int) {
|
||||
newItems[i], newItems[j] = newItems[j], newItems[i]
|
||||
})
|
||||
if idx < len(items) {
|
||||
nowPlayingID := items[idx].Metadata().ID
|
||||
p.setPlayQueue(deepCopyMediaItemSlice(items))
|
||||
p.setShuffledPlayQueue(sharedutil.ReorderItems(newItems, []int{p.GetTrackIdxByIdFrom(newItems, nowPlayingID)}, 0))
|
||||
p.PlayTrackAt(0)
|
||||
}
|
||||
} else {
|
||||
p.doLoaditems(newItems, Replace, shuffle)
|
||||
p.PlayTrackAt(idx)
|
||||
}
|
||||
|
||||
p.handleNextTrackUpdated()
|
||||
p.invokeNoArgCallbacks(p.onQueueChange)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load tracks into the play queue.
|
||||
// If replacing the current queue (!appendToQueue), playback will be stopped.
|
||||
func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) error {
|
||||
newTracks := copyTrackSliceToMediaItemSlice(tracks)
|
||||
newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks)
|
||||
return p.doLoaditems(newTracks, insertQueueMode, shuffle)
|
||||
}
|
||||
|
||||
func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error {
|
||||
queueType := PlayQueue
|
||||
|
||||
if insertQueueMode == Replace {
|
||||
p.player.Stop(false)
|
||||
p.nowPlayingIdx = -1
|
||||
p.playQueue = nil
|
||||
p.clearPlayQueue()
|
||||
}
|
||||
if nextChanged := len(items) > 0 && (insertQueueMode != Append || (p.nowPlayingIdx == len(p.playQueue)-1)); nextChanged {
|
||||
if nextChanged := len(items) > 0 && (insertQueueMode != Append || (p.nowPlayingIdx == p.getPlayQueueLength()-1)); nextChanged {
|
||||
defer p.handleNextTrackUpdated()
|
||||
}
|
||||
|
||||
if shuffle {
|
||||
rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] })
|
||||
}
|
||||
insertIdx := p.getPlayQueueLength()
|
||||
|
||||
insertIdx := len(p.playQueue)
|
||||
if insertQueueMode == InsertNext {
|
||||
insertIdx = p.nowPlayingIdx + 1
|
||||
if insertQueueMode == Replace {
|
||||
if shuffle || p.shuffle {
|
||||
shuffledItems := deepCopyMediaItemSlice(items)
|
||||
rand.Shuffle(len(shuffledItems), func(i, j int) { shuffledItems[i], shuffledItems[j] = shuffledItems[j], shuffledItems[i] })
|
||||
|
||||
p.insertItemsIntoPlayQueueAt(items, insertIdx, PlayQueue)
|
||||
p.insertItemsIntoPlayQueueAt(shuffledItems, insertIdx, ShuffledPlayQueue)
|
||||
} else {
|
||||
p.insertItemsIntoPlayQueueAt(items, insertIdx, queueType)
|
||||
}
|
||||
} else {
|
||||
if shuffle {
|
||||
rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] })
|
||||
}
|
||||
if insertQueueMode == InsertNext {
|
||||
insertIdx = p.nowPlayingIdx + 1
|
||||
}
|
||||
if p.shuffle {
|
||||
queueType = Both
|
||||
}
|
||||
p.insertItemsIntoPlayQueueAt(items, insertIdx, queueType)
|
||||
}
|
||||
p.playQueue = append(p.playQueue[:insertIdx], append(items, p.playQueue[insertIdx:]...)...)
|
||||
|
||||
p.invokeNoArgCallbacks(p.onQueueChange)
|
||||
return nil
|
||||
@@ -396,46 +587,32 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu
|
||||
|
||||
func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, insertMode InsertQueueMode) {
|
||||
if insertMode == Replace {
|
||||
p.player.Stop(false)
|
||||
p.nowPlayingIdx = -1
|
||||
p.playQueue = nil
|
||||
p.clearPlayQueue()
|
||||
}
|
||||
if nextChanged := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == len(p.playQueue)-1); nextChanged {
|
||||
if nextChanged := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == p.getPlayQueueLength()-1); nextChanged {
|
||||
p.handleNextTrackUpdated()
|
||||
}
|
||||
insertIdx := len(p.playQueue)
|
||||
insertIdx := p.getPlayQueueLength()
|
||||
if insertMode == InsertNext {
|
||||
insertIdx = p.nowPlayingIdx + 1
|
||||
}
|
||||
new := make([]mediaprovider.MediaItem, len(p.playQueue)+1)
|
||||
firstHalf := p.playQueue[:insertIdx]
|
||||
copy(new, firstHalf)
|
||||
new[len(firstHalf)] = radio
|
||||
copy(new[len(firstHalf)+1:], p.playQueue[insertIdx:])
|
||||
p.playQueue = new
|
||||
|
||||
p.insertItemsIntoPlayQueueAt([]mediaprovider.MediaItem{radio}, insertIdx, Both)
|
||||
p.invokeNoArgCallbacks(p.onQueueChange)
|
||||
}
|
||||
|
||||
// Stop playback and clear the play queue.
|
||||
func (p *playbackEngine) StopAndClearPlayQueue() {
|
||||
changed := len(p.playQueue) > 0
|
||||
p.player.Stop(false)
|
||||
p.playQueue = nil
|
||||
p.nowPlayingIdx = -1
|
||||
changed := p.getPlayQueueLength() > 0
|
||||
p.clearPlayQueue()
|
||||
if changed {
|
||||
p.invokeNoArgCallbacks(p.onQueueChange)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem {
|
||||
return deepCopyMediaItemSlice(p.playQueue)
|
||||
}
|
||||
|
||||
// Any time the user changes the favorite status of a track elsewhere in the app,
|
||||
// this should be called to ensure the in-memory track model is updated.
|
||||
func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) {
|
||||
if item := sharedutil.FindMediaItemByID(id, p.playQueue); item != nil {
|
||||
if item := sharedutil.FindMediaItemByID(id, p.getActivePlayQueue()); item != nil {
|
||||
if tr, ok := item.(*mediaprovider.Track); ok {
|
||||
tr.Favorite = fav
|
||||
}
|
||||
@@ -445,7 +622,7 @@ func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) {
|
||||
// Any time the user changes the rating of a track elsewhere in the app,
|
||||
// this should be called to ensure the in-memory track model is updated.
|
||||
func (p *playbackEngine) OnTrackRatingChanged(id string, rating int) {
|
||||
if item := sharedutil.FindMediaItemByID(id, p.playQueue); item != nil {
|
||||
if item := sharedutil.FindMediaItemByID(id, p.getActivePlayQueue()); item != nil {
|
||||
if tr, ok := item.(*mediaprovider.Track); ok {
|
||||
tr.Rating = rating
|
||||
}
|
||||
@@ -459,7 +636,7 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error
|
||||
newQueue := deepCopyMediaItemSlice(items)
|
||||
newNowPlayingIdx := -1
|
||||
if p.nowPlayingIdx >= 0 {
|
||||
nowPlayingID := p.playQueue[p.nowPlayingIdx].Metadata().ID
|
||||
nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID
|
||||
for i, tr := range newQueue {
|
||||
if tr.Metadata().ID == nowPlayingID {
|
||||
newNowPlayingIdx = i
|
||||
@@ -467,8 +644,11 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.playQueue = newQueue
|
||||
if p.shuffle {
|
||||
p.setShuffledPlayQueue(newQueue)
|
||||
} else {
|
||||
p.setPlayQueue(newQueue)
|
||||
}
|
||||
if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 {
|
||||
return p.Stop()
|
||||
}
|
||||
@@ -482,35 +662,39 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error
|
||||
}
|
||||
|
||||
func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) {
|
||||
newQueue := make([]mediaprovider.MediaItem, 0, len(p.playQueue)-len(idxs))
|
||||
idxSet := sharedutil.ToSet(idxs)
|
||||
isPlayingTrackRemoved := false
|
||||
isNextPlayingTrackremoved := false
|
||||
nowPlaying := p.NowPlayingIndex()
|
||||
newNowPlaying := nowPlaying
|
||||
for i, tr := range p.playQueue {
|
||||
if _, ok := idxSet[i]; ok {
|
||||
if i < nowPlaying {
|
||||
// if removing a track earlier than the currently playing one (if any),
|
||||
// decrement new now playing index by one to account for new position in queue
|
||||
newNowPlaying--
|
||||
} else if i == nowPlaying {
|
||||
isPlayingTrackRemoved = true
|
||||
// If we are removing the currently playing track, we need to scrobble it
|
||||
p.checkScrobble()
|
||||
p.alreadyScrobbled = true
|
||||
} else if nowPlaying >= 0 && i == nowPlaying+1 {
|
||||
isNextPlayingTrackremoved = true
|
||||
|
||||
if p.shuffle {
|
||||
// remove tracks by ID from playQueue
|
||||
ids := p.GetTrackIdsFromIdx(idxs)
|
||||
newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs))
|
||||
for _, tr := range p.getPlayQueue() {
|
||||
if slices.Contains(ids, tr.Metadata().ID) {
|
||||
//remove id from id list, handles having the same track present multiple times in playQueue
|
||||
idx := slices.Index(ids, tr.Metadata().ID)
|
||||
ids = slices.Delete(ids, idx, idx+1)
|
||||
} else {
|
||||
// not removing this track
|
||||
newPlayQueue = append(newPlayQueue, tr)
|
||||
}
|
||||
} else {
|
||||
// not removing this track
|
||||
newQueue = append(newQueue, tr)
|
||||
}
|
||||
p.setPlayQueue(newPlayQueue)
|
||||
|
||||
newShuffledQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs))
|
||||
p.RemoveTracksFromQueueByIdx(idxs, &newShuffledQueue, &newNowPlaying, &isPlayingTrackRemoved, &isNextPlayingTrackremoved)
|
||||
p.setShuffledPlayQueue(newShuffledQueue)
|
||||
} else {
|
||||
newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs))
|
||||
p.RemoveTracksFromQueueByIdx(idxs, &newPlayQueue, &newNowPlaying, &isPlayingTrackRemoved, &isNextPlayingTrackremoved)
|
||||
p.setPlayQueue(newPlayQueue)
|
||||
}
|
||||
p.playQueue = newQueue
|
||||
|
||||
p.nowPlayingIdx = newNowPlaying
|
||||
if isPlayingTrackRemoved {
|
||||
if newNowPlaying == len(newQueue) {
|
||||
if newNowPlaying == p.getPlayQueueLength() {
|
||||
// we had been playing the last track, and removed it
|
||||
p.Stop()
|
||||
} else {
|
||||
@@ -520,7 +704,7 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) {
|
||||
// setNextTrack and onSongChange callbacks will be handled
|
||||
// when we receive new track event from player
|
||||
} else if isNextPlayingTrackremoved {
|
||||
if newNowPlaying < len(newQueue)-1 {
|
||||
if newNowPlaying < p.getPlayQueueLength()-1 {
|
||||
p.handleNextTrackUpdated()
|
||||
} else {
|
||||
// no next track to play
|
||||
@@ -531,6 +715,39 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) {
|
||||
p.invokeNoArgCallbacks(p.onQueueChange)
|
||||
}
|
||||
|
||||
func (p *playbackEngine) RemoveTracksFromQueueByIdx(idxs []int, newQueue *[]mediaprovider.MediaItem, newNowPlaying *int, isPlayingTrackRemoved *bool, isNextPlayingTrackRemoved *bool) {
|
||||
idxSet := sharedutil.ToSet(idxs)
|
||||
nowPlaying := p.NowPlayingIndex()
|
||||
|
||||
for i, tr := range p.getActivePlayQueue() {
|
||||
if _, ok := idxSet[i]; ok {
|
||||
if i < nowPlaying {
|
||||
// if removing a track earlier than the currently playing one (if any),
|
||||
// decrement new now playing index by one to account for new position in queue
|
||||
*newNowPlaying--
|
||||
} else if i == nowPlaying {
|
||||
*isPlayingTrackRemoved = true
|
||||
// If we are removing the currently playing track, we need to scrobble it
|
||||
p.checkScrobble()
|
||||
p.alreadyScrobbled = true
|
||||
} else if nowPlaying >= 0 && i == nowPlaying+1 {
|
||||
*isNextPlayingTrackRemoved = true
|
||||
}
|
||||
} else {
|
||||
// not removing this track
|
||||
*newQueue = append(*newQueue, tr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playbackEngine) GetTrackIdsFromIdx(idx []int) []string {
|
||||
ids := make([]string, 0, len(idx))
|
||||
for _, v := range idx {
|
||||
ids = append(ids, p.getPlayQueueItemAt(v).Metadata().ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (p *playbackEngine) SetReplayGainOptions(config ReplayGainConfig) {
|
||||
rGainPlayer, ok := p.player.(player.ReplayGainPlayer)
|
||||
if !ok {
|
||||
@@ -577,11 +794,11 @@ func (p *playbackEngine) cacheNextTracks() {
|
||||
// the "currently" playing track, since we're probably about to play it
|
||||
npI := max(p.nowPlayingIdx, 0)
|
||||
for _, idx := range [3]int{npI, npI + 1, npI + 2} {
|
||||
if idx > 0 && idx < len(p.playQueue) {
|
||||
item := p.playQueue[idx]
|
||||
if idx > 0 && idx < p.getPlayQueueLength() {
|
||||
item := p.getPlayQueueItemAt(idx)
|
||||
if item.Metadata().Type == mediaprovider.MediaItemTypeTrack {
|
||||
fetch = append(fetch, AudioCacheRequest{
|
||||
ID: p.playQueue[idx].Metadata().ID,
|
||||
ID: p.getPlayQueueItemAt(idx).Metadata().ID,
|
||||
DownloadURL: p.getMediaURLForIdx(idx),
|
||||
})
|
||||
}
|
||||
@@ -606,14 +823,14 @@ func (p *playbackEngine) handleOnTrackChange() {
|
||||
}
|
||||
if p.pendingTrackChangeNum < 0 && (p.wasStopped || p.loopMode != LoopOne) {
|
||||
p.nowPlayingIdx++
|
||||
if p.loopMode == LoopAll && p.nowPlayingIdx == len(p.playQueue) {
|
||||
if p.loopMode == LoopAll && p.nowPlayingIdx == p.getPlayQueueLength() {
|
||||
p.nowPlayingIdx = 0 // wrapped around
|
||||
}
|
||||
} else if p.pendingTrackChangeNum >= 0 {
|
||||
p.nowPlayingIdx = p.pendingTrackChangeNum
|
||||
p.pendingTrackChangeNum = -1
|
||||
}
|
||||
nowPlaying := p.playQueue[p.nowPlayingIdx]
|
||||
nowPlaying := p.getPlayQueueItemAt(p.nowPlayingIdx)
|
||||
_, isRadio := nowPlaying.(*mediaprovider.RadioStation)
|
||||
p.isRadio = isRadio
|
||||
|
||||
@@ -656,7 +873,7 @@ func (p *playbackEngine) handleNextTrackUpdated() {
|
||||
for _, cb := range p.onBeforeSongChange {
|
||||
var item mediaprovider.MediaItem
|
||||
if idx := p.nextPlayingIndex(); idx >= 0 {
|
||||
item = p.playQueue[idx]
|
||||
item = p.getPlayQueueItemAt(idx)
|
||||
}
|
||||
cb(item)
|
||||
}
|
||||
@@ -665,14 +882,14 @@ func (p *playbackEngine) handleNextTrackUpdated() {
|
||||
func (p *playbackEngine) nextPlayingIndex() int {
|
||||
switch p.loopMode {
|
||||
case LoopNone:
|
||||
if p.nowPlayingIdx >= len(p.playQueue)-1 {
|
||||
if p.nowPlayingIdx >= p.getPlayQueueLength()-1 {
|
||||
return -1
|
||||
}
|
||||
return p.nowPlayingIdx + 1
|
||||
case LoopOne:
|
||||
return p.nowPlayingIdx
|
||||
case LoopAll:
|
||||
if p.nowPlayingIdx >= len(p.playQueue)-1 {
|
||||
if p.nowPlayingIdx >= p.getPlayQueueLength()-1 {
|
||||
return 0
|
||||
}
|
||||
return p.nowPlayingIdx + 1
|
||||
@@ -684,7 +901,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
|
||||
var item mediaprovider.MediaItem
|
||||
var url string
|
||||
if idx >= 0 {
|
||||
item = p.playQueue[idx]
|
||||
item = p.getPlayQueueItemAt(idx)
|
||||
url = p.getMediaURLForIdx(idx)
|
||||
}
|
||||
track, isTrack := item.(*mediaprovider.Track)
|
||||
@@ -709,7 +926,6 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
|
||||
} else {
|
||||
title = icytitle
|
||||
}
|
||||
log.Println("Radio metadata changed: ", icytitle)
|
||||
for _, cb := range p.onRadioMetadataChange {
|
||||
cb(meta.Name, title, artist)
|
||||
}
|
||||
@@ -728,7 +944,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
|
||||
} else if trP, ok := p.player.(player.TrackPlayer); ok {
|
||||
var track *mediaprovider.Track
|
||||
if idx >= 0 {
|
||||
track, ok = p.playQueue[idx].(*mediaprovider.Track)
|
||||
track, ok = p.getPlayQueueItemAt(idx).(*mediaprovider.Track)
|
||||
if !ok {
|
||||
return errors.New("cannot play non-Track media item with TrackPlayer")
|
||||
}
|
||||
@@ -743,7 +959,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
|
||||
|
||||
func (p *playbackEngine) getMediaURLForIdx(idx int) string {
|
||||
var url string
|
||||
item := p.playQueue[idx]
|
||||
item := p.getPlayQueueItemAt(idx)
|
||||
if tr, ok := item.(*mediaprovider.Track); ok {
|
||||
var ts *mediaprovider.TranscodeSettings
|
||||
if p.transcodeCfg.RequestTranscode {
|
||||
@@ -765,10 +981,10 @@ func (p *playbackEngine) setNextTrack(idx int) error {
|
||||
|
||||
// call BEFORE updating p.nowPlayingIdx
|
||||
func (p *playbackEngine) checkScrobble() {
|
||||
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
|
||||
if !p.scrobbleCfg.Enabled || p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 {
|
||||
return
|
||||
}
|
||||
track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track)
|
||||
track, ok := p.getPlayQueueItemAt(p.nowPlayingIdx).(*mediaprovider.Track)
|
||||
if !ok {
|
||||
return // radio stations are not scrobbled
|
||||
}
|
||||
@@ -794,10 +1010,10 @@ func (p *playbackEngine) checkScrobble() {
|
||||
}
|
||||
|
||||
func (p *playbackEngine) sendNowPlayingScrobble() {
|
||||
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
|
||||
if !p.scrobbleCfg.Enabled || p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 {
|
||||
return
|
||||
}
|
||||
track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track)
|
||||
track, ok := p.getPlayQueueItemAt(p.nowPlayingIdx).(*mediaprovider.Track)
|
||||
if !ok {
|
||||
return // radio stations are not scrobbled
|
||||
}
|
||||
@@ -821,14 +1037,6 @@ func deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem) []mediaprovider.Me
|
||||
return newTracks
|
||||
}
|
||||
|
||||
func copyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem {
|
||||
newTracks := make([]mediaprovider.MediaItem, len(tracks))
|
||||
for i, tr := range tracks {
|
||||
newTracks[i] = tr.Copy()
|
||||
}
|
||||
return newTracks
|
||||
}
|
||||
|
||||
func (p *playbackEngine) invokeOnSongChangeCallbacks() {
|
||||
if p.callbacksDisabled {
|
||||
return
|
||||
@@ -888,6 +1096,9 @@ func (p *playbackEngine) handleTimePosUpdate(seeked bool) {
|
||||
p.needToSetNextTrack = false
|
||||
if nextIdx := p.nextPlayingIndex(); nextIdx >= 0 && nextIdx < len(p.playQueue) {
|
||||
p.setNextTrack(nextIdx)
|
||||
} else {
|
||||
// no next track to play, ensure player knows this
|
||||
p.setNextTrack(-1)
|
||||
}
|
||||
}
|
||||
if p.callbacksDisabled {
|
||||
|
||||
@@ -109,7 +109,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
|
||||
|
||||
// enqueue autoplay tracks if enabled and nearing end of queue
|
||||
if p.cfg.Autoplay && !p.pendingAutoplay && totalTime-curTime < 10.0 &&
|
||||
p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
|
||||
p.NowPlayingIndex() == p.engine.getPlayQueueLength()-1 {
|
||||
p.enqueueAutoplayTracks()
|
||||
}
|
||||
})
|
||||
@@ -132,7 +132,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
|
||||
return
|
||||
}
|
||||
// workaround for https://github.com/dweymouth/supersonic/issues/483 (see above comment)
|
||||
if p.NowPlayingIndex() != len(p.engine.playQueue) && p.PlaybackStatus().State == player.Playing {
|
||||
if p.NowPlayingIndex() != p.engine.getPlayQueueLength() && p.PlaybackStatus().State == player.Playing {
|
||||
p.lastPlayTime = 0
|
||||
go func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
@@ -333,6 +333,11 @@ func (p *PlaybackManager) OnLoopModeChange(cb func(LoopMode)) {
|
||||
p.engine.onLoopModeChange = append(p.engine.onLoopModeChange, cb)
|
||||
}
|
||||
|
||||
// Registers a callback that is notified whenever the shuffle state changes.
|
||||
func (p *PlaybackManager) OnShuffleChange(cb func(bool)) {
|
||||
p.engine.onShuffleChange = append(p.engine.onShuffleChange, cb)
|
||||
}
|
||||
|
||||
// Registers a callback that is notified whenever the volume changes.
|
||||
func (p *PlaybackManager) OnVolumeChange(cb func(int)) {
|
||||
p.engine.onVolumeChange = append(p.engine.onVolumeChange, cb)
|
||||
@@ -373,6 +378,10 @@ func (p *PlaybackManager) LoadAlbum(albumID string, insertQueueMode InsertQueueM
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) IsShuffle() bool {
|
||||
return p.cfg.Shuffle
|
||||
}
|
||||
|
||||
// Loads the specified playlist into the play queue.
|
||||
func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode InsertQueueMode, shuffle bool) error {
|
||||
playlist, err := p.engine.sm.Server.GetPlaylist(playlistID)
|
||||
@@ -386,16 +395,30 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode Insert
|
||||
// Load tracks into the play queue.
|
||||
// If replacing the current queue (!appendToQueue), playback will be stopped.
|
||||
func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) {
|
||||
items := copyTrackSliceToMediaItemSlice(tracks)
|
||||
items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks)
|
||||
p.cmdQueue.LoadItems(items, insertQueueMode, shuffle)
|
||||
}
|
||||
|
||||
// Load items into the play queue.
|
||||
// Replaces the playQueue with tracks and moves the track at idx to position 0
|
||||
func (p *PlaybackManager) LoadTracksAndPlayAtIdx(tracks []*mediaprovider.Track, shuffle bool, idx int) {
|
||||
items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks)
|
||||
p.cmdQueue.LoadItemsAndPlayAtIdx(items, shuffle, idx)
|
||||
}
|
||||
|
||||
// Load items into the currently active queue. (shuffledPlayQueue/playQueue)
|
||||
// If replacing the current queue (!appendToQueue), playback will be stopped.
|
||||
// Loading items into the shuffledPlayQueue may also modify the playQueue
|
||||
func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) {
|
||||
p.cmdQueue.LoadItems(items, insertQueueMode, shuffle)
|
||||
}
|
||||
|
||||
// Replaces the specified queue (PlayQueue/ShuffledPlayQueue) with the given tracks.
|
||||
// This is used when starting supersonic to load the queue state and directly overrides any previous data.
|
||||
// For replacing the queue while supersonic is running, use LoadTracks
|
||||
func (p *PlaybackManager) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) {
|
||||
p.cmdQueue.SetQueueState(tracks, queueType)
|
||||
}
|
||||
|
||||
// Replaces the play queue with the given set of tracks.
|
||||
// Does not stop playback if the currently playing track is in the new queue,
|
||||
// but updates the now playing index to point to the first instance of the track in the new queue.
|
||||
@@ -566,8 +589,16 @@ func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Tr
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) GetActivePlayQueue() []mediaprovider.MediaItem {
|
||||
return p.engine.GetActivePlayQueueDeepCopy()
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) GetPlayQueue() []mediaprovider.MediaItem {
|
||||
return p.engine.GetPlayQueue()
|
||||
return p.engine.GetPlayQueueDeepCopy()
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) GetShuffledPlayQueue() []mediaprovider.MediaItem {
|
||||
return p.engine.GetShuffledPlayQueueDeepCopy()
|
||||
}
|
||||
|
||||
// Any time the user changes the favorite status of a track elsewhere in the app,
|
||||
@@ -634,11 +665,16 @@ func (p *PlaybackManager) SetVolume(vol int) {
|
||||
|
||||
func (p *PlaybackManager) SetAutoplay(autoplay bool) {
|
||||
p.cfg.Autoplay = autoplay
|
||||
if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
|
||||
if autoplay && p.NowPlayingIndex() == p.engine.getPlayQueueLength()-1 {
|
||||
p.enqueueAutoplayTracks()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) SetShuffle(shuffle bool) {
|
||||
p.cfg.Shuffle = shuffle
|
||||
p.engine.SetShuffle(shuffle)
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) Volume() int {
|
||||
return p.engine.CurrentPlayer().GetVolume()
|
||||
}
|
||||
@@ -726,7 +762,7 @@ func (p *PlaybackManager) enqueueAutoplayTracks() {
|
||||
}
|
||||
|
||||
// last 500 played items
|
||||
queue := p.GetPlayQueue()
|
||||
queue := p.GetActivePlayQueue()
|
||||
if l := len(queue); l > 500 {
|
||||
queue = queue[l-500:]
|
||||
}
|
||||
@@ -841,6 +877,19 @@ func (p *PlaybackManager) runCmdQueue(ctx context.Context) {
|
||||
c.Arg3.(bool),
|
||||
)
|
||||
logIfErr("LoadItems", err)
|
||||
case cmdLoadItemsAndPlayAtIdx:
|
||||
err := p.engine.LoadItemsAndPlayAtIdx(
|
||||
c.Arg.([]mediaprovider.MediaItem),
|
||||
c.Arg2.(bool),
|
||||
c.Arg3.(int),
|
||||
)
|
||||
logIfErr("LoadItemsAndPlayAtIdx", err)
|
||||
case cmdSetQueueState:
|
||||
err := p.engine.SetQueueState(
|
||||
c.Arg.([]*mediaprovider.Track),
|
||||
c.Arg2.(QueueType),
|
||||
)
|
||||
logIfErr("SetQueueState", err)
|
||||
case cmdLoadRadioStation:
|
||||
p.engine.LoadRadioStation(
|
||||
c.Arg.(*mediaprovider.RadioStation),
|
||||
|
||||
@@ -24,8 +24,7 @@ type serializedSavedPlayQueue struct {
|
||||
|
||||
// SavePlayQueue saves the current play queue and playback position to a JSON file.
|
||||
// If the provided CanSavePlayQueue server is non-nil, it will also save to the server.
|
||||
func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string, server mediaprovider.CanSavePlayQueue) error {
|
||||
queue := pm.GetPlayQueue()
|
||||
func SavePlayQueue(serverID string, queue []mediaprovider.MediaItem, pm *PlaybackManager, filepath string, server mediaprovider.CanSavePlayQueue) error {
|
||||
stats := pm.PlaybackStatus()
|
||||
trackIdx := pm.NowPlayingIndex()
|
||||
|
||||
|
||||
@@ -112,6 +112,11 @@ func TracksToIDs(tracks []*mediaprovider.Track) []string {
|
||||
// Reorder items and return a new track slice.
|
||||
// idxToMove must contain only valid indexes into tracks, and no repeats
|
||||
func ReorderItems[T any](items []T, idxToMove []int, insertIdx int) []T {
|
||||
if len(items) < 2 {
|
||||
cpy := make([]T, len(items))
|
||||
copy(cpy, items)
|
||||
return cpy
|
||||
}
|
||||
idxToMoveSet := ToSet(idxToMove)
|
||||
|
||||
newItems := make([]T, 0, len(items))
|
||||
@@ -186,3 +191,11 @@ func DownloadFileWithContext(ctx context.Context, url string, destPath string) (
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func CopyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem {
|
||||
newTracks := make([]mediaprovider.MediaItem, len(tracks))
|
||||
for i, tr := range tracks {
|
||||
newTracks[i] = tr.Copy()
|
||||
}
|
||||
return newTracks
|
||||
}
|
||||
|
||||
+7
-1
@@ -102,10 +102,13 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
|
||||
pm.SeekFraction(f)
|
||||
})
|
||||
|
||||
bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay())
|
||||
bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay(), pm.IsShuffle())
|
||||
pm.OnLoopModeChange(func(lm backend.LoopMode) {
|
||||
fyne.Do(func() { bp.AuxControls.SetLoopMode(lm) })
|
||||
})
|
||||
pm.OnShuffleChange(func(lm bool) {
|
||||
fyne.Do(func() { bp.AuxControls.SetShuffle(lm) })
|
||||
})
|
||||
pm.OnVolumeChange(func(vol int) {
|
||||
fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) })
|
||||
})
|
||||
@@ -122,6 +125,9 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
|
||||
bp.AuxControls.OnChangeAutoplay = func(autoplay bool) {
|
||||
pm.SetAutoplay(autoplay)
|
||||
}
|
||||
bp.AuxControls.OnChangeShuffle = func(shuffle bool) {
|
||||
pm.SetShuffle(shuffle)
|
||||
}
|
||||
bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue)
|
||||
bp.AuxControls.OnShowCastMenu(contr.ShowCastMenu)
|
||||
|
||||
|
||||
@@ -461,7 +461,7 @@ func (a *NowPlayingPage) Reload() {
|
||||
a.relatedList.DisableRating = !a.canRate
|
||||
a.relatedList.DisableSharing = !a.canShare
|
||||
|
||||
a.queue = a.pm.GetPlayQueue()
|
||||
a.queue = a.pm.GetActivePlayQueue()
|
||||
a.queueList.SetItems(a.queue)
|
||||
a.totalTime = 0.0
|
||||
for _, tr := range a.queue {
|
||||
|
||||
@@ -31,11 +31,10 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget
|
||||
m.App.PlaybackManager.LoadTracks(tracks, backend.Append, false)
|
||||
}
|
||||
tracklist.OnPlayTrackAt = func(idx int) {
|
||||
m.App.PlaybackManager.LoadTracks(tracklist.GetTracks(), backend.Replace, false)
|
||||
m.App.PlaybackManager.LoadTracksAndPlayAtIdx(tracklist.GetTracks(), false, idx)
|
||||
if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto {
|
||||
m.App.PlaybackManager.SetReplayGainMode(mode)
|
||||
}
|
||||
m.App.PlaybackManager.PlayTrackAt(idx)
|
||||
}
|
||||
tracklist.OnPlaySelection = func(tracks []*mediaprovider.Track, shuffle bool) {
|
||||
m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, shuffle)
|
||||
|
||||
@@ -75,7 +75,7 @@ func New(app *backend.App, appVersion string, mainWindow fyne.Window) *Controlle
|
||||
c.initVisualizations()
|
||||
c.App.PlaybackManager.OnQueueChange(util.FyneDoFunc(func() {
|
||||
if c.popUpQueue != nil {
|
||||
c.popUpQueueList.SetItems(c.App.PlaybackManager.GetPlayQueue())
|
||||
c.popUpQueueList.SetItems(c.App.PlaybackManager.GetActivePlayQueue())
|
||||
}
|
||||
}))
|
||||
c.App.PlaybackManager.OnSongChange(func(track mediaprovider.MediaItem, _ *mediaprovider.Track) {
|
||||
@@ -192,7 +192,7 @@ func (m *Controller) ShowPopUpPlayQueue() {
|
||||
if m.popUpQueue == nil {
|
||||
m.popUpQueueList = widgets.NewPlayQueueList(m.App.ImageManager, false)
|
||||
m.popUpQueueList.Reorderable = true
|
||||
m.popUpQueueList.SetItems(m.App.PlaybackManager.GetPlayQueue())
|
||||
m.popUpQueueList.SetItems(m.App.PlaybackManager.GetActivePlayQueue())
|
||||
m.ConnectPlayQueuelistActions(m.popUpQueueList)
|
||||
|
||||
title := widget.NewRichTextWithText(lang.L("Play Queue"))
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
||||
})
|
||||
})
|
||||
app.PlaybackManager.OnQueueChange(func() {
|
||||
fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetPlayQueue()) })
|
||||
fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetActivePlayQueue()) })
|
||||
})
|
||||
app.ServerManager.OnServerConnected(func(conf *backend.ServerConfig) {
|
||||
go m.RunOnServerConnectedTasks(conf, app, displayAppName)
|
||||
|
||||
@@ -22,8 +22,10 @@ type AuxControls struct {
|
||||
widget.BaseWidget
|
||||
|
||||
OnChangeAutoplay func(autoplay bool)
|
||||
OnChangeShuffle func(shuffle bool)
|
||||
|
||||
VolumeControl *VolumeControl
|
||||
shuffle *IconButton
|
||||
autoplay *IconButton
|
||||
loop *IconButton
|
||||
cast *IconButton
|
||||
@@ -32,15 +34,26 @@ type AuxControls struct {
|
||||
container *fyne.Container
|
||||
}
|
||||
|
||||
func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool) *AuxControls {
|
||||
func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool, initialShuffle bool) *AuxControls {
|
||||
a := &AuxControls{
|
||||
VolumeControl: NewVolumeControl(initialVolume),
|
||||
shuffle: NewIconButton(myTheme.ShuffleIcon, nil),
|
||||
autoplay: NewIconButton(myTheme.AutoplayIcon, nil),
|
||||
loop: NewIconButton(myTheme.RepeatIcon, nil),
|
||||
cast: NewIconButton(myTheme.CastIcon, nil),
|
||||
showQueue: NewIconButton(myTheme.PlayQueueIcon, nil),
|
||||
}
|
||||
|
||||
a.shuffle.Highlighted = initialShuffle
|
||||
a.shuffle.IconSize = IconButtonSizeSmaller
|
||||
a.shuffle.SetToolTip(lang.L("Shuffle"))
|
||||
a.shuffle.OnTapped = func() {
|
||||
a.SetShuffle(!a.shuffle.Highlighted)
|
||||
if a.OnChangeShuffle != nil {
|
||||
a.OnChangeShuffle(a.shuffle.Highlighted)
|
||||
}
|
||||
}
|
||||
|
||||
a.loop.IconSize = IconButtonSizeSmaller
|
||||
a.loop.SetToolTip(lang.L("Repeat"))
|
||||
a.SetLoopMode(initialLoopMode)
|
||||
@@ -68,7 +81,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial
|
||||
a.VolumeControl,
|
||||
container.New(
|
||||
layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5),
|
||||
layout.NewSpacer(), a.autoplay, a.loop, a.cast, a.showQueue, util.NewHSpace(5)),
|
||||
layout.NewSpacer(), a.autoplay, a.shuffle, a.loop, a.cast, a.showQueue, util.NewHSpace(5)),
|
||||
layout.NewSpacer(),
|
||||
),
|
||||
)
|
||||
@@ -98,6 +111,14 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuxControls) SetShuffle(isShuffle bool) {
|
||||
if isShuffle == a.shuffle.Highlighted {
|
||||
return
|
||||
}
|
||||
a.shuffle.Highlighted = isShuffle
|
||||
a.shuffle.Refresh()
|
||||
}
|
||||
|
||||
func (a *AuxControls) DisableCastButton() {
|
||||
a.cast.Disable()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user