soulsync/core/library
BoulderBadgeDad 01c51a3c0e #904: guard standalone Deep Scan against relocating a desynced library
Standalone _run_soulsync_deep_scan did a path-only diff (untracked = transfer files
not in the soulsync DB) and shutil.move'd EVERY untracked file to Staging — no guard.
When the DB is empty/out of sync with disk (volume swap, DB reset, external Picard
tag edits) but Transfer holds the real library, that flags the whole library as
untracked and relocates all of it; Phase 5 then deletes the rows, and with Staging
cleanup on the files are gone for good. Reporter lost ~1,500 tracks into Staging.

The stale_guard the orphan detector + media-server deep scan already use (#828, #908)
was never wired into this path. Fix:

- core/library/standalone_scan.py (pure, tested): plan_standalone_deep_scan() diffs
  untracked (separator-normalized) and decides whether the move is safe. Blocks when
  the untracked share is implausibly large (>20 files AND >50% of Transfer — the
  desync signature, via is_implausible_orphan_flood) or when the user marked Transfer
  permanent. A normal batch of new arrivals still moves.
- web_server: consult the planner before Phase 4; on block, move NOTHING, leave files
  in place, and surface a loud warning + activity item. Guard Phase 5 deletes too
  (skip on desync-block or implausible stale share).
- 'Transfer is my permanent library — never move files out' toggle
  (import.transfer_is_permanent) in Settings.
- tests/library/test_standalone_scan.py: seam coverage + the #904 regression
  (empty DB + 1,500 files -> blocked, nothing moved).

No behavior change for in-sync libraries; the guard only trips on the desync pattern.
2026-06-22 17:53:49 -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
existing_album_folder.py Downloads: reuse an album's existing folder so batches don't split it (#829) 2026-06-09 13:47:25 -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_resolve.py Delete: resolve the real on-disk file when DB metadata uses curly quotes (#833) 2026-06-09 22:28:53 -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
residual_files.py #891: clear dead folders left with only cover images / .lrc sidecars 2026-06-18 20:07:41 -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
stale_guard.py Orphan detector: hard-bail on a mass-orphan flood instead of warn-only 2026-06-12 08:15:23 -07:00
standalone_scan.py #904: guard standalone Deep Scan against relocating a desynced library 2026-06-22 17:53:49 -07:00
track_identity.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00