Incremental scan orphan cleanup drops playlist entries silently (no phantom preservation) #246

Open
opened 2026-09-09 13:57:20 +00:00 by yonlu · 1 comment
Owner

Report

The incremental scan's orphan cleanup deletes audio_files rows without
first populating the playlist phantom columns, so a track removed from
the library folder outside YellowJacket silently becomes an empty
playlist entry that can never be re-linked — even if the file is later
put back.

This is #183 on the fourth path that empties the table. The three paths
already fixed all preserve phantoms first:

  • full rescan — backend/library/rescan.go:145 calls
    database.PreservePlaylistPhantoms
  • stale-shape retire — backend/database/staleshape.go:277
  • RemoveLibrarybackend/library/crud.go:246 (inline UPDATE)

The incremental orphan loop (backend/library/library.go:923) calls
DeleteAudioFile directly and does not. RemoveFromLibrary
(backend/library/remove_tracks.go:70) is the same — there the
exclusion means the track is not coming back, so the entry is arguably
dead, but it still survives as an empty row rather than being cleaned up.

Findings

  • ScanLibrary (Settings → Rescan, and the launch-time soft scan in
    scan_queue.go:216) both reach scanInternal, whose Phase 5 orphan
    loop deletes every audio_files row whose path the walk did not
    revisit — i.e. every file removed or renamed outside the app.
  • ON DELETE SET NULL nulls playlist_tracks.audio_file_id; with
    phantom_file_path still NULL, ResolvePhantomTracksAfterScan
    (which matches on phantom_file_path) can never re-link the entry.
  • GetPlaylistTracksWithMetadata renders it as a blank row
    (COALESCE(tm.title, pt.phantom_title, '')), so the user sees an
    empty entry instead of a track they know they put there.
  • No test covers playlist survival across the incremental orphan path;
    leak_test.go and removal_test.go only cover RemoveLibrary.

Direction

Populate phantoms for the specific files about to be orphaned, before
DeleteAudioFile, inside the same scan. The bulk
PreservePlaylistPhantoms runs over all linked entries, so it needs a
per-file variant (or an id-list form) so a normal scan does not rewrite
every playlist row on every run. RemoveFromLibrary should either
delete the entries outright or populate phantoms — pick one and state
why, but do not leave the empty row.

**Report** The incremental scan's orphan cleanup deletes `audio_files` rows without first populating the playlist phantom columns, so a track removed from the library folder *outside* YellowJacket silently becomes an empty playlist entry that can never be re-linked — even if the file is later put back. This is #183 on the fourth path that empties the table. The three paths already fixed all preserve phantoms first: - full rescan — `backend/library/rescan.go:145` calls `database.PreservePlaylistPhantoms` - stale-shape retire — `backend/database/staleshape.go:277` - `RemoveLibrary` — `backend/library/crud.go:246` (inline UPDATE) The incremental orphan loop (`backend/library/library.go:923`) calls `DeleteAudioFile` directly and does not. `RemoveFromLibrary` (`backend/library/remove_tracks.go:70`) is the same — there the exclusion means the track is *not* coming back, so the entry is arguably dead, but it still survives as an empty row rather than being cleaned up. **Findings** - `ScanLibrary` (Settings → Rescan, and the launch-time soft scan in `scan_queue.go:216`) both reach `scanInternal`, whose Phase 5 orphan loop deletes every `audio_files` row whose path the walk did not revisit — i.e. every file removed or renamed outside the app. - `ON DELETE SET NULL` nulls `playlist_tracks.audio_file_id`; with `phantom_file_path` still NULL, `ResolvePhantomTracksAfterScan` (which matches on `phantom_file_path`) can never re-link the entry. - `GetPlaylistTracksWithMetadata` renders it as a blank row (`COALESCE(tm.title, pt.phantom_title, '')`), so the user sees an empty entry instead of a track they know they put there. - No test covers playlist survival across the incremental orphan path; `leak_test.go` and `removal_test.go` only cover `RemoveLibrary`. **Direction** Populate phantoms for the specific files about to be orphaned, before `DeleteAudioFile`, inside the same scan. The bulk `PreservePlaylistPhantoms` runs over *all* linked entries, so it needs a per-file variant (or an id-list form) so a normal scan does not rewrite every playlist row on every run. `RemoveFromLibrary` should either delete the entries outright or populate phantoms — pick one and state why, but do not leave the empty row.
yonlu self-assigned this 2026-09-09 13:58:01 +00:00
yonlu added the
Status
In Progress
label 2026-09-09 13:58:02 +00:00
Author
Owner

Branch: fix/246-incremental-scan-phantoms

Approach: add a per-file phantom populate step before DeleteAudioFile in the scan orphan loop, reusing PreservePlaylistPhantoms logic scoped to a file-id list. RemoveFromLibrary: delete the playlist entries for removed tracks (they are excluded and will not come back) rather than leaving empty rows. Tests: scan-level test that a file removed from disk keeps a re-linkable phantom, and a removal test asserting entries are gone.

Branch: fix/246-incremental-scan-phantoms Approach: add a per-file phantom populate step before DeleteAudioFile in the scan orphan loop, reusing PreservePlaylistPhantoms logic scoped to a file-id list. RemoveFromLibrary: delete the playlist entries for removed tracks (they are excluded and will not come back) rather than leaving empty rows. Tests: scan-level test that a file removed from disk keeps a re-linkable phantom, and a removal test asserting entries are gone.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#246