Næste version

This commit is contained in:
2026-04-12 10:25:41 +02:00
parent b678787236
commit 57f3c913b4
18 changed files with 2690 additions and 458 deletions

View File

@@ -1,22 +1,76 @@
"""
library_manager.py — Dialog til at se og fjerne musikbiblioteker.
library_manager.py — Dialog til at administrere musikbiblioteker med BPM-scanning.
"""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QWidget,
QPushButton, QListWidget, QListWidgetItem, QMessageBox,
QFrame, QSizePolicy,
)
from PyQt6.QtCore import Qt, pyqtSignal
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) # library_id
library_removed = pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Administrer musikbiblioteker")
self.setMinimumWidth(500)
self.setMinimumHeight(320)
self.setMinimumWidth(580)
self.setMinimumHeight(360)
self._bpm_workers = {} # library_id → BpmScanWorker
self._build_ui()
self._load()
@@ -29,8 +83,10 @@ class LibraryManagerDialog(QDialog):
lbl.setObjectName("track_meta")
layout.addWidget(lbl)
self._list = QListWidget()
layout.addWidget(self._list)
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"
@@ -44,16 +100,6 @@ class LibraryManagerDialog(QDialog):
btn_add = QPushButton("+ Tilføj mappe")
btn_add.clicked.connect(self._add_folder)
btn_row.addWidget(btn_add)
btn_remove = QPushButton("✕ Fjern valgt")
btn_remove.clicked.connect(self._remove_selected)
btn_row.addWidget(btn_remove)
btn_scan = QPushButton("⟳ Scan alle")
btn_scan.setToolTip("Scan alle mapper for nye og ændrede filer")
btn_scan.clicked.connect(self._scan_all)
btn_row.addWidget(btn_scan)
btn_row.addStretch()
btn_close = QPushButton("Luk")
btn_close.clicked.connect(self.accept)
@@ -61,41 +107,141 @@ class LibraryManagerDialog(QDialog):
layout.addLayout(btn_row)
def _load(self):
self._list.clear()
# Ryd eksisterende widgets
while self._libs_layout.count():
item = self._libs_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
try:
from local.local_db import get_libraries, get_db
libs = get_libraries(active_only=True) # kun aktive
libs = get_libraries(active_only=True)
for lib in libs:
from pathlib import Path
path = lib["path"]
exists = Path(path).exists()
last_scan = lib["last_full_scan"] or "aldrig"
if isinstance(last_scan, str) and len(last_scan) > 10:
last_scan = last_scan[:10]
with get_db() as conn:
count = conn.execute(
"SELECT COUNT(*) FROM songs WHERE library_id=? AND file_missing=0",
(lib["id"],)
).fetchone()[0]
exist_icon = "" if exists else " ⚠ mappe ikke fundet"
label = f"{path}{exist_icon}\n {count} sange · senest scannet: {last_scan}"
item = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, dict(lib))
if not exists:
from PyQt6.QtGui import QColor
item.setForeground(QColor("#5a6070"))
self._list.addItem(item)
self._libs_layout.addWidget(self._make_lib_row(lib))
except Exception as e:
print(f"Library manager load fejl: {e}")
pass
def _scan_all(self):
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]
try:
from local.local_db import get_db
with get_db() as conn:
total = 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]
except Exception:
total = 0
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(24)
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(24)
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(24)
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(24)
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, "start_scan"):
mw.start_scan()
self._set_status("Scanning startet...")
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 _set_status(self, text: str):
pass # kan udvides med statuslinje i dialogen
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
@@ -104,19 +250,14 @@ class LibraryManagerDialog(QDialog):
mw = self.parent()
if hasattr(mw, "add_library_path"):
mw.add_library_path(folder)
# Genindlæs listen efter kort pause så DB er opdateret
from PyQt6.QtCore import QTimer
QTimer.singleShot(600, self._load)
QTimer.singleShot(800, self._load)
def _remove_selected(self):
item = self._list.currentItem()
if not item:
return
lib = item.data(Qt.ItemDataRole.UserRole)
def _remove_library(self, lib: dict):
reply = QMessageBox.question(
self, "Fjern bibliotek",
f"Fjern overvågningen af:\n{lib['path']}\n\n"
"Sange i biblioteket forbliver i databasen men markeres som manglende.",
"Sange forbliver i databasen men markeres som manglende.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes: