From a9915c0cc93c302bf93bc03eb7dffd1ddfed9f81 Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Sun, 12 Apr 2026 14:29:54 +0200 Subject: [PATCH] =?UTF-8?q?Mappeh=C3=A5ndtering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- linedance-app/local/scanner.py | 7 + linedance-app/local/watchdog_process.py | 183 +++++++++++++++++ linedance-app/ui/bpm_worker.py | 69 +++++++ linedance-app/ui/library_manager.py | 248 +++++++++++++----------- linedance-app/ui/library_panel.py | 28 ++- linedance-app/ui/main_window.py | 88 +++++++-- 6 files changed, 486 insertions(+), 137 deletions(-) create mode 100644 linedance-app/local/watchdog_process.py create mode 100644 linedance-app/ui/bpm_worker.py diff --git a/linedance-app/local/scanner.py b/linedance-app/local/scanner.py index b97f327b..4ffe05bf 100644 --- a/linedance-app/local/scanner.py +++ b/linedance-app/local/scanner.py @@ -66,6 +66,8 @@ def scan_library(library_id: int, library_path: str, db_path: str, total = len(all_files) done = 0 + import time + for fp in all_files: path_str = str(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 if path_str in known and known[path_str] == mtime: done += 1 + # Yield hvert 100. fil så andre tråde kan køre + if done % 100 == 0: + time.sleep(0.005) continue 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}") done += 1 + # Lille pause efter hver scannet fil så GUI ikke hænger + time.sleep(0.02) # Marker manglende filer for path_str in known: diff --git a/linedance-app/local/watchdog_process.py b/linedance-app/local/watchdog_process.py new file mode 100644 index 00000000..b18f458c --- /dev/null +++ b/linedance-app/local/watchdog_process.py @@ -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 +""" +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 ") + sys.exit(1) + run(sys.argv[1]) diff --git a/linedance-app/ui/bpm_worker.py b/linedance-app/ui/bpm_worker.py new file mode 100644 index 00000000..79b44f12 --- /dev/null +++ b/linedance-app/ui/bpm_worker.py @@ -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) diff --git a/linedance-app/ui/library_manager.py b/linedance-app/ui/library_manager.py index dfd0e7e0..b10d6aba 100644 --- a/linedance-app/ui/library_manager.py +++ b/linedance-app/ui/library_manager.py @@ -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() diff --git a/linedance-app/ui/library_panel.py b/linedance-app/ui/library_panel.py index e1b3c742..4766d2e7 100644 --- a/linedance-app/ui/library_panel.py +++ b/linedance-app/ui/library_panel.py @@ -125,6 +125,12 @@ class LibraryPanel(QWidget): header.addWidget(lbl) 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.setFixedHeight(28) 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.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): from ui.library_manager import LibraryManagerDialog from local.local_db import DB_PATH dialog = LibraryManagerDialog(db_path=str(DB_PATH), parent=self.window()) dialog.libraries_changed.connect(self._on_libraries_changed) dialog.exec() - - def _on_libraries_changed(self): - """Kald reload på main_window når biblioteker ændres.""" + # Reload øjeblikkeligt når dialog lukkes mw = self.window() if hasattr(mw, "_reload_library"): - from PyQt6.QtCore import QTimer - QTimer.singleShot(500, mw._reload_library) + 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): from PyQt6.QtWidgets import QFileDialog diff --git a/linedance-app/ui/main_window.py b/linedance-app/ui/main_window.py index efc7a03b..c1ac381b 100644 --- a/linedance-app/ui/main_window.py +++ b/linedance-app/ui/main_window.py @@ -8,8 +8,9 @@ from PyQt6.QtWidgets import ( QSizePolicy, QMenuBar, QMenu, QStatusBar, QFileDialog, QMessageBox, ) -from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtCore import Qt, QTimer, QThread from PyQt6.QtGui import QAction +from pathlib import Path from ui.vu_meter import VUMeter from ui.playlist_panel import PlaylistPanel @@ -78,7 +79,8 @@ class MainWindow(QMainWindow): self._current_idx = -1 self._song_ended = 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._api_url: str | None = None self._api_token: str | None = None @@ -92,6 +94,7 @@ class MainWindow(QMainWindow): self._connect_player_signals() self._library_loaded.connect(self._apply_library) + self._db_ready.connect(self._on_db_ready) self._build_menu() self._build_ui() self._build_statusbar() @@ -383,8 +386,7 @@ class MainWindow(QMainWindow): try: from local.local_db import init_db init_db() - # Trigger library load via signal - self._library_loaded.emit([]) # tomt signal = "DB klar, load nu" + self._db_ready.emit() except Exception as e: pass @@ -431,7 +433,8 @@ class MainWindow(QMainWindow): QTimer.singleShot(200, self._reload_library) # 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() def _reload_library(self): @@ -483,12 +486,12 @@ class MainWindow(QMainWindow): except Exception: pass + def _on_db_ready(self): + """DB er initialiseret — indlæs bibliotek og start post-init.""" + self._reload_library() + self._post_init() + def _apply_library(self, songs: list): - if not songs: - # Tomt signal = DB er klar, start library load og post-init - self._reload_library() - self._post_init() - return self._library_panel.load_songs(songs) count = len(songs) self._set_status( @@ -496,7 +499,7 @@ class MainWindow(QMainWindow): ) def _post_init(self): - """Kør efter DB er initialiseret — gendan state.""" + """Kør efter DB er initialiseret — gendan state og start scan.""" try: restored = self._playlist_panel.restore_active_playlist() if restored: @@ -513,11 +516,48 @@ class MainWindow(QMainWindow): except Exception: pass - # Periodisk reload af bibliotek hvert 10. sekund — fanger ny-scannede sange - self._auto_reload_timer = QTimer(self) - self._auto_reload_timer.setInterval(10000) - self._auto_reload_timer.timeout.connect(self._reload_library) - self._auto_reload_timer.start() + # Scan 30 sek efter opstart — fanger ændringer siden sidst + QTimer.singleShot(30000, self.start_background_scan) + + def start_background_scan(self): + """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): try: @@ -984,9 +1024,15 @@ class MainWindow(QMainWindow): if self._scan_worker and self._scan_worker.isRunning(): self._scan_worker.quit() self._scan_worker.wait(2000) - try: - if self._watcher: - self._watcher.stop() - except Exception: - pass + # 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: + self._watchdog_proc.terminate() + except Exception: + pass event.accept()