Add read-only database V2 parser
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, Optional, Tuple
|
||||
|
||||
from serato_doctor.models.database import DatabaseTrack, SeratoDatabase
|
||||
|
||||
|
||||
TEXT_FIELDS = {
|
||||
b"tsng": "title",
|
||||
b"tart": "artist",
|
||||
b"talb": "album",
|
||||
b"tgen": "genre",
|
||||
}
|
||||
|
||||
|
||||
def iter_records(data: bytes) -> Iterator[Tuple[bytes, bytes]]:
|
||||
"""Yield complete big-endian tag-length-value records."""
|
||||
|
||||
offset = 0
|
||||
while offset + 8 <= len(data):
|
||||
tag = data[offset : offset + 4]
|
||||
length = int.from_bytes(data[offset + 4 : offset + 8], "big")
|
||||
payload_start = offset + 8
|
||||
payload_end = payload_start + length
|
||||
if payload_end > len(data):
|
||||
break
|
||||
yield tag, data[payload_start:payload_end]
|
||||
offset = payload_end
|
||||
|
||||
|
||||
def decode_text(payload: bytes) -> Optional[str]:
|
||||
value = payload.decode("utf-16-be", errors="ignore").strip("\x00").strip()
|
||||
return value or None
|
||||
|
||||
|
||||
def normalize_database_path(value: str) -> Path:
|
||||
if value.startswith(("Users/", "Volumes/")):
|
||||
value = "/" + value
|
||||
return Path(value)
|
||||
|
||||
|
||||
def parse_track(payload: bytes) -> Optional[DatabaseTrack]:
|
||||
fields: Dict[str, Optional[str]] = {}
|
||||
path = None
|
||||
for tag, value in iter_records(payload):
|
||||
if tag == b"pfil":
|
||||
decoded_path = decode_text(value)
|
||||
if decoded_path:
|
||||
path = normalize_database_path(decoded_path)
|
||||
elif tag in TEXT_FIELDS:
|
||||
fields[TEXT_FIELDS[tag]] = decode_text(value)
|
||||
if path is None:
|
||||
return None
|
||||
return DatabaseTrack(path=path, filename=path.name, **fields)
|
||||
|
||||
|
||||
def parse_database(database_path: Path) -> SeratoDatabase:
|
||||
data = database_path.read_bytes()
|
||||
version = None
|
||||
tracks = []
|
||||
for tag, payload in iter_records(data):
|
||||
if tag == b"vrsn":
|
||||
version = decode_text(payload)
|
||||
elif tag == b"otrk":
|
||||
track = parse_track(payload)
|
||||
if track is not None:
|
||||
tracks.append(track)
|
||||
return SeratoDatabase(database_path, version, tuple(tracks))
|
||||
Reference in New Issue
Block a user