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:
@@ -171,6 +171,14 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
|
|||||||
PostScan: yj.playlist.RestoreAllPlaylists,
|
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.
|
// Register playback finished handler to drive queue auto-advance.
|
||||||
yj.player.SetPlaybackFinishedHandler(yj.queue.OnPlaybackFinished)
|
yj.player.SetPlaybackFinishedHandler(yj.queue.OnPlaybackFinished)
|
||||||
|
|
||||||
|
|||||||
@@ -1246,6 +1246,86 @@ func (q *Queue) onQueueExhausted() {
|
|||||||
q.persistState()
|
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.
|
// reindexPositions updates the Position field of all tracks to match slice index.
|
||||||
func (q *Queue) reindexPositions() {
|
func (q *Queue) reindexPositions() {
|
||||||
for i := range q.tracks {
|
for i := range q.tracks {
|
||||||
|
|||||||
Reference in New Issue
Block a user