This commit is contained in:
2026-04-10 21:59:36 +02:00
parent de04ba84eb
commit 9d7adf42c1
8 changed files with 138 additions and 155 deletions

View File

@@ -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',
)

View File

@@ -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(

View File

@@ -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