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

@@ -66,6 +66,8 @@ def scan_library(library_id: int, library_path: str, db_path: str,
total = len(all_files) total = len(all_files)
done = 0 done = 0
import time
for fp in all_files: for fp in all_files:
path_str = str(fp) path_str = str(fp)
mtime = get_file_mtime(fp) mtime = get_file_mtime(fp)
@@ -76,6 +78,9 @@ def scan_library(library_id: int, library_path: str, db_path: str,
# Spring over hvis ikke ændret # Spring over hvis ikke ændret
if path_str in known and known[path_str] == mtime: if path_str in known and known[path_str] == mtime:
done += 1 done += 1
# Yield hvert 100. fil så andre tråde kan køre
if done % 100 == 0:
time.sleep(0.005)
continue continue
try: try:
@@ -117,6 +122,8 @@ def scan_library(library_id: int, library_path: str, db_path: str,
logger.warning(f"Scan fejl {fp.name}: {e}") logger.warning(f"Scan fejl {fp.name}: {e}")
done += 1 done += 1
# Lille pause efter hver scannet fil så GUI ikke hænger
time.sleep(0.02)
# Marker manglende filer # Marker manglende filer
for path_str in known: for path_str in known:

View File

@@ -0,0 +1,183 @@
"""
watchdog_process.py — Kører som selvstændig subprocess.
Overvåger musikmapper og opdaterer SQLite ved fil-ændringer.
Start: python watchdog_process.py <db_path>
"""
import sys
import os
import time
import json
import sqlite3
import logging
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [watchdog] %(message)s",
stream=sys.stderr
)
logger = logging.getLogger(__name__)
SUPPORTED = {'.mp3', '.flac', '.m4a', '.ogg', '.wav', '.aiff', '.wma'}
def is_supported(path: Path) -> bool:
return path.suffix.lower() in SUPPORTED
def get_libraries(db_path: str) -> list[dict]:
try:
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
libs = conn.execute(
"SELECT id, path FROM libraries WHERE is_active=1"
).fetchall()
conn.close()
return [dict(l) for l in libs]
except Exception:
return []
def process_file(db_path: str, library_id: int, file_path: str,
deleted: bool = False):
"""Opdater SQLite for én fil."""
try:
# Tilføj app-mappen til sys.path så tag_reader kan importeres
app_dir = str(Path(__file__).parent.parent)
if app_dir not in sys.path:
sys.path.insert(0, app_dir)
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
if deleted:
conn.execute(
"UPDATE songs SET file_missing=1 WHERE local_path=?",
(file_path,)
)
conn.commit()
conn.close()
return
from local.tag_reader import read_tags
import uuid
mtime = str(os.path.getmtime(file_path))
tags = read_tags(Path(file_path))
extra = json.dumps(tags.get("extra_tags", {}), ensure_ascii=False)
existing = conn.execute(
"SELECT id, bpm FROM songs WHERE local_path=?", (file_path,)
).fetchone()
if existing:
bpm = tags.get("bpm", 0) or existing["bpm"] or 0
conn.execute("""
UPDATE songs SET
library_id=?, title=?, artist=?, album=?,
bpm=?, duration_sec=?, file_format=?,
file_modified_at=?, file_missing=0, extra_tags=?
WHERE id=?
""", (library_id, tags.get("title",""), tags.get("artist",""),
tags.get("album",""), bpm, tags.get("duration_sec",0),
tags.get("file_format",""), mtime, extra, existing["id"]))
else:
conn.execute("""
INSERT OR IGNORE INTO songs
(id, library_id, local_path, title, artist, album,
bpm, duration_sec, file_format, file_modified_at, extra_tags)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
""", (str(uuid.uuid4()), library_id, file_path,
tags.get("title",""), tags.get("artist",""),
tags.get("album",""), tags.get("bpm",0),
tags.get("duration_sec",0), tags.get("file_format",""),
mtime, extra))
conn.commit()
conn.close()
logger.info(f"Opdateret: {Path(file_path).name}")
except Exception as e:
logger.error(f"Fejl ved {file_path}: {e}")
def run(db_path: str):
try:
from watchdog.observers.polling import PollingObserver
from watchdog.events import FileSystemEventHandler
except ImportError:
logger.error("watchdog ikke installeret")
sys.exit(1)
class Handler(FileSystemEventHandler):
def __init__(self, library_id: int):
self.library_id = library_id
def on_created(self, event):
if not event.is_directory and is_supported(Path(event.src_path)):
time.sleep(0.5) # Vent til filen er skrevet færdig
process_file(db_path, self.library_id, event.src_path)
def on_modified(self, event):
if not event.is_directory and is_supported(Path(event.src_path)):
process_file(db_path, self.library_id, event.src_path)
def on_deleted(self, event):
if not event.is_directory and is_supported(Path(event.src_path)):
process_file(db_path, self.library_id, event.src_path,
deleted=True)
def on_moved(self, event):
if not event.is_directory:
if is_supported(Path(event.src_path)):
process_file(db_path, self.library_id, event.src_path,
deleted=True)
if is_supported(Path(event.dest_path)):
process_file(db_path, self.library_id, event.dest_path)
# Brug 60 sekunders poll-interval — opdager ændringer inden for 1 minut
observer = PollingObserver(timeout=60)
libraries = get_libraries(db_path)
if not libraries:
logger.info("Ingen biblioteker — venter...")
for lib in libraries:
path = Path(lib["path"])
if path.exists():
observer.schedule(Handler(lib["id"]), str(path), recursive=True)
logger.info(f"Overvåger: {path}")
else:
logger.warning(f"Mappe ikke fundet: {path}")
observer.start()
logger.info("Watchdog kører")
try:
while True:
time.sleep(30)
# Tjek om der er kommet nye biblioteker siden start
current = get_libraries(db_path)
current_paths = {lib["path"] for lib in current}
watched_paths = {str(w.path) for w in observer.emitters}
for lib in current:
if lib["path"] not in watched_paths:
path = Path(lib["path"])
if path.exists():
observer.schedule(
Handler(lib["id"]), str(path), recursive=True
)
logger.info(f"Tilføjet overvågning: {path}")
except KeyboardInterrupt:
pass
finally:
observer.stop()
observer.join()
logger.info("Watchdog stoppet")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Brug: python watchdog_process.py <db_path>")
sys.exit(1)
run(sys.argv[1])

View File

@@ -0,0 +1,69 @@
"""
bpm_worker.py — QThread til BPM-analyse i baggrunden.
"""
import sqlite3
from PyQt6.QtCore import QThread, pyqtSignal
class BpmScanWorker(QThread):
progress = pyqtSignal(int, int) # done, total
finished = pyqtSignal(int) # antal analyseret
def __init__(self, library_id: int, db_path: str,
scan_all: bool = False):
super().__init__()
self._library_id = library_id
self._db_path = db_path
self._scan_all = scan_all
def cancel(self):
self.requestInterruption()
# Afbryd hurtigt ved at sætte et flag
self._cancelled = True
def run(self):
import time
self._cancelled = False
try:
from local.tag_reader import analyze_bpm
conn = sqlite3.connect(self._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._cancelled or self.isInterruptionRequested():
break
try:
bpm = analyze_bpm(song["local_path"])
if bpm and bpm > 0:
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)
time.sleep(0.01) # Yield så GUI ikke hænger
conn.close()
self.finished.emit(done)
except Exception as e:
self.finished.emit(0)

View File

@@ -1,25 +1,25 @@
""" """
library_manager.py — Håndter musikmapper. 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 import sqlite3
from pathlib import Path from pathlib import Path
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QMessageBox, QProgressBar, QPushButton, QFrame, QMessageBox, QScrollArea, QWidget,
) )
from PyQt6.QtCore import Qt, pyqtSignal, QTimer from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QThread
class LibraryManagerDialog(QDialog): 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): def __init__(self, db_path: str, parent=None):
super().__init__(parent) super().__init__(parent)
self._db_path = db_path self._db_path = db_path
self._workers = {} # library_id → ScanWorker self._workers = {} # library_id → ScanWorker
self._scanning = False
self.setWindowTitle("Musikmapper") self.setWindowTitle("Musikmapper")
self.setMinimumWidth(600) self.setMinimumWidth(600)
@@ -34,26 +34,24 @@ class LibraryManagerDialog(QDialog):
layout.setContentsMargins(12, 12, 12, 12) layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8) 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.setObjectName("result_count")
lbl.setWordWrap(True) lbl.setWordWrap(True)
layout.addWidget(lbl) 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) self._libs_layout.setSpacing(6)
layout.addLayout(self._libs_layout) self._libs_layout.addStretch()
scroll.setWidget(self._scroll_content)
layout.addStretch() layout.addWidget(scroll, stretch=1)
# 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 # Knap-række
btn_row = QHBoxLayout() btn_row = QHBoxLayout()
@@ -62,53 +60,71 @@ class LibraryManagerDialog(QDialog):
btn_row.addWidget(btn_add) btn_row.addWidget(btn_add)
btn_row.addStretch() btn_row.addStretch()
btn_close = QPushButton("Luk") btn_close = QPushButton("Luk")
btn_close.clicked.connect(self._on_close) btn_close.clicked.connect(self.accept)
btn_row.addWidget(btn_close) btn_row.addWidget(btn_close)
layout.addLayout(btn_row) layout.addLayout(btn_row)
def _load(self): def _load(self):
"""Indlæs biblioteker fra DB og vis dem — ingen scanning.""" """Indlæs biblioteker fra DB og vis dem."""
while self._libs_layout.count(): from PyQt6.QtWidgets import QApplication
# Ryd eksisterende rækker (ikke stretch)
while self._libs_layout.count() > 1:
item = self._libs_layout.takeAt(0) item = self._libs_layout.takeAt(0)
if item.widget(): if item.widget():
item.widget().deleteLater() item.widget().deleteLater()
QApplication.processEvents() # Lad Qt rydde op før vi bygger nyt
try: try:
conn = sqlite3.connect(self._db_path) conn = sqlite3.connect(self._db_path)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
libs = conn.execute( 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() ).fetchall()
total_songs = {}
counts = {}
bpm_missing = {}
for lib in libs: for lib in libs:
cnt = conn.execute( counts[lib["id"]] = conn.execute(
"SELECT COUNT(*) FROM songs WHERE library_id=? AND file_missing=0", "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"],) (lib["id"],)
).fetchone()[0] ).fetchone()[0]
total_songs[lib["id"]] = cnt
conn.close() conn.close()
for lib in libs:
self._libs_layout.addWidget(
self._make_lib_row(dict(lib), total_songs[lib["id"]])
)
if not libs: if not libs:
lbl = QLabel("Ingen musikmapper tilføjet endnu.") lbl = QLabel("Ingen musikmapper tilføjet endnu.")
lbl.setObjectName("result_count") lbl.setObjectName("result_count")
self._libs_layout.addWidget(lbl) self._libs_layout.insertWidget(0, lbl)
except Exception as e: return
lbl = QLabel(f"Fejl ved indlæsning: {e}")
self._libs_layout.addWidget(lbl)
def _make_lib_row(self, lib: dict, song_count: int) -> QFrame: 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"] lib_id = lib["id"]
path = lib["path"] path = lib["path"]
exists = Path(path).exists() exists = Path(path).exists()
last = lib.get("last_full_scan") or "aldrig" last = lib.get("last_full_scan") or "aldrig"
if isinstance(last, str) and len(last) > 16: if isinstance(last, str) and len(last) > 16:
last = last[:16] last = last[:16]
scanning = lib_id in self._workers
frame = QFrame() frame = QFrame()
frame.setObjectName("track_display") frame.setObjectName("track_display")
@@ -122,37 +138,43 @@ class LibraryManagerDialog(QDialog):
vbox.addWidget(lbl_path) vbox.addWidget(lbl_path)
# Info # 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") lbl_info.setObjectName("result_count")
vbox.addWidget(lbl_info) vbox.addWidget(lbl_info)
# Scan-status label # Status-label til scanning
lbl_scan = QLabel("") lbl_status = QLabel("")
lbl_scan.setObjectName("result_count") lbl_status.setObjectName("result_count")
lbl_scan.hide() lbl_status.hide()
vbox.addWidget(lbl_scan) vbox.addWidget(lbl_status)
# Knapper # Knapper
btn_row = QHBoxLayout() btn_row = QHBoxLayout()
btn_row.setSpacing(6) btn_row.setSpacing(6)
btn_scan = QPushButton(" Scan nye filer") btn_bpm = QPushButton(f" BPM manglende ({bpm_missing})")
btn_scan.setFixedHeight(30) btn_bpm.setFixedHeight(30)
btn_scan.setEnabled(exists and not scanning) btn_bpm.setEnabled(exists and bpm_missing > 0
btn_scan.clicked.connect( and lib_id not in self._workers)
lambda _, lid=lib_id, p=path, b=btn_scan, s=lbl_scan: btn_bpm.clicked.connect(
self._start_scan(lid, p, False, b, s) 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_bpm_all = QPushButton(" BPM alle")
btn_scan_all.setFixedHeight(30) btn_bpm_all.setFixedHeight(30)
btn_scan_all.setEnabled(exists and not scanning) btn_bpm_all.setEnabled(exists and lib_id not in self._workers)
btn_scan_all.clicked.connect( btn_bpm_all.clicked.connect(
lambda _, lid=lib_id, p=path, b=btn_scan_all, s=lbl_scan: lambda _, lid=lib_id, p=path, b=btn_bpm_all, s=lbl_status:
self._start_scan(lid, p, True, b, s) self._start_bpm(lid, p, True, b, s)
) )
btn_row.addWidget(btn_scan_all) btn_row.addWidget(btn_bpm_all)
btn_row.addStretch() btn_row.addStretch()
@@ -173,17 +195,31 @@ class LibraryManagerDialog(QDialog):
return return
try: try:
conn = sqlite3.connect(self._db_path) 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( existing = conn.execute(
"SELECT id FROM libraries WHERE path=?", (folder,) "SELECT id, is_active FROM libraries WHERE path=?", (folder,)
).fetchone() ).fetchone()
if existing: if existing:
QMessageBox.information(self, "Allerede tilføjet", if existing["is_active"]:
"Denne mappe er allerede i listen.") QMessageBox.information(
self, "Allerede tilføjet",
"Denne mappe er allerede i listen."
)
conn.close() conn.close()
return return
else:
# Reaktiver en tidligere fjernet mappe
conn.execute( conn.execute(
"INSERT INTO libraries (path, is_active) VALUES (?, 1)", (folder,) "UPDATE libraries SET is_active=1 WHERE path=?",
(folder,)
)
else:
conn.execute(
"INSERT INTO libraries (path, is_active) VALUES (?, 1)",
(folder,)
) )
conn.commit() conn.commit()
conn.close() conn.close()
@@ -196,79 +232,69 @@ class LibraryManagerDialog(QDialog):
reply = QMessageBox.question( reply = QMessageBox.question(
self, "Fjern mappe", self, "Fjern mappe",
f"Fjern:\n{lib['path']}\n\n" 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.", "Dans-tags og playlister bevares.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.StandardButton.Yes: if reply != QMessageBox.StandardButton.Yes:
return return
# Stop evt. scanning på dette bibliotek # Stop evt. BPM-scanning på dette bibliotek
if lib["id"] in self._workers: if lib["id"] in self._workers:
self._workers[lib["id"]].cancel() self._workers[lib["id"]].cancel()
self._workers.pop(lib["id"], None) self._workers.pop(lib["id"], None)
try: try:
conn = sqlite3.connect(self._db_path) conn = sqlite3.connect(self._db_path)
# Slet sange der tilhører dette bibliotek # Slet sange fra biblioteket
conn.execute("DELETE FROM songs WHERE library_id=?", (lib["id"],)) conn.execute(
# Deaktiver biblioteket "DELETE FROM songs WHERE library_id=?", (lib["id"],)
conn.execute("UPDATE libraries SET is_active=0 WHERE id=?", (lib["id"],)) )
# Slet selve biblioteks-rækken helt
conn.execute(
"DELETE FROM libraries WHERE id=?", (lib["id"],)
)
conn.commit() conn.commit()
conn.close() conn.close()
self._load() self._load()
self.libraries_changed.emit() QTimer.singleShot(300, self.libraries_changed.emit)
except Exception as e: except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke fjerne: {e}") QMessageBox.warning(self, "Fejl", f"Kunne ikke fjerne: {e}")
# ── Scanning ────────────────────────────────────────────────────────────── # ── BPM-scanning ──────────────────────────────────────────────────────────
def _start_scan(self, library_id: int, path: str, def _start_bpm(self, library_id: int, path: str,
scan_all: bool, btn: QPushButton, lbl: QLabel): scan_all: bool, btn: QPushButton, lbl: QLabel):
if library_id in self._workers: if library_id in self._workers:
return return
from local.local_db import DB_PATH 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), worker = BpmScanWorker(library_id, str(DB_PATH),
overwrite_bpm=scan_all) scan_all=scan_all)
def on_progress(done, total, filename): def on_progress(done, total):
if total > 0: lbl.setText(f"{done}/{total} analyseret...")
lbl.setText(f"{done}/{total}{filename}")
lbl.show() lbl.show()
btn.setEnabled(False) btn.setEnabled(False)
def on_finished(count, lib_path): def on_finished(count):
lbl.setText(f"{count} sange scannet") lbl.setText(f"{count} analyseret")
btn.setEnabled(True) btn.setEnabled(True)
self._workers.pop(library_id, None) self._workers.pop(library_id, None)
QTimer.singleShot(300, self._load) 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.progress.connect(on_progress)
worker.finished.connect(on_finished) worker.finished.connect(on_finished)
worker.error.connect(on_error)
self._workers[library_id] = worker self._workers[library_id] = worker
worker.setPriority(QThread.Priority.LowestPriority)
worker.start() worker.start()
worker.setPriority(QThread.Priority.LowestPriority)
# ── Luk ─────────────────────────────────────────────────────────────────── # ── Luk ───────────────────────────────────────────────────────────────────
def _on_close(self):
# Stop alle aktive scannere
for worker in self._workers.values():
worker.cancel()
self.accept()
def closeEvent(self, event): def closeEvent(self, event):
for worker in self._workers.values(): for w in list(self._workers.values()):
worker.cancel() w.cancel()
w.wait(2000) # Vent max 2 sek på at tråden stopper
event.accept() event.accept()

View File

@@ -125,6 +125,12 @@ class LibraryPanel(QWidget):
header.addWidget(lbl) header.addWidget(lbl)
header.addStretch() header.addStretch()
btn_refresh = QPushButton("↻ Opdater")
btn_refresh.setFixedHeight(28)
btn_refresh.setToolTip("Opdater bibliotek fra database")
btn_refresh.clicked.connect(self._refresh_library)
header.addWidget(btn_refresh)
btn_manage = QPushButton("⚙ Mapper") btn_manage = QPushButton("⚙ Mapper")
btn_manage.setFixedHeight(28) btn_manage.setFixedHeight(28)
btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker") btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker")
@@ -376,19 +382,31 @@ class LibraryPanel(QWidget):
self._bpm_worker.done.connect(on_bpm_done) self._bpm_worker.done.connect(on_bpm_done)
self._bpm_worker.start() self._bpm_worker.start()
def _refresh_library(self):
"""Genindlæs bibliotek fra database."""
mw = self.window()
if hasattr(mw, "_reload_library"):
mw._reload_library()
def _manage_libraries(self): def _manage_libraries(self):
from ui.library_manager import LibraryManagerDialog from ui.library_manager import LibraryManagerDialog
from local.local_db import DB_PATH from local.local_db import DB_PATH
dialog = LibraryManagerDialog(db_path=str(DB_PATH), parent=self.window()) dialog = LibraryManagerDialog(db_path=str(DB_PATH), parent=self.window())
dialog.libraries_changed.connect(self._on_libraries_changed) dialog.libraries_changed.connect(self._on_libraries_changed)
dialog.exec() dialog.exec()
# Reload øjeblikkeligt når dialog lukkes
def _on_libraries_changed(self):
"""Kald reload på main_window når biblioteker ændres."""
mw = self.window() mw = self.window()
if hasattr(mw, "_reload_library"): if hasattr(mw, "_reload_library"):
from PyQt6.QtCore import QTimer mw._reload_library()
QTimer.singleShot(500, mw._reload_library) # Start scanning
if hasattr(mw, "start_background_scan"):
QTimer.singleShot(1000, mw.start_background_scan)
def _on_libraries_changed(self):
"""Kaldes ved tilføj/fjern — reload øjeblikkeligt."""
mw = self.window()
if hasattr(mw, "_reload_library"):
mw._reload_library()
def _add_folder(self): def _add_folder(self):
from PyQt6.QtWidgets import QFileDialog from PyQt6.QtWidgets import QFileDialog

View File

@@ -8,8 +8,9 @@ from PyQt6.QtWidgets import (
QSizePolicy, QMenuBar, QMenu, QStatusBar, QFileDialog, QSizePolicy, QMenuBar, QMenu, QStatusBar, QFileDialog,
QMessageBox, QMessageBox,
) )
from PyQt6.QtCore import Qt, QTimer from PyQt6.QtCore import Qt, QTimer, QThread
from PyQt6.QtGui import QAction from PyQt6.QtGui import QAction
from pathlib import Path
from ui.vu_meter import VUMeter from ui.vu_meter import VUMeter
from ui.playlist_panel import PlaylistPanel from ui.playlist_panel import PlaylistPanel
@@ -79,6 +80,7 @@ class MainWindow(QMainWindow):
self._song_ended = False self._song_ended = False
self._demo_active = False self._demo_active = False
self._watcher = None self._watcher = None
self._scan_workers = [] # Hold referencer til aktive scan-tråde
self._scan_worker = None self._scan_worker = None
self._api_url: str | None = None self._api_url: str | None = None
self._api_token: str | None = None self._api_token: str | None = None
@@ -92,6 +94,7 @@ class MainWindow(QMainWindow):
self._connect_player_signals() self._connect_player_signals()
self._library_loaded.connect(self._apply_library) self._library_loaded.connect(self._apply_library)
self._db_ready.connect(self._on_db_ready)
self._build_menu() self._build_menu()
self._build_ui() self._build_ui()
self._build_statusbar() self._build_statusbar()
@@ -383,8 +386,7 @@ class MainWindow(QMainWindow):
try: try:
from local.local_db import init_db from local.local_db import init_db
init_db() init_db()
# Trigger library load via signal self._db_ready.emit()
self._library_loaded.emit([]) # tomt signal = "DB klar, load nu"
except Exception as e: except Exception as e:
pass pass
@@ -432,6 +434,7 @@ class MainWindow(QMainWindow):
# Signal til at opdatere biblioteket fra baggrundstråd # Signal til at opdatere biblioteket fra baggrundstråd
_library_loaded = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(list) _library_loaded = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(list)
_db_ready = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
_file_changed_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal() _file_changed_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
def _reload_library(self): def _reload_library(self):
@@ -483,12 +486,12 @@ class MainWindow(QMainWindow):
except Exception: except Exception:
pass pass
def _apply_library(self, songs: list): def _on_db_ready(self):
if not songs: """DB er initialiseret — indlæs bibliotek og start post-init."""
# Tomt signal = DB er klar, start library load og post-init
self._reload_library() self._reload_library()
self._post_init() self._post_init()
return
def _apply_library(self, songs: list):
self._library_panel.load_songs(songs) self._library_panel.load_songs(songs)
count = len(songs) count = len(songs)
self._set_status( self._set_status(
@@ -496,7 +499,7 @@ class MainWindow(QMainWindow):
) )
def _post_init(self): def _post_init(self):
"""Kør efter DB er initialiseret — gendan state.""" """Kør efter DB er initialiseret — gendan state og start scan."""
try: try:
restored = self._playlist_panel.restore_active_playlist() restored = self._playlist_panel.restore_active_playlist()
if restored: if restored:
@@ -513,11 +516,48 @@ class MainWindow(QMainWindow):
except Exception: except Exception:
pass pass
# Periodisk reload af bibliotek hvert 10. sekund — fanger ny-scannede sange # Scan 30 sek efter opstart — fanger ændringer siden sidst
self._auto_reload_timer = QTimer(self) QTimer.singleShot(30000, self.start_background_scan)
self._auto_reload_timer.setInterval(10000)
self._auto_reload_timer.timeout.connect(self._reload_library) def start_background_scan(self):
self._auto_reload_timer.start() """Start scanning af alle aktive biblioteker i baggrunden."""
try:
import sqlite3
from local.local_db import DB_PATH
from ui.scan_worker import ScanWorker
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
libs = conn.execute(
"SELECT id, path FROM libraries WHERE is_active=1"
).fetchall()
conn.close()
pending = [lib for lib in libs if Path(lib["path"]).exists()]
if not pending:
return
self._set_status("Scanner biblioteker i baggrunden...", 4000)
self._scan_workers = []
finished_count = [0]
def on_one_finished(count, p):
finished_count[0] += 1
self._set_status(f"Scanning færdig — {count} filer", 4000)
# Ryd færdige workers ud
self._scan_workers = [w for w in self._scan_workers
if w.isRunning()]
for lib in pending:
worker = ScanWorker(lib["id"], lib["path"], str(DB_PATH),
overwrite_bpm=False)
worker.finished.connect(on_one_finished)
worker.start()
worker.setPriority(QThread.Priority.LowestPriority)
self._scan_workers.append(worker)
except Exception:
pass
def add_library_path(self, path: str): def add_library_path(self, path: str):
try: try:
@@ -984,9 +1024,15 @@ class MainWindow(QMainWindow):
if self._scan_worker and self._scan_worker.isRunning(): if self._scan_worker and self._scan_worker.isRunning():
self._scan_worker.quit() self._scan_worker.quit()
self._scan_worker.wait(2000) self._scan_worker.wait(2000)
# Stop scan workers
if hasattr(self, "_scan_workers"):
for w in self._scan_workers:
if w.isRunning():
w.cancel()
# Stop watchdog subprocess
if hasattr(self, "_watchdog_proc") and self._watchdog_proc:
try: try:
if self._watcher: self._watchdog_proc.terminate()
self._watcher.stop()
except Exception: except Exception:
pass pass
event.accept() event.accept()