305 lines
12 KiB
Python
Executable File
305 lines
12 KiB
Python
Executable File
#!/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() |