Add synthetic sample library
This commit is contained in:
@@ -4,6 +4,7 @@ __pycache__/
|
||||
.venv/
|
||||
*.egg-info/
|
||||
reports/
|
||||
samples/small-library/generated/
|
||||
*.sqlite
|
||||
*.db
|
||||
*.csv
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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"}
|
||||
Reference in New Issue
Block a user