Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a384cdc88d | |||
| 71bca064ed |
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user