""" 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}")