Merge feature/sample-library into develop
This commit is contained in:
@@ -4,6 +4,7 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
reports/
|
reports/
|
||||||
|
samples/small-library/generated/
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.db
|
*.db
|
||||||
*.csv
|
*.csv
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
- [x] Grouped missing reference report
|
- [x] Grouped missing reference report
|
||||||
- [ ] HTML health dashboard
|
- [ ] HTML health dashboard
|
||||||
- [x] Test suite
|
- [x] Test suite
|
||||||
- [ ] Sample library fixtures
|
- [x] Sample library fixtures
|
||||||
- [ ] Database V2 read-only parser
|
- [ ] Database V2 read-only parser
|
||||||
|
|
||||||
## v0.2 — Diagnostics
|
## 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