Discover Serato smart crate definitions

This commit is contained in:
Philip Guzman
2026-07-01 08:06:19 -07:00
parent 71bca064ed
commit a384cdc88d
12 changed files with 117 additions and 18 deletions
+7 -2
View File
@@ -9,10 +9,15 @@ broken manual references or given the same health-score weight.
## Architecture ## Architecture
Crates carry a `static`, `smart`, or `unknown` kind. Folder provenance is the Crates carry a `static`, `smart`, or `unknown` kind. Folder provenance is the
primary signal: Serato stores regular definitions in `Subcrates` and smart primary signal: Serato stores regular `.crate` files in `Subcrates` and smart
definitions in `Smartcrates`. `Compatible by key.crate` is also treated as smart `.scrate` definitions in `SmartCrates`. `Compatible by key.crate` is also treated as smart
when encountered in `Subcrates`, based on the original migration case study. when encountered in `Subcrates`, based on the original migration case study.
Smart crate names use `≫≫` to encode hierarchy. The model preserves those segments,
so `Compatible by key≫≫10A.scrate` has a parent of `Compatible by key` and a display
name of `10A`. Smart definitions and dynamic `.crate` containers are counted
separately.
The library loader reads both folders. Health analysis reports all references but The library loader reads both folders. Health analysis reports all references but
scores only non-smart references. Unknown crates remain scoreable so incomplete scores only non-smart references. Unknown crates remain scoreable so incomplete
classification cannot silently hide potential problems. classification cannot silently hide potential problems.
+19
View File
@@ -0,0 +1,19 @@
# Smart Crate Discovery Correction
## Problem
The first classifier searched `SmartCrates` for `*.crate`. Real Serato smart-crate
definitions use `.scrate`, so a library with 26 definitions displayed only the
single name-based `Compatible by key.crate` fallback.
## Correction
Library discovery now reads `.scrate` definitions case-insensitively from the
`SmartCrates` folder. Dynamic `.crate` containers remain excluded from manual
reference scoring but are reported separately. The `≫≫` filename separator is
preserved as smart-crate hierarchy metadata.
## Verification
Synthetic tests cover `.scrate` discovery, case-correct folder names, hierarchy,
definition/container counts, and the existing five-static/two-smart sample.
+16 -6
View File
@@ -28,18 +28,28 @@ def build_sample(output: Optional[Path] = None) -> Path:
) )
for crate in manifest["crates"]: for crate in manifest["crates"]:
folder_name = "Smartcrates" if crate["type"] == "smart" else "Subcrates" is_smart = crate["type"] == "smart"
folder_name = "SmartCrates" if is_smart else "Subcrates"
crate_root = serato_root / folder_name crate_root = serato_root / folder_name
crate_root.mkdir(parents=True, exist_ok=True) crate_root.mkdir(parents=True, exist_ok=True)
other_folder = "Subcrates" if folder_name == "Smartcrates" else "Smartcrates" output_name = (
stale_path = serato_root / other_folder / crate["name"] Path(crate["name"]).with_suffix(".scrate").name
if stale_path.exists(): if is_smart
stale_path.unlink() else crate["name"]
)
for other_folder in ("Subcrates", "Smartcrates", "SmartCrates"):
for stale_name in (
crate["name"],
Path(crate["name"]).with_suffix(".scrate").name,
):
stale_path = serato_root / other_folder / stale_name
if stale_path.exists() and stale_path != crate_root / output_name:
stale_path.unlink()
records = "".join( records = "".join(
f"{SERATO_PATH_PREFIX}{relative_path}otrk" f"{SERATO_PATH_PREFIX}{relative_path}otrk"
for relative_path in crate["references"] for relative_path in crate["references"]
) )
(crate_root / crate["name"]).write_bytes(records.encode("utf-16-le")) (crate_root / output_name).write_bytes(records.encode("utf-16-le"))
return root return root
+1
View File
@@ -95,6 +95,7 @@ def main():
print(f"Broken Symlinks: {health.broken_symlinks}") print(f"Broken Symlinks: {health.broken_symlinks}")
print(f"Static Crates: {health.static_crates}") print(f"Static Crates: {health.static_crates}")
print(f"Smart Crates: {health.smart_crates}") print(f"Smart Crates: {health.smart_crates}")
print(f"Smart Crate Containers: {health.smart_crate_containers}")
print( print(
f"Dynamic References Excluded: {health.dynamic_references_excluded}" f"Dynamic References Excluded: {health.dynamic_references_excluded}"
) )
+14 -4
View File
@@ -103,9 +103,19 @@ def load_library_crates(
serato_root: Path, reference_roots: Iterable[Path] = () serato_root: Path, reference_roots: Iterable[Path] = ()
) -> Tuple[Crate, ...]: ) -> Tuple[Crate, ...]:
reference_roots = tuple(reference_roots) reference_roots = tuple(reference_roots)
if not serato_root.is_dir():
return ()
crates = [] crates = []
for folder_name in ("Subcrates", "Smartcrates"): folder_patterns = {
folder = serato_root / folder_name "subcrates": ("*.crate",),
for crate_path in folder.rglob("*.crate"): "smartcrates": ("*.scrate", "*.crate"),
crates.append(load_crate(crate_path, reference_roots)) }
for folder in serato_root.iterdir():
patterns = folder_patterns.get(folder.name.casefold())
if patterns is None or not folder.is_dir():
continue
for pattern in patterns:
for crate_path in folder.rglob(pattern):
crates.append(load_crate(crate_path, reference_roots))
return tuple(sorted(crates, key=lambda crate: str(crate.path))) return tuple(sorted(crates, key=lambda crate: str(crate.path)))
+6 -1
View File
@@ -67,7 +67,12 @@ def analyze_health(library: Library) -> HealthReport:
crate.kind is CrateKind.STATIC for crate in library.crates crate.kind is CrateKind.STATIC for crate in library.crates
), ),
smart_crates=sum( smart_crates=sum(
crate.kind is CrateKind.SMART for crate in library.crates crate.kind is CrateKind.SMART and crate.is_smart_definition
for crate in library.crates
),
smart_crate_containers=sum(
crate.kind is CrateKind.SMART and not crate.is_smart_definition
for crate in library.crates
), ),
unknown_crates=sum( unknown_crates=sum(
crate.kind is CrateKind.UNKNOWN for crate in library.crates crate.kind is CrateKind.UNKNOWN for crate in library.crates
+12
View File
@@ -19,3 +19,15 @@ class Crate:
path: Path path: Path
references: Tuple[TrackReference, ...] references: Tuple[TrackReference, ...]
kind: CrateKind = CrateKind.UNKNOWN kind: CrateKind = CrateKind.UNKNOWN
@property
def hierarchy(self) -> Tuple[str, ...]:
return tuple(self.path.stem.split("≫≫"))
@property
def display_name(self) -> str:
return self.hierarchy[-1]
@property
def is_smart_definition(self) -> bool:
return self.path.suffix.casefold() == ".scrate"
+1
View File
@@ -22,6 +22,7 @@ class HealthReport:
broken_symlinks: int broken_symlinks: int
static_crates: int static_crates: int
smart_crates: int smart_crates: int
smart_crate_containers: int
unknown_crates: int unknown_crates: int
dynamic_references_excluded: int dynamic_references_excluded: int
+1 -1
View File
@@ -73,7 +73,7 @@
<div id="diagnostics" class="diagnostics panel"> <div id="diagnostics" class="diagnostics panel">
<div class="section-heading"><div><p class="eyebrow">Full picture</p><h2>Diagnostics</h2></div><span class="read-only-tag">No changes made</span></div> <div class="section-heading"><div><p class="eyebrow">Full picture</p><h2>Diagnostics</h2></div><span class="read-only-tag">No changes made</span></div>
<div class="diagnostic-list"> <div class="diagnostic-list">
<div><span class="diag-icon violet"></span><p><strong>Crates</strong><small><b data-field="static_crates"></b> static · <b data-field="smart_crates"></b> smart · <b data-field="dynamic_references_excluded"></b> dynamic references excluded</small></p></div> <div><span class="diag-icon violet"></span><p><strong>Crates</strong><small><b data-field="static_crates"></b> static · <b data-field="smart_crates"></b> smart · <b data-field="smart_crate_containers"></b> dynamic containers · <b data-field="dynamic_references_excluded"></b> references excluded</small></p></div>
<div><span class="diag-icon amber"></span><p><strong>Duplicate filenames</strong><small><b data-field="duplicate_filename_groups"></b> exact groups · <b data-field="duplicate_files"></b> extra files</small></p></div> <div><span class="diag-icon amber"></span><p><strong>Duplicate filenames</strong><small><b data-field="duplicate_filename_groups"></b> exact groups · <b data-field="duplicate_files"></b> extra files</small></p></div>
<div><span class="diag-icon blue"></span><p><strong>Cloud conflicts</strong><small><b data-field="suspected_cloud_conflict_groups"></b> suspected groups · <b data-field="suspected_cloud_conflict_files"></b> extra files</small></p></div> <div><span class="diag-icon blue"></span><p><strong>Cloud conflicts</strong><small><b data-field="suspected_cloud_conflict_groups"></b> suspected groups · <b data-field="suspected_cloud_conflict_files"></b> extra files</small></p></div>
<div><span class="diag-icon red"></span><p><strong>Broken symlinks</strong><small><b data-field="broken_symlinks"></b> unresolved links</small></p></div> <div><span class="diag-icon red"></span><p><strong>Broken symlinks</strong><small><b data-field="broken_symlinks"></b> unresolved links</small></p></div>
+34 -1
View File
@@ -6,6 +6,7 @@ from serato_doctor.crate_parser import (
classify_crate, classify_crate,
clean_path, clean_path,
load_crate, load_crate,
load_library_crates,
parse_crate, parse_crate,
parse_crates, parse_crates,
path_markers, path_markers,
@@ -92,7 +93,7 @@ def test_parse_crates_reuses_configured_roots_for_every_crate(tmp_path):
("relative_path", "expected"), ("relative_path", "expected"),
[ [
("_Serato_/Subcrates/House.crate", CrateKind.STATIC), ("_Serato_/Subcrates/House.crate", CrateKind.STATIC),
("_Serato_/Smartcrates/Warmup.crate", CrateKind.SMART), ("_Serato_/SmartCrates/Warmup.scrate", CrateKind.SMART),
("_Serato_/Subcrates/Compatible by key.crate", CrateKind.SMART), ("_Serato_/Subcrates/Compatible by key.crate", CrateKind.SMART),
("fixtures/Unknown.crate", CrateKind.UNKNOWN), ("fixtures/Unknown.crate", CrateKind.UNKNOWN),
], ],
@@ -101,3 +102,35 @@ def test_classify_crate_uses_provenance_and_known_dynamic_name(
tmp_path, relative_path, expected tmp_path, relative_path, expected
): ):
assert classify_crate(tmp_path / relative_path) is expected assert classify_crate(tmp_path / relative_path) is expected
def test_smart_crate_preserves_encoded_hierarchy(tmp_path):
crate_path = (
tmp_path / "_Serato_" / "SmartCrates" / "Compatible by key≫≫10A.scrate"
)
crate_path.parent.mkdir(parents=True)
crate_path.write_bytes(b"")
crate = load_crate(crate_path)
assert crate.kind is CrateKind.SMART
assert crate.hierarchy == ("Compatible by key", "10A")
assert crate.display_name == "10A"
assert crate.is_smart_definition
def test_load_library_crates_discovers_scrate_definitions(tmp_path):
smart_folder = tmp_path / "SmartCrates"
static_folder = tmp_path / "Subcrates"
smart_folder.mkdir()
static_folder.mkdir()
(smart_folder / "New EDM.scrate").write_bytes(b"")
(smart_folder / "Re-Drums.scrate").write_bytes(b"")
(static_folder / "House.crate").write_bytes(b"")
crates = load_library_crates(tmp_path)
assert [(crate.path.name, crate.kind) for crate in crates] == [
("New EDM.scrate", CrateKind.SMART),
("Re-Drums.scrate", CrateKind.SMART),
("House.crate", CrateKind.STATIC),
]
+3 -2
View File
@@ -63,14 +63,14 @@ def test_empty_library_has_no_health_score():
def test_smart_crate_references_are_reported_but_not_scored(): def test_smart_crate_references_are_reported_but_not_scored():
static_reference = reference("Found.mp3") static_reference = reference("Found.mp3")
smart_reference = TrackReference( smart_reference = TrackReference(
Path("Smartcrates/Dynamic.crate"), Path("SmartCrates/Dynamic.scrate"),
Path("/old/House/Dynamic.mp3"), Path("/old/House/Dynamic.mp3"),
"Dynamic.mp3", "Dynamic.mp3",
) )
crates = [ crates = [
Crate(Path("Subcrates/Static.crate"), (static_reference,), CrateKind.STATIC), Crate(Path("Subcrates/Static.crate"), (static_reference,), CrateKind.STATIC),
Crate( Crate(
Path("Smartcrates/Dynamic.crate"), Path("SmartCrates/Dynamic.scrate"),
(smart_reference,), (smart_reference,),
CrateKind.SMART, CrateKind.SMART,
), ),
@@ -85,6 +85,7 @@ def test_smart_crate_references_are_reported_but_not_scored():
assert report.missing_references == 0 assert report.missing_references == 0
assert report.static_crates == 1 assert report.static_crates == 1
assert report.smart_crates == 1 assert report.smart_crates == 1
assert report.smart_crate_containers == 0
assert report.dynamic_references_excluded == 1 assert report.dynamic_references_excluded == 1
+3 -1
View File
@@ -24,7 +24,9 @@ def test_generated_sample_library_has_expected_scenario(tmp_path):
if not result.exists_by_filename if not result.exists_by_filename
} }
assert len(list((sample_root / "Serato").rglob("*.crate"))) == 7 crate_files = list((sample_root / "Serato").rglob("*.crate"))
smart_files = list((sample_root / "Serato").rglob("*.scrate"))
assert len(crate_files) + len(smart_files) == 7
assert len(library.references) == 13 assert len(library.references) == 13
assert len(tracks) == 10 assert len(tracks) == 10
assert missing == {"Missing.mp3", "Old Name.mp3"} assert missing == {"Missing.mp3", "Old Name.mp3"}