""" library_manager.py — Håndter musikmapper. Tilføj/fjern mapper. Scan KUN ved eksplicit knap-tryk. """ import sqlite3 from pathlib import Path from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QPushButton, QFrame, QMessageBox, QProgressBar, ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer class LibraryManagerDialog(QDialog): libraries_changed = pyqtSignal() # signal til main_window om at genindlæse def __init__(self, db_path: str, parent=None): super().__init__(parent) self._db_path = db_path self._workers = {} # library_id → ScanWorker self._scanning = False 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. Scan starter kun ved klik på knappen.") lbl.setObjectName("result_count") lbl.setWordWrap(True) layout.addWidget(lbl) self._libs_layout = QVBoxLayout() self._libs_layout.setSpacing(6) layout.addLayout(self._libs_layout) layout.addStretch() # Global status self._lbl_status = QLabel("") self._lbl_status.setObjectName("result_count") self._lbl_status.hide() layout.addWidget(self._lbl_status) self._progress = QProgressBar() self._progress.hide() layout.addWidget(self._progress) # 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._on_close) btn_row.addWidget(btn_close) layout.addLayout(btn_row) def _load(self): """Indlæs biblioteker fra DB og vis dem — ingen scanning.""" while self._libs_layout.count(): item = self._libs_layout.takeAt(0) if item.widget(): item.widget().deleteLater() try: conn = sqlite3.connect(self._db_path) conn.row_factory = sqlite3.Row libs = conn.execute( "SELECT id, path, last_full_scan FROM libraries WHERE is_active=1 ORDER BY path" ).fetchall() total_songs = {} for lib in libs: cnt = conn.execute( "SELECT COUNT(*) FROM songs WHERE library_id=? AND file_missing=0", (lib["id"],) ).fetchone()[0] total_songs[lib["id"]] = cnt conn.close() for lib in libs: self._libs_layout.addWidget( self._make_lib_row(dict(lib), total_songs[lib["id"]]) ) if not libs: lbl = QLabel("Ingen musikmapper tilføjet endnu.") lbl.setObjectName("result_count") self._libs_layout.addWidget(lbl) except Exception as e: lbl = QLabel(f"Fejl ved indlæsning: {e}") self._libs_layout.addWidget(lbl) def _make_lib_row(self, lib: dict, song_count: int) -> QFrame: lib_id = lib["id"] path = lib["path"] exists = Path(path).exists() last = lib.get("last_full_scan") or "aldrig" if isinstance(last, str) and len(last) > 16: last = last[:16] scanning = lib_id in self._workers 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 · senest scannet: {last}") lbl_info.setObjectName("result_count") vbox.addWidget(lbl_info) # Scan-status label lbl_scan = QLabel("") lbl_scan.setObjectName("result_count") lbl_scan.hide() vbox.addWidget(lbl_scan) # Knapper btn_row = QHBoxLayout() btn_row.setSpacing(6) btn_scan = QPushButton("⟳ Scan nye filer") btn_scan.setFixedHeight(30) btn_scan.setEnabled(exists and not scanning) btn_scan.clicked.connect( lambda _, lid=lib_id, p=path, b=btn_scan, s=lbl_scan: self._start_scan(lid, p, False, b, s) ) btn_row.addWidget(btn_scan) btn_scan_all = QPushButton("⟳ Scan alle filer") btn_scan_all.setFixedHeight(30) btn_scan_all.setEnabled(exists and not scanning) btn_scan_all.clicked.connect( lambda _, lid=lib_id, p=path, b=btn_scan_all, s=lbl_scan: self._start_scan(lid, p, True, b, s) ) btn_row.addWidget(btn_scan_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) # Tjek om mappen allerede er tilføjet existing = conn.execute( "SELECT id FROM libraries WHERE path=?", (folder,) ).fetchone() if existing: QMessageBox.information(self, "Allerede tilføjet", "Denne mappe er allerede i listen.") conn.close() return 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 også fra databasen.\n" "Dans-tags og playlister bevares.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply != QMessageBox.StandardButton.Yes: return # Stop evt. 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) # Slet sange der tilhører dette bibliotek conn.execute("DELETE FROM songs WHERE library_id=?", (lib["id"],)) # Deaktiver biblioteket conn.execute("UPDATE libraries SET is_active=0 WHERE id=?", (lib["id"],)) conn.commit() conn.close() self._load() self.libraries_changed.emit() except Exception as e: QMessageBox.warning(self, "Fejl", f"Kunne ikke fjerne: {e}") # ── Scanning ────────────────────────────────────────────────────────────── def _start_scan(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.scan_worker import ScanWorker worker = ScanWorker(library_id, path, str(DB_PATH), overwrite_bpm=scan_all) def on_progress(done, total, filename): if total > 0: lbl.setText(f"⟳ {done}/{total} — {filename}") lbl.show() btn.setEnabled(False) def on_finished(count, lib_path): lbl.setText(f"✓ {count} sange scannet") btn.setEnabled(True) self._workers.pop(library_id, None) QTimer.singleShot(300, self._load) self.libraries_changed.emit() def on_error(msg): lbl.setText(f"⚠ Fejl: {msg}") lbl.show() btn.setEnabled(True) self._workers.pop(library_id, None) worker.progress.connect(on_progress) worker.finished.connect(on_finished) worker.error.connect(on_error) self._workers[library_id] = worker worker.setPriority(QThread.Priority.LowestPriority) worker.start() # ── Luk ─────────────────────────────────────────────────────────────────── def _on_close(self): # Stop alle aktive scannere for worker in self._workers.values(): worker.cancel() self.accept() def closeEvent(self, event): for worker in self._workers.values(): worker.cancel() event.accept()