275 lines
9.8 KiB
Python
275 lines
9.8 KiB
Python
"""
|
||
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()
|