Classify static and smart crates

This commit is contained in:
Philip Guzman
2026-06-30 18:20:05 -07:00
parent c92d929f37
commit 35713bbbb3
15 changed files with 199 additions and 23 deletions
+10 -5
View File
@@ -2,7 +2,7 @@ from pathlib import Path
import argparse
from serato_doctor.config import ScanConfig
from serato_doctor.crate_parser import parse_crates
from serato_doctor.crate_parser import load_library_crates
from serato_doctor.health import analyze_health
from serato_doctor.logging import configure_logging
from serato_doctor.models.library import Library
@@ -55,10 +55,9 @@ def main():
logger.debug("Serato directory: %s", config.serato)
logger.debug("Music directory: %s", config.music)
library = Library.build(
references=parse_crates(
config.serato / "Subcrates", config.reference_roots
),
crates = load_library_crates(config.serato, config.reference_roots)
library = Library.from_crates(
crates=crates,
tracks=scan_audio(config.music),
)
results = library.reconcile_by_filename()
@@ -75,6 +74,7 @@ def main():
print(f"Score Basis: {health.score_basis}")
print(f"Tracks: {health.disk_tracks}")
print(f"Crate References: {health.total_references}")
print(f"References Scored: {health.scored_references}")
print(f"Healthy References: {health.healthy_references}")
print(f"Broken References: {health.missing_references}")
print(f"Unique Missing Filenames: {health.unique_missing_filenames}")
@@ -82,6 +82,11 @@ def main():
print(f"Duplicate Files: {health.duplicate_files}")
print(f"Unused Tracks: {health.unused_tracks}")
print(f"Suggested Matches: {health.suggested_matches}")
print(f"Static Crates: {health.static_crates}")
print(f"Smart Crates: {health.smart_crates}")
print(
f"Dynamic References Excluded: {health.dynamic_references_excluded}"
)
else:
write_csv(results, config.out)
write_missing_report(results, config.report)
+29 -2
View File
@@ -1,7 +1,7 @@
from pathlib import Path
from typing import Iterable, Tuple
from serato_doctor.models.crate import Crate
from serato_doctor.models.crate import Crate, CrateKind
from serato_doctor.models.reference import TrackReference
@@ -32,6 +32,17 @@ def path_markers(reference_roots: Iterable[Path]) -> Tuple[str, ...]:
return configured or DEFAULT_PATH_MARKERS
def classify_crate(crate_path: Path) -> CrateKind:
parent_names = {parent.name.casefold() for parent in crate_path.parents}
if "smartcrates" in parent_names:
return CrateKind.SMART
if crate_path.name.casefold() == "compatible by key.crate":
return CrateKind.SMART
if "subcrates" in parent_names:
return CrateKind.STATIC
return CrateKind.UNKNOWN
def load_crate(crate_path: Path, reference_roots: Iterable[Path] = ()) -> Crate:
text = read_crate_text(crate_path)
refs = []
@@ -63,7 +74,11 @@ def load_crate(crate_path: Path, reference_roots: Iterable[Path] = ()) -> Crate:
)
)
return Crate(path=crate_path, references=tuple(refs))
return Crate(
path=crate_path,
references=tuple(refs),
kind=classify_crate(crate_path),
)
def parse_crate(
@@ -82,3 +97,15 @@ def parse_crates(
for crate in root.rglob("*.crate"):
refs.extend(parse_crate(crate, reference_roots))
return refs
def load_library_crates(
serato_root: Path, reference_roots: Iterable[Path] = ()
) -> Tuple[Crate, ...]:
reference_roots = tuple(reference_roots)
crates = []
for folder_name in ("Subcrates", "Smartcrates"):
folder = serato_root / folder_name
for crate_path in folder.rglob("*.crate"):
crates.append(load_crate(crate_path, reference_roots))
return tuple(sorted(crates, key=lambda crate: str(crate.path)))
+25 -2
View File
@@ -1,6 +1,7 @@
from collections import Counter
from serato_doctor.matching import MatchingEngine
from serato_doctor.models.crate import CrateKind
from serato_doctor.models.health import HealthReport
from serato_doctor.models.library import Library
@@ -8,7 +9,14 @@ from serato_doctor.models.library import Library
def analyze_health(library: Library) -> HealthReport:
"""Calculate defensible health metrics without changing the library."""
results = library.reconcile_by_filename()
dynamic_sources = {
crate.path for crate in library.crates if crate.kind is CrateKind.SMART
}
results = tuple(
result
for result in library.reconcile_by_filename()
if result.reference.source not in dynamic_sources
)
missing = [result for result in results if not result.exists_by_filename]
healthy_count = len(results) - len(missing)
score = (
@@ -29,7 +37,8 @@ def analyze_health(library: Library) -> HealthReport:
return HealthReport(
score=score,
total_references=len(results),
total_references=len(library.references),
scored_references=len(results),
healthy_references=healthy_count,
missing_references=len(missing),
unique_missing_filenames=len(
@@ -40,4 +49,18 @@ def analyze_health(library: Library) -> HealthReport:
duplicate_files=sum(count - 1 for count in duplicate_counts),
unused_tracks=unused_count,
suggested_matches=suggested_count,
static_crates=sum(
crate.kind is CrateKind.STATIC for crate in library.crates
),
smart_crates=sum(
crate.kind is CrateKind.SMART for crate in library.crates
),
unknown_crates=sum(
crate.kind is CrateKind.UNKNOWN for crate in library.crates
),
dynamic_references_excluded=sum(
len(crate.references)
for crate in library.crates
if crate.kind is CrateKind.SMART
),
)
+2 -1
View File
@@ -1,4 +1,4 @@
from serato_doctor.models.crate import Crate
from serato_doctor.models.crate import Crate, CrateKind
from serato_doctor.models.health import HealthReport
from serato_doctor.models.library import Library
from serato_doctor.models.match import MatchEvidence, TrackMatch
@@ -7,6 +7,7 @@ from serato_doctor.models.track import DiskTrack
__all__ = [
"Crate",
"CrateKind",
"DiskTrack",
"HealthReport",
"Library",
+8
View File
@@ -1,13 +1,21 @@
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Tuple
from serato_doctor.models.reference import TrackReference
class CrateKind(str, Enum):
STATIC = "static"
SMART = "smart"
UNKNOWN = "unknown"
@dataclass(frozen=True)
class Crate:
"""A Serato crate and the track references parsed from it."""
path: Path
references: Tuple[TrackReference, ...]
kind: CrateKind = CrateKind.UNKNOWN
+6 -1
View File
@@ -8,6 +8,7 @@ class HealthReport:
score: Optional[float]
total_references: int
scored_references: int
healthy_references: int
missing_references: int
unique_missing_filenames: int
@@ -16,7 +17,11 @@ class HealthReport:
duplicate_files: int
unused_tracks: int
suggested_matches: int
static_crates: int
smart_crates: int
unknown_crates: int
dynamic_references_excluded: int
@property
def score_basis(self) -> str:
return "Resolved crate references / total crate references"
return "Resolved non-dynamic references / non-dynamic references scored"
+14
View File
@@ -1,6 +1,7 @@
from dataclasses import dataclass
from typing import Iterable, Tuple
from serato_doctor.models.crate import Crate
from serato_doctor.models.reference import ReferenceResult, TrackReference
from serato_doctor.models.track import DiskTrack
@@ -11,6 +12,7 @@ class Library:
references: Tuple[TrackReference, ...]
tracks: Tuple[DiskTrack, ...]
crates: Tuple[Crate, ...] = ()
@classmethod
def build(
@@ -20,6 +22,18 @@ class Library:
) -> "Library":
return cls(tuple(references), tuple(tracks))
@classmethod
def from_crates(
cls, crates: Iterable[Crate], tracks: Iterable[DiskTrack]
) -> "Library":
crate_tuple = tuple(crates)
references = tuple(
reference
for crate in crate_tuple
for reference in crate.references
)
return cls(references, tuple(tracks), crate_tuple)
def reconcile_by_filename(self) -> Tuple[ReferenceResult, ...]:
disk_names = {track.filename for track in self.tracks}
return tuple(