Mappehåndtering

This commit is contained in:
2026-04-12 14:29:54 +02:00
parent bdb1f5915a
commit a9915c0cc9
6 changed files with 486 additions and 137 deletions

View File

@@ -1,25 +1,25 @@
"""
library_manager.py — Håndter musikmapper.
Tilføj/fjern mapper. Scan KUN ved eksplicit knap-tryk.
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, QWidget,
QPushButton, QFrame, QMessageBox, QProgressBar,
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QMessageBox, QScrollArea, QWidget,
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QThread
class LibraryManagerDialog(QDialog):
libraries_changed = pyqtSignal() # signal til main_window om at genindlæse
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._scanning = False
self._db_path = db_path
self._workers = {} # library_id → ScanWorker
self.setWindowTitle("Musikmapper")
self.setMinimumWidth(600)
@@ -34,26 +34,24 @@ class LibraryManagerDialog(QDialog):
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
lbl = QLabel("Tilføj eller fjern musikmapper. Scan starter kun ved klik på knappen.")
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)
self._libs_layout = QVBoxLayout()
# 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)
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)
self._libs_layout.addStretch()
scroll.setWidget(self._scroll_content)
layout.addWidget(scroll, stretch=1)
# Knap-række
btn_row = QHBoxLayout()
@@ -62,53 +60,71 @@ class LibraryManagerDialog(QDialog):
btn_row.addWidget(btn_add)
btn_row.addStretch()
btn_close = QPushButton("Luk")
btn_close.clicked.connect(self._on_close)
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 — ingen scanning."""
while self._libs_layout.count():
"""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, last_full_scan FROM libraries WHERE is_active=1 ORDER BY path"
"SELECT id, path, last_full_scan FROM libraries "
"WHERE is_active=1 ORDER BY path"
).fetchall()
total_songs = {}
counts = {}
bpm_missing = {}
for lib in libs:
cnt = conn.execute(
"SELECT COUNT(*) FROM songs WHERE library_id=? AND file_missing=0",
counts[lib["id"]] = conn.execute(
"SELECT COUNT(*) FROM songs "
"WHERE library_id=? AND file_missing=0",
(lib["id"],)
).fetchone()[0]
bpm_missing[lib["id"]] = conn.execute(
"SELECT COUNT(*) FROM songs "
"WHERE library_id=? AND file_missing=0 "
"AND (bpm IS NULL OR bpm=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)
self._libs_layout.insertWidget(0, lbl)
return
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"
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 = 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")
@@ -122,37 +138,43 @@ class LibraryManagerDialog(QDialog):
vbox.addWidget(lbl_path)
# Info
lbl_info = QLabel(f" {song_count} sange · senest scannet: {last}")
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)
# Scan-status label
lbl_scan = QLabel("")
lbl_scan.setObjectName("result_count")
lbl_scan.hide()
vbox.addWidget(lbl_scan)
# 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_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_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_scan)
btn_row.addWidget(btn_bpm)
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_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_scan_all)
btn_row.addWidget(btn_bpm_all)
btn_row.addStretch()
@@ -173,18 +195,32 @@ class LibraryManagerDialog(QDialog):
return
try:
conn = sqlite3.connect(self._db_path)
# Tjek om mappen allerede er tilføjet
conn.row_factory = sqlite3.Row
# Tjek om mappen allerede er aktiv
existing = conn.execute(
"SELECT id FROM libraries WHERE path=?", (folder,)
"SELECT id, is_active 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,)
)
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()
@@ -196,79 +232,69 @@ class LibraryManagerDialog(QDialog):
reply = QMessageBox.question(
self, "Fjern mappe",
f"Fjern:\n{lib['path']}\n\n"
"Alle sange fra denne mappe slettes også fra databasen.\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. scanning på dette bibliotek
# 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)
# 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"],))
# Slet sange fra biblioteket
conn.execute(
"DELETE FROM songs WHERE library_id=?", (lib["id"],)
)
# Slet selve biblioteks-rækken helt
conn.execute(
"DELETE FROM libraries WHERE id=?", (lib["id"],)
)
conn.commit()
conn.close()
self._load()
self.libraries_changed.emit()
QTimer.singleShot(300, self.libraries_changed.emit)
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke fjerne: {e}")
# ── Scanning ──────────────────────────────────────────────────────────────
# ── BPM-scanning ──────────────────────────────────────────────────────────
def _start_scan(self, library_id: int, path: str,
scan_all: bool, btn: QPushButton, lbl: QLabel):
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.scan_worker import ScanWorker
from ui.bpm_worker import BpmScanWorker
worker = ScanWorker(library_id, path, str(DB_PATH),
overwrite_bpm=scan_all)
worker = BpmScanWorker(library_id, str(DB_PATH),
scan_all=scan_all)
def on_progress(done, total, filename):
if total > 0:
lbl.setText(f"{done}/{total}{filename}")
lbl.show()
def on_progress(done, total):
lbl.setText(f"{done}/{total} analyseret...")
lbl.show()
btn.setEnabled(False)
def on_finished(count, lib_path):
lbl.setText(f"{count} sange scannet")
def on_finished(count):
lbl.setText(f"{count} analyseret")
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()
worker.setPriority(QThread.Priority.LowestPriority)
# ── 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()
for w in list(self._workers.values()):
w.cancel()
w.wait(2000) # Vent max 2 sek på at tråden stopper
event.accept()