Add duplicate filename diagnostics
This commit is contained in:
@@ -80,6 +80,14 @@ def main():
|
||||
print(f"Unique Missing Filenames: {health.unique_missing_filenames}")
|
||||
print(f"Duplicate Filename Groups: {health.duplicate_filename_groups}")
|
||||
print(f"Duplicate Files: {health.duplicate_files}")
|
||||
print(
|
||||
"Suspected Cloud Conflict Groups: "
|
||||
f"{health.suspected_cloud_conflict_groups}"
|
||||
)
|
||||
print(
|
||||
"Suspected Cloud Conflict Files: "
|
||||
f"{health.suspected_cloud_conflict_files}"
|
||||
)
|
||||
print(f"Unused Tracks: {health.unused_tracks}")
|
||||
print(f"Suggested Matches: {health.suggested_matches}")
|
||||
print(f"Static Crates: {health.static_crates}")
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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."""
|
||||
|
||||
track_tuple = tuple(tracks)
|
||||
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)
|
||||
+19
-6
@@ -1,7 +1,7 @@
|
||||
from collections import Counter
|
||||
|
||||
from serato_doctor.duplicates import find_duplicate_groups
|
||||
from serato_doctor.matching import MatchingEngine
|
||||
from serato_doctor.models.crate import CrateKind
|
||||
from serato_doctor.models.duplicate import DuplicateKind
|
||||
from serato_doctor.models.health import HealthReport
|
||||
from serato_doctor.models.library import Library
|
||||
|
||||
@@ -23,8 +23,17 @@ def analyze_health(library: Library) -> HealthReport:
|
||||
round(healthy_count / len(results) * 100, 1) if results else None
|
||||
)
|
||||
|
||||
disk_name_counts = Counter(track.filename for track in library.tracks)
|
||||
duplicate_counts = [count for count in disk_name_counts.values() if count > 1]
|
||||
duplicate_groups = find_duplicate_groups(library.tracks)
|
||||
exact_duplicates = [
|
||||
group
|
||||
for group in duplicate_groups
|
||||
if group.kind is DuplicateKind.EXACT_NAME
|
||||
]
|
||||
cloud_conflicts = [
|
||||
group
|
||||
for group in duplicate_groups
|
||||
if group.kind is DuplicateKind.CLOUD_CONFLICT
|
||||
]
|
||||
referenced_names = {reference.filename for reference in library.references}
|
||||
unused_count = sum(
|
||||
1 for track in library.tracks if track.filename not in referenced_names
|
||||
@@ -45,8 +54,12 @@ def analyze_health(library: Library) -> HealthReport:
|
||||
{result.reference.filename for result in missing}
|
||||
),
|
||||
disk_tracks=len(library.tracks),
|
||||
duplicate_filename_groups=len(duplicate_counts),
|
||||
duplicate_files=sum(count - 1 for count in duplicate_counts),
|
||||
duplicate_filename_groups=len(exact_duplicates),
|
||||
duplicate_files=sum(group.extra_files for group in exact_duplicates),
|
||||
suspected_cloud_conflict_groups=len(cloud_conflicts),
|
||||
suspected_cloud_conflict_files=sum(
|
||||
group.extra_files for group in cloud_conflicts
|
||||
),
|
||||
unused_tracks=unused_count,
|
||||
suggested_matches=suggested_count,
|
||||
static_crates=sum(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from serato_doctor.models.crate import Crate, CrateKind
|
||||
from serato_doctor.models.duplicate import DuplicateGroup, DuplicateKind
|
||||
from serato_doctor.models.health import HealthReport
|
||||
from serato_doctor.models.library import Library
|
||||
from serato_doctor.models.match import MatchEvidence, TrackMatch
|
||||
@@ -9,6 +10,8 @@ __all__ = [
|
||||
"Crate",
|
||||
"CrateKind",
|
||||
"DiskTrack",
|
||||
"DuplicateGroup",
|
||||
"DuplicateKind",
|
||||
"HealthReport",
|
||||
"Library",
|
||||
"MatchEvidence",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Tuple
|
||||
|
||||
from serato_doctor.models.track import DiskTrack
|
||||
|
||||
|
||||
class DuplicateKind(str, Enum):
|
||||
EXACT_NAME = "exact_name"
|
||||
CLOUD_CONFLICT = "cloud_conflict"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DuplicateGroup:
|
||||
"""A deterministic group of files that warrants duplicate review."""
|
||||
|
||||
kind: DuplicateKind
|
||||
comparison_key: str
|
||||
tracks: Tuple[DiskTrack, ...]
|
||||
|
||||
@property
|
||||
def extra_files(self) -> int:
|
||||
return max(0, len(self.tracks) - 1)
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return min(
|
||||
(track.filename for track in self.tracks),
|
||||
key=lambda name: (len(name), name.casefold()),
|
||||
)
|
||||
@@ -15,6 +15,8 @@ class HealthReport:
|
||||
disk_tracks: int
|
||||
duplicate_filename_groups: int
|
||||
duplicate_files: int
|
||||
suspected_cloud_conflict_groups: int
|
||||
suspected_cloud_conflict_files: int
|
||||
unused_tracks: int
|
||||
suggested_matches: int
|
||||
static_crates: int
|
||||
|
||||
Reference in New Issue
Block a user