from pathlib import Path import re from serato_doctor.models import TrackReference AUDIO_EXTS = "mp3|m4a|wav|aif|aiff|flac|MP3|M4A|WAV|AIF|AIFF|FLAC" def read_crate_text(crate_path: Path) -> str: raw = crate_path.read_bytes() for enc in ("utf-16-be", "utf-16-le", "utf-8", "latin1"): text = raw.decode(enc, errors="ignore") if "Users" in text or "Jukebox" in text: return text return raw.decode("latin1", errors="ignore") def parse_crate(crate_path: Path) -> list[TrackReference]: text = read_crate_text(crate_path) # Serato crate files often decode with weird spacing/null-ish characters. # This finds paths from /Users/... through the audio extension without # greedily scanning the entire file. pattern = rf"/?Users/[^\r\n]+?\.(?:{AUDIO_EXTS})" refs = [] for match in re.finditer(pattern, text): raw_path = "/" + match.group(0).lstrip("/") raw_path = raw_path.replace("\x00", "") path = Path(raw_path) refs.append( TrackReference( source=crate_path, path=path, filename=path.name, ) ) return refs def parse_crates(root: Path) -> list[TrackReference]: refs = [] for crate in root.rglob("*.crate"): refs.extend(parse_crate(crate)) return refs