#!/usr/bin/env python3 """ mixxx_export.py - Exporte TOUTES les métadonnées de préparation Mixxx (cues, BPM, grille de beat, clé) vers des fichiers .mixxx JSON. """ import sqlite3 import json import sys import os from pathlib import Path DEFAULT_DB = Path.home() / ".mixxx" / "mixxxdb.sqlite" def export_cues(db_path: Path, output_dir: Path = None): con = sqlite3.connect(db_path) con.row_factory = sqlite3.Row # Récupérer cues + métadonnées de library en une seule requête par piste rows = con.execute(""" SELECT tl.location AS audio_path, tl.filename AS filename, lib.artist AS artist, lib.title AS title, lib.album AS album, lib.genre AS genre, lib.year AS year, lib.comment AS comment, lib.bpm AS bpm, lib.bpm_lock AS bpm_lock, lib.key AS key, lib.key_id AS key_id, lib.cuepoint AS cuepoint, lib.beats AS beats, lib.beats_version AS beats_version, lib.beats_sub_version AS beats_sub_version, c.type AS type, c.position AS position, c.length AS length, c.hotcue AS hotcue, c.label AS label, c.color AS color FROM cues c JOIN library lib ON lib.id = c.track_id JOIN track_locations tl ON tl.id = lib.location WHERE tl.fs_deleted = 0 ORDER BY tl.location, c.hotcue, c.type """).fetchall() # Grouper par fichier audio tracks: dict[str, dict] = {} for r in rows: path = r["audio_path"] if path not in tracks: tracks[path] = { "filename": r["filename"], "artist": r["artist"] or "", "title": r["title"] or "", "album": r["album"] or "", "genre": r["genre"] or "", "year": r["year"] or "", "comment": r["comment"] or "", "bpm": r["bpm"] or 0.0, "bpm_lock": bool(r["bpm_lock"]), "key": r["key"] or "", "key_id": r["key_id"] or 0, "cuepoint": r["cuepoint"] or 0, "beats_version": r["beats_version"] or "", "beats_sub_version": r["beats_sub_version"] or "", "beats": None, # sera rempli une seule fois "cues": [] } # Encoder le BLOB beats en base64 s'il existe if r["beats"] is not None: import base64 tracks[path]["beats"] = base64.b64encode(r["beats"]).decode("ascii") tracks[path]["cues"].append({ "type": r["type"], "position": r["position"], "length": r["length"], "hotcue": r["hotcue"], "label": r["label"] or "", "color": r["color"], }) con.close() exported = 0 for audio_path, data in tracks.items(): audio_file = Path(audio_path) if not audio_file.exists(): print(f"[SKIP] fichier introuvable : {audio_path}") continue if output_dir: out = output_dir / (audio_file.stem + audio_file.suffix + ".mixxx") else: out = audio_file.with_suffix(audio_file.suffix + ".mixxx") payload = { "version": 2, # version incrémentée car nouveau format "filename": data["filename"], "artist": data["artist"], "title": data["title"], "album": data["album"], "genre": data["genre"], "year": data["year"], "comment": data["comment"], "bpm": data["bpm"], "bpm_lock": data["bpm_lock"], "key": data["key"], "key_id": data["key_id"], "cuepoint": data["cuepoint"], "beats": data["beats"], "beats_version": data["beats_version"], "beats_sub_version": data["beats_sub_version"], "cues": data["cues"], } try: out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(payload, indent=2, ensure_ascii=False)) print(f"[OK] {out.name}") exported += 1 except Exception as e: print(f"[ERR] {out.name} : {e}") print(f"\n{exported} fichier(s) .mixxx exporté(s).") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Export complet Mixxx (cues + BPM + grille + clé)") parser.add_argument("db_path", nargs="?", default=str(DEFAULT_DB), help="Chemin vers mixxxdb.sqlite") parser.add_argument("--output-dir", help="Dossier de sortie pour les .mixxx (par défaut : à côté de l'audio)") args = parser.parse_args() db_path = Path(args.db_path) if not db_path.exists(): print(f"DB introuvable : {db_path}") sys.exit(1) out_dir = Path(args.output_dir) if args.output_dir else None export_cues(db_path, out_dir)