Classify static and smart crates
This commit is contained in:
+10
-5
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user