feat: first tested version

This commit is contained in:
2026-06-17 21:35:32 +02:00
parent 8c6a25cdb0
commit a82f503e79
4 changed files with 280 additions and 291 deletions

1
.gitignore vendored
View File

@@ -24,6 +24,7 @@ test_*.sqlite
# Ignorer la base de données si jamais elle se retrouve dans le projet # Ignorer la base de données si jamais elle se retrouve dans le projet
mixxxdb.sqlite mixxxdb.sqlite
mixxxdb.sqlite.bak mixxxdb.sqlite.bak
Old/
# Ignorer tous les fichiers .mixxx exportés (sauf exemples) # Ignorer tous les fichiers .mixxx exportés (sauf exemples)
*.mixxx *.mixxx

6
create_windows_exe.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
export WINEPREFIX="$HOME/.wine_mixxx"
winecfg
wine pyinstaller --onefile --windowed --name mixxx_import mixxx_import.py

View File

@@ -1,9 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Exporte tous les cue points de Mixxx vers des fichiers .mixxx (JSON) mixxx_export.py - Exporte TOUTES les métadonnées de préparation Mixxx
placés à côté de chaque fichier audio. (cues, BPM, grille de beat, clé) vers des fichiers .mixxx JSON.
Usage : python3 mixxx_export.py [chemin_db]
""" """
import sqlite3 import sqlite3
@@ -18,10 +16,25 @@ def export_cues(db_path: Path, output_dir: Path = None):
con = sqlite3.connect(db_path) con = sqlite3.connect(db_path)
con.row_factory = sqlite3.Row con.row_factory = sqlite3.Row
# Récupérer cues + métadonnées de library en une seule requête par piste
rows = con.execute(""" rows = con.execute("""
SELECT SELECT
tl.location AS audio_path, tl.location AS audio_path,
tl.filename AS filename, 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.type AS type,
c.position AS position, c.position AS position,
c.length AS length, c.length AS length,
@@ -29,16 +42,41 @@ def export_cues(db_path: Path, output_dir: Path = None):
c.label AS label, c.label AS label,
c.color AS color c.color AS color
FROM cues c FROM cues c
JOIN library l ON l.id = c.track_id JOIN library lib ON lib.id = c.track_id
JOIN track_locations tl ON tl.id = l.location JOIN track_locations tl ON tl.id = lib.location
WHERE tl.fs_deleted = 0 WHERE tl.fs_deleted = 0
ORDER BY tl.location, c.hotcue, c.type ORDER BY tl.location, c.hotcue, c.type
""").fetchall() """).fetchall()
by_file = {} # Grouper par fichier audio
tracks: dict[str, dict] = {}
for r in rows: for r in rows:
path = r["audio_path"] path = r["audio_path"]
by_file.setdefault(path, []).append({ 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"], "type": r["type"],
"position": r["position"], "position": r["position"],
"length": r["length"], "length": r["length"],
@@ -50,23 +88,37 @@ def export_cues(db_path: Path, output_dir: Path = None):
con.close() con.close()
exported = 0 exported = 0
for audio_path, cues in by_file.items(): for audio_path, data in tracks.items():
audio_file = Path(audio_path) audio_file = Path(audio_path)
if not audio_file.exists(): if not audio_file.exists():
print(f"[SKIP] fichier introuvable : {audio_path}") print(f"[SKIP] fichier introuvable : {audio_path}")
continue continue
# Déterminer où écrire le .mixxx
if output_dir: if output_dir:
out = output_dir / (audio_file.stem + audio_file.suffix + ".mixxx") out = output_dir / (audio_file.stem + audio_file.suffix + ".mixxx")
else: else:
out = audio_file.with_suffix(audio_file.suffix + ".mixxx") out = audio_file.with_suffix(audio_file.suffix + ".mixxx")
payload = { payload = {
"version": 1, "version": 2, # version incrémentée car nouveau format
"filename": audio_file.name, "filename": data["filename"],
"cues": cues, "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: try:
out.parent.mkdir(parents=True, exist_ok=True) out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(payload, indent=2, ensure_ascii=False)) out.write_text(json.dumps(payload, indent=2, ensure_ascii=False))
@@ -79,7 +131,7 @@ def export_cues(db_path: Path, output_dir: Path = None):
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
parser = argparse.ArgumentParser() 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("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)") parser.add_argument("--output-dir", help="Dossier de sortie pour les .mixxx (par défaut : à côté de l'audio)")
args = parser.parse_args() args = parser.parse_args()

View File

@@ -1,280 +1,249 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
mixxx_import.py - Importe les cues Mixxx (fichiers .mixxx JSON) dans la base Mixxx Import Final - Importe les hot cues et la playlist après avoir ajouté
de données, crée les pistes manquantes et importe une playlist .m3u. le dossier dans Mixxx manuellement.
Version avec interface graphique corrigée. Usage : python mixxx_import_final.py
""" """
import sqlite3 import sqlite3
import json import json
import sys import sys
import os import os
import argparse import base64
import threading import threading
from pathlib import Path from pathlib import Path
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
# ------------------------------------------------------------
# 1. Fonctions de base
# ------------------------------------------------------------
AUDIO_EXTENSIONS = {".flac", ".mp3", ".opus", ".ogg", ".wav", ".m4a"}
def default_db(): def default_db():
if sys.platform == "win32": if sys.platform == "win32":
return Path(os.environ["APPDATA"]) / "Mixxx" / "mixxxdb.sqlite" return Path(os.environ["LOCALAPPDATA"]) / "Mixxx" / "mixxxdb.sqlite"
return Path.home() / ".mixxx" / "mixxxdb.sqlite" return Path.home() / ".mixxx" / "mixxxdb.sqlite"
def ensure_track(con, file_path: Path) -> int: def import_cues_and_playlist(music_dir: Path, db_path: Path, log_callback=print):
location = str(file_path.resolve()) """
row = con.execute("SELECT id FROM track_locations WHERE location = ?", (location,)).fetchone() Importe les cues depuis les fichiers .mixxx et la playlist depuis le .m3u.
if row: Les pistes doivent déjà exister dans la base (ajoutées par Mixxx).
return row[0] """
con = sqlite3.connect(str(db_path))
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: try:
log_callback("Scan des fichiers audio...") # --- 1. Import des cues ---
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")) mixxx_files = list(music_dir.rglob("*.mixxx"))
log_callback(f"{len(mixxx_files)} fichier(s) .mixxx trouvé(s).") log_callback(f"Fichiers .mixxx trouvés : {len(mixxx_files)}")
total = 0
total_cues = 0
tracks_updated = 0
for mf in mixxx_files: for mf in mixxx_files:
try: try:
payload = json.loads(mf.read_text(encoding="utf-8")) payload = json.loads(mf.read_text(encoding="utf-8"))
except Exception as e:
log_callback(f"[ERREUR] {mf.name} : {e}") if payload.get("version") != 2:
log_callback(f"[IGNORÉ] {mf.name} : version invalide")
continue continue
if payload.get("version") != 1: fname = payload.get("filename", "")
log_callback(f"[IGNORÉ] {mf.name} : version inconnue")
# 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 continue
target = payload.get("filename", "") tid = row[0]
tid = filename_to_trackid.get(target)
if tid is None:
log_callback(f"[PAS TROUVÉ] {target}")
continue
cues = payload.get("cues", []) # Supprimer les anciens cues
if not cues:
continue
if overwrite and not dry_run:
con.execute("DELETE FROM cues WHERE track_id = ?", (tid,)) con.execute("DELETE FROM cues WHERE track_id = ?", (tid,))
existing = set() # Insérer les nouveaux cues
if not overwrite: cues = payload.get("cues", [])
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: 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( con.execute(
"INSERT INTO cues (track_id, type, position, length, hotcue, label, color) VALUES (?,?,?,?,?,?,?)", "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)) (tid, c["type"], c["position"], c.get("length", 0),
c["hotcue"], c.get("label", ""), c.get("color", 16744448))
) )
inserted += 1
status = "[DRY]" if dry_run else "[OK]" # Mettre à jour les métadonnées (BPM, clé, grille)
log_callback(f"{status} {target} : {inserted} cues") artist = payload.get("artist", "")
total += inserted 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
if m3u_file and m3u_file.exists(): con.execute(
log_callback(f"\nImport playlist : {m3u_file.name}") "UPDATE library SET artist=?, bpm=?, bpm_lock=?, key=?, key_id=?, "
with open(m3u_file, "r", encoding="utf-8") as f: "beats=?, beats_version=?, beats_sub_version=? WHERE id=?",
lines = [l.strip() for l in f if l.strip() and not l.startswith("#")] (artist, bpm, bpm_lock, key, key_id,
beats,
payload.get("beats_version", ""),
payload.get("beats_sub_version", ""),
tid)
)
track_ids = [] tracks_updated += 1
m3u_dir = m3u_file.parent total_cues += len(cues)
for rel in lines: log_callback(f"[OK] {fname} : {len(cues)} cues")
abs_path = (m3u_dir / rel).resolve()
if not abs_path.exists(): except Exception as e:
log_callback(f"[FICHIER ABSENT] {abs_path}") 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 continue
tid = ensure_track(con, abs_path)
track_ids.append(tid)
if track_ids and not dry_run: if content:
name = m3u_file.stem # Extraire les noms de fichiers
add_playlist(con, name, track_ids) lines = []
log_callback(f"Playlist '{name}' créée ({len(track_ids)} pistes).") for l in content.split('\n'):
elif track_ids: l = l.strip()
log_callback(f"(dry-run) Playlist '{m3u_file.stem}' aurait {len(track_ids)} pistes.") 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é]")
if not dry_run:
con.commit() con.commit()
log_callback("\n✅ IMPORT TERMINÉ")
log_callback(f"\n✅ IMPORT TERMINÉ : {total} cues importés.")
finally: finally:
con.close() con.close()
# ------------------------------------------------------------ # ------------------------------------------------------------
# 2. Interface graphique Tkinter (CORRIGÉE) # Interface graphique
# ------------------------------------------------------------ # ------------------------------------------------------------
class ImportGUI: class ImportGUI:
def __init__(self): def __init__(self):
self.root = tk.Tk() self.root = tk.Tk()
self.root.title("Mixxx Import Cues") self.root.title("Mixxx Import Cues & Playlist")
self.root.geometry("600x500") # Fenêtre plus grande self.root.geometry("600x500")
self.root.resizable(True, True) self.root.resizable(True, True)
# Variables
self.folder_path = tk.StringVar() 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())) self.db_path = tk.StringVar(value=str(default_db()))
# Style
self.root.configure(bg="#f0f0f0") self.root.configure(bg="#f0f0f0")
self.create_widgets() self.create_widgets()
def create_widgets(self): def create_widgets(self):
# Frame principal avec padding
main_frame = ttk.Frame(self.root, padding="20") main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill="both", expand=True) main_frame.pack(fill="both", expand=True)
# Titre ttk.Label(main_frame, text="Import Cues & Playlist Mixxx",
title_label = ttk.Label(main_frame, text="Import des Cues Mixxx", font=("Arial", 14, "bold")) font=("Arial", 14, "bold")).pack(pady=(0, 20))
title_label.pack(pady=(0, 20))
# Dossier musique # Dossier musique
folder_frame = ttk.LabelFrame(main_frame, text="Dossier contenant les fichiers audio et .mixxx", padding="10") frm = ttk.LabelFrame(main_frame, text="Dossier contenant les fichiers .mixxx et .m3u", padding="10")
folder_frame.pack(fill="x", pady=(0, 10)) frm.pack(fill="x", pady=(0, 10))
ef = ttk.Frame(frm); ef.pack(fill="x")
folder_entry_frame = ttk.Frame(folder_frame) ttk.Entry(ef, textvariable=self.folder_path).pack(side="left", fill="x", expand=True, padx=(0, 5))
folder_entry_frame.pack(fill="x") ttk.Button(ef, text="📁 Parcourir", command=self.browse_folder).pack(side="right")
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 # Base de données
db_frame = ttk.LabelFrame(main_frame, text="Base de données Mixxx", padding="10") frm = ttk.LabelFrame(main_frame, text="Base de données Mixxx", padding="10")
db_frame.pack(fill="x", pady=(0, 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")
db_entry_frame = ttk.Frame(db_frame) # Instructions
db_entry_frame.pack(fill="x") instr = ttk.LabelFrame(main_frame, text="Instructions", padding="10")
ttk.Entry(db_entry_frame, textvariable=self.db_path).pack(side="left", fill="x", expand=True, padx=(0, 5)) instr.pack(fill="x", pady=(0, 10))
ttk.Button(db_entry_frame, text="📁 Parcourir", command=self.browse_db).pack(side="right") ttk.Label(instr, text="1. Dans Mixxx : Fichier → Ajouter un dossier (votre dossier Flac)",
font=("Arial", 9)).pack(anchor="w")
# Options ttk.Label(instr, text="2. Fermer Mixxx",
options_frame = ttk.LabelFrame(main_frame, text="Options", padding="10") font=("Arial", 9)).pack(anchor="w")
options_frame.pack(fill="x", pady=(0, 10)) ttk.Label(instr, text="3. Cliquer sur LANCER L'IMPORT ci-dessous",
font=("Arial", 9)).pack(anchor="w")
ttk.Checkbutton(options_frame, text="Dry-run (simulation sans écriture)", variable=self.dry_run).pack(anchor="w", pady=2) ttk.Label(instr, text="4. Rouvrir Mixxx",
ttk.Checkbutton(options_frame, text="Écraser les cues existants", variable=self.overwrite).pack(anchor="w", pady=2) font=("Arial", 9)).pack(anchor="w")
# Zone de log # Zone de log
log_frame = ttk.LabelFrame(main_frame, text="Journal", padding="5") logf = ttk.LabelFrame(main_frame, text="Journal", padding="5")
log_frame.pack(fill="both", expand=True, pady=(0, 10)) logf.pack(fill="both", expand=True, pady=(0, 10))
self.log_text = tk.Text(logf, height=6, width=70, state="disabled",
self.log_text = tk.Text(log_frame, height=8, width=70, state="disabled", bg="#1e1e1e", fg="#00ff00", font=("Courier", 9)) bg="#1e1e1e", fg="#00ff00", font=("Courier", 9))
self.log_text.pack(fill="both", expand=True) 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)
scrollbar = ttk.Scrollbar(self.log_text, orient="vertical", command=self.log_text.yview) # Boutons
scrollbar.pack(side="right", fill="y") bf = ttk.Frame(main_frame); bf.pack(fill="x", pady=(10, 0))
self.log_text.configure(yscrollcommand=scrollbar.set) self.run_btn = tk.Button(bf, text="🚀 LANCER L'IMPORT", command=self.start_import,
bg="#4CAF50", fg="white", font=("Arial", 12, "bold"),
# Frame pour les boutons padx=30, pady=10, relief="raised", cursor="hand2")
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)) self.run_btn.pack(side="left", expand=True, fill="x", padx=(0, 5))
tk.Button(bf, text="❌ Quitter", command=self.root.quit,
# Bouton Quitter bg="#f44336", fg="white", font=("Arial", 12),
quit_btn = tk.Button( padx=20, pady=10, cursor="hand2").pack(side="right", expand=True, fill="x", padx=(5, 0))
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): def log(self, message):
self.log_text.configure(state="normal") self.log_text.configure(state="normal")
@@ -288,28 +257,20 @@ class ImportGUI:
if path: if path:
self.folder_path.set(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): def browse_db(self):
path = filedialog.askopenfilename(title="Sélectionner mixxxdb.sqlite", filetypes=[("SQLite", "*.sqlite"), ("All files", "*.*")]) path = filedialog.askopenfilename(
title="Sélectionner mixxxdb.sqlite",
filetypes=[("SQLite", "*.sqlite"), ("All files", "*.*")]
)
if path: if path:
self.db_path.set(path) self.db_path.set(path)
def validate(self): def validate(self):
music = Path(self.folder_path.get()) if not Path(self.folder_path.get()).is_dir():
if not music.is_dir():
messagebox.showerror("Erreur", "Le dossier de musique n'existe pas.") messagebox.showerror("Erreur", "Le dossier de musique n'existe pas.")
return False return False
db = Path(self.db_path.get()) if not Path(self.db_path.get()).exists():
if not db.exists(): messagebox.showerror("Erreur", "La base de données est introuvable.")
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 False
return True return True
@@ -324,19 +285,15 @@ class ImportGUI:
music_dir = Path(self.folder_path.get()) music_dir = Path(self.folder_path.get())
db_path = Path(self.db_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(): def task():
try: try:
import_cues_v2(music_dir, db_path, dry, overwrite, m3u, log_callback=self.log) import_cues_and_playlist(music_dir, db_path, log_callback=self.log)
except Exception as e: except Exception as e:
self.log(f"❌ ERREUR FATALE : {e}") self.log(f"❌ ERREUR FATALE : {e}")
finally: finally:
self.run_btn.configure(state="normal", text="🚀 LANCER L'IMPORT") self.run_btn.configure(state="normal", text="🚀 LANCER L'IMPORT")
if not dry: messagebox.showinfo("Terminé", "Import terminé !\nRouvrez Mixxx.")
messagebox.showinfo("Terminé", "Import terminé avec succès !")
threading.Thread(target=task, daemon=True).start() threading.Thread(target=task, daemon=True).start()
@@ -344,32 +301,5 @@ class ImportGUI:
self.root.mainloop() self.root.mainloop()
# ------------------------------------------------------------
# Point d'entrée
# ------------------------------------------------------------
if __name__ == "__main__": 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() ImportGUI().run()