Files
LinedanceAfspiller/linedance-app/ui/library_manager.py
2026-04-12 14:29:54 +02:00

301 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
library_manager.py — Håndter musikmapper.
Tilføj/fjern mapper. BPM-scanning per bibliotek.
Fil-scanning starter automatisk når vinduet lukkes.
"""
import sqlite3
from pathlib import Path
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QMessageBox, QScrollArea, QWidget,
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QThread
class LibraryManagerDialog(QDialog):
libraries_changed = pyqtSignal()
def __init__(self, db_path: str, parent=None):
super().__init__(parent)
self._db_path = db_path
self._workers = {} # library_id → ScanWorker
self.setWindowTitle("Musikmapper")
self.setMinimumWidth(600)
self.setMinimumHeight(300)
self._build_ui()
self._load()
# ── UI ────────────────────────────────────────────────────────────────────
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
lbl = QLabel(
"Tilføj eller fjern musikmapper. "
"Fil-scanning starter automatisk når vinduet lukkes."
)
lbl.setObjectName("result_count")
lbl.setWordWrap(True)
layout.addWidget(lbl)
# Scrollbart område til biblioteksliste
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
self._scroll_content = QWidget()
self._libs_layout = QVBoxLayout(self._scroll_content)
self._libs_layout.setSpacing(6)
self._libs_layout.addStretch()
scroll.setWidget(self._scroll_content)
layout.addWidget(scroll, stretch=1)
# Knap-række
btn_row = QHBoxLayout()
btn_add = QPushButton(" Tilføj mappe")
btn_add.clicked.connect(self._add_folder)
btn_row.addWidget(btn_add)
btn_row.addStretch()
btn_close = QPushButton("Luk")
btn_close.clicked.connect(self.accept)
btn_row.addWidget(btn_close)
layout.addLayout(btn_row)
def _load(self):
"""Indlæs biblioteker fra DB og vis dem."""
from PyQt6.QtWidgets import QApplication
# Ryd eksisterende rækker (ikke stretch)
while self._libs_layout.count() > 1:
item = self._libs_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
QApplication.processEvents() # Lad Qt rydde op før vi bygger nyt
try:
conn = sqlite3.connect(self._db_path)
conn.row_factory = sqlite3.Row
libs = conn.execute(
"SELECT id, path, last_full_scan FROM libraries "
"WHERE is_active=1 ORDER BY path"
).fetchall()
counts = {}
bpm_missing = {}
for lib in libs:
counts[lib["id"]] = conn.execute(
"SELECT COUNT(*) FROM songs "
"WHERE library_id=? AND file_missing=0",
(lib["id"],)
).fetchone()[0]
bpm_missing[lib["id"]] = conn.execute(
"SELECT COUNT(*) FROM songs "
"WHERE library_id=? AND file_missing=0 "
"AND (bpm IS NULL OR bpm=0)",
(lib["id"],)
).fetchone()[0]
conn.close()
if not libs:
lbl = QLabel("Ingen musikmapper tilføjet endnu.")
lbl.setObjectName("result_count")
self._libs_layout.insertWidget(0, lbl)
return
for i, lib in enumerate(libs):
row = self._make_lib_row(
dict(lib),
counts[lib["id"]],
bpm_missing[lib["id"]]
)
self._libs_layout.insertWidget(i, row)
except Exception as e:
lbl = QLabel(f"Fejl: {e}")
self._libs_layout.insertWidget(0, lbl)
def _make_lib_row(self, lib: dict, song_count: int,
bpm_missing: int) -> QFrame:
lib_id = lib["id"]
path = lib["path"]
exists = Path(path).exists()
last = lib.get("last_full_scan") or "aldrig"
if isinstance(last, str) and len(last) > 16:
last = last[:16]
frame = QFrame()
frame.setObjectName("track_display")
vbox = QVBoxLayout(frame)
vbox.setContentsMargins(10, 8, 10, 8)
vbox.setSpacing(4)
# Sti
lbl_path = QLabel(("" if not exists else "") + path)
lbl_path.setObjectName("track_title" if exists else "result_count")
vbox.addWidget(lbl_path)
# Info
lbl_info = QLabel(
f" {song_count} sange · "
f"senest scannet: {last} · "
f"{bpm_missing} uden BPM"
+ (" · ⚠ mappe ikke fundet" if not exists else "")
)
lbl_info.setObjectName("result_count")
vbox.addWidget(lbl_info)
# Status-label til scanning
lbl_status = QLabel("")
lbl_status.setObjectName("result_count")
lbl_status.hide()
vbox.addWidget(lbl_status)
# Knapper
btn_row = QHBoxLayout()
btn_row.setSpacing(6)
btn_bpm = QPushButton(f"♩ BPM manglende ({bpm_missing})")
btn_bpm.setFixedHeight(30)
btn_bpm.setEnabled(exists and bpm_missing > 0
and lib_id not in self._workers)
btn_bpm.clicked.connect(
lambda _, lid=lib_id, p=path, b=btn_bpm, s=lbl_status:
self._start_bpm(lid, p, False, b, s)
)
btn_row.addWidget(btn_bpm)
btn_bpm_all = QPushButton("♩ BPM alle")
btn_bpm_all.setFixedHeight(30)
btn_bpm_all.setEnabled(exists and lib_id not in self._workers)
btn_bpm_all.clicked.connect(
lambda _, lid=lib_id, p=path, b=btn_bpm_all, s=lbl_status:
self._start_bpm(lid, p, True, b, s)
)
btn_row.addWidget(btn_bpm_all)
btn_row.addStretch()
btn_remove = QPushButton("✕ Fjern")
btn_remove.setFixedHeight(30)
btn_remove.clicked.connect(lambda _, l=lib: self._remove_library(l))
btn_row.addWidget(btn_remove)
vbox.addLayout(btn_row)
return frame
# ── Tilføj / fjern ────────────────────────────────────────────────────────
def _add_folder(self):
from PyQt6.QtWidgets import QFileDialog
folder = QFileDialog.getExistingDirectory(self, "Vælg musikmappe")
if not folder:
return
try:
conn = sqlite3.connect(self._db_path)
conn.row_factory = sqlite3.Row
# Tjek om mappen allerede er aktiv
existing = conn.execute(
"SELECT id, is_active FROM libraries WHERE path=?", (folder,)
).fetchone()
if existing:
if existing["is_active"]:
QMessageBox.information(
self, "Allerede tilføjet",
"Denne mappe er allerede i listen."
)
conn.close()
return
else:
# Reaktiver en tidligere fjernet mappe
conn.execute(
"UPDATE libraries SET is_active=1 WHERE path=?",
(folder,)
)
else:
conn.execute(
"INSERT INTO libraries (path, is_active) VALUES (?, 1)",
(folder,)
)
conn.commit()
conn.close()
self._load()
self.libraries_changed.emit()
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke tilføje: {e}")
def _remove_library(self, lib: dict):
reply = QMessageBox.question(
self, "Fjern mappe",
f"Fjern:\n{lib['path']}\n\n"
"Alle sange fra denne mappe slettes fra databasen.\n"
"Dans-tags og playlister bevares.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
# Stop evt. BPM-scanning på dette bibliotek
if lib["id"] in self._workers:
self._workers[lib["id"]].cancel()
self._workers.pop(lib["id"], None)
try:
conn = sqlite3.connect(self._db_path)
# Slet sange fra biblioteket
conn.execute(
"DELETE FROM songs WHERE library_id=?", (lib["id"],)
)
# Slet selve biblioteks-rækken helt
conn.execute(
"DELETE FROM libraries WHERE id=?", (lib["id"],)
)
conn.commit()
conn.close()
self._load()
QTimer.singleShot(300, self.libraries_changed.emit)
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke fjerne: {e}")
# ── BPM-scanning ──────────────────────────────────────────────────────────
def _start_bpm(self, library_id: int, path: str,
scan_all: bool, btn: QPushButton, lbl: QLabel):
if library_id in self._workers:
return
from local.local_db import DB_PATH
from ui.bpm_worker import BpmScanWorker
worker = BpmScanWorker(library_id, str(DB_PATH),
scan_all=scan_all)
def on_progress(done, total):
lbl.setText(f"{done}/{total} analyseret...")
lbl.show()
btn.setEnabled(False)
def on_finished(count):
lbl.setText(f"{count} analyseret")
btn.setEnabled(True)
self._workers.pop(library_id, None)
QTimer.singleShot(300, self._load)
worker.progress.connect(on_progress)
worker.finished.connect(on_finished)
self._workers[library_id] = worker
worker.start()
worker.setPriority(QThread.Priority.LowestPriority)
# ── Luk ───────────────────────────────────────────────────────────────────
def closeEvent(self, event):
for w in list(self._workers.values()):
w.cancel()
w.wait(2000) # Vent max 2 sek på at tråden stopper
event.accept()