Files
LinedanceAfspiller/linedance-app/ui/library_manager.py
2026-04-20 00:01:41 +02:00

300 lines
11 KiB
Python
Raw Permalink 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 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 files "
"WHERE file_missing=0 AND local_path LIKE ?",
(lib["path"] + "%",)
).fetchone()[0]
bpm_missing[lib["id"]] = conn.execute(
"SELECT COUNT(*) FROM files f "
"JOIN songs s ON s.id = f.song_id "
"WHERE f.file_missing=0 AND f.local_path LIKE ? "
"AND (s.bpm IS NULL OR s.bpm=0)",
(lib["path"] + "%",)
).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 = ""
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)
# Marker filer fra denne mappe som missing
conn.execute(
"UPDATE files SET file_missing=1 WHERE local_path LIKE ?",
(lib["path"] + "%",)
)
# Slet selve biblioteks-rækken
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()