#!/usr/bin/env python3 """Sanity-check a rekordbox XML before importing it. Verifies the file is well-formed, that every Location resolves to a file that actually exists, that TrackIDs are unique, and that no playlist references a track missing from the collection. Usage: python validate_rekordbox_xml.py rekordbox.xml """ import argparse import os import random import sys import xml.etree.ElementTree as ET from urllib.parse import unquote def to_path(loc): p = unquote(loc.replace("file://localhost", "")) if os.name == "nt" and p.startswith("/"): p = p[1:] return p.replace("/", os.sep) def main(): ap = argparse.ArgumentParser(description="Validate a rekordbox XML.") ap.add_argument("xml") ap.add_argument("--samples", type=int, default=5, help="how many random tracks to print (default: 5)") args = ap.parse_args() try: root = ET.parse(args.xml).getroot() except ET.ParseError as e: sys.exit(f"FAIL - not well-formed XML: {e}") print("well-formed XML : OK") coll = root.find("COLLECTION") if coll is None: sys.exit("FAIL - no COLLECTION element") tracks = coll.findall("TRACK") declared = int(coll.get("Entries", 0)) print(f"collection entries : {len(tracks)} " f"(attribute says {declared}) " f"{'OK' if declared == len(tracks) else '<-- MISMATCH'}") ids = {t.get("TrackID") for t in tracks} print(f"unique TrackIDs : {len(ids)} " f"{'OK' if len(ids) == len(tracks) else '<-- DUPLICATES'}") missing = [to_path(t.get("Location")) for t in tracks if not os.path.exists(to_path(t.get("Location")))] print(f"files not found on disk : {len(missing)}") for m in missing[:10]: print(" MISSING:", m) print(f"\nwithout title : {sum(1 for t in tracks if not t.get('Name'))}") print(f"without artist : {sum(1 for t in tracks if not t.get('Artist'))}") print(f"without BPM : {sum(1 for t in tracks if not t.get('AverageBpm'))}") print(f"zero length : " f"{sum(1 for t in tracks if t.get('TotalTime') in ('0', '', None))}") pl = root.find("PLAYLISTS") stats = {"pl": 0, "folder": 0, "refs": 0, "bad": 0, "mismatch": []} def walk(n): if n.get("Type") == "1": stats["pl"] += 1 kids = n.findall("TRACK") stats["refs"] += len(kids) if int(n.get("Entries", -1)) != len(kids): stats["mismatch"].append(n.get("Name")) for k in kids: if k.get("Key") not in ids: stats["bad"] += 1 else: stats["folder"] += 1 for c in n.findall("NODE"): walk(c) if pl is not None and pl.find("NODE") is not None: walk(pl.find("NODE")) print(f"\nplaylists : {stats['pl']}") print(f"folder nodes : {stats['folder']}") print(f"track references : {stats['refs']}") print(f"broken references : {stats['bad']}") for m in stats["mismatch"][:10]: print(f" Entries mismatch in playlist {m!r}") if tracks and args.samples: print("\n--- random sample ---") for t in random.sample(tracks, min(args.samples, len(tracks))): print(f" {(t.get('Name') or '')[:40]:42} | " f"{(t.get('Artist') or '')[:20]:22} | " f"{t.get('TotalTime'):>5}s | " f"BPM {t.get('AverageBpm') or '-':>6} | {t.get('Kind')}") ok = (not missing and not stats["bad"] and declared == len(tracks) and len(ids) == len(tracks)) print(f"\nRESULT: {'PASS' if ok else 'PROBLEMS FOUND'}") return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())