Videre
This commit is contained in:
@@ -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',
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -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(
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user