Merge feature/sample-library into develop

This commit is contained in:
Philip Guzman
2026-06-30 17:18:06 -07:00
7 changed files with 187 additions and 1 deletions
+1
View File
@@ -4,6 +4,7 @@ __pycache__/
.venv/
*.egg-info/
reports/
samples/small-library/generated/
*.sqlite
*.db
*.csv
+1 -1
View File
@@ -9,7 +9,7 @@
- [x] Grouped missing reference report
- [ ] HTML health dashboard
- [x] Test suite
- [ ] Sample library fixtures
- [x] Sample library fixtures
- [ ] Database V2 read-only parser
## v0.2 — Diagnostics
+26
View File
@@ -0,0 +1,26 @@
# Synthetic Sample Library
## Problem
Real Serato libraries contain private paths, listening history, and copyrighted
music. Contributors still need a repeatable library for tests and demonstrations.
## Architecture
`samples/small-library/manifest.json` describes a compact migration scenario.
`generate.py` turns that manifest into fake audio files and UTF-16-LE crate files
inside an ignored `generated` directory. The generated files are disposable and
contain no real audio or user library data.
## Edge Cases
- Five manually maintained crates and two smart/dynamic crates.
- Mixed supported audio extensions.
- One reference whose file is absent.
- One stale reference whose file has been renamed.
- References shared by static and smart crates.
## Verification
`pytest` generates the sample in a temporary directory and verifies its crate,
track, and missing-reference counts through the production parser and scanner.
+25
View File
@@ -0,0 +1,25 @@
# Small Synthetic Library
This fixture models a small Serato migration without containing music or personal
library data. It has 10 fake audio files, five static crates, two smart crates,
one absent song, and one renamed song.
Generate it with:
```shell
python3 samples/small-library/generate.py
```
Analyze it with:
```shell
python3 -m serato_doctor.cli \
--serato samples/small-library/generated/Serato/_Serato_ \
--music samples/small-library/generated/Music \
--out samples/small-library/generated/scan.csv \
--report samples/small-library/generated/report.txt
```
Expected CLI counts are 13 crate references, 10 disk tracks, and 2 references
missing by filename. Everything under `generated/` is disposable and ignored by
Git.
+50
View File
@@ -0,0 +1,50 @@
"""Generate a disposable, synthetic Serato library from the sample manifest."""
import argparse
import json
from pathlib import Path
from typing import Optional
SAMPLE_ROOT = Path(__file__).parent
SERATO_PATH_PREFIX = "Users/djsplice/OneDrive/Jukebox/"
def load_manifest() -> dict:
return json.loads((SAMPLE_ROOT / "manifest.json").read_text(encoding="utf-8"))
def build_sample(output: Optional[Path] = None) -> Path:
root = output or SAMPLE_ROOT / "generated"
manifest = load_manifest()
music_root = root / "Music"
crate_root = root / "Serato" / "_Serato_" / "Subcrates"
crate_root.mkdir(parents=True, exist_ok=True)
for relative_path in manifest["tracks"]:
track_path = music_root / relative_path
track_path.parent.mkdir(parents=True, exist_ok=True)
track_path.write_bytes(
f"Synthetic Serato Doctor fixture: {relative_path}\n".encode("utf-8")
)
for crate in manifest["crates"]:
records = "".join(
f"{SERATO_PATH_PREFIX}{relative_path}otrk"
for relative_path in crate["references"]
)
(crate_root / crate["name"]).write_bytes(records.encode("utf-16-le"))
return root
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
root = build_sample(args.output)
print(f"Generated synthetic library: {root}")
if __name__ == "__main__":
main()
+56
View File
@@ -0,0 +1,56 @@
{
"tracks": [
"House/First.mp3",
"House/Second.m4a",
"Open Format/Third.wav",
"Open Format/Fourth.aif",
"Classics/Fifth.mp3",
"Classics/Sixth.flac",
"Warmup/Seventh.mp3",
"Warmup/Eighth.mp3",
"Renamed/New Name.mp3",
"Bonus/Ninth.MP3"
],
"crates": [
{
"name": "House.crate",
"type": "static",
"references": ["House/First.mp3", "House/Second.m4a", "House/Missing.mp3"]
},
{
"name": "Open Format.crate",
"type": "static",
"references": ["Open Format/Third.wav", "Open Format/Fourth.aif"]
},
{
"name": "Classics.crate",
"type": "static",
"references": ["Classics/Fifth.mp3", "Classics/Sixth.flac"]
},
{
"name": "Renamed Tracks.crate",
"type": "static",
"references": ["Renamed/Old Name.mp3"]
},
{
"name": "Bonus.crate",
"type": "static",
"references": ["Bonus/Ninth.MP3"]
},
{
"name": "Compatible by key.crate",
"type": "smart",
"references": ["House/First.mp3", "House/Second.m4a"]
},
{
"name": "Smart Warmup.crate",
"type": "smart",
"references": ["Warmup/Seventh.mp3", "Warmup/Eighth.mp3"]
}
],
"expected": {
"disk_tracks": 10,
"crate_references": 13,
"missing_by_filename": ["Missing.mp3", "Old Name.mp3"]
}
}
+28
View File
@@ -0,0 +1,28 @@
import runpy
from pathlib import Path
from serato_doctor.crate_parser import parse_crates
from serato_doctor.models.library import Library
from serato_doctor.scanner import scan_audio
def test_generated_sample_library_has_expected_scenario(tmp_path):
generator_path = (
Path(__file__).parents[1] / "samples" / "small-library" / "generate.py"
)
build_sample = runpy.run_path(str(generator_path))["build_sample"]
sample_root = build_sample(tmp_path / "sample")
references = parse_crates(sample_root / "Serato" / "_Serato_" / "Subcrates")
tracks = scan_audio(sample_root / "Music")
results = Library.build(references, tracks).reconcile_by_filename()
missing = {
result.reference.filename
for result in results
if not result.exists_by_filename
}
assert len(list((sample_root / "Serato").rglob("*.crate"))) == 7
assert len(references) == 13
assert len(tracks) == 10
assert missing == {"Missing.mp3", "Old Name.mp3"}