#!/usr/bin/env python3 """ Mixxx Import Final - Importe les hot cues et la playlist après avoir ajouté le dossier dans Mixxx manuellement. Usage : python mixxx_import_final.py """ import sqlite3 import json import sys import os import base64 import threading from pathlib import Path import tkinter as tk from tkinter import filedialog, messagebox, ttk def default_db(): if sys.platform == "win32": return Path(os.environ["LOCALAPPDATA"]) / "Mixxx" / "mixxxdb.sqlite" return Path.home() / ".mixxx" / "mixxxdb.sqlite" def import_cues_and_playlist(music_dir: Path, db_path: Path, log_callback=print): """ Importe les cues depuis les fichiers .mixxx et la playlist depuis le .m3u. Les pistes doivent déjà exister dans la base (ajoutées par Mixxx). """ con = sqlite3.connect(str(db_path)) try: # --- 1. Import des cues --- mixxx_files = list(music_dir.rglob("*.mixxx")) log_callback(f"Fichiers .mixxx trouvés : {len(mixxx_files)}") total_cues = 0 tracks_updated = 0 for mf in mixxx_files: try: payload = json.loads(mf.read_text(encoding="utf-8")) if payload.get("version") != 2: log_callback(f"[IGNORÉ] {mf.name} : version invalide") continue fname = payload.get("filename", "") # Trouver la piste existante (créée par Mixxx) row = con.execute( "SELECT id FROM track_locations WHERE filename = ?", (fname,) ).fetchone() if not row: log_callback(f"[ABSENT] {fname}") continue tid = row[0] # Supprimer les anciens cues con.execute("DELETE FROM cues WHERE track_id = ?", (tid,)) # Insérer les nouveaux cues cues = payload.get("cues", []) for c in cues: con.execute( "INSERT INTO cues (track_id, type, position, length, hotcue, label, color) VALUES (?,?,?,?,?,?,?)", (tid, c["type"], c["position"], c.get("length", 0), c["hotcue"], c.get("label", ""), c.get("color", 16744448)) ) # Mettre à jour les métadonnées (BPM, clé, grille) artist = payload.get("artist", "") bpm = payload.get("bpm", 0.0) bpm_lock = 1 if payload.get("bpm_lock") else 0 key = payload.get("key", "") key_id = payload.get("key_id", 0) beats = None if payload.get("beats"): try: beats = base64.b64decode(payload["beats"]) except: pass con.execute( "UPDATE library SET artist=?, bpm=?, bpm_lock=?, key=?, key_id=?, " "beats=?, beats_version=?, beats_sub_version=? WHERE id=?", (artist, bpm, bpm_lock, key, key_id, beats, payload.get("beats_version", ""), payload.get("beats_sub_version", ""), tid) ) tracks_updated += 1 total_cues += len(cues) log_callback(f"[OK] {fname} : {len(cues)} cues") except Exception as e: log_callback(f"[ERREUR] {mf.name} : {e}") log_callback(f"\nCues importés : {total_cues} sur {tracks_updated} pistes") # --- 2. Import de la playlist --- m3u_files = list(music_dir.rglob("*.m3u")) if m3u_files: m3u = m3u_files[0] log_callback(f"\nImport playlist : {m3u.name}") # Lire le .m3u content = None for enc in ['utf-8', 'latin-1', 'cp1252']: try: with open(m3u, "r", encoding=enc) as f: content = f.read() break except: continue if content: # Extraire les noms de fichiers lines = [] for l in content.split('\n'): l = l.strip() if l and not l.startswith('#EXT'): lines.append(l) log_callback(f"Pistes dans le .m3u : {len(lines)}") # Trouver les track_ids track_ids = [] for line in lines: fname = Path(line).name row = con.execute( "SELECT id FROM track_locations WHERE filename = ?", (fname,) ).fetchone() if row: track_ids.append(row[0]) else: log_callback(f" [ABSENT] {fname}") if track_ids: # Supprimer l'ancienne playlist con.execute( "DELETE FROM PlaylistTracks WHERE playlist_id IN " "(SELECT id FROM Playlists WHERE name = ?)", (m3u.stem,) ) con.execute("DELETE FROM Playlists WHERE name = ?", (m3u.stem,)) # Créer la nouvelle playlist cur = con.execute( "INSERT INTO Playlists (name, position, hidden, date_created, date_modified) " "VALUES (?, 0, 0, datetime('now'), datetime('now'))", (m3u.stem,) ) pl_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'))", (pl_id, tid, pos) ) log_callback(f"Playlist créée : {len(track_ids)} pistes") else: log_callback("[ERREUR] Impossible de lire le .m3u") else: log_callback("\n[Aucun fichier .m3u trouvé]") con.commit() log_callback("\n✅ IMPORT TERMINÉ") finally: con.close() # ------------------------------------------------------------ # Interface graphique # ------------------------------------------------------------ class ImportGUI: def __init__(self): self.root = tk.Tk() self.root.title("Mixxx Import Cues & Playlist") self.root.geometry("600x500") self.root.resizable(True, True) self.folder_path = tk.StringVar() self.db_path = tk.StringVar(value=str(default_db())) self.root.configure(bg="#f0f0f0") self.create_widgets() def create_widgets(self): main_frame = ttk.Frame(self.root, padding="20") main_frame.pack(fill="both", expand=True) ttk.Label(main_frame, text="Import Cues & Playlist Mixxx", font=("Arial", 14, "bold")).pack(pady=(0, 20)) # Dossier musique frm = ttk.LabelFrame(main_frame, text="Dossier contenant les fichiers .mixxx et .m3u", padding="10") frm.pack(fill="x", pady=(0, 10)) ef = ttk.Frame(frm); ef.pack(fill="x") ttk.Entry(ef, textvariable=self.folder_path).pack(side="left", fill="x", expand=True, padx=(0, 5)) ttk.Button(ef, text="📁 Parcourir", command=self.browse_folder).pack(side="right") # Base de données frm = ttk.LabelFrame(main_frame, text="Base de données Mixxx", padding="10") frm.pack(fill="x", pady=(0, 10)) ef = ttk.Frame(frm); ef.pack(fill="x") ttk.Entry(ef, textvariable=self.db_path).pack(side="left", fill="x", expand=True, padx=(0, 5)) ttk.Button(ef, text="📁 Parcourir", command=self.browse_db).pack(side="right") # Instructions instr = ttk.LabelFrame(main_frame, text="Instructions", padding="10") instr.pack(fill="x", pady=(0, 10)) ttk.Label(instr, text="1. Dans Mixxx : Fichier → Ajouter un dossier (votre dossier Flac)", font=("Arial", 9)).pack(anchor="w") ttk.Label(instr, text="2. Fermer Mixxx", font=("Arial", 9)).pack(anchor="w") ttk.Label(instr, text="3. Cliquer sur LANCER L'IMPORT ci-dessous", font=("Arial", 9)).pack(anchor="w") ttk.Label(instr, text="4. Rouvrir Mixxx", font=("Arial", 9)).pack(anchor="w") # Zone de log logf = ttk.LabelFrame(main_frame, text="Journal", padding="5") logf.pack(fill="both", expand=True, pady=(0, 10)) self.log_text = tk.Text(logf, height=6, width=70, state="disabled", bg="#1e1e1e", fg="#00ff00", font=("Courier", 9)) self.log_text.pack(fill="both", expand=True) scroll = ttk.Scrollbar(self.log_text, orient="vertical", command=self.log_text.yview) scroll.pack(side="right", fill="y") self.log_text.configure(yscrollcommand=scroll.set) # Boutons bf = ttk.Frame(main_frame); bf.pack(fill="x", pady=(10, 0)) self.run_btn = tk.Button(bf, text="🚀 LANCER L'IMPORT", command=self.start_import, bg="#4CAF50", 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)) tk.Button(bf, text="❌ Quitter", command=self.root.quit, bg="#f44336", fg="white", font=("Arial", 12), padx=20, pady=10, cursor="hand2").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_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): if not Path(self.folder_path.get()).is_dir(): messagebox.showerror("Erreur", "Le dossier de musique n'existe pas.") return False if not Path(self.db_path.get()).exists(): messagebox.showerror("Erreur", "La base de données est introuvable.") 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()) def task(): try: import_cues_and_playlist(music_dir, db_path, 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") messagebox.showinfo("Terminé", "Import terminé !\nRouvrez Mixxx.") threading.Thread(target=task, daemon=True).start() def run(self): self.root.mainloop() if __name__ == "__main__": ImportGUI().run()