280 lines
9.8 KiB
Python
280 lines
9.8 KiB
Python
"""
|
|
library_manager.py — Dialog til at administrere musikbiblioteker med BPM-scanning.
|
|
"""
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QWidget,
|
|
QPushButton, QListWidget, QListWidgetItem, QMessageBox,
|
|
QFrame, QSizePolicy,
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QThread
|
|
from PyQt6.QtGui import QColor
|
|
|
|
|
|
class BpmScanWorker(QThread):
|
|
progress = pyqtSignal(int, int) # done, total
|
|
finished = pyqtSignal(int) # antal scannet
|
|
|
|
def __init__(self, library_id: int, scan_all: bool = False):
|
|
super().__init__()
|
|
self._library_id = library_id
|
|
self._scan_all = scan_all # False = kun manglende, True = alle
|
|
|
|
def run(self):
|
|
import sqlite3
|
|
from local.local_db import DB_PATH
|
|
from local.tag_reader import analyze_bpm
|
|
|
|
conn = sqlite3.connect(str(DB_PATH))
|
|
conn.row_factory = sqlite3.Row
|
|
|
|
if self._scan_all:
|
|
songs = conn.execute(
|
|
"SELECT id, local_path FROM songs WHERE library_id=? AND file_missing=0",
|
|
(self._library_id,)
|
|
).fetchall()
|
|
else:
|
|
songs = conn.execute(
|
|
"SELECT id, local_path FROM songs "
|
|
"WHERE library_id=? AND file_missing=0 AND (bpm IS NULL OR bpm=0)",
|
|
(self._library_id,)
|
|
).fetchall()
|
|
|
|
total = len(songs)
|
|
done = 0
|
|
for song in songs:
|
|
if self.isInterruptionRequested():
|
|
break
|
|
try:
|
|
bpm = analyze_bpm(song["local_path"])
|
|
if bpm:
|
|
conn.execute(
|
|
"UPDATE songs SET bpm=? WHERE id=?",
|
|
(int(round(bpm)), song["id"])
|
|
)
|
|
conn.commit()
|
|
except Exception:
|
|
pass
|
|
done += 1
|
|
self.progress.emit(done, total)
|
|
|
|
conn.close()
|
|
self.finished.emit(done)
|
|
|
|
|
|
class LibraryManagerDialog(QDialog):
|
|
library_removed = pyqtSignal(int)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Administrer musikbiblioteker")
|
|
self.setMinimumWidth(580)
|
|
self.setMinimumHeight(360)
|
|
self._bpm_workers = {} # library_id → BpmScanWorker
|
|
self._build_ui()
|
|
self._load()
|
|
|
|
def _build_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(10)
|
|
|
|
lbl = QLabel("Aktive musikbiblioteker:")
|
|
lbl.setObjectName("track_meta")
|
|
layout.addWidget(lbl)
|
|
|
|
self._libs_layout = QVBoxLayout()
|
|
self._libs_layout.setSpacing(6)
|
|
layout.addLayout(self._libs_layout)
|
|
layout.addStretch()
|
|
|
|
note = QLabel(
|
|
"Når du fjerner et bibliotek, slettes det fra overvågningen.\n"
|
|
"Sangene forbliver i databasen men markeres som manglende (⚠)."
|
|
)
|
|
note.setObjectName("result_count")
|
|
note.setWordWrap(True)
|
|
layout.addWidget(note)
|
|
|
|
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):
|
|
while self._libs_layout.count():
|
|
item = self._libs_layout.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
try:
|
|
import sqlite3
|
|
from local.local_db import DB_PATH, get_libraries
|
|
libs = get_libraries(active_only=True)
|
|
for lib in libs:
|
|
conn = sqlite3.connect(str(DB_PATH))
|
|
conn.row_factory = sqlite3.Row
|
|
count = conn.execute(
|
|
"SELECT COUNT(*) FROM songs WHERE library_id=? AND file_missing=0",
|
|
(lib["id"],)
|
|
).fetchone()[0]
|
|
missing_bpm = 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()
|
|
lib_dict = dict(lib)
|
|
lib_dict["_count"] = count
|
|
lib_dict["_missing_bpm"] = missing_bpm
|
|
self._libs_layout.addWidget(self._make_lib_row(lib_dict))
|
|
except Exception as e:
|
|
lbl = QLabel(f"Fejl: {e}")
|
|
self._libs_layout.addWidget(lbl)
|
|
|
|
def _make_lib_row(self, lib: dict) -> QFrame:
|
|
from pathlib import Path
|
|
lib_id = lib["id"]
|
|
path = lib["path"]
|
|
exists = Path(path).exists()
|
|
|
|
frame = QFrame()
|
|
frame.setObjectName("track_display")
|
|
vbox = QVBoxLayout(frame)
|
|
vbox.setContentsMargins(10, 8, 10, 8)
|
|
vbox.setSpacing(4)
|
|
|
|
# Sti + scan-info
|
|
last_scan = lib.get("last_full_scan") or "aldrig"
|
|
if isinstance(last_scan, str) and len(last_scan) > 10:
|
|
last_scan = last_scan[:10]
|
|
total = lib.get("_count", 0)
|
|
missing_bpm = lib.get("_missing_bpm", 0)
|
|
|
|
lbl_path = QLabel(("⚠ " if not exists else "") + path)
|
|
lbl_path.setObjectName("track_title" if exists else "result_count")
|
|
vbox.addWidget(lbl_path)
|
|
|
|
lbl_info = QLabel(
|
|
f" {total} sange · senest scannet: {last_scan} · "
|
|
f"{missing_bpm} uden BPM"
|
|
)
|
|
lbl_info.setObjectName("result_count")
|
|
vbox.addWidget(lbl_info)
|
|
|
|
# Statuslinje til BPM-fremgang
|
|
lbl_status = QLabel("")
|
|
lbl_status.setObjectName("result_count")
|
|
lbl_status.hide()
|
|
vbox.addWidget(lbl_status)
|
|
|
|
# Knap-række
|
|
btn_row = QHBoxLayout()
|
|
btn_row.setSpacing(6)
|
|
|
|
btn_scan = QPushButton("⟳ Fil-scan")
|
|
btn_scan.setFixedHeight(30)
|
|
btn_scan.setToolTip("Scan for nye og ændrede filer")
|
|
btn_scan.clicked.connect(lambda _, lid=lib_id, p=path: self._scan_files(lid, p))
|
|
btn_row.addWidget(btn_scan)
|
|
|
|
btn_bpm = QPushButton(f"♩ BPM manglende ({missing_bpm})")
|
|
btn_bpm.setFixedHeight(30)
|
|
btn_bpm.setToolTip("Analysér BPM på sange der mangler det")
|
|
btn_bpm.setEnabled(missing_bpm > 0)
|
|
btn_bpm.clicked.connect(
|
|
lambda _, lid=lib_id, b=btn_bpm, s=lbl_status: self._start_bpm(lid, False, b, s)
|
|
)
|
|
btn_row.addWidget(btn_bpm)
|
|
|
|
btn_bpm_all = QPushButton("♩ BPM alle")
|
|
btn_bpm_all.setFixedHeight(30)
|
|
btn_bpm_all.setToolTip("Genanalysér BPM på alle sange (overskriver eksisterende)")
|
|
btn_bpm_all.clicked.connect(
|
|
lambda _, lid=lib_id, b=btn_bpm_all, s=lbl_status: self._start_bpm(lid, 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
|
|
|
|
def _scan_files(self, library_id: int, path: str):
|
|
mw = self.parent()
|
|
if hasattr(mw, "_watcher") and mw._watcher:
|
|
mw._watcher._full_scan_library(library_id, path)
|
|
from PyQt6.QtCore import QTimer
|
|
QTimer.singleShot(1000, self._load)
|
|
|
|
def _start_bpm(self, library_id: int, scan_all: bool,
|
|
btn: QPushButton, lbl_status: QLabel):
|
|
if library_id in self._bpm_workers:
|
|
return # allerede i gang
|
|
|
|
worker = BpmScanWorker(library_id, scan_all=scan_all)
|
|
|
|
def on_progress(done, total):
|
|
lbl_status.setText(f"♩ {done}/{total} analyseret...")
|
|
lbl_status.show()
|
|
btn.setEnabled(False)
|
|
|
|
def on_finished(count):
|
|
lbl_status.setText(f"✓ {count} sange analyseret")
|
|
btn.setEnabled(True)
|
|
self._bpm_workers.pop(library_id, None)
|
|
# Opdater UI og bibliotek
|
|
from PyQt6.QtCore import QTimer
|
|
QTimer.singleShot(500, self._load)
|
|
mw = self.parent()
|
|
if hasattr(mw, "_reload_library"):
|
|
QTimer.singleShot(600, mw._reload_library)
|
|
|
|
worker.progress.connect(on_progress)
|
|
worker.finished.connect(on_finished)
|
|
self._bpm_workers[library_id] = worker
|
|
worker.start()
|
|
worker.setPriority(QThread.Priority.LowestPriority)
|
|
|
|
def _add_folder(self):
|
|
from PyQt6.QtWidgets import QFileDialog
|
|
folder = QFileDialog.getExistingDirectory(self, "Vælg musikmappe")
|
|
if folder:
|
|
mw = self.parent()
|
|
if hasattr(mw, "add_library_path"):
|
|
mw.add_library_path(folder)
|
|
from PyQt6.QtCore import QTimer
|
|
QTimer.singleShot(800, self._load)
|
|
|
|
def _remove_library(self, lib: dict):
|
|
reply = QMessageBox.question(
|
|
self, "Fjern bibliotek",
|
|
f"Fjern overvågningen af:\n{lib['path']}\n\n"
|
|
"Sange forbliver i databasen men markeres som manglende.",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
)
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
try:
|
|
mw = self.parent()
|
|
if hasattr(mw, "_watcher") and mw._watcher:
|
|
mw._watcher.remove_library(lib["id"])
|
|
else:
|
|
from local.local_db import remove_library
|
|
remove_library(lib["id"])
|
|
self.library_removed.emit(lib["id"])
|
|
if hasattr(mw, "_reload_library"):
|
|
mw._reload_library()
|
|
self._load()
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Fejl", f"Kunne ikke fjerne: {e}")
|