soulsync/core/library/duplicate_keep.py
BoulderBadgeDad 28850672a6 Fix: duplicate detector kept lossy over lossless (rank format first)
The Duplicate Detector's 'Keep Best' auto-selection ranked copies by highest
bitrate -> duration -> track number, with no notion of format. A FLAC whose
bitrate the library scan never populated (a common gap) therefore lost to a
282 kbps MP3: 282 > 0, so the MP3 was kept and the FLAC deleted (reported on
Havok 'Prepare For Attack', and again on Kendrick GNX).

Fix: rank by format/lossless tier FIRST, then bitrate, duration, track number.
A lossless file now always beats a lossy one regardless of the recorded
bitrate; bitrate/duration/track# only break ties within the same format.

- core/library/duplicate_keep.py (new): pure, importable pick_duplicate_to_keep
  + duplicate_keep_sort_key + format_rank_for_path (extension rank mirroring
  auto_import_worker._quality_rank: flac=10 ... mp3=5 ... unknown=1).
- core/repair_worker.py: _fix_duplicates auto-pick now calls
  pick_duplicate_to_keep instead of the bitrate-first max().
- webui/static/enrichment.js: the KEEP/REMOVE recommendation mirrors the same
  format-first ranking so the badge matches what the backend will delete.

Parity: Python uses '.ext' keys (os.path.splitext), JS uses 'ext'
(split('.').pop()) -> identical results; both keep the first copy on a full
tie. Verified the only other dedup path (the standalone Duplicate Cleaner
automation, core/library/duplicate_cleaner.py) was already format-priority-first
and correct -- no change needed there.

Tests: tests/test_duplicate_keep.py (11 -- incl. the exact FLAC-with-missing-
bitrate vs 282 kbps MP3 case, format ranking, within-format tie-breakers, and
edge cases). 147 repair/duplicate tests still pass.

Note: why FLAC bitrate is NULL in the DB is a separate library-scan gap;
format-first ranking makes the keep decision correct regardless.
2026-06-01 12:49:34 -07:00

57 lines
2.2 KiB
Python

"""Choosing which copy of a duplicate track to keep.
The keeper is the highest-quality copy, and **format/lossless is the primary
criterion**: a lossless FLAC must beat a lossy MP3 regardless of the recorded
bitrate number — a FLAC whose bitrate the library scan never populated (a
common gap) still has to win over a 282 kbps MP3. Only *within* the same format
tier do bitrate, then duration, then track number break the tie.
This was lifted out of the repair worker so the deletion path and the UI's
"Keep Best" recommendation rank identically (previously both ranked by bitrate
first, which kept the MP3 when the FLAC's bitrate was missing), and so the
ranking is unit-testable in isolation.
"""
from __future__ import annotations
import os
from typing import Dict, List, Optional, Tuple
# Format quality rank by file extension (higher = better). Mirrors
# ``auto_import_worker._quality_rank`` so lossless always outranks lossy.
_FORMAT_RANK = {
".flac": 10, ".wav": 9, ".aiff": 9, ".aif": 9, ".ape": 8,
".m4a": 7, ".ogg": 6, ".opus": 6, ".mp3": 5, ".aac": 5, ".wma": 3,
}
def format_rank_for_path(file_path: Optional[str]) -> int:
"""Quality rank for a file by extension (higher = better, unknown = 1)."""
if not file_path:
return 1
ext = os.path.splitext(str(file_path))[1].lower()
return _FORMAT_RANK.get(ext, 1)
def duplicate_keep_sort_key(track: Dict) -> Tuple[int, int, float, int]:
"""Sort key for picking the keeper — the higher tuple wins.
Order of precedence: format/lossless tier, then bitrate, then duration,
then track number (a real number over a placeholder ``01``). Putting format
first is the whole point — it makes FLAC beat MP3 even when the FLAC's
bitrate is 0/missing in the DB.
"""
return (
format_rank_for_path(track.get("file_path")),
track.get("bitrate") or 0,
track.get("duration") or 0,
track.get("track_number") or 0,
)
def pick_duplicate_to_keep(tracks: List[Dict]) -> Optional[Dict]:
"""Return the copy to keep from a duplicate group (highest sort key), or
``None`` if the group is empty."""
if not tracks:
return None
return max(tracks, key=duplicate_keep_sort_key)