Diverse rettelser
This commit is contained in:
@@ -744,4 +744,28 @@ def get_community_alts_for_song(song_id: str) -> list:
|
|||||||
WHERE cad.song_id = ?
|
WHERE cad.song_id = ?
|
||||||
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}")
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -580,4 +582,4 @@ class LibraryPanel(QWidget):
|
|||||||
if folder:
|
if folder:
|
||||||
mw = self.window()
|
mw = self.window()
|
||||||
if hasattr(mw, "add_library_path"):
|
if hasattr(mw, "add_library_path"):
|
||||||
mw.add_library_path(folder)
|
mw.add_library_path(folder)
|
||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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, self._named_playlist_id, idx + 1)
|
||||||
(alt_dance, pl_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)
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ from PyQt6.QtCore import QThread, pyqtSignal
|
|||||||
|
|
||||||
|
|
||||||
class ScanWorker(QThread):
|
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,
|
||||||
@@ -44,4 +49,4 @@ class ScanWorker(QThread):
|
|||||||
except InterruptedError:
|
except InterruptedError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error.emit(str(e))
|
self.error.emit(str(e))
|
||||||
Reference in New Issue
Block a user