Med install

This commit is contained in:
2026-04-10 15:09:37 +02:00
parent e5a4711004
commit 0f54f6d908
8 changed files with 229 additions and 88 deletions

View File

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

View File

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

30
linedance-app/build_linux.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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