From 324c94fde291db1cf174b5671eac725bdf84a00f Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Sat, 25 Apr 2026 21:28:31 +0200 Subject: [PATCH] Diverse rettelser --- linedance-app/local/local_db.py | 26 +++++++++- linedance-app/local/tag_reader.py | 12 +++-- linedance-app/ui/library_panel.py | 8 +-- linedance-app/ui/main_window.py | 36 +++++++++++-- linedance-app/ui/playlist_panel.py | 81 +++++++++++++++++++----------- linedance-app/ui/scan_worker.py | 13 +++-- 6 files changed, 132 insertions(+), 44 deletions(-) diff --git a/linedance-app/local/local_db.py b/linedance-app/local/local_db.py index 0abc1981..41b495c7 100644 --- a/linedance-app/local/local_db.py +++ b/linedance-app/local/local_db.py @@ -744,4 +744,28 @@ def get_community_alts_for_song(song_id: str) -> list: WHERE cad.song_id = ? ORDER BY cad.avg_rating DESC """, (song_id,)).fetchall() - return [dict(r) for r in rows] \ No newline at end of file + return [dict(r) for r in rows] + +def refresh_file_availability(): + """Tjek hurtigt om alle kendte filer stadig eksisterer — opdater file_missing. + Køres ved opstart i baggrundstråd.""" + from pathlib import Path + try: + with get_db() as conn: + rows = conn.execute( + "SELECT id, local_path, file_missing FROM files" + ).fetchall() + for row in rows: + try: + exists = Path(row["local_path"]).exists() + expected = 0 if exists else 1 + if row["file_missing"] != expected: + conn.execute( + "UPDATE files SET file_missing=? WHERE id=?", + (expected, row["id"]) + ) + except Exception: + pass + logger.info("Fil-tilgængelighed opdateret") + except Exception as e: + logger.warning(f"refresh_file_availability fejl: {e}") \ No newline at end of file diff --git a/linedance-app/local/tag_reader.py b/linedance-app/local/tag_reader.py index 1c0bce61..1f22cacc 100644 --- a/linedance-app/local/tag_reader.py +++ b/linedance-app/local/tag_reader.py @@ -410,19 +410,21 @@ def read_dances_from_file(path: str | Path) -> list[str]: return tags.get("dances", []) -def write_dance_to_file(path: str | Path, dances: list[str]) -> bool: - """Alias for write_dances — skriv danse-liste til fil.""" - return write_dances(path, dances) - - # ── BPM-analyse ─────────────────────────────────────────────────────────────── +# Formater der ikke understøttes af librosa uden ffmpeg +_BPM_UNSUPPORTED = {".wma", ".ac3", ".dts", ".ra", ".rm", ".rmvb"} + def analyze_bpm(path: str | Path) -> float | None: """ Analysér BPM fra lydfilen ved hjælp af librosa. Returnerer BPM som float eller None ved fejl. Tager 2-5 sekunder per sang — kør i baggrundstråd. """ + suffix = Path(path).suffix.lower() + if suffix in _BPM_UNSUPPORTED: + logger.debug(f"BPM-analyse ikke understøttet for {suffix}: {path}") + return None try: import librosa # Indlæs kun de første 60 sekunder for hastighed diff --git a/linedance-app/ui/library_panel.py b/linedance-app/ui/library_panel.py index 548957c3..4eb41764 100644 --- a/linedance-app/ui/library_panel.py +++ b/linedance-app/ui/library_panel.py @@ -549,9 +549,11 @@ class LibraryPanel(QWidget): self._bpm_worker.start() def _refresh_library(self): - """Genindlæs bibliotek fra database.""" + """Opdater fil-tilgængelighed og genindlæs bibliotek.""" mw = self.window() - if hasattr(mw, "_reload_library"): + if hasattr(mw, "_run_availability_check"): + mw._run_availability_check() + elif hasattr(mw, "_reload_library"): mw._reload_library() def _manage_libraries(self): @@ -580,4 +582,4 @@ class LibraryPanel(QWidget): if folder: mw = self.window() if hasattr(mw, "add_library_path"): - mw.add_library_path(folder) + mw.add_library_path(folder) \ No newline at end of file diff --git a/linedance-app/ui/main_window.py b/linedance-app/ui/main_window.py index 3d4dc7b8..71f3240b 100644 --- a/linedance-app/ui/main_window.py +++ b/linedance-app/ui/main_window.py @@ -379,6 +379,12 @@ class MainWindow(QMainWindow): self._sync_periodic.timeout.connect(self._manual_sync) self._sync_periodic.start() + # Periodisk fil-tilgængeligheds-check — opdager USB tilslutning/fjernelse + self._availability_timer = QTimer(self) + self._availability_timer.setInterval(30 * 1000) # hvert 30. sekund + self._availability_timer.timeout.connect(self._run_availability_check) + self._availability_timer.start() + self._library_panel = LibraryPanel() self._library_panel.set_preview_player(self._preview_player) @@ -438,9 +444,30 @@ class MainWindow(QMainWindow): from local.local_db import init_db init_db() self._db_ready.emit() + # Tjek fil-tilgængelighed i separat tråd + import threading + threading.Thread( + target=self._refresh_availability, daemon=True + ).start() except Exception as e: pass + def _refresh_availability(self): + """Opdater file_missing for alle kendte filer og genindlæs biblioteket.""" + try: + from local.local_db import refresh_file_availability + refresh_file_availability() + QTimer.singleShot(0, self._reload_library) + except Exception: + pass + + def _run_availability_check(self): + """Kør periodisk fil-check i baggrundstråd — opdager USB til/fra.""" + import threading + threading.Thread( + target=self._refresh_availability, daemon=True + ).start() + def _start_watcher(self): """Start fil-watcher i baggrundstråd — blokerer aldrig GUI.""" import threading @@ -655,6 +682,8 @@ class MainWindow(QMainWindow): def on_one_finished(count, p): finished_count[0] += 1 self._set_status(f"Scanning færdig — {count} filer", 4000) + # Genindlæs biblioteket når scanning er færdig + QTimer.singleShot(200, self._reload_library) # Ryd færdige workers ud self._scan_workers = [w for w in self._scan_workers if w.isRunning()] @@ -663,6 +692,9 @@ class MainWindow(QMainWindow): worker = ScanWorker(lib["id"], lib["path"], str(DB_PATH), overwrite_bpm=False) worker.finished.connect(on_one_finished) + worker.batch_ready.connect( + lambda n: QTimer.singleShot(0, self._reload_library) + ) worker.start() worker.setPriority(QThread.Priority.LowestPriority) self._scan_workers.append(worker) @@ -804,11 +836,9 @@ class MainWindow(QMainWindow): threading.Thread(target=_run, daemon=True).start() def _on_playlist_changed(self): - """Danseliste ændret — start debounce-timer til auto-sync og opdater live-status.""" + """Danseliste ændret — start debounce-timer til auto-sync.""" if hasattr(self, "_sync_debounce"): self._sync_debounce.start() - # Opdater storskærm med det samme - self._sync_event_status_to_playlist() def _auto_sync(self): """Kør sync hvis vi er online — kaldes af debounce-timer.""" diff --git a/linedance-app/ui/playlist_panel.py b/linedance-app/ui/playlist_panel.py index ddfea53a..1b4f3b57 100644 --- a/linedance-app/ui/playlist_panel.py +++ b/linedance-app/ui/playlist_panel.py @@ -2,6 +2,45 @@ playlist_panel.py — Danseliste med Ny/Gem/Hent knapper, autogem og event-overblik. """ +import sys as _sys +from pathlib import Path as _Path + + +def _is_local_path(path: str) -> bool: + """Returnerer True hvis stien er på et lokalt/USB-drev, False hvis netværk.""" + try: + if _sys.platform == "win32": + import ctypes + drive = path[:3] + # GetDriveType: 2=Removable, 3=Fixed, 4=Remote(netværk), 5=CDROM, 6=RAMdisk + dtype = ctypes.windll.kernel32.GetDriveTypeW(drive) + return dtype not in (4,) # 4 = netværksdrev + else: + # Linux/Mac — tjek /proc/mounts + NETWORK_FS = { + "nfs", "nfs4", "cifs", "smb", "smb2", "smb3", + "fuse.sshfs", "fuse.gvfsd-fuse", "fuse.s3fs", + "davfs", "ncpfs", "afs", "glusterfs", "fuse.glusterfs", + } + try: + with open("/proc/mounts") as f: + mounts = [] + for line in f: + parts = line.split() + if len(parts) >= 3: + mounts.append((parts[1], parts[2])) + # Find længste matchende mount-punkt + mounts.sort(key=lambda x: len(x[0]), reverse=True) + for mount_point, fs_type in mounts: + if path.startswith(mount_point): + return fs_type not in NETWORK_FS + except Exception: + pass + return True # Antag lokal + except Exception: + return True # Antag lokal ved fejl + + from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QListWidget, QListWidgetItem, QLabel, QHBoxLayout, QPushButton, QMenu, QAbstractItemView, @@ -799,30 +838,20 @@ class PlaylistPanel(QWidget): self._save_alt_dance_rating(song, chosen, rating) def _sync_alt_dance_to_db(self, idx: int, song: dict, alt_dance: str): - """Gem alt_dance_override til playlist_songs — både aktiv og navngiven liste.""" - pl_ids = [] - if self._active_playlist_id: - pl_ids.append(self._active_playlist_id) - if self._named_playlist_id and self._named_playlist_id not in pl_ids: - pl_ids.append(self._named_playlist_id) - if not pl_ids: + """Gem alt_dance_override til playlist_songs.""" + if not self._named_playlist_id: return try: - import logging from local.local_db import get_db with get_db() as conn: - for pl_id in pl_ids: - conn.execute( - "UPDATE playlist_songs SET alt_dance_override=? " - "WHERE playlist_id=? AND position=?", - (alt_dance, pl_id, idx + 1) - ) - logging.getLogger(__name__).info( - f"alt_dance_override='{alt_dance}' gemt på pos {idx+1} i {pl_id}" - ) + conn.execute( + "UPDATE playlist_songs SET alt_dance_override=? " + "WHERE playlist_id=? AND position=?", + (alt_dance, self._named_playlist_id, idx + 1) + ) except Exception as e: import logging - logging.getLogger(__name__).warning(f"alt_dance_to_db fejl: {e}", exc_info=True) + logging.getLogger(__name__).warning(f"alt_dance_to_db fejl: {e}") def _save_alt_dance_rating(self, song: dict, dance_name: str, rating: int): """Gem brugerens rating på en alternativ-dans.""" @@ -1148,7 +1177,8 @@ class PlaylistPanel(QWidget): for song in songs: path = song.get("local_path", "") if path and Path(path).exists(): - song["availability"] = "green" + # Grøn = lokal, Gul = netværk men tilgængeligt + song["availability"] = "green" if _is_local_path(path) else "yellow" continue # Forsøg auto-match via titel+artist @@ -1185,9 +1215,9 @@ class PlaylistPanel(QWidget): with get_db() as conn: for song in self._songs: path = song.get("local_path", "") - # Grøn — filen eksisterer lokalt + # Grøn = lokal, Gul = netværk men tilgængeligt if path and Path(path).exists(): - song["availability"] = "green" + song["availability"] = "green" if _is_local_path(path) else "yellow" song["file_missing"] = False # Opdater files tabellen conn.execute( @@ -1211,7 +1241,7 @@ class PlaylistPanel(QWidget): if match and Path(match["local_path"]).exists(): song["local_path"] = match["local_path"] song["file_id"] = match["file_id"] - song["availability"] = "green" + song["availability"] = "green" if _is_local_path(match["local_path"]) else "yellow" song["file_missing"] = False # Opdater playlist_songs til at pege på den fundne fil if self._named_playlist_id: @@ -1439,7 +1469,6 @@ class PlaylistPanel(QWidget): if not active: dances = song.get("dances", []) active = dances[0] if dances else "— ingen dans —" - alt = song.get("alt_dance", "") ws_tag = " 🎓" if song.get("is_workshop") else "" # Tilgængeligheds-dot til højre — kun hvis tjekket (ikke yellow) @@ -1447,11 +1476,7 @@ class PlaylistPanel(QWidget): avail_color = {"green": "#27ae60", "red": "#e74c3c"}.get(avail, None) avail_tip = {"green": "Tilgængelig lokalt", "red": "Ikke fundet lokalt"}.get(avail, "") - dance_line = f"{active}{ws_tag}" - if alt: - dance_line += f" / {alt}" - - text = (f"{i+1:>2}. {dance_line}\n" + text = (f"{i+1:>2}. {active}{ws_tag}\n" f" {song.get('title','—')} · {song.get('artist','')}") item = QListWidgetItem(f"{icon} {text}") item.setData(Qt.ItemDataRole.UserRole, i) diff --git a/linedance-app/ui/scan_worker.py b/linedance-app/ui/scan_worker.py index 2b09d37a..3df92c03 100644 --- a/linedance-app/ui/scan_worker.py +++ b/linedance-app/ui/scan_worker.py @@ -6,9 +6,10 @@ from PyQt6.QtCore import QThread, pyqtSignal class ScanWorker(QThread): - progress = pyqtSignal(int, int, str) # done, total, filename - finished = pyqtSignal(int, str) # antal, library_path - error = pyqtSignal(str) + progress = pyqtSignal(int, int, str) # done, total, filename + finished = pyqtSignal(int, str) # antal, library_path + error = pyqtSignal(str) + batch_ready = pyqtSignal(int) # antal sange scannet så langt def __init__(self, library_id: int, library_path: str, db_path: str, overwrite_bpm: bool = False): @@ -26,11 +27,15 @@ class ScanWorker(QThread): def run(self): try: from local.scanner import scan_library + self._batch_count = 0 def on_progress(done, total, filename): if self.isInterruptionRequested(): raise InterruptedError() self.progress.emit(done, total, filename) + self._batch_count += 1 + if self._batch_count % 50 == 0: + self.batch_ready.emit(self._batch_count) count = scan_library( self._library_id, @@ -44,4 +49,4 @@ class ScanWorker(QThread): except InterruptedError: pass except Exception as e: - self.error.emit(str(e)) + self.error.emit(str(e)) \ No newline at end of file