Næste version
This commit is contained in:
@@ -1,22 +1,76 @@
|
||||
"""
|
||||
library_manager.py — Dialog til at se og fjerne musikbiblioteker.
|
||||
library_manager.py — Dialog til at administrere musikbiblioteker med BPM-scanning.
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QWidget,
|
||||
QPushButton, QListWidget, QListWidgetItem, QMessageBox,
|
||||
QFrame, QSizePolicy,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
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) # library_id
|
||||
library_removed = pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Administrer musikbiblioteker")
|
||||
self.setMinimumWidth(500)
|
||||
self.setMinimumHeight(320)
|
||||
self.setMinimumWidth(580)
|
||||
self.setMinimumHeight(360)
|
||||
self._bpm_workers = {} # library_id → BpmScanWorker
|
||||
self._build_ui()
|
||||
self._load()
|
||||
|
||||
@@ -29,8 +83,10 @@ class LibraryManagerDialog(QDialog):
|
||||
lbl.setObjectName("track_meta")
|
||||
layout.addWidget(lbl)
|
||||
|
||||
self._list = QListWidget()
|
||||
layout.addWidget(self._list)
|
||||
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"
|
||||
@@ -44,16 +100,6 @@ class LibraryManagerDialog(QDialog):
|
||||
btn_add = QPushButton("+ Tilføj mappe")
|
||||
btn_add.clicked.connect(self._add_folder)
|
||||
btn_row.addWidget(btn_add)
|
||||
|
||||
btn_remove = QPushButton("✕ Fjern valgt")
|
||||
btn_remove.clicked.connect(self._remove_selected)
|
||||
btn_row.addWidget(btn_remove)
|
||||
|
||||
btn_scan = QPushButton("⟳ Scan alle")
|
||||
btn_scan.setToolTip("Scan alle mapper for nye og ændrede filer")
|
||||
btn_scan.clicked.connect(self._scan_all)
|
||||
btn_row.addWidget(btn_scan)
|
||||
|
||||
btn_row.addStretch()
|
||||
btn_close = QPushButton("Luk")
|
||||
btn_close.clicked.connect(self.accept)
|
||||
@@ -61,41 +107,141 @@ class LibraryManagerDialog(QDialog):
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
def _load(self):
|
||||
self._list.clear()
|
||||
# Ryd eksisterende widgets
|
||||
while self._libs_layout.count():
|
||||
item = self._libs_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
try:
|
||||
from local.local_db import get_libraries, get_db
|
||||
libs = get_libraries(active_only=True) # kun aktive
|
||||
libs = get_libraries(active_only=True)
|
||||
for lib in libs:
|
||||
from pathlib import Path
|
||||
path = lib["path"]
|
||||
exists = Path(path).exists()
|
||||
last_scan = lib["last_full_scan"] or "aldrig"
|
||||
if isinstance(last_scan, str) and len(last_scan) > 10:
|
||||
last_scan = last_scan[:10]
|
||||
with get_db() as conn:
|
||||
count = conn.execute(
|
||||
"SELECT COUNT(*) FROM songs WHERE library_id=? AND file_missing=0",
|
||||
(lib["id"],)
|
||||
).fetchone()[0]
|
||||
exist_icon = "" if exists else " ⚠ mappe ikke fundet"
|
||||
label = f"{path}{exist_icon}\n {count} sange · senest scannet: {last_scan}"
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.ItemDataRole.UserRole, dict(lib))
|
||||
if not exists:
|
||||
from PyQt6.QtGui import QColor
|
||||
item.setForeground(QColor("#5a6070"))
|
||||
self._list.addItem(item)
|
||||
self._libs_layout.addWidget(self._make_lib_row(lib))
|
||||
except Exception as e:
|
||||
print(f"Library manager load fejl: {e}")
|
||||
pass
|
||||
|
||||
def _scan_all(self):
|
||||
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]
|
||||
try:
|
||||
from local.local_db import get_db
|
||||
with get_db() as conn:
|
||||
total = 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]
|
||||
except Exception:
|
||||
total = 0
|
||||
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(24)
|
||||
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(24)
|
||||
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(24)
|
||||
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(24)
|
||||
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, "start_scan"):
|
||||
mw.start_scan()
|
||||
self._set_status("Scanning startet...")
|
||||
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 _set_status(self, text: str):
|
||||
pass # kan udvides med statuslinje i dialogen
|
||||
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
|
||||
@@ -104,19 +250,14 @@ class LibraryManagerDialog(QDialog):
|
||||
mw = self.parent()
|
||||
if hasattr(mw, "add_library_path"):
|
||||
mw.add_library_path(folder)
|
||||
# Genindlæs listen efter kort pause så DB er opdateret
|
||||
from PyQt6.QtCore import QTimer
|
||||
QTimer.singleShot(600, self._load)
|
||||
QTimer.singleShot(800, self._load)
|
||||
|
||||
def _remove_selected(self):
|
||||
item = self._list.currentItem()
|
||||
if not item:
|
||||
return
|
||||
lib = item.data(Qt.ItemDataRole.UserRole)
|
||||
def _remove_library(self, lib: dict):
|
||||
reply = QMessageBox.question(
|
||||
self, "Fjern bibliotek",
|
||||
f"Fjern overvågningen af:\n{lib['path']}\n\n"
|
||||
"Sange i biblioteket forbliver i databasen men markeres som manglende.",
|
||||
"Sange forbliver i databasen men markeres som manglende.",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
|
||||
Reference in New Issue
Block a user