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