soulsync/core/library
BoulderBadgeDad 0c1dd6c2a9 Delete: resolve the real on-disk file when DB metadata uses curly quotes (#833)
the-hang-man: tracks with an apostrophe (e.g. "I'm Upset") deleted the DB row
but left the file. The library DB stored the title with U+2019 (the curly form
Spotify/Apple metadata uses) while the file was written to disk with U+0027
(ASCII). _resolve_library_file_path compared the curly path byte-for-byte via
os.path.exists, missed every time, and reported "could not be deleted".

Fix: resolve confusable-tolerantly. New core/library/path_resolve.find_on_disk
descends the path component by component, taking an exact match when present and
otherwise folding a small set of typographic look-alikes (curly vs straight
quotes, en/em dash, ellipsis, nbsp) for the comparison ONLY — it never renames,
just finds the file that's actually there. Exact matches always win per
component, so paths that already resolved are byte-for-byte unaffected. This
also fixes existing mismatched files (no re-import) and every caller of
_resolve_library_file_path (sidecar cleanup, dead-file checks, streaming), not
just delete.

Case is deliberately NOT folded: a case-sensitive dataset (ext4/ZFS) can hold
names differing only by case, and folding could resolve the wrong file. The
reported failure is purely typographic.

Tests: real temp-file fixtures exercising the actual byte mismatch — curly-DB →
ascii-disk resolves, exact still works, confusable in a folder component, exact
wins when both encodings present, genuinely-different name does NOT collide,
missing file → None. 10 new tests; 949 resolver-adjacent tests pass.
2026-06-09 22:28:53 -07:00
..
__init__.py
artist_image.py
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
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
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
redownload.py
reorganize_tag_source.py
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