Med install
This commit is contained in:
47
linedance-app/BUILD_VEJLEDNING.md
Normal file
47
linedance-app/BUILD_VEJLEDNING.md
Normal 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.
|
||||
@@ -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
30
linedance-app/build_linux.sh
Executable 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
|
||||
66
linedance-app/build_windows.spec
Normal file
66
linedance-app/build_windows.spec
Normal 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',
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user