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.saveState()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -349,6 +350,7 @@ func (p *Player) LoadFile(filePath string) error {
|
||||
p.startPaused()
|
||||
p.emitPlaybackStateChanged(p.state)
|
||||
p.emitTrackChanged()
|
||||
p.saveState()
|
||||
p.logger.Info("File loaded, state set to paused", "file", filePath)
|
||||
|
||||
return nil
|
||||
@@ -433,6 +435,7 @@ func (p *Player) Pause() error {
|
||||
p.state = Paused
|
||||
p.logger.Info("Paused playback")
|
||||
p.emitPlaybackStateChanged(p.state)
|
||||
p.saveState()
|
||||
} else {
|
||||
p.logger.Info("Already paused or not playing")
|
||||
}
|
||||
@@ -440,6 +443,11 @@ func (p *Player) Pause() error {
|
||||
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
|
||||
// chain. The player returns to the initial "no track loaded" state and emits
|
||||
// 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.
|
||||
p.emitPlaybackStateChanged(p.state)
|
||||
runtime.EventsEmit(p.ctx, events.TrackChanged, nil)
|
||||
p.saveState()
|
||||
|
||||
p.logger.Info("Track unloaded")
|
||||
}
|
||||
@@ -503,6 +512,7 @@ func (p *Player) getUserVolume() UserVolume {
|
||||
// MuteToggle toggles the mute state.
|
||||
func (p *Player) MuteToggle() error {
|
||||
p.volume.Silent = !p.volume.Silent
|
||||
p.saveState()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -649,7 +659,16 @@ func (p *Player) TrackLengthInSeconds() (int, error) {
|
||||
}
|
||||
|
||||
// SaveState persists the current player state to the database.
|
||||
// This is called during shutdown to capture the final state.
|
||||
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 {
|
||||
p.logger.Warn("No database available, cannot save player state")
|
||||
|
||||
|
||||
+63
-22
@@ -35,6 +35,7 @@ const PreviousRestartThreshold = 3
|
||||
type TrackLoader interface {
|
||||
LoadFile(filePath string) error
|
||||
Play() error
|
||||
IsPlaying() bool
|
||||
CurrentPositionSeconds() (int, error)
|
||||
UnloadTrack()
|
||||
}
|
||||
@@ -462,6 +463,7 @@ func (q *Queue) AddTrack(filePath string) {
|
||||
q.playCurrentTrack()
|
||||
}
|
||||
|
||||
q.persistState()
|
||||
q.emitQueueChanged()
|
||||
}
|
||||
|
||||
@@ -618,6 +620,7 @@ func (q *Queue) InsertNext(filePath string) {
|
||||
}
|
||||
|
||||
q.persistTracks()
|
||||
q.persistState()
|
||||
q.emitQueueChanged()
|
||||
}
|
||||
|
||||
@@ -653,8 +656,9 @@ func (q *Queue) RemoveTrack(position int) {
|
||||
q.emitQueueChanged()
|
||||
}
|
||||
|
||||
// Next advances to the next track. In RepeatOne mode, the current
|
||||
// track is replayed instead of advancing.
|
||||
// Next advances to the next track. If the player was paused, the next
|
||||
// track is loaded but not played. In RepeatOne mode, the current track
|
||||
// is replayed instead of advancing.
|
||||
func (q *Queue) Next() {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
@@ -663,9 +667,11 @@ func (q *Queue) Next() {
|
||||
return
|
||||
}
|
||||
|
||||
wasPlaying := q.player != nil && q.player.IsPlaying()
|
||||
|
||||
// Repeat One: replay the current track.
|
||||
if q.repeatMode == RepeatOne {
|
||||
q.playCurrentTrack()
|
||||
q.playOrLoadCurrentTrack(wasPlaying)
|
||||
q.emitQueueChanged()
|
||||
|
||||
return
|
||||
@@ -679,11 +685,12 @@ func (q *Queue) Next() {
|
||||
}
|
||||
|
||||
q.currentIndex = nextIdx
|
||||
q.playCurrentTrack()
|
||||
q.playOrLoadCurrentTrack(wasPlaying)
|
||||
q.emitQueueChanged()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (q *Queue) Previous() {
|
||||
q.mu.Lock()
|
||||
@@ -693,9 +700,11 @@ func (q *Queue) Previous() {
|
||||
return
|
||||
}
|
||||
|
||||
wasPlaying := q.player != nil && q.player.IsPlaying()
|
||||
|
||||
// Repeat One: replay the current track.
|
||||
if q.repeatMode == RepeatOne {
|
||||
q.playCurrentTrack()
|
||||
q.playOrLoadCurrentTrack(wasPlaying)
|
||||
q.emitQueueChanged()
|
||||
|
||||
return
|
||||
@@ -705,7 +714,7 @@ func (q *Queue) Previous() {
|
||||
if q.player != nil {
|
||||
posSecs, err := q.player.CurrentPositionSeconds()
|
||||
if err == nil && posSecs > PreviousRestartThreshold {
|
||||
q.playCurrentTrack()
|
||||
q.playOrLoadCurrentTrack(wasPlaying)
|
||||
q.emitQueueChanged()
|
||||
|
||||
return
|
||||
@@ -715,14 +724,14 @@ func (q *Queue) Previous() {
|
||||
prevIdx := q.previousIndex()
|
||||
if prevIdx == -1 {
|
||||
// At the beginning — just restart the current track.
|
||||
q.playCurrentTrack()
|
||||
q.playOrLoadCurrentTrack(wasPlaying)
|
||||
q.emitQueueChanged()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
q.currentIndex = prevIdx
|
||||
q.playCurrentTrack()
|
||||
q.playOrLoadCurrentTrack(wasPlaying)
|
||||
q.emitQueueChanged()
|
||||
}
|
||||
|
||||
@@ -1046,12 +1055,25 @@ func (q *Queue) generateShuffleOrder() {
|
||||
q.shuffleOrder = order
|
||||
}
|
||||
|
||||
// playCurrentTrack tells the player to load and play the current track.
|
||||
func (q *Queue) playCurrentTrack() {
|
||||
if q.player == nil {
|
||||
q.logger.Error("No player set, cannot play track")
|
||||
// playOrLoadCurrentTrack loads the current track and optionally starts
|
||||
// playback. When autoPlay is true it behaves like playCurrentTrack;
|
||||
// when false it only loads the file (leaving the player paused).
|
||||
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) {
|
||||
@@ -1060,28 +1082,44 @@ func (q *Queue) playCurrentTrack() {
|
||||
"index", q.currentIndex, "trackCount", len(q.tracks),
|
||||
)
|
||||
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
track := q.tracks[q.currentIndex]
|
||||
q.logger.Info(
|
||||
"Playing track from queue",
|
||||
"Loading track from queue",
|
||||
"filePath", track.FilePath, "position", q.currentIndex,
|
||||
)
|
||||
|
||||
err := q.player.LoadFile(track.FilePath)
|
||||
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
|
||||
}
|
||||
|
||||
err = q.player.Play()
|
||||
if err != nil {
|
||||
q.logger.Error("Failed to play file from queue", "filePath", track.FilePath, "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -1108,6 +1146,9 @@ func (q *Queue) reindexPositions() {
|
||||
}
|
||||
|
||||
// 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() {
|
||||
err := q.db.Queries.ClearQueueTracks(q.db.Ctx)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user