From 0f54f6d908bc80d15ce67169f7e54996a2dd769d Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Fri, 10 Apr 2026 15:09:37 +0200 Subject: [PATCH] Med install --- linedance-app/BUILD_VEJLEDNING.md | 47 ++++++++++++++++++ linedance-app/build.bat | 76 ++++++++++-------------------- linedance-app/build_linux.sh | 30 ++++++++++++ linedance-app/build_windows.spec | 66 ++++++++++++++++++++++++++ linedance-app/ui/library_panel.py | 30 ++++++++---- linedance-app/ui/main_window.py | 19 ++++---- linedance-app/ui/playlist_panel.py | 8 ++-- linedance-app/ui/tag_editor.py | 41 +++++++++------- 8 files changed, 229 insertions(+), 88 deletions(-) create mode 100644 linedance-app/BUILD_VEJLEDNING.md create mode 100755 linedance-app/build_linux.sh create mode 100644 linedance-app/build_windows.spec diff --git a/linedance-app/BUILD_VEJLEDNING.md b/linedance-app/BUILD_VEJLEDNING.md new file mode 100644 index 00000000..e22b5a92 --- /dev/null +++ b/linedance-app/BUILD_VEJLEDNING.md @@ -0,0 +1,47 @@ +# Byg LineDance Player til Windows .exe + +## Krav + +1. **Python 3.11+** installeret +2. **VLC** installeret (skal også være på den maskine der kører .exe) +3. Alle Python-pakker installeret (`pip install -r requirements.txt`) + +## Bygge på Windows + +```cmd +cd linedance-app +build.bat +``` + +Det færdige program ligger i `dist\LineDancePlayer\LineDancePlayer.exe` + +## Bygge på Linux (til Linux) + +```bash +cd linedance-app +./build_linux.sh +``` + +## Distribuere til andre + +Kopiér hele `dist\LineDancePlayer\` mappen — IKKE kun .exe filen! +Mappen indeholder alle nødvendige DLL-filer og biblioteker. + +Modtageren skal stadig have **VLC installeret**: +- Windows: https://www.videolan.org/vlc/ +- Linux: `sudo apt install vlc` + +## Hvis VLC ikke kan findes + +PyInstaller kan ikke automatisk inkludere VLC da det er et system-program. +Alternativt kan du kopiere `libvlc.dll` og `libvlccore.dll` fra +`C:\Program Files\VideoLAN\VLC\` ind i `dist\LineDancePlayer\`-mappen. + +## Fejlsøgning + +Hvis .exe crasher uden fejlbesked, byg med `console=True` i spec-filen +og kør fra kommandoprompten for at se fejlbeskeder. + +## Størrelse + +Den færdige mappe er typisk 80-150 MB med PyQt6. diff --git a/linedance-app/build.bat b/linedance-app/build.bat index c3b050b9..69ebca24 100644 --- a/linedance-app/build.bat +++ b/linedance-app/build.bat @@ -1,68 +1,44 @@ @echo off -echo ================================================ -echo LineDance Player - Byg EXE -echo ================================================ +echo === LineDance Player - Windows Build === echo. -REM Tjek at vi er i det rigtige bibliotek -if not exist main.py ( - echo FEJL: Kør build.bat fra LinedanceAfspiller\linedance-app mappen - pause - exit /b 1 +REM Aktiver venv hvis den eksisterer +if exist "venv\Scripts\activate.bat" ( + call venv\Scripts\activate.bat +) else ( + echo ADVARSEL: venv ikke fundet - bruger system Python ) -REM Aktiver venv -if not exist venv\Scripts\activate.bat ( - echo Opretter virtuelt miljø... - python -m venv venv -) -call venv\Scripts\activate.bat - -REM Installer/opdater pakker -echo Installerer pakker... -pip install -r requirements.txt --quiet -pip install pyinstaller --quiet - -REM Tjek VLC -if not exist "C:\Program Files\VideoLAN\VLC\libvlc.dll" ( - if not exist "C:\Program Files (x86)\VideoLAN\VLC\libvlc.dll" ( - echo. - echo ADVARSEL: VLC ser ikke ud til at vaere installeret! - echo Download VLC fra: https://www.videolan.org/vlc/ - echo Vaelg 64-bit versionen. - echo. - pause - exit /b 1 - ) +REM Installer PyInstaller hvis ikke installeret +pip show pyinstaller >nul 2>&1 +if errorlevel 1 ( + echo Installerer PyInstaller... + pip install pyinstaller ) -REM Ryd gamle build-filer -echo Rydder gamle build-filer... -if exist build rmdir /s /q build -if exist dist rmdir /s /q dist +REM Ryd tidligere build +if exist "dist\LineDancePlayer" rmdir /s /q "dist\LineDancePlayer" +if exist "build\LineDancePlayer" rmdir /s /q "build\LineDancePlayer" -REM Byg EXE +echo Bygger LineDance Player... +echo Dette tager 1-3 minutter... echo. -echo Bygger LineDancePlayer.exe ... -echo (Dette tager typisk 1-3 minutter) -echo. -pyinstaller LineDancePlayer.spec -if %ERRORLEVEL% neq 0 ( +pyinstaller build_windows.spec --clean + +if errorlevel 1 ( echo. - echo FEJL under build! Se fejlbesked ovenfor. + echo FEJL: Build mislykkedes! pause exit /b 1 ) echo. -echo ================================================ -echo BUILD FAERDIG! -echo Filen ligger i: dist\LineDancePlayer.exe -echo ================================================ +echo === BUILD FAERDIG === +echo. +echo Programmet ligger i: dist\LineDancePlayer\LineDancePlayer.exe +echo. +echo HUSK: VLC skal stadig vaere installeret paa maskinen! +echo Download VLC fra: https://www.videolan.org/vlc/ echo. - -REM Vis filstoerrelse -for %%A in (dist\LineDancePlayer.exe) do echo Filstoerrelse: %%~zA bytes - pause diff --git a/linedance-app/build_linux.sh b/linedance-app/build_linux.sh new file mode 100755 index 00000000..1df1469b --- /dev/null +++ b/linedance-app/build_linux.sh @@ -0,0 +1,30 @@ +#!/bin/bash +echo "=== LineDance Player - Linux Build ===" +echo + +# Aktiver venv +source venv/bin/activate 2>/dev/null || echo "ADVARSEL: venv ikke aktiveret" + +# Installer PyInstaller +pip show pyinstaller > /dev/null 2>&1 || pip install pyinstaller + +# Ryd tidligere build +rm -rf dist/LineDancePlayer build/LineDancePlayer + +echo "Bygger LineDance Player..." +echo "Dette tager 1-3 minutter..." +echo + +pyinstaller build_windows.spec --clean + +if [ $? -eq 0 ]; then + echo + echo "=== BUILD FÆRDIG ===" + echo "Programmet ligger i: dist/LineDancePlayer/LineDancePlayer" + echo + echo "HUSK: VLC skal stadig være installeret på maskinen!" + echo " sudo apt install vlc" +else + echo "FEJL: Build mislykkedes!" + exit 1 +fi diff --git a/linedance-app/build_windows.spec b/linedance-app/build_windows.spec new file mode 100644 index 00000000..e20bc70f --- /dev/null +++ b/linedance-app/build_windows.spec @@ -0,0 +1,66 @@ +# -*- mode: python ; coding: utf-8 -*- +# PyInstaller spec-fil til LineDance Player + +block_cipher = None + +a = Analysis( + ['main.py'], + pathex=['.'], + binaries=[], + datas=[], + hiddenimports=[ + 'PyQt6.QtCore', + 'PyQt6.QtGui', + 'PyQt6.QtWidgets', + 'mutagen', + 'mutagen.mp3', + 'mutagen.id3', + 'mutagen.flac', + 'mutagen.mp4', + 'mutagen.oggvorbis', + 'watchdog', + 'watchdog.observers', + 'watchdog.events', + 'vlc', + 'sqlite3', + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=['tkinter', 'matplotlib', 'numpy', 'pandas'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='LineDancePlayer', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, # Ingen sort konsol-vindue + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=None, # Tilføj .ico fil her hvis du har et ikon +) + +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='LineDancePlayer', +) diff --git a/linedance-app/ui/library_panel.py b/linedance-app/ui/library_panel.py index ab3721ff..62f6fa63 100644 --- a/linedance-app/ui/library_panel.py +++ b/linedance-app/ui/library_panel.py @@ -169,10 +169,20 @@ class LibraryPanel(QWidget): q = self._search.text().strip().lower() for song in self._filtered: dances = song.get("dances", []) - dance_str = " · " + " / ".join(dances) if dances else "" + dance_levels = song.get("dance_levels", []) missing = song.get("file_missing", False) + + # Byg dans-streng med niveau hvis tilgængeligt + dance_parts = [] + for i, d in enumerate(dances): + lvl = dance_levels[i] if i < len(dance_levels) else "" + dance_parts.append(f"{d} / {lvl}" if lvl else d) + dance_str = " · " + " | ".join(dance_parts) if dance_parts else "" + line1 = ("⚠ " if missing else "") + song.get("title", "—") - line2 = f" {song.get('artist','—')} · {song.get('bpm',0)} BPM · {song.get('file_format','').upper()}{dance_str}" + bpm = song.get("bpm", 0) + bpm_str = f"{bpm} BPM" if bpm else "? BPM" + line2 = f" {song.get('artist','—')} · {bpm_str} · {song.get('file_format','').upper()}{dance_str}" item = QListWidgetItem(f"{line1}\n{line2}") item.setData(Qt.ItemDataRole.UserRole, song) if missing: @@ -237,12 +247,16 @@ class LibraryPanel(QWidget): self.done.emit(bpm) self._bpm_worker = BpmWorker(path, song_id) - self._bpm_worker.done.connect( - lambda bpm: ( - self._do_search(), - print(f"BPM analyseret: {bpm}") - ) - ) + + def on_bpm_done(bpm): + # Opdater sangen i _all_songs listen direkte + for s in self._all_songs: + if s.get("id") == song_id: + s["bpm"] = int(round(bpm)) + break + self._do_search() + + self._bpm_worker.done.connect(on_bpm_done) self._bpm_worker.start() def _manage_libraries(self): diff --git a/linedance-app/ui/main_window.py b/linedance-app/ui/main_window.py index 7937cbac..e5c829cb 100644 --- a/linedance-app/ui/main_window.py +++ b/linedance-app/ui/main_window.py @@ -439,9 +439,11 @@ class MainWindow(QMainWindow): songs = [] for row in songs_raw: with get_db() as conn: - dances = conn.execute( - "SELECT dance_name FROM song_dances " - "WHERE song_id=? ORDER BY dance_order", + dances_raw = conn.execute( + "SELECT sd.dance_name, dl.name as level_name " + "FROM song_dances sd " + "LEFT JOIN dance_levels dl ON dl.id = sd.level_id " + "WHERE sd.song_id=? ORDER BY sd.dance_order", (row["id"],) ).fetchall() songs.append({ @@ -454,7 +456,8 @@ class MainWindow(QMainWindow): "local_path": row["local_path"], "file_format": row["file_format"], "file_missing": bool(row["file_missing"]), - "dances": [d["dance_name"] for d in dances], + "dances": [d["dance_name"] for d in dances_raw], + "dance_levels": [d["level_name"] or "" for d in dances_raw], }) self._library_panel.load_songs(songs) count = len(songs) @@ -836,15 +839,13 @@ class MainWindow(QMainWindow): self._vu.reset() # Markér den afspillede sang - prev_idx = self._current_idx - self._playlist_panel.mark_played(prev_idx) + self._playlist_panel.mark_played(self._current_idx) # Synkroniser event-status til den gemte navngivne liste self._sync_event_status_to_playlist() - # Find næste afspilbare sang — fra 0 hvis ingen sang var i gang - search_from = max(0, prev_idx + 1) - ni = self._playlist_panel.next_playable_idx(search_from) + # Find første ikke-afspillede og ikke-skippede sang fra TOPPEN + ni = self._playlist_panel.next_playable_idx() next_song = self._playlist_panel.get_song(ni) if ni is not None else None if next_song: self._current_idx = ni diff --git a/linedance-app/ui/playlist_panel.py b/linedance-app/ui/playlist_panel.py index 63da0629..3e378989 100644 --- a/linedance-app/ui/playlist_panel.py +++ b/linedance-app/ui/playlist_panel.py @@ -215,7 +215,7 @@ class PlaylistPanel(QWidget): self._trigger_autosave() # Find første afspilbare sang og udsend signal så afspilleren opdateres - ni = self.next_playable_idx(0) + ni = self.next_playable_idx() if ni is not None: self._current_idx = ni self._refresh() @@ -253,9 +253,9 @@ class PlaylistPanel(QWidget): print(f"Event-state gendan fejl: {e}") return False - def next_playable_idx(self, from_idx: int) -> int | None: - """Find næste sang der ikke er 'skipped' eller 'played' fra from_idx.""" - for i in range(from_idx, len(self._songs)): + def next_playable_idx(self) -> int | None: + """Find første sang fra toppen der ikke er 'skipped' eller 'played'.""" + for i in range(len(self._songs)): if self._statuses[i] not in ("skipped", "played"): return i return None diff --git a/linedance-app/ui/tag_editor.py b/linedance-app/ui/tag_editor.py index fbee58a6..07e9d88d 100644 --- a/linedance-app/ui/tag_editor.py +++ b/linedance-app/ui/tag_editor.py @@ -391,6 +391,7 @@ class TagEditorDialog(QDialog): dances = [(r.get_name(), r.get_level_id()) for r in self._my_dance_rows if r.get_name()] + dance_ids = [] with get_db() as conn: # Slet eksisterende danse og alternativer old_dances = conn.execute( @@ -400,28 +401,34 @@ class TagEditorDialog(QDialog): conn.execute("DELETE FROM dance_alternatives WHERE song_dance_id=?", (od["id"],)) conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,)) - # Indsæt nye danse - dance_ids = [] + # Indsæt nye danse og hent IDs for i, (name, level_id) in enumerate(dances, start=1): - cur = conn.execute( - "INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) VALUES (?,?,?,?)", + conn.execute( + "INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) " + "VALUES (?,?,?,?)", (song_id, name, i, level_id) ) - dance_ids.append(cur.lastrowid) + new_id = conn.execute( + "SELECT id FROM song_dances WHERE song_id=? AND dance_order=?", + (song_id, i) + ).fetchone()["id"] + dance_ids.append(new_id) register_dance_name(name) - # Indsæt alternativer (knyttet til første dans hvis flere) - if dance_ids and self._my_alt_rows: - first_dance_id = dance_ids[0] - for row in self._my_alt_rows: - name = row.get_name() - if name: - add_alternative( - first_dance_id, name, - level_id=row.get_level_id(), - note=row.get_note(), - source="local", - ) + # Indsæt alternativer knyttet til første dans + if dance_ids and self._my_alt_rows: + first_dance_id = dance_ids[0] + for row in self._my_alt_rows: + name = row.get_name() + if name: + import uuid as _uuid + conn.execute(""" + INSERT INTO dance_alternatives + (id, song_dance_id, alt_dance_name, level_id, note, source) + VALUES (?,?,?,?,?,'local') + """, (str(_uuid.uuid4()), first_dance_id, + name, row.get_level_id(), row.get_note())) + register_dance_name(name) # Skriv til fil if local_path and can_write_dances(local_path):