from collections import defaultdict from typing import DefaultDict, Iterable, List, Tuple from serato_doctor.matching import cloud_conflict_name, normalize from serato_doctor.models.duplicate import DuplicateGroup, DuplicateKind from serato_doctor.models.track import DiskTrack def find_duplicate_groups( tracks: Iterable[DiskTrack], ) -> Tuple[DuplicateGroup, ...]: """Find exact-name duplicates and suspected numeric conflict copies.""" # Healthy symlinks preserve legacy Serato paths without consuming another # copy of the audio, so they are aliases rather than duplicate files. track_tuple = tuple(track for track in tracks if not track.path.is_symlink()) by_name: DefaultDict[str, List[DiskTrack]] = defaultdict(list) by_conflict_name: DefaultDict[str, List[DiskTrack]] = defaultdict(list) for track in track_tuple: by_name[normalize(track.filename)].append(track) by_conflict_name[cloud_conflict_name(track.filename)].append(track) groups = [] for key, matches in by_name.items(): if len(matches) > 1: groups.append(_group(DuplicateKind.EXACT_NAME, key, matches)) for key, matches in by_conflict_name.items(): distinct_names = {normalize(track.filename) for track in matches} if len(distinct_names) > 1: groups.append(_group(DuplicateKind.CLOUD_CONFLICT, key, matches)) return tuple( sorted(groups, key=lambda group: (group.kind.value, group.comparison_key)) ) def _group( kind: DuplicateKind, key: str, tracks: Iterable[DiskTrack] ) -> DuplicateGroup: ordered = tuple(sorted(tracks, key=lambda track: str(track.path))) return DuplicateGroup(kind, key, ordered)