diff --git a/linedance-app/build_windows.spec b/linedance-app/build_windows.spec index a1832064..d98dd1d2 100644 --- a/linedance-app/build_windows.spec +++ b/linedance-app/build_windows.spec @@ -1,21 +1,21 @@ # -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_all, collect_submodules block_cipher = None +# Saml ALT fra PyQt6 inkl. plugins og DLL-filer +pyqt6_datas, pyqt6_binaries, pyqt6_hiddenimports = collect_all('PyQt6') + a = Analysis( ['main.py'], pathex=['.'], - binaries=[], - datas=[], - hiddenimports=[ - # PyQt6 — skal alle med eksplicit - 'PyQt6', + binaries=pyqt6_binaries, + datas=pyqt6_datas, + hiddenimports=pyqt6_hiddenimports + [ + 'PyQt6.sip', 'PyQt6.QtCore', 'PyQt6.QtGui', 'PyQt6.QtWidgets', - 'PyQt6.QtNetwork', - 'PyQt6.sip', - 'PyQt6.QtPrintSupport', # UI moduler 'ui.main_window', 'ui.playlist_panel', @@ -29,50 +29,23 @@ a = Analysis( 'ui.settings_dialog', 'ui.playlist_manager', 'ui.next_up_bar', - # Player + # Player + local 'player.player', - # Local 'local.local_db', 'local.tag_reader', 'local.file_watcher', # Biblioteker - 'mutagen', - 'mutagen.mp3', - 'mutagen.id3', - 'mutagen.flac', - 'mutagen.mp4', - 'mutagen.oggvorbis', - 'mutagen.ogg', - 'mutagen.wave', - 'mutagen.aiff', - 'mutagen.asf', - 'watchdog', - 'watchdog.observers', - 'watchdog.observers.fsevents', - 'watchdog.observers.inotify', + 'mutagen', 'mutagen.mp3', 'mutagen.id3', 'mutagen.flac', + 'mutagen.mp4', 'mutagen.oggvorbis', 'mutagen.ogg', + 'mutagen.wave', 'mutagen.aiff', 'mutagen.asf', + 'watchdog', 'watchdog.observers', 'watchdog.events', 'watchdog.observers.winapi', - 'watchdog.events', - 'watchdog.tricks', - 'vlc', - 'sqlite3', - 'json', - 'threading', - 'pathlib', - 'urllib.request', - 'urllib.parse', + 'vlc', 'sqlite3', ], hookspath=[], hooksconfig={}, runtime_hooks=[], - excludes=[ - 'tkinter', - 'matplotlib', - 'pandas', - 'scipy', - 'PIL', - 'IPython', - 'jupyter', - ], + excludes=['tkinter', 'matplotlib', 'pandas', 'scipy', 'IPython'], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, @@ -90,8 +63,8 @@ exe = EXE( debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, - console=True, # Slå til så du kan se fejlbeskeder + upx=False, # UPX kan give problemer med PyQt6 DLL-filer + console=True, # Vis fejlbeskeder disable_windowed_traceback=False, target_arch=None, codesign_identity=None, @@ -105,7 +78,7 @@ coll = COLLECT( a.zipfiles, a.datas, strip=False, - upx=True, + upx=False, upx_exclude=[], name='LineDancePlayer', ) diff --git a/linedance-app/local/__pycache__/local_db.cpython-312.pyc b/linedance-app/local/__pycache__/local_db.cpython-312.pyc index 0885929d..17c6bce0 100644 Binary files a/linedance-app/local/__pycache__/local_db.cpython-312.pyc and b/linedance-app/local/__pycache__/local_db.cpython-312.pyc differ diff --git a/linedance-app/local/local_db.py b/linedance-app/local/local_db.py index 04ba88d1..464dc378 100644 --- a/linedance-app/local/local_db.py +++ b/linedance-app/local/local_db.py @@ -44,102 +44,56 @@ def get_db(): def init_db(): """Opret alle tabeller hvis de ikke findes.""" - with get_db() as conn: - conn.executescript(""" - -- Musikbiblioteker der overvåges - CREATE TABLE IF NOT EXISTS libraries ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - path TEXT NOT NULL UNIQUE, - is_active INTEGER NOT NULL DEFAULT 1, - last_full_scan TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); + conn = _get_conn() - -- Sange høstet fra filsystemet - CREATE TABLE IF NOT EXISTS songs ( - id TEXT PRIMARY KEY, - library_id INTEGER REFERENCES libraries(id), - local_path TEXT NOT NULL UNIQUE, - title TEXT NOT NULL DEFAULT '', - artist TEXT NOT NULL DEFAULT '', - album TEXT NOT NULL DEFAULT '', - bpm INTEGER NOT NULL DEFAULT 0, - duration_sec INTEGER NOT NULL DEFAULT 0, - file_format TEXT NOT NULL DEFAULT '', - file_modified_at TEXT NOT NULL, - file_missing INTEGER NOT NULL DEFAULT 0, - api_song_id TEXT, -- NULL hvis ikke synkroniseret - last_synced_at TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Danse knyttet til en sang (kun MP3 kan skrive tags) - CREATE TABLE IF NOT EXISTS song_dances ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - song_id TEXT NOT NULL REFERENCES songs(id) ON DELETE CASCADE, - dance_name TEXT NOT NULL, - dance_order INTEGER NOT NULL DEFAULT 1 - ); - - -- Alternativ-danse relationer (kun online hvis logget ind, men caches lokalt) - CREATE TABLE IF NOT EXISTS dance_alternatives ( - id TEXT PRIMARY KEY, - song_dance_id INTEGER NOT NULL REFERENCES song_dances(id) ON DELETE CASCADE, - alt_song_dance_id INTEGER NOT NULL REFERENCES song_dances(id) ON DELETE CASCADE, - note TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE(song_dance_id, alt_song_dance_id) - ); - - -- Lokale afspilningslister (offline-projekter) - CREATE TABLE IF NOT EXISTS playlists ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - description TEXT NOT NULL DEFAULT '', - api_project_id TEXT, -- NULL hvis ikke synkroniseret - last_synced_at TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Sange i en afspilningsliste - CREATE TABLE IF NOT EXISTS playlist_songs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - playlist_id INTEGER NOT NULL REFERENCES playlists(id) ON DELETE CASCADE, - song_id TEXT NOT NULL REFERENCES songs(id), - position INTEGER NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', -- pending|playing|played|skipped - UNIQUE(playlist_id, position) - ); - - -- Synkroniseringskø — ændringer der venter på at komme online - CREATE TABLE IF NOT EXISTS sync_queue ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - entity_type TEXT NOT NULL, -- 'song'|'playlist'|'playlist_song' - entity_id TEXT NOT NULL, - action TEXT NOT NULL, -- 'create'|'update'|'delete' - payload TEXT NOT NULL, -- JSON - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - -- Indekser til hurtig søgning - CREATE INDEX IF NOT EXISTS idx_songs_title ON songs(title); - CREATE INDEX IF NOT EXISTS idx_songs_artist ON songs(artist); - CREATE INDEX IF NOT EXISTS idx_songs_missing ON songs(file_missing); - CREATE INDEX IF NOT EXISTS idx_songs_library ON songs(library_id); - CREATE INDEX IF NOT EXISTS idx_song_dances ON song_dances(song_id); - """) - - # Migration: tilføj tabeller der måske mangler i ældre databaser - _run_migrations(conn) - - -def _run_migrations(conn): - """Kør migrations sikkert — CREATE IF NOT EXISTS er idempotent.""" + # Brug executescript direkte (ikke via context manager) da det auto-committer conn.executescript(""" + CREATE TABLE IF NOT EXISTS libraries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + path TEXT NOT NULL UNIQUE, + is_active INTEGER NOT NULL DEFAULT 1, + last_full_scan TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS songs ( + id TEXT PRIMARY KEY, + library_id INTEGER REFERENCES libraries(id), + local_path TEXT NOT NULL UNIQUE, + title TEXT NOT NULL DEFAULT '', + artist TEXT NOT NULL DEFAULT '', + album TEXT NOT NULL DEFAULT '', + bpm INTEGER NOT NULL DEFAULT 0, + duration_sec INTEGER NOT NULL DEFAULT 0, + file_format TEXT NOT NULL DEFAULT '', + file_modified_at TEXT NOT NULL, + file_missing INTEGER NOT NULL DEFAULT 0, + extra_tags TEXT NOT NULL DEFAULT '{}', + api_song_id TEXT, + last_synced_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS dance_levels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sort_order INTEGER NOT NULL, + name TEXT NOT NULL UNIQUE, + description TEXT NOT NULL DEFAULT '', + synced_at TEXT + ); + + CREATE TABLE IF NOT EXISTS song_dances ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + song_id TEXT NOT NULL REFERENCES songs(id) ON DELETE CASCADE, + dance_name TEXT NOT NULL, + dance_order INTEGER NOT NULL DEFAULT 1, + level_id INTEGER REFERENCES dance_levels(id) + ); + CREATE TABLE IF NOT EXISTS dance_alternatives ( id TEXT PRIMARY KEY, song_dance_id INTEGER NOT NULL REFERENCES song_dances(id) ON DELETE CASCADE, - alt_dance_name TEXT NOT NULL, + alt_dance_name TEXT NOT NULL DEFAULT '', level_id INTEGER REFERENCES dance_levels(id), note TEXT NOT NULL DEFAULT '', source TEXT NOT NULL DEFAULT 'local', @@ -147,11 +101,6 @@ def _run_migrations(conn): created_at TEXT NOT NULL DEFAULT (datetime('now')) ); - CREATE TABLE IF NOT EXISTS event_state ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS dance_names ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE COLLATE NOCASE, @@ -160,16 +109,46 @@ def _run_migrations(conn): synced_at TEXT ); - CREATE TABLE IF NOT EXISTS dance_levels ( + CREATE TABLE IF NOT EXISTS playlists ( id INTEGER PRIMARY KEY AUTOINCREMENT, - sort_order INTEGER NOT NULL, - name TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', - synced_at TEXT + api_project_id TEXT, + last_synced_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) ); + + CREATE TABLE IF NOT EXISTS playlist_songs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + playlist_id INTEGER NOT NULL REFERENCES playlists(id) ON DELETE CASCADE, + song_id TEXT NOT NULL REFERENCES songs(id), + position INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + UNIQUE(playlist_id, position) + ); + + CREATE TABLE IF NOT EXISTS sync_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entity_type TEXT NOT NULL, + entity_id TEXT NOT NULL, + action TEXT NOT NULL, + payload TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS event_state ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_songs_title ON songs(title); + CREATE INDEX IF NOT EXISTS idx_songs_artist ON songs(artist); + CREATE INDEX IF NOT EXISTS idx_songs_missing ON songs(file_missing); + CREATE INDEX IF NOT EXISTS idx_songs_library ON songs(library_id); + CREATE INDEX IF NOT EXISTS idx_song_dances ON song_dances(song_id); """) - # Tilføj kolonner der måske mangler i ældre databaser + # Kør migrations for ældre databaser (each separately) migrations = [ "ALTER TABLE songs ADD COLUMN extra_tags TEXT NOT NULL DEFAULT '{}'", "ALTER TABLE song_dances ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)", @@ -181,27 +160,35 @@ def _run_migrations(conn): for sql in migrations: try: conn.execute(sql) + conn.commit() except Exception: - pass # kolonnen eksisterer allerede + pass - # Indlæs standard-niveauer hvis tabellen er tom + # Seed standard-niveauer — KUN hvis tabellen er tom count = conn.execute("SELECT COUNT(*) FROM dance_levels").fetchone()[0] if count == 0: defaults = [ - (1, "Begynder", "Passer til alle"), - (2, "Let øvet", "Lidt erfaring kræves"), - (3, "Øvet", "Kræver regelmæssig træning"), - (4, "Erfaren", "For dedikerede dansere"), - (5, "Ekspert", "Konkurrenceniveau"), + (1, "Begynder", "Passer til alle"), + (2, "Let øvet", "Lidt erfaring kræves"), + (3, "Øvet", "Kræver regelmæssig træning"), + (4, "Erfaren", "For dedikerede dansere"), + (5, "Ekspert", "Konkurrenceniveau"), ] conn.executemany( "INSERT OR IGNORE INTO dance_levels (sort_order, name, description) VALUES (?,?,?)", defaults ) + conn.commit() + print(f"Dans-niveauer seedet: {len(defaults)} niveauer") + else: + print(f"Dans-niveauer: {count} niveauer i databasen") + + # ── Biblioteker ─────────────────────────────────────────────────────────────── + def add_library(path: str) -> int: with get_db() as conn: cur = conn.execute( diff --git a/linedance-app/player/__pycache__/player.cpython-312.pyc b/linedance-app/player/__pycache__/player.cpython-312.pyc index 659a5c89..0e5a5622 100644 Binary files a/linedance-app/player/__pycache__/player.cpython-312.pyc and b/linedance-app/player/__pycache__/player.cpython-312.pyc differ diff --git a/linedance-app/player/player.py b/linedance-app/player/player.py index 2fec70d1..cba6d2fe 100644 --- a/linedance-app/player/player.py +++ b/linedance-app/player/player.py @@ -33,6 +33,7 @@ class Player(QObject): self._duration: int = 0 self._demo_mode = False self._demo_stop_sec = 10 + self._demo_fading = False self._volume = 78 if VLC_AVAILABLE: @@ -78,11 +79,13 @@ class Player(QObject): self.state_changed.emit("playing") def play_demo(self, stop_at_sec: int = 10): - """Afspil fra start og stop automatisk ved stop_at_sec.""" + """Afspil fra start og stop automatisk ved stop_at_sec med 2 sek fade-out.""" self._demo_mode = True self._demo_stop_sec = stop_at_sec + self._demo_fading = False if VLC_AVAILABLE and self._media_player: self._media_player.set_time(0) + self._media_player.audio_set_volume(self._volume) self._media_player.play() self._poll_timer.start() self.state_changed.emit("playing") @@ -94,7 +97,9 @@ class Player(QObject): def stop(self): self._demo_mode = False + self._demo_fading = False if VLC_AVAILABLE and self._media_player: + self._media_player.audio_set_volume(self._volume) self._media_player.stop() self._poll_timer.stop() self.position_changed.emit(0.0) @@ -138,15 +143,33 @@ class Player(QObject): self.position_changed.emit(pos) self.time_changed.emit(cur, self._duration) - # Demo-stop + # Demo fade-out og stop if self._demo_mode and cur >= self._demo_stop_sec: + # Færdig — gendan volumen og stop + if VLC_AVAILABLE and self._media_player: + self._media_player.audio_set_volume(self._volume) self.stop() self._demo_mode = False + self._demo_fading = False self.position_changed.emit(0.0) self.time_changed.emit(0, self._duration) self.state_changed.emit("demo_ended") return + # Demo fade-out — de sidste 2 sekunder + FADE_SEC = 2.0 + if self._demo_mode and VLC_AVAILABLE and self._media_player: + secs_left = self._demo_stop_sec - cur + if secs_left <= FADE_SEC and secs_left > 0: + # Fade fra fuld volumen til 0 over FADE_SEC sekunder + fade_fraction = secs_left / FADE_SEC # 1.0 → 0.0 + faded_vol = int(self._volume * fade_fraction) + self._media_player.audio_set_volume(max(0, faded_vol)) + self._demo_fading = True + elif not self._demo_fading: + # Ikke i fade-zone endnu — sørg for fuld volumen + self._media_player.audio_set_volume(self._volume) + # VU-meter: brug VLC's audio-amplitude hvis tilgængelig, ellers simulér if VLC_AVAILABLE and self._media_player and self._media_player.is_playing(): # VLC eksponerer ikke amplitude direkte — vi bruger en blød simulation diff --git a/linedance-app/ui/__pycache__/library_panel.cpython-312.pyc b/linedance-app/ui/__pycache__/library_panel.cpython-312.pyc index f714b12b..43491e1d 100644 Binary files a/linedance-app/ui/__pycache__/library_panel.cpython-312.pyc and b/linedance-app/ui/__pycache__/library_panel.cpython-312.pyc differ diff --git a/linedance-app/ui/__pycache__/main_window.cpython-312.pyc b/linedance-app/ui/__pycache__/main_window.cpython-312.pyc index 95383bf5..196baa3c 100644 Binary files a/linedance-app/ui/__pycache__/main_window.cpython-312.pyc and b/linedance-app/ui/__pycache__/main_window.cpython-312.pyc differ diff --git a/linedance-app/ui/__pycache__/playlist_panel.cpython-312.pyc b/linedance-app/ui/__pycache__/playlist_panel.cpython-312.pyc index 48e4c3bd..588b14e8 100644 Binary files a/linedance-app/ui/__pycache__/playlist_panel.cpython-312.pyc and b/linedance-app/ui/__pycache__/playlist_panel.cpython-312.pyc differ