Migration 10: - play_history table (audio_file_id FK, played_at DATETIME, CASCADE delete) - play_count + last_played columns on audio_files (denormalized) - Recreated track_metadata VIEW with play_count and last_played columns Play recording: - queue.recordPlay() inserts play_history row + updates denormalized columns - Called from OnPlaybackFinished after queue advance completes - Mutex released before DB write to avoid MaxOpenConns(1) deadlock - Natural finish only — skip/stop does not count Tests: - TestMigration10PlayHistory: schema, columns, VIEW, round-trip verification - All 49 smart playlist + 15 service + existing DB tests still pass
54 lines
1005 B
Go
54 lines
1005 B
Go
package queue
|
|
|
|
// OnPlaybackFinished is called when a track finishes playing naturally.
|
|
// This drives the auto-advance behavior and records the play.
|
|
func (q *Queue) OnPlaybackFinished() {
|
|
q.mu.Lock()
|
|
|
|
if len(q.tracks) == 0 {
|
|
q.mu.Unlock()
|
|
|
|
return
|
|
}
|
|
|
|
// Capture the track that just finished before advancing.
|
|
finishedID := q.tracks[q.currentIndex].AudioFileID
|
|
|
|
// Repeat One: replay the current track.
|
|
if q.repeatMode == RepeatOne {
|
|
if q.playCurrentTrack() {
|
|
q.emitIndexChanged()
|
|
}
|
|
|
|
q.mu.Unlock()
|
|
q.recordPlay(finishedID)
|
|
|
|
return
|
|
}
|
|
|
|
nextIdx := q.nextIndex()
|
|
if nextIdx == -1 {
|
|
// Queue exhausted — this is the extension point for a future fallback playlist.
|
|
q.onQueueExhausted()
|
|
q.mu.Unlock()
|
|
q.recordPlay(finishedID)
|
|
|
|
return
|
|
}
|
|
|
|
prevIndex := q.currentIndex
|
|
q.currentIndex = nextIdx
|
|
|
|
if !q.playCurrentTrack() {
|
|
q.currentIndex = prevIndex
|
|
q.mu.Unlock()
|
|
q.recordPlay(finishedID)
|
|
|
|
return
|
|
}
|
|
|
|
q.emitIndexChanged()
|
|
q.mu.Unlock()
|
|
q.recordPlay(finishedID)
|
|
}
|