From 8c6a25cdb0813c92d730434709807ce15360e001 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 17 Jun 2026 11:47:28 +0200 Subject: [PATCH] feat: initial commit for export and import --- .gitignore | 47 ++++++ mixxx_export.py | 93 +++++++++++ mixxx_import.py | 375 ++++++++++++++++++++++++++++++++++++++++++ mixxx_import.spec | 38 +++++ test_export_import.py | 183 +++++++++++++++++++++ 5 files changed, 736 insertions(+) create mode 100644 .gitignore create mode 100755 mixxx_export.py create mode 100755 mixxx_import.py create mode 100644 mixxx_import.spec create mode 100755 test_export_import.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1e7e19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# === Python === +__pycache__/ +*.py[cod] +*.pyo +*.egg-info/ +dist/ +build/ +*.spec # spé PyInstaller (peut être conservé si personnalisé) +*.manifest + +# === Environnements virtuels === +venv/ +env/ +.venv/ +.env + +# === Fichiers de test / temporaires === +*.tmp +*.bak +test_*.db +test_*.sqlite + +# === Mixxx === +# Ignorer la base de données si jamais elle se retrouve dans le projet +mixxxdb.sqlite +mixxxdb.sqlite.bak + +# Ignorer tous les fichiers .mixxx exportés (sauf exemples) +*.mixxx +!example.mixxx # autoriser explicitement un fichier exemple + +# === Playlists exportées === +*.m3u +!example.m3u + +# === Système === +.DS_Store +Thumbs.db +*.swp +*~ + +# === IDE === +.vscode/ +.idea/ + +# === Logs === +*.log \ No newline at end of file diff --git a/mixxx_export.py b/mixxx_export.py new file mode 100755 index 0000000..5826911 --- /dev/null +++ b/mixxx_export.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +Exporte tous les cue points de Mixxx vers des fichiers .mixxx (JSON) +placés à côté de chaque fichier audio. + +Usage : python3 mixxx_export.py [chemin_db] +""" + +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 + + rows = con.execute(""" + SELECT + tl.location AS audio_path, + tl.filename AS filename, + 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 l ON l.id = c.track_id + JOIN track_locations tl ON tl.id = l.location + WHERE tl.fs_deleted = 0 + ORDER BY tl.location, c.hotcue, c.type + """).fetchall() + + by_file = {} + for r in rows: + path = r["audio_path"] + by_file.setdefault(path, []).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, cues in by_file.items(): + audio_file = Path(audio_path) + if not audio_file.exists(): + print(f"[SKIP] fichier introuvable : {audio_path}") + continue + + # Déterminer où écrire le .mixxx + 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": 1, + "filename": audio_file.name, + "cues": 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() + 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) \ No newline at end of file diff --git a/mixxx_import.py b/mixxx_import.py new file mode 100755 index 0000000..645e1fc --- /dev/null +++ b/mixxx_import.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 +""" +mixxx_import.py - Importe les cues Mixxx (fichiers .mixxx JSON) dans la base +de données, crée les pistes manquantes et importe une playlist .m3u. +Version avec interface graphique corrigée. +""" + +import sqlite3 +import json +import sys +import os +import argparse +import threading +from pathlib import Path +import tkinter as tk +from tkinter import filedialog, messagebox, ttk + +# ------------------------------------------------------------ +# 1. Fonctions de base +# ------------------------------------------------------------ + +AUDIO_EXTENSIONS = {".flac", ".mp3", ".opus", ".ogg", ".wav", ".m4a"} + +def default_db(): + if sys.platform == "win32": + return Path(os.environ["APPDATA"]) / "Mixxx" / "mixxxdb.sqlite" + return Path.home() / ".mixxx" / "mixxxdb.sqlite" + +def ensure_track(con, file_path: Path) -> int: + location = str(file_path.resolve()) + row = con.execute("SELECT id FROM track_locations WHERE location = ?", (location,)).fetchone() + if row: + return row[0] + + filename = file_path.name + directory = str(file_path.parent) + filesize = file_path.stat().st_size + + cur = con.execute( + "INSERT INTO track_locations (location, filename, directory, filesize, fs_deleted, needs_verification) VALUES (?,?,?,?,0,0)", + (location, filename, directory, filesize) + ) + tl_id = cur.lastrowid + + title = file_path.stem + con.execute( + "INSERT INTO library (id, artist, title, album, genre, tracknumber, year, duration, samplerate, bitrate, channels, bpm, key, rating, comment) " + "VALUES (?, '', ?, '', '', '', 0, 0, 0, 0, 0, 0, '', 0, '')", + (tl_id, title) + ) + return tl_id + +def add_playlist(con, name, track_ids): + cur = con.execute( + "INSERT INTO Playlists (name, position, hidden, date_created, date_modified) VALUES (?, 0, 0, datetime('now'), datetime('now'))", + (name,) + ) + playlist_id = cur.lastrowid + for pos, tid in enumerate(track_ids): + con.execute( + "INSERT INTO PlaylistTracks (playlist_id, track_id, position, pl_datetime_added) VALUES (?, ?, ?, datetime('now'))", + (playlist_id, tid, pos) + ) + return playlist_id + +def import_cues_v2(music_dir: Path, db_path: Path, dry_run=False, overwrite=False, m3u_file=None, log_callback=print): + con = sqlite3.connect(db_path) + try: + log_callback("Scan des fichiers audio...") + audio_files = {} + for ext in AUDIO_EXTENSIONS: + for f in music_dir.rglob(f"*{ext}"): + if f.name not in audio_files: + audio_files[f.name] = f.resolve() + + log_callback(f"{len(audio_files)} fichiers audio trouvés.") + + filename_to_trackid = {} + for fname, fpath in audio_files.items(): + try: + tid = ensure_track(con, fpath) + filename_to_trackid[fname] = tid + except Exception as e: + log_callback(f"[ERREUR] {fname} : {e}") + + mixxx_files = list(music_dir.rglob("*.mixxx")) + log_callback(f"{len(mixxx_files)} fichier(s) .mixxx trouvé(s).") + total = 0 + + for mf in mixxx_files: + try: + payload = json.loads(mf.read_text(encoding="utf-8")) + except Exception as e: + log_callback(f"[ERREUR] {mf.name} : {e}") + continue + + if payload.get("version") != 1: + log_callback(f"[IGNORÉ] {mf.name} : version inconnue") + continue + + target = payload.get("filename", "") + tid = filename_to_trackid.get(target) + if tid is None: + log_callback(f"[PAS TROUVÉ] {target}") + continue + + cues = payload.get("cues", []) + if not cues: + continue + + if overwrite and not dry_run: + con.execute("DELETE FROM cues WHERE track_id = ?", (tid,)) + + existing = set() + if not overwrite: + rows = con.execute( + "SELECT type, position, length, hotcue, label FROM cues WHERE track_id = ?", + (tid,) + ).fetchall() + existing = {(r[0], r[1], r[2] or 0.0, r[3], r[4] or "") for r in rows} + + inserted = 0 + for c in cues: + key = (c["type"], c["position"], c.get("length", 0.0) or 0.0, c["hotcue"], c.get("label", "") or "") + if key in existing: + continue + if not dry_run: + con.execute( + "INSERT INTO cues (track_id, type, position, length, hotcue, label, color) VALUES (?,?,?,?,?,?,?)", + (tid, c["type"], c["position"], c.get("length", 0.0), c["hotcue"], c.get("label", ""), c.get("color", 16744448)) + ) + inserted += 1 + + status = "[DRY]" if dry_run else "[OK]" + log_callback(f"{status} {target} : {inserted} cues") + total += inserted + + if m3u_file and m3u_file.exists(): + log_callback(f"\nImport playlist : {m3u_file.name}") + with open(m3u_file, "r", encoding="utf-8") as f: + lines = [l.strip() for l in f if l.strip() and not l.startswith("#")] + + track_ids = [] + m3u_dir = m3u_file.parent + for rel in lines: + abs_path = (m3u_dir / rel).resolve() + if not abs_path.exists(): + log_callback(f"[FICHIER ABSENT] {abs_path}") + continue + tid = ensure_track(con, abs_path) + track_ids.append(tid) + + if track_ids and not dry_run: + name = m3u_file.stem + add_playlist(con, name, track_ids) + log_callback(f"Playlist '{name}' créée ({len(track_ids)} pistes).") + elif track_ids: + log_callback(f"(dry-run) Playlist '{m3u_file.stem}' aurait {len(track_ids)} pistes.") + + if not dry_run: + con.commit() + + log_callback(f"\n✅ IMPORT TERMINÉ : {total} cues importés.") + + finally: + con.close() + + +# ------------------------------------------------------------ +# 2. Interface graphique Tkinter (CORRIGÉE) +# ------------------------------------------------------------ + +class ImportGUI: + def __init__(self): + self.root = tk.Tk() + self.root.title("Mixxx Import Cues") + self.root.geometry("600x500") # Fenêtre plus grande + self.root.resizable(True, True) + + # Variables + self.folder_path = tk.StringVar() + self.m3u_path = tk.StringVar() + self.dry_run = tk.BooleanVar(value=False) + self.overwrite = tk.BooleanVar(value=False) + self.db_path = tk.StringVar(value=str(default_db())) + + # Style + self.root.configure(bg="#f0f0f0") + + self.create_widgets() + + def create_widgets(self): + # Frame principal avec padding + main_frame = ttk.Frame(self.root, padding="20") + main_frame.pack(fill="both", expand=True) + + # Titre + title_label = ttk.Label(main_frame, text="Import des Cues Mixxx", font=("Arial", 14, "bold")) + title_label.pack(pady=(0, 20)) + + # Dossier musique + folder_frame = ttk.LabelFrame(main_frame, text="Dossier contenant les fichiers audio et .mixxx", padding="10") + folder_frame.pack(fill="x", pady=(0, 10)) + + folder_entry_frame = ttk.Frame(folder_frame) + folder_entry_frame.pack(fill="x") + ttk.Entry(folder_entry_frame, textvariable=self.folder_path).pack(side="left", fill="x", expand=True, padx=(0, 5)) + ttk.Button(folder_entry_frame, text="📁 Parcourir", command=self.browse_folder).pack(side="right") + + # Fichier .m3u + m3u_frame = ttk.LabelFrame(main_frame, text="Fichier .m3u (optionnel)", padding="10") + m3u_frame.pack(fill="x", pady=(0, 10)) + + m3u_entry_frame = ttk.Frame(m3u_frame) + m3u_entry_frame.pack(fill="x") + ttk.Entry(m3u_entry_frame, textvariable=self.m3u_path).pack(side="left", fill="x", expand=True, padx=(0, 5)) + ttk.Button(m3u_entry_frame, text="📁 Parcourir", command=self.browse_m3u).pack(side="right") + + # Base de données + db_frame = ttk.LabelFrame(main_frame, text="Base de données Mixxx", padding="10") + db_frame.pack(fill="x", pady=(0, 10)) + + db_entry_frame = ttk.Frame(db_frame) + db_entry_frame.pack(fill="x") + ttk.Entry(db_entry_frame, textvariable=self.db_path).pack(side="left", fill="x", expand=True, padx=(0, 5)) + ttk.Button(db_entry_frame, text="📁 Parcourir", command=self.browse_db).pack(side="right") + + # Options + options_frame = ttk.LabelFrame(main_frame, text="Options", padding="10") + options_frame.pack(fill="x", pady=(0, 10)) + + ttk.Checkbutton(options_frame, text="Dry-run (simulation sans écriture)", variable=self.dry_run).pack(anchor="w", pady=2) + ttk.Checkbutton(options_frame, text="Écraser les cues existants", variable=self.overwrite).pack(anchor="w", pady=2) + + # Zone de log + log_frame = ttk.LabelFrame(main_frame, text="Journal", padding="5") + log_frame.pack(fill="both", expand=True, pady=(0, 10)) + + self.log_text = tk.Text(log_frame, height=8, width=70, state="disabled", bg="#1e1e1e", fg="#00ff00", font=("Courier", 9)) + self.log_text.pack(fill="both", expand=True) + + scrollbar = ttk.Scrollbar(self.log_text, orient="vertical", command=self.log_text.yview) + scrollbar.pack(side="right", fill="y") + self.log_text.configure(yscrollcommand=scrollbar.set) + + # Frame pour les boutons + button_frame = ttk.Frame(main_frame) + button_frame.pack(fill="x", pady=(10, 0)) + + # Bouton LANCER (bien visible) + self.run_btn = tk.Button( + button_frame, + text="🚀 LANCER L'IMPORT", + command=self.start_import, + bg="#4CAF50", # Vert + fg="white", + font=("Arial", 12, "bold"), + padx=30, + pady=10, + relief="raised", + cursor="hand2" + ) + self.run_btn.pack(side="left", expand=True, fill="x", padx=(0, 5)) + + # Bouton Quitter + quit_btn = tk.Button( + button_frame, + text="❌ Quitter", + command=self.root.quit, + bg="#f44336", # Rouge + fg="white", + font=("Arial", 12), + padx=20, + pady=10, + cursor="hand2" + ) + quit_btn.pack(side="right", expand=True, fill="x", padx=(5, 0)) + + def log(self, message): + self.log_text.configure(state="normal") + self.log_text.insert("end", message + "\n") + self.log_text.see("end") + self.log_text.configure(state="disabled") + self.root.update_idletasks() + + def browse_folder(self): + path = filedialog.askdirectory(title="Sélectionner le dossier de musique") + if path: + self.folder_path.set(path) + + def browse_m3u(self): + path = filedialog.askopenfilename(title="Sélectionner un fichier .m3u", filetypes=[("M3U files", "*.m3u"), ("All files", "*.*")]) + if path: + self.m3u_path.set(path) + + def browse_db(self): + path = filedialog.askopenfilename(title="Sélectionner mixxxdb.sqlite", filetypes=[("SQLite", "*.sqlite"), ("All files", "*.*")]) + if path: + self.db_path.set(path) + + def validate(self): + music = Path(self.folder_path.get()) + if not music.is_dir(): + messagebox.showerror("Erreur", "Le dossier de musique n'existe pas.") + return False + db = Path(self.db_path.get()) + if not db.exists(): + messagebox.showerror("Erreur", "Le fichier de base de données est introuvable.") + return False + m3u = self.m3u_path.get().strip() + if m3u and not Path(m3u).exists(): + messagebox.showerror("Erreur", "Le fichier .m3u indiqué n'existe pas.") + return False + return True + + def start_import(self): + if not self.validate(): + return + + self.run_btn.configure(state="disabled", text="⏳ Import en cours...") + self.log("=" * 50) + self.log("DÉBUT DE L'IMPORT") + self.log("=" * 50) + + music_dir = Path(self.folder_path.get()) + db_path = Path(self.db_path.get()) + dry = self.dry_run.get() + overwrite = self.overwrite.get() + m3u = Path(self.m3u_path.get()) if self.m3u_path.get().strip() else None + + def task(): + try: + import_cues_v2(music_dir, db_path, dry, overwrite, m3u, log_callback=self.log) + except Exception as e: + self.log(f"❌ ERREUR FATALE : {e}") + finally: + self.run_btn.configure(state="normal", text="🚀 LANCER L'IMPORT") + if not dry: + messagebox.showinfo("Terminé", "Import terminé avec succès !") + + threading.Thread(target=task, daemon=True).start() + + def run(self): + self.root.mainloop() + + +# ------------------------------------------------------------ +# Point d'entrée +# ------------------------------------------------------------ +if __name__ == "__main__": + if len(sys.argv) > 1: + parser = argparse.ArgumentParser(description="Import cues Mixxx (console ou GUI)") + parser.add_argument("music_dir", nargs="?", help="Dossier contenant musique + .mixxx") + parser.add_argument("db_path", nargs="?", default=str(default_db()), help="Chemin vers mixxxdb.sqlite") + parser.add_argument("--dry-run", action="store_true", help="Simulation") + parser.add_argument("--overwrite", action="store_true", help="Écraser les cues existants") + parser.add_argument("--m3u", help="Fichier .m3u à importer") + parser.add_argument("--gui", action="store_true", help="Forcer l'interface graphique") + args = parser.parse_args() + + if args.gui or not args.music_dir: + ImportGUI().run() + else: + music_dir = Path(args.music_dir) + db_path = Path(args.db_path) + if not music_dir.is_dir(): + print("Dossier introuvable") + sys.exit(1) + if not Path(db_path).exists(): + print("DB introuvable") + sys.exit(1) + m3u = Path(args.m3u) if args.m3u else None + import_cues_v2(music_dir, db_path, args.dry_run, args.overwrite, m3u, log_callback=print) + else: + ImportGUI().run() \ No newline at end of file diff --git a/mixxx_import.spec b/mixxx_import.spec new file mode 100644 index 0000000..7aaaec9 --- /dev/null +++ b/mixxx_import.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['mixxx_import.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='mixxx_import', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/test_export_import.py b/test_export_import.py new file mode 100755 index 0000000..8a4836d --- /dev/null +++ b/test_export_import.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +Test d'intégration : export + import des cues Mixxx via les modules mixxx_export et mixxx_import. +Nécessite que mixxx_export.py et mixxx_import_gui.py (ou mixxx_import.py) soient dans le même répertoire, +ou ajustez les imports. +""" + +import sqlite3 +import tempfile +import os +import json +from pathlib import Path +import subprocess +import sys + +# ------------------------------------------------------------ +# 1. Créer une base Mixxx temporaire avec les bonnes tables +# ------------------------------------------------------------ +def create_temp_db(db_path: Path): + con = sqlite3.connect(db_path) + con.executescript(""" + CREATE TABLE track_locations ( + id INTEGER PRIMARY KEY, + location TEXT, + filename TEXT, + directory TEXT, + filesize INTEGER, + fs_deleted INTEGER DEFAULT 0, + needs_verification INTEGER DEFAULT 0 + ); + CREATE TABLE library ( + id INTEGER PRIMARY KEY, + artist TEXT, + title TEXT, + album TEXT, + genre TEXT, + tracknumber INTEGER, + year INTEGER, + duration REAL, + samplerate INTEGER, + bitrate INTEGER, + channels INTEGER, + bpm REAL, + key TEXT, + rating INTEGER, + comment TEXT, + location INTEGER REFERENCES track_locations(id) -- clé manquante dans l'exemple précédent + ); + CREATE TABLE cues ( + id INTEGER PRIMARY KEY, + track_id INTEGER, + type INTEGER, + position REAL, + length REAL, + hotcue INTEGER, + label TEXT, + color INTEGER + ); + CREATE TABLE IF NOT EXISTS Playlists ( + id INTEGER PRIMARY KEY, + name TEXT, + position INTEGER, + hidden INTEGER DEFAULT 0, + date_created TEXT, + date_modified TEXT + ); + CREATE TABLE IF NOT EXISTS PlaylistTracks ( + id INTEGER PRIMARY KEY, + playlist_id INTEGER, + track_id INTEGER, + position INTEGER, + pl_datetime_added TEXT + ); + """) + con.commit() + return con + +# ------------------------------------------------------------ +# 2. Insérer des données de test +# ------------------------------------------------------------ +def populate_test_data(con, audio_file_path: Path): + # Une piste "test.flac" + tl_id = 1 + con.execute(""" + INSERT INTO track_locations (id, location, filename, directory, filesize) + VALUES (?, ?, ?, ?, ?) + """, (tl_id, str(audio_file_path), audio_file_path.name, str(audio_file_path.parent), 12345)) + con.execute(""" + INSERT INTO library (id, artist, title, album, genre, year, duration, location) + VALUES (?, 'Test', 'Ma Chanson', '', '', 2025, 200, ?) + """, (tl_id, tl_id)) # location pointe vers track_locations.id + # Deux hot cues + con.execute("INSERT INTO cues (track_id, type, position, length, hotcue, label, color) VALUES (?,1,10.5,0,1,'Intro',16744448)", (tl_id,)) + con.execute("INSERT INTO cues (track_id, type, position, length, hotcue, label, color) VALUES (?,1,45.0,0,2,'Drop',0xFF8000)", (tl_id,)) + # Une boucle (type=4) + con.execute("INSERT INTO cues (track_id, type, position, length, hotcue, label, color) VALUES (?,4,60.0,15.0,-1,'',0)", (tl_id,)) + con.commit() + +# ------------------------------------------------------------ +# 3. Fonctions de test +# ------------------------------------------------------------ +def run_export(db_path: Path, output_dir: Path): + # Utiliser le module mixxx_export directement + import mixxx_export + mixxx_export.export_cues(db_path, output_dir) + +def run_import(music_dir: Path, db_path: Path, dry_run=False): + # Utiliser le module mixxx_import (v2) + import mixxx_import + mixxx_import.import_cues_v2(music_dir, db_path, dry_run=dry_run, log_callback=print) + +def main(): + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + db_path = tmp / "test_mixxxdb.sqlite" + audio_dir = tmp / "music" + audio_dir.mkdir() + audio_file = audio_dir / "test.flac" + audio_file.write_text("fake audio") # fichier factice + + # 1. Créer DB et peupler + con = create_temp_db(db_path) + populate_test_data(con, audio_file) + con.close() + + # 2. Exporter les cues dans un sous-dossier + export_dir = tmp / "export" + export_dir.mkdir() + run_export(db_path, export_dir) + expected_json = export_dir / "test.flac.mixxx" + assert expected_json.exists(), "Le fichier .mixxx n'a pas été créé" + + with open(expected_json, "r") as f: + data = json.load(f) + assert data["version"] == 1 + assert data["filename"] == "test.flac" + assert len(data["cues"]) == 3 + print("[PASS] Export OK") + + # 3. Préparer l'import : copier le .mixxx dans le dossier audio + import_audio_dir = tmp / "import" + import_audio_dir.mkdir() + import_audio_file = import_audio_dir / "test.flac" + import_audio_file.write_text("fake audio") # recréer le fichier audio + import_json = import_audio_dir / "test.flac.mixxx" + import_json.write_text(expected_json.read_text()) + + # Créer une nouvelle DB vierge pour l'import (même structure) + fresh_db = tmp / "fresh_mixxxdb.sqlite" + create_temp_db(fresh_db).close() + + # 4. Importer + run_import(import_audio_dir, fresh_db, dry_run=False) + + # 5. Vérifier que les cues ont été insérés + con = sqlite3.connect(fresh_db) + con.row_factory = sqlite3.Row + # Vérifier la piste + tl = con.execute("SELECT * FROM track_locations WHERE filename='test.flac'").fetchone() + assert tl is not None, "Piste non importée" + track_id = tl["id"] + # Vérifier les cues + cues = con.execute("SELECT * FROM cues WHERE track_id=?", (track_id,)).fetchall() + assert len(cues) == 3, f"Nombre de cues incorrect : {len(cues)} au lieu de 3" + # Vérifier les types + types = {c["type"] for c in cues} + assert types == {1, 4}, f"Types de cues inattendus: {types}" + print("[PASS] Import OK") + + # 6. Test dry-run + # Créer une autre DB vide pour simuler + dry_db = tmp / "dry_db.sqlite" + create_temp_db(dry_db).close() + run_import(import_audio_dir, dry_db, dry_run=True) + con = sqlite3.connect(dry_db) + cues_dry = con.execute("SELECT count(*) FROM cues").fetchone()[0] + assert cues_dry == 0, "Le dry-run a écrit des données !" + print("[PASS] Dry-run OK") + + print("\n✅ Tous les tests ont réussi !") + +if __name__ == "__main__": + main() \ No newline at end of file