Diverse rettelser

This commit is contained in:
2026-04-25 21:28:31 +02:00
parent 8d4c4a81c1
commit 324c94fde2
6 changed files with 132 additions and 44 deletions

View File

@@ -745,3 +745,27 @@ def get_community_alts_for_song(song_id: str) -> list:
ORDER BY cad.avg_rating DESC ORDER BY cad.avg_rating DESC
""", (song_id,)).fetchall() """, (song_id,)).fetchall()
return [dict(r) for r in rows] 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}")

View File

@@ -410,19 +410,21 @@ def read_dances_from_file(path: str | Path) -> list[str]:
return tags.get("dances", []) 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 ─────────────────────────────────────────────────────────────── # ── 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: def analyze_bpm(path: str | Path) -> float | None:
""" """
Analysér BPM fra lydfilen ved hjælp af librosa. Analysér BPM fra lydfilen ved hjælp af librosa.
Returnerer BPM som float eller None ved fejl. Returnerer BPM som float eller None ved fejl.
Tager 2-5 sekunder per sang — kør i baggrundstråd. 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: try:
import librosa import librosa
# Indlæs kun de første 60 sekunder for hastighed # Indlæs kun de første 60 sekunder for hastighed

View File

@@ -549,9 +549,11 @@ class LibraryPanel(QWidget):
self._bpm_worker.start() self._bpm_worker.start()
def _refresh_library(self): def _refresh_library(self):
"""Genindlæs bibliotek fra database.""" """Opdater fil-tilgængelighed og genindlæs bibliotek."""
mw = self.window() 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() mw._reload_library()
def _manage_libraries(self): def _manage_libraries(self):

View File

@@ -379,6 +379,12 @@ class MainWindow(QMainWindow):
self._sync_periodic.timeout.connect(self._manual_sync) self._sync_periodic.timeout.connect(self._manual_sync)
self._sync_periodic.start() 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 = LibraryPanel()
self._library_panel.set_preview_player(self._preview_player) self._library_panel.set_preview_player(self._preview_player)
@@ -438,9 +444,30 @@ class MainWindow(QMainWindow):
from local.local_db import init_db from local.local_db import init_db
init_db() init_db()
self._db_ready.emit() 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: except Exception as e:
pass 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): def _start_watcher(self):
"""Start fil-watcher i baggrundstråd — blokerer aldrig GUI.""" """Start fil-watcher i baggrundstråd — blokerer aldrig GUI."""
import threading import threading
@@ -655,6 +682,8 @@ class MainWindow(QMainWindow):
def on_one_finished(count, p): def on_one_finished(count, p):
finished_count[0] += 1 finished_count[0] += 1
self._set_status(f"Scanning færdig — {count} filer", 4000) 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 # Ryd færdige workers ud
self._scan_workers = [w for w in self._scan_workers self._scan_workers = [w for w in self._scan_workers
if w.isRunning()] if w.isRunning()]
@@ -663,6 +692,9 @@ class MainWindow(QMainWindow):
worker = ScanWorker(lib["id"], lib["path"], str(DB_PATH), worker = ScanWorker(lib["id"], lib["path"], str(DB_PATH),
overwrite_bpm=False) overwrite_bpm=False)
worker.finished.connect(on_one_finished) worker.finished.connect(on_one_finished)
worker.batch_ready.connect(
lambda n: QTimer.singleShot(0, self._reload_library)
)
worker.start() worker.start()
worker.setPriority(QThread.Priority.LowestPriority) worker.setPriority(QThread.Priority.LowestPriority)
self._scan_workers.append(worker) self._scan_workers.append(worker)
@@ -804,11 +836,9 @@ class MainWindow(QMainWindow):
threading.Thread(target=_run, daemon=True).start() threading.Thread(target=_run, daemon=True).start()
def _on_playlist_changed(self): 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"): if hasattr(self, "_sync_debounce"):
self._sync_debounce.start() self._sync_debounce.start()
# Opdater storskærm med det samme
self._sync_event_status_to_playlist()
def _auto_sync(self): def _auto_sync(self):
"""Kør sync hvis vi er online — kaldes af debounce-timer.""" """Kør sync hvis vi er online — kaldes af debounce-timer."""

View File

@@ -2,6 +2,45 @@
playlist_panel.py — Danseliste med Ny/Gem/Hent knapper, autogem og event-overblik. 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 ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QListWidget, QListWidgetItem, QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
QLabel, QHBoxLayout, QPushButton, QMenu, QAbstractItemView, QLabel, QHBoxLayout, QPushButton, QMenu, QAbstractItemView,
@@ -799,30 +838,20 @@ class PlaylistPanel(QWidget):
self._save_alt_dance_rating(song, chosen, rating) self._save_alt_dance_rating(song, chosen, rating)
def _sync_alt_dance_to_db(self, idx: int, song: dict, alt_dance: str): 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.""" """Gem alt_dance_override til playlist_songs."""
pl_ids = [] if not self._named_playlist_id:
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:
return return
try: try:
import logging
from local.local_db import get_db from local.local_db import get_db
with get_db() as conn: with get_db() as conn:
for pl_id in pl_ids:
conn.execute( conn.execute(
"UPDATE playlist_songs SET alt_dance_override=? " "UPDATE playlist_songs SET alt_dance_override=? "
"WHERE playlist_id=? AND position=?", "WHERE playlist_id=? AND position=?",
(alt_dance, pl_id, idx + 1) (alt_dance, self._named_playlist_id, idx + 1)
)
logging.getLogger(__name__).info(
f"alt_dance_override='{alt_dance}' gemt på pos {idx+1} i {pl_id}"
) )
except Exception as e: except Exception as e:
import logging 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): def _save_alt_dance_rating(self, song: dict, dance_name: str, rating: int):
"""Gem brugerens rating på en alternativ-dans.""" """Gem brugerens rating på en alternativ-dans."""
@@ -1148,7 +1177,8 @@ class PlaylistPanel(QWidget):
for song in songs: for song in songs:
path = song.get("local_path", "") path = song.get("local_path", "")
if path and Path(path).exists(): 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 continue
# Forsøg auto-match via titel+artist # Forsøg auto-match via titel+artist
@@ -1185,9 +1215,9 @@ class PlaylistPanel(QWidget):
with get_db() as conn: with get_db() as conn:
for song in self._songs: for song in self._songs:
path = song.get("local_path", "") 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(): if path and Path(path).exists():
song["availability"] = "green" song["availability"] = "green" if _is_local_path(path) else "yellow"
song["file_missing"] = False song["file_missing"] = False
# Opdater files tabellen # Opdater files tabellen
conn.execute( conn.execute(
@@ -1211,7 +1241,7 @@ class PlaylistPanel(QWidget):
if match and Path(match["local_path"]).exists(): if match and Path(match["local_path"]).exists():
song["local_path"] = match["local_path"] song["local_path"] = match["local_path"]
song["file_id"] = match["file_id"] 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 song["file_missing"] = False
# Opdater playlist_songs til at pege på den fundne fil # Opdater playlist_songs til at pege på den fundne fil
if self._named_playlist_id: if self._named_playlist_id:
@@ -1439,7 +1469,6 @@ class PlaylistPanel(QWidget):
if not active: if not active:
dances = song.get("dances", []) dances = song.get("dances", [])
active = dances[0] if dances else "— ingen dans —" active = dances[0] if dances else "— ingen dans —"
alt = song.get("alt_dance", "")
ws_tag = " 🎓" if song.get("is_workshop") else "" ws_tag = " 🎓" if song.get("is_workshop") else ""
# Tilgængeligheds-dot til højre — kun hvis tjekket (ikke yellow) # 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_color = {"green": "#27ae60", "red": "#e74c3c"}.get(avail, None)
avail_tip = {"green": "Tilgængelig lokalt", "red": "Ikke fundet lokalt"}.get(avail, "") avail_tip = {"green": "Tilgængelig lokalt", "red": "Ikke fundet lokalt"}.get(avail, "")
dance_line = f"{active}{ws_tag}" text = (f"{i+1:>2}. {active}{ws_tag}\n"
if alt:
dance_line += f" / {alt}"
text = (f"{i+1:>2}. {dance_line}\n"
f" {song.get('title','')} · {song.get('artist','')}") f" {song.get('title','')} · {song.get('artist','')}")
item = QListWidgetItem(f"{icon} {text}") item = QListWidgetItem(f"{icon} {text}")
item.setData(Qt.ItemDataRole.UserRole, i) item.setData(Qt.ItemDataRole.UserRole, i)

View File

@@ -9,6 +9,7 @@ class ScanWorker(QThread):
progress = pyqtSignal(int, int, str) # done, total, filename progress = pyqtSignal(int, int, str) # done, total, filename
finished = pyqtSignal(int, str) # antal, library_path finished = pyqtSignal(int, str) # antal, library_path
error = pyqtSignal(str) error = pyqtSignal(str)
batch_ready = pyqtSignal(int) # antal sange scannet så langt
def __init__(self, library_id: int, library_path: str, def __init__(self, library_id: int, library_path: str,
db_path: str, overwrite_bpm: bool = False): db_path: str, overwrite_bpm: bool = False):
@@ -26,11 +27,15 @@ class ScanWorker(QThread):
def run(self): def run(self):
try: try:
from local.scanner import scan_library from local.scanner import scan_library
self._batch_count = 0
def on_progress(done, total, filename): def on_progress(done, total, filename):
if self.isInterruptionRequested(): if self.isInterruptionRequested():
raise InterruptedError() raise InterruptedError()
self.progress.emit(done, total, filename) 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( count = scan_library(
self._library_id, self._library_id,