Næste version

This commit is contained in:
2026-04-12 10:25:41 +02:00
parent b678787236
commit 57f3c913b4
18 changed files with 2690 additions and 458 deletions

View File

@@ -91,6 +91,7 @@ class MainWindow(QMainWindow):
self._demo_fade_seconds = self._settings.get("demo_fade_seconds", 5)
self._connect_player_signals()
self._library_loaded.connect(self._apply_library)
self._build_menu()
self._build_ui()
self._build_statusbar()
@@ -367,66 +368,60 @@ class MainWindow(QMainWindow):
# ── Lokal DB + scanning ───────────────────────────────────────────────────
def _init_local_db(self):
# Debounce-timer til reload (skal oprettes i GUI-tråden)
self._reload_timer = QTimer(self)
self._reload_timer.setSingleShot(True)
self._reload_timer.setInterval(2000)
self._reload_timer.timeout.connect(self._reload_library)
# Kør init_db i baggrundstråd — blokerer ikke GUI
import threading
threading.Thread(target=self._init_db_background, daemon=True).start()
def _init_db_background(self):
"""Kører i baggrundstråd — initialiserer DB og loader bibliotek."""
try:
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from local.local_db import init_db
from local.file_watcher import get_watcher
init_db()
# Brug et Qt signal til thread-safe reload fra watcher-tråden
from PyQt6.QtCore import QMetaObject, Q_ARG
def on_file_change(event_type, path, song_id):
QTimer.singleShot(0, self._reload_library)
self._watcher = get_watcher(on_change=on_file_change)
self._watcher.start()
# Indlæs hvad vi allerede kender fra SQLite
self._reload_library()
# Gendan sidst aktive danseliste
restored = self._playlist_panel.restore_active_playlist()
# Gendan event-fremgang hvis liste blev gendannet
if restored:
if self._playlist_panel.restore_event_state():
# Indlæs den sang vi var nået til
idx = self._playlist_panel._current_idx
song = self._playlist_panel.get_song(idx)
if song:
self._current_idx = idx
self._load_song(song)
self._set_status(
f"Event genoptaget ved: {song.get('title','')} — tryk ▶ for at fortsætte",
6000,
)
# Kør automatisk scanning ved opstart
self._set_status("Starter scanning af biblioteker...")
QTimer.singleShot(100, self.start_scan)
# Trigger library load via signal
self._library_loaded.emit([]) # tomt signal = "DB klar, load nu"
except Exception as e:
self._set_status(f"DB fejl: {e}")
pass
def start_scan(self):
"""Start fuld scanning af alle biblioteker i baggrundstråd."""
if self._scan_worker and self._scan_worker.isRunning():
return # Scanning kører allerede
def _start_watcher(self):
"""Start fil-watcher i baggrundstråd — blokerer aldrig GUI."""
import threading
def _start():
try:
from local.file_watcher import get_watcher
def on_file_change(event_type, path, song_id):
# QMetaObject.invokeMethod er den korrekte måde at kalde
# en slot fra en ikke-GUI-tråd
from PyQt6.QtCore import QMetaObject, Qt
QMetaObject.invokeMethod(
self._reload_timer, "start",
Qt.ConnectionType.QueuedConnection
)
watcher = get_watcher(on_change=on_file_change)
watcher.start()
self._watcher = watcher # sæt først når den er klar
except Exception:
pass
threading.Thread(target=_start, daemon=True).start()
def start_scan(self):
"""Start fuld scanning af alle biblioteker — watcher kører i egne baggrundstråde."""
if not self._watcher:
self._set_status("Ingen biblioteker at scanne — tilføj en mappe først")
return
self._library_panel.set_scanning(True, "Forbereder scanning...")
self._act_scan.setEnabled(False)
self._scan_worker = ScanWorker(self._watcher, parent=self)
self._scan_worker.status_update.connect(self._on_scan_status)
self._scan_worker.scan_done.connect(self._on_scan_done)
self._scan_worker.start()
self._set_status("Scanner biblioteker i baggrunden...")
self._watcher._full_scan_all()
# Genindlæs bibliotekslisten efter et øjeblik
QTimer.singleShot(3000, self._reload_library)
def _on_scan_status(self, text: str):
self._set_status(text)
@@ -440,20 +435,41 @@ class MainWindow(QMainWindow):
# Genindlæs biblioteket
QTimer.singleShot(200, self._reload_library)
# Signal til at opdatere biblioteket fra baggrundstråd
_library_loaded = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(list)
def _reload_library(self):
"""Hent sange fra DB i baggrundstråd — thread-safe via signal."""
import threading
threading.Thread(target=self._fetch_library, daemon=True).start()
def _fetch_library(self):
"""Kører i baggrundstråd — henter sange og sender til GUI via signal."""
try:
from local.local_db import search_songs, get_db
songs_raw = search_songs("", limit=5000)
import sqlite3
from local.local_db import DB_PATH
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
rows = conn.execute("""
SELECT s.id, s.title, s.artist, s.album, s.bpm,
s.duration_sec, s.local_path, s.file_format,
s.file_missing,
GROUP_CONCAT(d.name, '||') AS dance_names,
GROUP_CONCAT(COALESCE(dl.name,''), '||') AS dance_levels
FROM songs s
LEFT JOIN song_dances sd ON sd.song_id = s.id
LEFT JOIN dances d ON d.id = sd.dance_id
LEFT JOIN dance_levels dl ON dl.id = d.level_id
WHERE s.file_missing = 0
GROUP BY s.id
ORDER BY s.artist, s.title
""").fetchall()
conn.close()
songs = []
for row in songs_raw:
with get_db() as conn:
dances_raw = conn.execute("""
SELECT d.name, dl.name as level_name
FROM song_dances sd
JOIN dances d ON d.id = sd.dance_id
LEFT JOIN dance_levels dl ON dl.id = d.level_id
WHERE sd.song_id=? ORDER BY sd.dance_order
""", (row["id"],)).fetchall()
for row in rows:
dances = row["dance_names"].split("||") if row["dance_names"] else []
levels = row["dance_levels"].split("||") if row["dance_levels"] else []
songs.append({
"id": row["id"],
"title": row["title"],
@@ -464,25 +480,54 @@ class MainWindow(QMainWindow):
"local_path": row["local_path"],
"file_format": row["file_format"],
"file_missing": bool(row["file_missing"]),
"dances": [d["name"] for d in dances_raw],
"dance_levels": [d["level_name"] or "" for d in dances_raw],
"dances": dances,
"dance_levels": levels,
})
self._library_panel.load_songs(songs)
count = len(songs)
self._set_status(f"Bibliotek: {count} sang{'e' if count != 1 else ''}", 3000)
except Exception as e:
self._library_loaded.emit(songs)
except Exception:
pass
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(
f"Bibliotek: {count} sang{'e' if count != 1 else ''}", 3000
)
def _post_init(self):
"""Kør efter DB er initialiseret — gendan state og start watcher."""
try:
restored = self._playlist_panel.restore_active_playlist()
if restored:
if self._playlist_panel.restore_event_state():
idx = self._playlist_panel._current_idx
song = self._playlist_panel.get_song(idx)
if song:
self._current_idx = idx
self._load_song(song)
self._set_status(
f"Event genoptaget ved: {song.get('title','')} — tryk ▶ for at fortsætte",
6000,
)
except Exception:
pass
QTimer.singleShot(5000, self._start_watcher)
QTimer.singleShot(60000, self.start_scan)
def add_library_path(self, path: str):
try:
if not self._watcher:
self._set_status("Watcher ikke klar endnu — prøv igen om et øjeblik", 3000)
return
self._watcher.add_library(path)
self._set_status(f"Tilføjet: {path} — scanner...")
# Genindlæs bibliotekslisten og start scan
QTimer.singleShot(500, self._reload_library)
QTimer.singleShot(1000, self.start_scan)
self._set_status(f"Tilføjet: {path} — scanner i baggrunden...")
# Genindlæs bibliotekslisten efter kort pause
QTimer.singleShot(800, self._reload_library)
except Exception as e:
self._set_status(f"Fejl ved tilføjelse: {e}")
@@ -820,6 +865,10 @@ class MainWindow(QMainWindow):
def _on_library_song_selected(self, song: dict):
self._load_song(song)
# VLC er asynkron — vent kort på at media er klar
QTimer.singleShot(150, self._play_after_load)
def _play_after_load(self):
self._player.play()
self._btn_play.setText("")