soulsync/core/library
BoulderBadgeDad 696119d5ac Expired Download Cleaner: retention-based cleanup of watchlist/playlist downloads (Boulder)
A Library Maintenance job that cleans up downloads tracked by Download Origins
once they pass a per-origin retention window — findings by default, opt-in
auto-delete.

A download is only ever proposed for deletion when ALL hold: older than its
origin's retention, NOT still in an actively-mirrored playlist / watched
artist, and played fewer than the keep-threshold (default 2 → "played more
than once is kept"). Only touches downloads recorded from the Download Origins
feature forward — never pre-existing or manual library.

- core/library/expired_cleanup.py: pure decision core (retention_cutoff,
  is_expired, select_expired) — no DB/clock, fully tested. play_count is the
  reliable listen signal (last_played is often unpopulated, so recency isn't
  used).
- ExpiredDownloadCleanerJob: gathers facts (play_count via a new
  get_origin_cleanup_candidates join; active-mirror via get_mirrored_playlists;
  watch via get_watchlist_artists) and either creates 'expired_download'
  findings or, with auto_delete on, deletes in-scan. Default OFF, both
  retentions default 'off'. Settings auto-render in the Library Maintenance
  panel (same as Cover Art / Lyrics / Re-tag).
- delete_origin_download(): shared delete (resolve path → remove file → drop
  track row → drop history row); a file that won't delete keeps its row +
  reports. Used by auto mode AND the _fix_expired_download apply handler.
- Frontend: type/action ('Delete')/result labels + finding detail render.

Tests: 9 on the pure brain (windows, off, per-origin, protected, play-count
threshold, bad age) + 7 on the job (no-op when off, findings, mirror/watch
protection, auto-delete, delete helper missing/real file). 185 repair/origin
tests pass.
2026-06-07 22:06:56 -07:00
..
__init__.py Lift _execute_retag to core/library/retag.py 2026-04-29 09:03:42 -07:00
artist_image.py Write artist.jpg to artist folder so Navidrome shows real photos 2026-05-13 11:48:09 -07:00
direct_id.py Manual match: paste a MusicBrainz ID/URL to match directly (Ashh) 2026-06-07 13:04:17 -07:00
duplicate_cleaner.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
duplicate_keep.py Fix: duplicate detector kept lossy over lossless (rank format first) 2026-06-01 12:49:34 -07:00
embedded_id_reconcile.py Auto-reconcile embedded IDs for new tracks on library scans 2026-06-05 18:31:11 -07:00
expired_cleanup.py Expired Download Cleaner: retention-based cleanup of watchlist/playlist downloads (Boulder) 2026-06-07 22:06:56 -07:00
file_tags.py Add per-download Audit Trail modal to Library History 2026-05-13 09:50:24 -07:00
manual_library_match.py Manual library match: accept non-numeric library track ids (#754) 2026-05-31 09:11:46 -07:00
missing_track_import.py Test missing-track existing file imports 2026-05-17 14:18:17 -07:00
path_resolver.py Album Completeness: surface diagnostic when resolver can't find album folder 2026-05-12 14:04:15 -07:00
redownload.py Lift shared download dataclasses + boot via singleton factory 2026-05-05 09:08:39 -07:00
reorganize_tag_source.py Reorganize: optional embedded-tag mode (closes #592) 2026-05-15 07:56:18 -07:00
retag_planner.py Library re-tag (1/3): pure planner — match source tracklist + per-field tag diff 2026-06-04 08:50:40 -07:00
service_search.py Manual match: paste a MusicBrainz ID/URL to match directly (Ashh) 2026-06-07 13:04:17 -07:00
track_identity.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00