From 9d7adf42c13b6a4a13b3b2d8f25ae7002c6aee6f Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Fri, 10 Apr 2026 21:59:36 +0200 Subject: [PATCH] Videre --- linedance-app/build_windows.spec | 63 ++---- .../__pycache__/local_db.cpython-312.pyc | Bin 29095 -> 27731 bytes linedance-app/local/local_db.py | 203 ++++++++---------- .../player/__pycache__/player.cpython-312.pyc | Bin 9430 -> 10510 bytes linedance-app/player/player.py | 27 ++- .../__pycache__/library_panel.cpython-312.pyc | Bin 17867 -> 18441 bytes .../__pycache__/main_window.cpython-312.pyc | Bin 57303 -> 57420 bytes .../playlist_panel.cpython-312.pyc | Bin 31148 -> 31112 bytes 8 files changed, 138 insertions(+), 155 deletions(-) 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 0885929db66d9abbcce24d2b02cd4ac12f29647f..17c6bce07f8d48a244ce93d36fdda66215a75428 100644 GIT binary patch delta 1906 zcmY*YZA?>V6n@XWE!=*$&=y*OUcM<%P!vThpnw_$r_;Cv#c1Wd2v~}is=zRi%pWqH z=*}OV)1fZT#2+rZi+^UK(JW@sWi0BZ#A%lKLzlQUer(fZAEyNxZ1Udsyytn&^PKbE z+s}W6TX(^H!(`Gk`aN8Kue1A|Bj$7n9x41bL$Ps5Nom(`(>~3HRKA#-6+2}d3Urm? zV$s!L*{9u*#;wE=xQ#gJQ*jE#E}p8miEX2LJDq272gL$+Qk)Ts35)40X-f471IdgL zFO9HFzJ#X}>QYh?USr_0xydUlrk}`C2Q{;(!ZVe1E4G+}iv}UB3_@gH&ofCYYig1t z?je=zgn}%%hGgR9@CKeu$~nZ#r9Ivcu~OP4o<|kAJfE@!@mUIqpBHCKikp0j=$y~J zgcsAz*rwb|C|f`Zg-S{y8}DpVo9>~M7)9LIypL`0@-n;McHy4(=om*jxD4PsuRO6 zq7!*?KdsnHH1itrp3Q4xSIq0Ec22+GU9O%Kqgvq;!0ezCKMLfyfo)@4I1PJY5E|L0 zJjV74IyT6n|~MZ)!O;<+p$P3z>bun4S$ z9Xz*(^@%NP4|t6OMXPs&m53Y&2GOPWBn4hu8PnrV{UW?+v*HzfIX>4{px01^iXjJ| zW=ZJtNO;^Z8(SPkd}Z-qiP1{j6ufVfahI_kzc$Xtm&R(GYpTYbv@V$Z_y?_Wb2WCG z%W=%?*DHOIaA0#Ju&oz|v$D1C?JX?T<85<3=&{IBF=eC29TdlG`t`HWA}8V6>_4&9 zp(EQ$vNU3?wH!N~W%#L8#%tC>{M}lB|H@`VP}!{nH+OPbmho~<9;v?VuqRZuWM0QL zW|vx-)dH$Tt^vH@knm}0Y>Y|Go7pZrWH#e=dj+1d+o}Gs-52l5@0yqB=z=3NQDJee zr%wEM&RL84o^!RQ%u69=qO|4u?G5IOfj6 zJFXl&=FUgiog3eSZZGZ4_{80$Hs|=np#f)OAk^y%b?#OIyOglpt0-KFRLcW8c{(>> zd4ocjBZ0}CG7m%oTLZmH=q;}H>4^U>=4nI6OJk->go z(QsoliD`8|X`DZ3oKe;W>Cy{&YIV<=#pINv;gwM-nN( z++24@TKq^d9kz^VkM0>SSTt_--;rwme&J+dZQ-D$@SCH})Sqn_XWgT&>eJ z7_`^)&|pwc6dVTdVOh~axT1Cx%@*K>daU>=z-_g)^cR2!YHitX66B(H!5QLxx8N2$ zR!>#k5MiO(?azqSeo#{ai`9!Y8%0>IrYzYHuu(l;uN9z8J=ORrVo8 zcJy5?z%BKkmg_O-o7Oq-Q2ncQtw^dXw+sm{LA|l%GC-1gU@MAXRz2-w0?6u>j*9}6 psC%~e0r)W*?1u{M>JC9AKIpy)Pw{+cdvcQiwb6Esn%DEb@IS~`9uEKj delta 2567 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 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 659a5c89fcaf410324caa627762e345ab5d26b5c..0e5a562224c59b4fc5c5ae841ecd06d7f0bc1c39 100644 GIT binary patch delta 2933 zcma(TZERat^*#H&#NT$~=g-=)ow#Y7&}42Bwk+M6DoLBPQM%Gd?)kX)eBJl{)YD&b{mSXIaX3s5|2F^i_N%VbX8tsM+x+-|sl;VkXorR@ z;HtHTbDZiiFm_J$8u*+979ruZ8|xA`=Z90_E^xUmb`*EkHy|Dk@=;iGgdIqA8|-i) zBz{r`C{gHR?aV+xb}F< z*LeOmyx*E`jU+0aASw%oR zj)Qd*Q79C%^0NJ*LiS9K_0eJ&-m<^Or{IHl2)^m~%fW_jO;|)O7T0wtYL=;N2yr%m z;8mWHHHBPM)6DKiw4s0Dj@}3bOeU4_{5h;L|oQoC^Bkw$%wI6B7K~>cs?c!k^nD-iMBi@su>%;boiK zw7d_FxCd6^=z#dnxRN^=M}Ova6qnS#BYPRyWy1(&Qcgx0$+i$^Bhb9OMaiidH9wzY z6g)=Rs$Ws}2OFVf!y74+?hW<;QWa9KdwUPM+e(hmL%Qs#`@BIpMBEhwPja`y?G@py zyV{V*Uy& zZrO#(UfUAtyNsOAax#y>K)%}0SENB7DrJv8*a5g6!_e%e307SaR`#5&p1}@yH|B+Z zwMlwN)S!V6!L-v3f5%w%yL}|!$Eu5gB1lM(!0T;Wp+c&Kqz^G8BA*RLGX$ETsKv!v z40e(vB_kUoKn7`VP@Y5ZBp0fNW4i0>F8{qfc<#tXw0}Jcc1H*u zABA_l`_W%+sj|K4cw1h{P$02L|HJ2#@V4aV=ixo659I{kE8TkXG=%lr7%4MFM=??c zZumNQ2A}xuL&1Lk>*BCZsU;4bkO>N%hvxP)&03rwgo{Jp5V*YvJEe`lOEgqH+e)TSq%f9 zq1p_pG4tG+)|3Zgkw^I*a3+#8oH}|0eh}Ga3D?^x{4&zN?;$+4X3j6DwL*>IlHFCo z&0gGWxm3Hg)~t8iNw_E4y<*qft!Bo0&`k4+W}aPQnq}{v@rlggi80NpPEBDQqG(%7Bt~lR>^66LF1W1zaI2N)`$L5vLq54trJVHP*_2V@_iEr zFGc?}@<(^%ytI)RSWgU8J<^7!W6jgC;Tc%-3|txi>GajpCQT2s3d|m6luHdcDIW+vC{dw#@--!JaE4&nsw9sz@i@}TW zt^Cj6t#}u1eh~k3n7Z{$N@Q``rYLDPrMPe+lUqO!*V?ikvAdgb9zq>m{5vq(u?IDM zt7G4ao&}GPnn40b3Cxp1v!b$EN2k(Y_Y$DL3^q!Dp0=7{;aIU)V3V{yKwyRdg^%f3 zmPbr;W-_ydtfI{33WZE2#k1{5)n7{UDC>p;oik!U9Qx>s@M34sZxegZAO4ub=}yor z_S~^q#oiD7@axW`=RR@dj>-$-?oS+o*vnGiO-hGPkSXe{Odx;_h+~fu@DY#@X!fc3 z#UfKD7%MVOTx3)S^jrKY$EZ*18TSv%uKA+8RLDKZ&cKOe+FCI2y_HA)#ueZ~a_HZl C1b8w4 delta 2133 zcma)6Uu;uV7{8~zx9!^Aw%cvDwkzw{MrpUPu?=*%$w1J#5e6tj0pqpoHmsv-=ec(T zY6%cxGLuCgz84Z7AZi3djKRHW;)6b#h$KemjT()H2VaB{jqy+7_uc+m&sDY)ad{BKTeUvq4zpHBE71UIYk24qBbRu*Vz+KG0@cl2 zy9N9H1_hrr*9AADvHrYM!wYNpg0Z=3T@r4v@*2;B6)p6|!r*RG;L)fT`eQAummpa` z+>0r8VQwqFOaZUb0DnnaSp>E?cCEbRIL+7{SaS}n^tc-A9tELzm_##-d3sqB5N=_t9n;zVp1xk}Jzz2b-(`o8khR zaN@$0bR*3Tz75xW5XC!;(^WcstqjX|;If8Fo)hqmJk63Y=u5CL9QADs81xWp@ZCtA z18zqZX}h(eKz-a8gaz3HA*BV9@;C-fh6_rv(OzYc31`KWRNMxi`-hJA9p*N~;=?GW zQf^N1;gk-%{sWhuF;pWvpRrzNeT6?lCbf^GY}QBkC~9@WBbMM|pgnq!G_xo!2;WEB zmc(WMYFlba{KC}=p9H$$_h1vWG{*3M%iaj=Z+vTK{aR@uy{KpEm+%q7TF2F6xG#Du z5wMJpL4VUIg5{bbwj&I#c`rg=q!s+K#N1Z8y?_}}ki9UWbUQRVau&eEi^GgR2z2z3 zUp_CSq^T!(FJhOO*@CMoG4;oZx=~mtQ!zlM6iz-w!cW3t=E3*w#gkfW$zMfsPhdZmQln~kc$$v31PRNZFChB&x(AwOGK$Feal1*q* zk15}Z+iKLF#C1Jtk(LfN52G!^=(yPO-nvZoQXVW6^sJFB z9Bx(M&$b{Ojrri#h7q=jI?--^Tytxlb)AE0-XiwHYW5c;d>&qQPBi4|otjh<4XSq< za77A<8q;Jr-xdl>|HsPj$cvhIhMkknIq*BZaI-Z5{h-sJ6yIR& z+a^mrmzBE$PS^L`r)jN2l6vm>TvE@~r|%0mtp)8;_nO-&_1tX&G1;{zA&smZWTG_o zn^%;2II+$tgeecwtZIcBRplp0NnCT01SY+}pCu6_p}?tRJY+d;#{D0z(fNY5n9J= 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 f714b12b7e5382201cd5118c7685cf3b763d606a..43491e1d94a433b27593c501342eccd2f42ced25 100644 GIT binary patch delta 2102 zcmY*adu&rx7(eH}``Wtov5mpF8yoEI1w+aPmaTihh9Ew`M~ql2w`_A=x14qm=Pk@= zvQ9C!9CcMdiyEC`BFi-LmqDXZV$_yuR;ux>F&eRKXne%Pb2>Zp{P8>A{l4GtJKs4y z-|71g!N~|Lc;95w12R6moE{ieW(s1pLNN!xL?P~2*>KdT$GVCV{73B`rclF0tWDny zB5?2X0E8gUDkUb^2Tn0mPdp6u7?>*_d{!xQvnsd5;QBy_4;C%*0nnIJaMA7tdp7`Z zJINFhLZT)FElV6W&lx09#A~c3l#!qxs(eV&czpdTn<61m6ORE^(4=GF1nY;Im{&n? zWFrSO9$8gUm2jfYTA}gz;8;G9PU8@x>S$8Bi6Xsf$QMmHAw720RoO!Z%|I$Jkd+Ef zFoawfGOA`OuoYbqkf^8`DbfgEtn#NA++4ius8MP-1~eV1Pz;%ZC6z~i^PpR4R69X@T$i3(2j3ztS`}d{i8!(7szlz5jw;wj6O5#E-6lk^R70M9*tuJF(tbN3Tf`JSiK(Ep$>pS4f=oTY^AjGG9Nw?5Gt-*vh(Ro)WbnYJ&R>W}uv4o#1y?2d3xRsiKI!aF`It&Fcul`8Xq zF?GNw7lD!Hv{Lu-?$gUtjcwwZF%G zB%PBIH{6kMds4l3&Gq&td;3$pcPDxV6TZ>J;DLnoAg)_`FVkqBePOKv;oGyxbz3?3 zE-rBl!*}q6qnFqT2ZrZn)s6QvJhDXDreidCy9154?pc9l)Nf}~b*x%n5-J#JB=q+ET85LDHdu+}gR74-TV2FG9If z-$`Q;0_0DCR-Nnr3^(El_M@DR{sL%$!p{`u2?Qhpcjg+{<;raeC9%J)LjOGt9wP83 ze!T6P>R&lQSCKUXooP-gO-iM6(u$OUXBB<-w!U%A-x1$+~=SW%vAs+86TmCn(Gh)9d;0Xp1V#xg~*?GwJ4%K`_VVpvO!g>5~*GqP8orHf-N)O@_?qlymamJu7XX>& z_2vA!?B)u-GHzV}WSVzDj<^426!->IL)Bdbb}1#Ooqn9dzjSYem+|VYd!Py5yS2Tj PkA)49mR|v#bAo>WOam?K delta 1610 zcmYLIeQZ-z6unS0g*aQpq9PDifuP<3%b!F zL=2r8vvH%F0pkcJj?6zsW`dXqi2ooYrn7%+6i6fzj2e@{&_ImFb64ta@_y&MbAIRC zbMHBKU=ZJU4;yae^Bw7%5Y2=Mg#~(WYlBPU^K(^Nf}LO2~p89yFs^LKZO)JB-`k%RXPE51M`ddiZIU68eVzt0ncoH=E&|sWSAm@$$60Ga9akT)<`@eyX3OaZifF+i6JA2^x-9foxq+Tn6>eUZ z^C|_oOgV{Bquv}b*uRugeEU^`|sia*j7 z@I_-Jqkg(UMhpcbftsvvky^$*4G`4;P`S#9!?d%irBSabO}5Th?8BWyog>|2T-s8e zY`^(j(}*$ zNfoqSeOywcxG)Ych^>Uk!6?QdDmUf>$R}id2o2CP%Pq9NzRZ+F5UV*ZElfey7t)ze z>UEQ6>n~w^|Loodyz(vtliLjL0AvKEClVmXK$ZDy7v~8RbfD3Lf2C86FX?_}WH#dD z9$me@Qp|vQj|SG46#oKR7aa8eC4y#xZ*C*Nhj1bo|TMZ$9c(3AL z{{YO<5`f1?(TveNoIjMGHZHquw4`ihW2P}NRp?9`t7b*sZa9iaDG+6exIkp|+xvFJ zYMnb`Ej=VaX23)Y%&zmn^!wiEA+H4pnLqv(<{Jc%)wx+;$8N9JAM4Jjqdk%RzMzuL zz%GT{2Z*WxUUjgwk+Ay6b%1_56mQqE4n&UtJfS~q+*C3n>8B;zlw_NhmQG1ar=`*< zsdSFx^9+x8QKOyJBTe39LsJ(P$!`!u&o#TazY)FDT!&|=wZ#e8-qOfDMw9Qf)L`{h z;E>0Ne$#4S@(A>dy4xT1$6|y$fj?Quo&g9CTU)q)5p}m6v0P#?*BBgS@I8YY^m^Ma zJu91ZF#u|L`wILWeWCsI@_ey$b_Fl48uiT~5D&GB#p>DmB5~QMc@BYixQ-Th7UQe5 e(o>6nr;6uAEYORd4FxarxMI-#Ct`BGAO8X9oud5! diff --git a/linedance-app/ui/__pycache__/main_window.cpython-312.pyc b/linedance-app/ui/__pycache__/main_window.cpython-312.pyc index 95383bf53ec71b111ee75cffc88023ff077fe6d0..196baa3cc9566e935a74857f339230e6784d78fe 100644 GIT binary patch delta 2897 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 diff --git a/linedance-app/ui/__pycache__/playlist_panel.cpython-312.pyc b/linedance-app/ui/__pycache__/playlist_panel.cpython-312.pyc index 48e4c3bd4e0ed9c8bde1f9553ea716e826d9a551..588b14e8966d72e27fc0119987f7fce313524db0 100644 GIT binary patch delta 948 zcmYk4OGuPa6vw~w%{Sv?92hY*D`kRA$MG>fOQoo^vW-TgRuiH!chm`+k^7C~0|OTp zMxwK%EJA08mbP9`TF({R#1y)q|ID*~9kN-L6f6qPla>wSOe;!QZ zCR4N|*6_llXWtFeZ-8lxmIt8#hvh;j#HaFFD8gBJ*puv5SWI_>BsIw_txs{Lvh+U5 zxsh1~(^!UJI{MTZ=g=e7%-FLZ9d)CtZ! zhh*KPebu#C;g06a%9YW+E>b(2^93LYkLAACO_)-(!+FzsY$!}H`^DVnBtOvnw6J@# z)w~>)OLd8obQF4Ut|ER*45t-k+b?f%v=1;)`=wg}JAQ+{1Aqt=Goc9w;06SVCG4({By zVX4ieh=X!lYcNXnd7IUG-q^v9Q%=Wy9XzBdyF8H!orSHHYw%=ci#dESwRuT+g}SOD z<4ST$E3XRa;SFZhIN>Mu*IY0>Bi~z0tt~-TYY(3ro`VWAgu8?o!XLsfyj7dxnk6|T zU^R!|Xk}u3t4H;#{2nFd~o%vse(QhB-VDXn-r) kOkghSO@J3kv=kX!s{5GsvMFeWy6XJTp`>&-Z5CvR3liXkOe zTW@=+w?*Q;YHLfAPqa5nbg;=`z^4vpG>kaNLVPmLIyrdDEGd&-J8PWASeMiZ$5y1n zl#Pab!NWYrd(u0lW{EQglUavk(X~}#{&HVH7y~y>1w$TV&>h$SpUUPbMz89|8QTJ6 zl+Vi0sSD7o-XvT`1Zf{eu$S91m!jIvUQNx|-+m(XXx-_CV+}*ItwU37+MaTdlkQN; zUBW%cU)K8nyLNKfIho&q)s^4*0Iah2B7fU+C3O^xRm)?}2b4JsTdGzYCMrFKTUFV7 zTzg&Ri=PoIOOAfReZl}?5H?o(Q~rH^NWn)iTD>soUofJXYR2^Z38d7!_)pkWe+1W8 zoQoxO4D8O5xfAQf)~EEhgsX&P!XLsfIP9Eden)a2!jckeQ*5Ugi0JA1gX(p1e1I3u zR5Qa{Q}@$*gbZ<;{Y?RPi${G=p}lDD%no3)U`fE3`3y~MbZalSAK03ZXq+7Ca|i|0?1=`M zsE!#CNpjRx)YUthR#w(jW8ZYG$Fb$7TPyTD4aP%*#c6nmh%OjX)pSy_@HL0jE=nIE zD0Ie)q%5Qg;7wZre+SdTb^H_f!|Qmjb~jwhl`}aXnO>iEA?K3Zmj^lR?~{fp3IATE P&ViX7)A>p$?Qr}BU}h$i