feat(12-01): add queue compaction method and wire removal hooks

- Add CompactAfterLibraryRemoval on Queue: reloads surviving tracks from DB, resets currentIndex, unloads player if current track was removed, clears shuffle order
- Wire RemovalHooks in app.go OnStartup: StopPlayback → player.UnloadTrack, CompactQueue → queue.CompactAfterLibraryRemoval
- All existing tests pass with no regressions
This commit is contained in:
2026-03-12 19:38:36 -04:00
parent bd44f8306c
commit 5995dfd01d
2 changed files with 88 additions and 0 deletions
+8
View File
@@ -171,6 +171,14 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
PostScan: yj.playlist.RestoreAllPlaylists,
})
// Wire removal hooks so the library can stop playback and
// compact the queue during library removal without depending
// on the player or queue packages directly.
yj.library.SetRemovalHooks(library.RemovalHooks{
StopPlayback: func() { yj.player.UnloadTrack() },
CompactQueue: yj.queue.CompactAfterLibraryRemoval,
})
// Register playback finished handler to drive queue auto-advance.
yj.player.SetPlaybackFinishedHandler(yj.queue.OnPlaybackFinished)
+80
View File
@@ -1246,6 +1246,86 @@ func (q *Queue) onQueueExhausted() {
q.persistState()
}
// CompactAfterLibraryRemoval reloads queue state from the database
// after a library removal has cascade-deleted queue_tracks rows.
// It resets currentIndex to 0 (or -1 if empty), clears shuffleOrder,
// unloads the current track if it was removed, and emits QueueChanged.
func (q *Queue) CompactAfterLibraryRemoval() {
q.mu.Lock()
defer q.mu.Unlock()
// Remember the current track's file path so we can detect if it survived.
var previousFilePath string
if q.currentIndex >= 0 && q.currentIndex < len(q.tracks) {
previousFilePath = q.tracks[q.currentIndex].FilePath
}
// Reload surviving tracks from the database. The CASCADE delete
// already removed the rows from queue_tracks — we just need to
// reload and reindex.
rows, err := q.db.Queries.GetQueueTracks(q.db.Ctx)
if err != nil {
q.logger.Error("could not reload queue tracks after library removal", "err", err)
return
}
q.tracks = make([]Track, 0, len(rows))
for _, row := range rows {
q.tracks = append(q.tracks, Track{
ID: row.ID,
AudioFileID: row.AudioFileID,
FilePath: row.FilePath,
Position: row.Position,
Title: row.Title,
Artist: row.Artist,
})
}
// Check if the previously-playing track survived.
found := false
if previousFilePath != "" {
for i, t := range q.tracks {
if t.FilePath == previousFilePath {
q.currentIndex = i
found = true
break
}
}
}
if !found {
if len(q.tracks) > 0 {
q.currentIndex = 0
} else {
q.currentIndex = -1
}
// The current track was removed — unload it from the player.
if q.player != nil {
q.player.UnloadTrack()
}
}
// Clear shuffle order — it will be regenerated on next shuffle toggle.
q.shuffleOrder = nil
// Reindex positions and persist the compacted state.
q.reindexPositions()
q.persistTracks()
q.persistState()
q.emitQueueChanged()
q.logger.Info("queue compacted after library removal",
"survivingTracks", len(q.tracks),
"currentIndex", q.currentIndex,
)
}
// reindexPositions updates the Position field of all tracks to match slice index.
func (q *Queue) reindexPositions() {
for i := range q.tracks {