fixed some state persistence issues. player state now saves on changes. next track button doesn't autoplay when paused.
This commit is contained in:
@@ -151,6 +151,7 @@ func (p *Player) registerEventHandlers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.emitVolumeChanged()
|
p.emitVolumeChanged()
|
||||||
|
p.saveState()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +350,7 @@ func (p *Player) LoadFile(filePath string) error {
|
|||||||
p.startPaused()
|
p.startPaused()
|
||||||
p.emitPlaybackStateChanged(p.state)
|
p.emitPlaybackStateChanged(p.state)
|
||||||
p.emitTrackChanged()
|
p.emitTrackChanged()
|
||||||
|
p.saveState()
|
||||||
p.logger.Info("File loaded, state set to paused", "file", filePath)
|
p.logger.Info("File loaded, state set to paused", "file", filePath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -433,6 +435,7 @@ func (p *Player) Pause() error {
|
|||||||
p.state = Paused
|
p.state = Paused
|
||||||
p.logger.Info("Paused playback")
|
p.logger.Info("Paused playback")
|
||||||
p.emitPlaybackStateChanged(p.state)
|
p.emitPlaybackStateChanged(p.state)
|
||||||
|
p.saveState()
|
||||||
} else {
|
} else {
|
||||||
p.logger.Info("Already paused or not playing")
|
p.logger.Info("Already paused or not playing")
|
||||||
}
|
}
|
||||||
@@ -440,6 +443,11 @@ func (p *Player) Pause() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsPlaying reports whether the player is currently playing audio.
|
||||||
|
func (p *Player) IsPlaying() bool {
|
||||||
|
return p.state == Playing
|
||||||
|
}
|
||||||
|
|
||||||
// UnloadTrack tears down the current track, releasing the file and streamer
|
// UnloadTrack tears down the current track, releasing the file and streamer
|
||||||
// chain. The player returns to the initial "no track loaded" state and emits
|
// chain. The player returns to the initial "no track loaded" state and emits
|
||||||
// events so the frontend clears its current-track display.
|
// events so the frontend clears its current-track display.
|
||||||
@@ -473,6 +481,7 @@ func (p *Player) UnloadTrack() {
|
|||||||
// Notify frontend that there is no longer a current track.
|
// Notify frontend that there is no longer a current track.
|
||||||
p.emitPlaybackStateChanged(p.state)
|
p.emitPlaybackStateChanged(p.state)
|
||||||
runtime.EventsEmit(p.ctx, events.TrackChanged, nil)
|
runtime.EventsEmit(p.ctx, events.TrackChanged, nil)
|
||||||
|
p.saveState()
|
||||||
|
|
||||||
p.logger.Info("Track unloaded")
|
p.logger.Info("Track unloaded")
|
||||||
}
|
}
|
||||||
@@ -503,6 +512,7 @@ func (p *Player) getUserVolume() UserVolume {
|
|||||||
// MuteToggle toggles the mute state.
|
// MuteToggle toggles the mute state.
|
||||||
func (p *Player) MuteToggle() error {
|
func (p *Player) MuteToggle() error {
|
||||||
p.volume.Silent = !p.volume.Silent
|
p.volume.Silent = !p.volume.Silent
|
||||||
|
p.saveState()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -649,7 +659,16 @@ func (p *Player) TrackLengthInSeconds() (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SaveState persists the current player state to the database.
|
// SaveState persists the current player state to the database.
|
||||||
|
// This is called during shutdown to capture the final state.
|
||||||
func (p *Player) SaveState() {
|
func (p *Player) SaveState() {
|
||||||
|
p.saveState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveState is the internal helper that writes the current player state to the
|
||||||
|
// database. It is called both from the public SaveState (shutdown) and from
|
||||||
|
// individual operations that change state (volume, mute, track load/unload)
|
||||||
|
// so that the persisted state stays up-to-date between clean shutdowns.
|
||||||
|
func (p *Player) saveState() {
|
||||||
if p.db == nil {
|
if p.db == nil {
|
||||||
p.logger.Warn("No database available, cannot save player state")
|
p.logger.Warn("No database available, cannot save player state")
|
||||||
|
|
||||||
|
|||||||
+63
-22
@@ -35,6 +35,7 @@ const PreviousRestartThreshold = 3
|
|||||||
type TrackLoader interface {
|
type TrackLoader interface {
|
||||||
LoadFile(filePath string) error
|
LoadFile(filePath string) error
|
||||||
Play() error
|
Play() error
|
||||||
|
IsPlaying() bool
|
||||||
CurrentPositionSeconds() (int, error)
|
CurrentPositionSeconds() (int, error)
|
||||||
UnloadTrack()
|
UnloadTrack()
|
||||||
}
|
}
|
||||||
@@ -462,6 +463,7 @@ func (q *Queue) AddTrack(filePath string) {
|
|||||||
q.playCurrentTrack()
|
q.playCurrentTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
q.persistState()
|
||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,6 +620,7 @@ func (q *Queue) InsertNext(filePath string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
q.persistTracks()
|
q.persistTracks()
|
||||||
|
q.persistState()
|
||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,8 +656,9 @@ func (q *Queue) RemoveTrack(position int) {
|
|||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next advances to the next track. In RepeatOne mode, the current
|
// Next advances to the next track. If the player was paused, the next
|
||||||
// track is replayed instead of advancing.
|
// track is loaded but not played. In RepeatOne mode, the current track
|
||||||
|
// is replayed instead of advancing.
|
||||||
func (q *Queue) Next() {
|
func (q *Queue) Next() {
|
||||||
q.mu.Lock()
|
q.mu.Lock()
|
||||||
defer q.mu.Unlock()
|
defer q.mu.Unlock()
|
||||||
@@ -663,9 +667,11 @@ func (q *Queue) Next() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wasPlaying := q.player != nil && q.player.IsPlaying()
|
||||||
|
|
||||||
// Repeat One: replay the current track.
|
// Repeat One: replay the current track.
|
||||||
if q.repeatMode == RepeatOne {
|
if q.repeatMode == RepeatOne {
|
||||||
q.playCurrentTrack()
|
q.playOrLoadCurrentTrack(wasPlaying)
|
||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -679,11 +685,12 @@ func (q *Queue) Next() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
q.currentIndex = nextIdx
|
q.currentIndex = nextIdx
|
||||||
q.playCurrentTrack()
|
q.playOrLoadCurrentTrack(wasPlaying)
|
||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Previous goes to the previous track (or restarts current if >3s in).
|
// Previous goes to the previous track (or restarts current if >3s in).
|
||||||
|
// If the player was paused, the track is loaded but not played.
|
||||||
// In RepeatOne mode, the current track is replayed instead of navigating.
|
// In RepeatOne mode, the current track is replayed instead of navigating.
|
||||||
func (q *Queue) Previous() {
|
func (q *Queue) Previous() {
|
||||||
q.mu.Lock()
|
q.mu.Lock()
|
||||||
@@ -693,9 +700,11 @@ func (q *Queue) Previous() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wasPlaying := q.player != nil && q.player.IsPlaying()
|
||||||
|
|
||||||
// Repeat One: replay the current track.
|
// Repeat One: replay the current track.
|
||||||
if q.repeatMode == RepeatOne {
|
if q.repeatMode == RepeatOne {
|
||||||
q.playCurrentTrack()
|
q.playOrLoadCurrentTrack(wasPlaying)
|
||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -705,7 +714,7 @@ func (q *Queue) Previous() {
|
|||||||
if q.player != nil {
|
if q.player != nil {
|
||||||
posSecs, err := q.player.CurrentPositionSeconds()
|
posSecs, err := q.player.CurrentPositionSeconds()
|
||||||
if err == nil && posSecs > PreviousRestartThreshold {
|
if err == nil && posSecs > PreviousRestartThreshold {
|
||||||
q.playCurrentTrack()
|
q.playOrLoadCurrentTrack(wasPlaying)
|
||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -715,14 +724,14 @@ func (q *Queue) Previous() {
|
|||||||
prevIdx := q.previousIndex()
|
prevIdx := q.previousIndex()
|
||||||
if prevIdx == -1 {
|
if prevIdx == -1 {
|
||||||
// At the beginning — just restart the current track.
|
// At the beginning — just restart the current track.
|
||||||
q.playCurrentTrack()
|
q.playOrLoadCurrentTrack(wasPlaying)
|
||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
q.currentIndex = prevIdx
|
q.currentIndex = prevIdx
|
||||||
q.playCurrentTrack()
|
q.playOrLoadCurrentTrack(wasPlaying)
|
||||||
q.emitQueueChanged()
|
q.emitQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,12 +1055,25 @@ func (q *Queue) generateShuffleOrder() {
|
|||||||
q.shuffleOrder = order
|
q.shuffleOrder = order
|
||||||
}
|
}
|
||||||
|
|
||||||
// playCurrentTrack tells the player to load and play the current track.
|
// playOrLoadCurrentTrack loads the current track and optionally starts
|
||||||
func (q *Queue) playCurrentTrack() {
|
// playback. When autoPlay is true it behaves like playCurrentTrack;
|
||||||
if q.player == nil {
|
// when false it only loads the file (leaving the player paused).
|
||||||
q.logger.Error("No player set, cannot play track")
|
func (q *Queue) playOrLoadCurrentTrack(autoPlay bool) {
|
||||||
|
if autoPlay {
|
||||||
|
q.playCurrentTrack()
|
||||||
|
} else {
|
||||||
|
q.loadCurrentTrack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return
|
// loadCurrentTrack tells the player to load the current track without
|
||||||
|
// starting playback. It persists the updated queue state. Returns true
|
||||||
|
// if the file was loaded successfully.
|
||||||
|
func (q *Queue) loadCurrentTrack() bool {
|
||||||
|
if q.player == nil {
|
||||||
|
q.logger.Error("No player set, cannot load track")
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if q.currentIndex < 0 || q.currentIndex >= len(q.tracks) {
|
if q.currentIndex < 0 || q.currentIndex >= len(q.tracks) {
|
||||||
@@ -1060,28 +1082,44 @@ func (q *Queue) playCurrentTrack() {
|
|||||||
"index", q.currentIndex, "trackCount", len(q.tracks),
|
"index", q.currentIndex, "trackCount", len(q.tracks),
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
track := q.tracks[q.currentIndex]
|
track := q.tracks[q.currentIndex]
|
||||||
q.logger.Info(
|
q.logger.Info(
|
||||||
"Playing track from queue",
|
"Loading track from queue",
|
||||||
"filePath", track.FilePath, "position", q.currentIndex,
|
"filePath", track.FilePath, "position", q.currentIndex,
|
||||||
)
|
)
|
||||||
|
|
||||||
err := q.player.LoadFile(track.FilePath)
|
err := q.player.LoadFile(track.FilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.logger.Error("Failed to load file from queue", "filePath", track.FilePath, "err", err)
|
q.logger.Error(
|
||||||
|
"Failed to load file from queue",
|
||||||
|
"filePath", track.FilePath, "err", err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
err = q.player.Play()
|
|
||||||
if err != nil {
|
|
||||||
q.logger.Error("Failed to play file from queue", "filePath", track.FilePath, "err", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
q.persistState()
|
q.persistState()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// playCurrentTrack tells the player to load and play the current track.
|
||||||
|
func (q *Queue) playCurrentTrack() {
|
||||||
|
if !q.loadCurrentTrack() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := q.player.Play()
|
||||||
|
if err != nil {
|
||||||
|
track := q.tracks[q.currentIndex]
|
||||||
|
q.logger.Error(
|
||||||
|
"Failed to play file from queue",
|
||||||
|
"filePath", track.FilePath, "err", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// onQueueExhausted is called when there are no more tracks to play.
|
// onQueueExhausted is called when there are no more tracks to play.
|
||||||
@@ -1108,6 +1146,9 @@ func (q *Queue) reindexPositions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// persistTracks writes the current queue tracks to the database.
|
// persistTracks writes the current queue tracks to the database.
|
||||||
|
// TODO: wrap in a transaction so the DELETE-all + INSERT-all is atomic.
|
||||||
|
// Without a transaction a crash mid-write could leave queue_tracks empty or
|
||||||
|
// partially populated. Low practical risk but worth addressing for robustness.
|
||||||
func (q *Queue) persistTracks() {
|
func (q *Queue) persistTracks() {
|
||||||
err := q.db.Queries.ClearQueueTracks(q.db.Ctx)
|
err := q.db.Queries.ClearQueueTracks(q.db.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user