Files
mixxx/mixxx_export.py
2026-06-17 21:35:32 +02:00

145 lines
5.1 KiB
Python
Executable File

#!/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)