Orphaned cover_art rows and files leak when an album's last track is removed #247

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

Report

Removing the last track of an album leaves an orphaned cover_art row
and its on-disk files behind forever. pruneEmptyEntities deletes the
now-empty album but never the cover_art row it referenced, and the
janitor's cover sweep computes its live set from cover_art.file_path
— so an orphaned row keeps its files "live" and nothing ever reclaims
either.

Findings

  • albums.cover_art_id is NO ACTION, so deleting the album does not
    touch the cover row.
  • pruneEmptyEntities (backend/library/library.go:1143) sweeps empty
    albums, unreferenced artists and unused genres only.
  • Both RemoveFromLibrary (remove_tracks.go, which calls
    pruneEmptyEntities) and the incremental scan's orphan cleanup
    (library.go:997) take this path.
  • DeleteCoverArt (database/sql/queries/cover_art.sql:26) has zero
    callers
    — no code path removes an individual cover row. Only
    RemoveLibrary (inline orphan sweep, crud.go:290-330) and
    FullRescan (DeleteAllCoverArt) do.
  • OrphanedCoverFilesJob (maintenance/sweeps.go) removes files not
    in cover_art.file_path, so the orphaned row's file is exempt.

Direction

Sweep orphaned cover_art rows (and their file set, via
CoverArtFileSet) alongside the album sweep in pruneEmptyEntities, so
every track-removal path gets it. DeleteCoverArt then finally has a
caller. Keep the RemoveLibrary inline sweep or fold it into the shared
helper, but there should be one implementation, not two.

**Report** Removing the last track of an album leaves an orphaned `cover_art` row and its on-disk files behind forever. `pruneEmptyEntities` deletes the now-empty album but never the `cover_art` row it referenced, and the janitor's cover sweep computes its live set *from* `cover_art.file_path` — so an orphaned row keeps its files "live" and nothing ever reclaims either. **Findings** - `albums.cover_art_id` is `NO ACTION`, so deleting the album does not touch the cover row. - `pruneEmptyEntities` (`backend/library/library.go:1143`) sweeps empty albums, unreferenced artists and unused genres only. - Both `RemoveFromLibrary` (`remove_tracks.go`, which calls `pruneEmptyEntities`) and the incremental scan's orphan cleanup (`library.go:997`) take this path. - `DeleteCoverArt` (`database/sql/queries/cover_art.sql:26`) has **zero callers** — no code path removes an individual cover row. Only `RemoveLibrary` (inline orphan sweep, `crud.go:290-330`) and `FullRescan` (`DeleteAllCoverArt`) do. - `OrphanedCoverFilesJob` (`maintenance/sweeps.go`) removes *files* not in `cover_art.file_path`, so the orphaned row's file is exempt. **Direction** Sweep orphaned `cover_art` rows (and their file set, via `CoverArtFileSet`) alongside the album sweep in `pruneEmptyEntities`, so every track-removal path gets it. `DeleteCoverArt` then finally has a caller. Keep the `RemoveLibrary` inline sweep or fold it into the shared helper, but there should be one implementation, not two.
yonlu self-assigned this 2026-09-09 13:58:07 +00:00
yonlu added the
Status
In Progress
label 2026-09-09 13:58:08 +00:00
Author
Owner

Branch: fix/247-cover-art-orphans

Approach: sweep orphaned cover_art rows (and their CoverArtFileSet) inside pruneEmptyEntities so every track-removal path shares it; DeleteCoverArt gains its first caller. Fold RemoveLibrary's inline cover sweep into the same helper so there is one implementation.

Branch: fix/247-cover-art-orphans Approach: sweep orphaned cover_art rows (and their CoverArtFileSet) inside pruneEmptyEntities so every track-removal path shares it; DeleteCoverArt gains its first caller. Fold RemoveLibrary's inline cover sweep into the same helper so there is one implementation.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#247