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 off
|
||||||
echo ================================================
|
echo === LineDance Player - Windows Build ===
|
||||||
echo LineDance Player - Byg EXE
|
|
||||||
echo ================================================
|
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Tjek at vi er i det rigtige bibliotek
|
REM Aktiver venv hvis den eksisterer
|
||||||
if not exist main.py (
|
if exist "venv\Scripts\activate.bat" (
|
||||||
echo FEJL: Kør build.bat fra LinedanceAfspiller\linedance-app mappen
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Aktiver venv
|
|
||||||
if not exist venv\Scripts\activate.bat (
|
|
||||||
echo Opretter virtuelt miljø...
|
|
||||||
python -m venv venv
|
|
||||||
)
|
|
||||||
call venv\Scripts\activate.bat
|
call venv\Scripts\activate.bat
|
||||||
|
) else (
|
||||||
|
echo ADVARSEL: venv ikke fundet - bruger system Python
|
||||||
|
)
|
||||||
|
|
||||||
REM Installer/opdater pakker
|
REM Installer PyInstaller hvis ikke installeret
|
||||||
echo Installerer pakker...
|
pip show pyinstaller >nul 2>&1
|
||||||
pip install -r requirements.txt --quiet
|
if errorlevel 1 (
|
||||||
pip install pyinstaller --quiet
|
echo Installerer PyInstaller...
|
||||||
|
pip install pyinstaller
|
||||||
|
)
|
||||||
|
|
||||||
REM Tjek VLC
|
REM Ryd tidligere build
|
||||||
if not exist "C:\Program Files\VideoLAN\VLC\libvlc.dll" (
|
if exist "dist\LineDancePlayer" rmdir /s /q "dist\LineDancePlayer"
|
||||||
if not exist "C:\Program Files (x86)\VideoLAN\VLC\libvlc.dll" (
|
if exist "build\LineDancePlayer" rmdir /s /q "build\LineDancePlayer"
|
||||||
|
|
||||||
|
echo Bygger LineDance Player...
|
||||||
|
echo Dette tager 1-3 minutter...
|
||||||
echo.
|
echo.
|
||||||
echo ADVARSEL: VLC ser ikke ud til at vaere installeret!
|
|
||||||
|
pyinstaller build_windows.spec --clean
|
||||||
|
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo FEJL: Build mislykkedes!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
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 Download VLC fra: https://www.videolan.org/vlc/
|
||||||
echo Vaelg 64-bit versionen.
|
|
||||||
echo.
|
echo.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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 Byg EXE
|
|
||||||
echo.
|
|
||||||
echo Bygger LineDancePlayer.exe ...
|
|
||||||
echo (Dette tager typisk 1-3 minutter)
|
|
||||||
echo.
|
|
||||||
pyinstaller LineDancePlayer.spec
|
|
||||||
|
|
||||||
if %ERRORLEVEL% neq 0 (
|
|
||||||
echo.
|
|
||||||
echo FEJL under build! Se fejlbesked ovenfor.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ================================================
|
|
||||||
echo BUILD FAERDIG!
|
|
||||||
echo Filen ligger i: dist\LineDancePlayer.exe
|
|
||||||
echo ================================================
|
|
||||||
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()
|
q = self._search.text().strip().lower()
|
||||||
for song in self._filtered:
|
for song in self._filtered:
|
||||||
dances = song.get("dances", [])
|
dances = song.get("dances", [])
|
||||||
dance_str = " · " + " / ".join(dances) if dances else ""
|
dance_levels = song.get("dance_levels", [])
|
||||||
missing = song.get("file_missing", False)
|
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", "—")
|
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 = QListWidgetItem(f"{line1}\n{line2}")
|
||||||
item.setData(Qt.ItemDataRole.UserRole, song)
|
item.setData(Qt.ItemDataRole.UserRole, song)
|
||||||
if missing:
|
if missing:
|
||||||
@@ -237,12 +247,16 @@ class LibraryPanel(QWidget):
|
|||||||
self.done.emit(bpm)
|
self.done.emit(bpm)
|
||||||
|
|
||||||
self._bpm_worker = BpmWorker(path, song_id)
|
self._bpm_worker = BpmWorker(path, song_id)
|
||||||
self._bpm_worker.done.connect(
|
|
||||||
lambda bpm: (
|
def on_bpm_done(bpm):
|
||||||
self._do_search(),
|
# Opdater sangen i _all_songs listen direkte
|
||||||
print(f"BPM analyseret: {bpm}")
|
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()
|
self._bpm_worker.start()
|
||||||
|
|
||||||
def _manage_libraries(self):
|
def _manage_libraries(self):
|
||||||
|
|||||||
@@ -439,9 +439,11 @@ class MainWindow(QMainWindow):
|
|||||||
songs = []
|
songs = []
|
||||||
for row in songs_raw:
|
for row in songs_raw:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
dances = conn.execute(
|
dances_raw = conn.execute(
|
||||||
"SELECT dance_name FROM song_dances "
|
"SELECT sd.dance_name, dl.name as level_name "
|
||||||
"WHERE song_id=? ORDER BY dance_order",
|
"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"],)
|
(row["id"],)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
songs.append({
|
songs.append({
|
||||||
@@ -454,7 +456,8 @@ class MainWindow(QMainWindow):
|
|||||||
"local_path": row["local_path"],
|
"local_path": row["local_path"],
|
||||||
"file_format": row["file_format"],
|
"file_format": row["file_format"],
|
||||||
"file_missing": bool(row["file_missing"]),
|
"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)
|
self._library_panel.load_songs(songs)
|
||||||
count = len(songs)
|
count = len(songs)
|
||||||
@@ -836,15 +839,13 @@ class MainWindow(QMainWindow):
|
|||||||
self._vu.reset()
|
self._vu.reset()
|
||||||
|
|
||||||
# Markér den afspillede sang
|
# Markér den afspillede sang
|
||||||
prev_idx = self._current_idx
|
self._playlist_panel.mark_played(self._current_idx)
|
||||||
self._playlist_panel.mark_played(prev_idx)
|
|
||||||
|
|
||||||
# Synkroniser event-status til den gemte navngivne liste
|
# Synkroniser event-status til den gemte navngivne liste
|
||||||
self._sync_event_status_to_playlist()
|
self._sync_event_status_to_playlist()
|
||||||
|
|
||||||
# Find næste afspilbare sang — fra 0 hvis ingen sang var i gang
|
# Find første ikke-afspillede og ikke-skippede sang fra TOPPEN
|
||||||
search_from = max(0, prev_idx + 1)
|
ni = self._playlist_panel.next_playable_idx()
|
||||||
ni = self._playlist_panel.next_playable_idx(search_from)
|
|
||||||
next_song = self._playlist_panel.get_song(ni) if ni is not None else None
|
next_song = self._playlist_panel.get_song(ni) if ni is not None else None
|
||||||
if next_song:
|
if next_song:
|
||||||
self._current_idx = ni
|
self._current_idx = ni
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class PlaylistPanel(QWidget):
|
|||||||
self._trigger_autosave()
|
self._trigger_autosave()
|
||||||
|
|
||||||
# Find første afspilbare sang og udsend signal så afspilleren opdateres
|
# 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:
|
if ni is not None:
|
||||||
self._current_idx = ni
|
self._current_idx = ni
|
||||||
self._refresh()
|
self._refresh()
|
||||||
@@ -253,9 +253,9 @@ class PlaylistPanel(QWidget):
|
|||||||
print(f"Event-state gendan fejl: {e}")
|
print(f"Event-state gendan fejl: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def next_playable_idx(self, from_idx: int) -> int | None:
|
def next_playable_idx(self) -> int | None:
|
||||||
"""Find næste sang der ikke er 'skipped' eller 'played' fra from_idx."""
|
"""Find første sang fra toppen der ikke er 'skipped' eller 'played'."""
|
||||||
for i in range(from_idx, len(self._songs)):
|
for i in range(len(self._songs)):
|
||||||
if self._statuses[i] not in ("skipped", "played"):
|
if self._statuses[i] not in ("skipped", "played"):
|
||||||
return i
|
return i
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ class TagEditorDialog(QDialog):
|
|||||||
dances = [(r.get_name(), r.get_level_id())
|
dances = [(r.get_name(), r.get_level_id())
|
||||||
for r in self._my_dance_rows if r.get_name()]
|
for r in self._my_dance_rows if r.get_name()]
|
||||||
|
|
||||||
|
dance_ids = []
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
# Slet eksisterende danse og alternativer
|
# Slet eksisterende danse og alternativer
|
||||||
old_dances = conn.execute(
|
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 dance_alternatives WHERE song_dance_id=?", (od["id"],))
|
||||||
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
|
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
|
||||||
|
|
||||||
# Indsæt nye danse
|
# Indsæt nye danse og hent IDs
|
||||||
dance_ids = []
|
|
||||||
for i, (name, level_id) in enumerate(dances, start=1):
|
for i, (name, level_id) in enumerate(dances, start=1):
|
||||||
cur = conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) VALUES (?,?,?,?)",
|
"INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) "
|
||||||
|
"VALUES (?,?,?,?)",
|
||||||
(song_id, name, i, level_id)
|
(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)
|
register_dance_name(name)
|
||||||
|
|
||||||
# Indsæt alternativer (knyttet til første dans hvis flere)
|
# Indsæt alternativer knyttet til første dans
|
||||||
if dance_ids and self._my_alt_rows:
|
if dance_ids and self._my_alt_rows:
|
||||||
first_dance_id = dance_ids[0]
|
first_dance_id = dance_ids[0]
|
||||||
for row in self._my_alt_rows:
|
for row in self._my_alt_rows:
|
||||||
name = row.get_name()
|
name = row.get_name()
|
||||||
if name:
|
if name:
|
||||||
add_alternative(
|
import uuid as _uuid
|
||||||
first_dance_id, name,
|
conn.execute("""
|
||||||
level_id=row.get_level_id(),
|
INSERT INTO dance_alternatives
|
||||||
note=row.get_note(),
|
(id, song_dance_id, alt_dance_name, level_id, note, source)
|
||||||
source="local",
|
VALUES (?,?,?,?,?,'local')
|
||||||
)
|
""", (str(_uuid.uuid4()), first_dance_id,
|
||||||
|
name, row.get_level_id(), row.get_note()))
|
||||||
|
register_dance_name(name)
|
||||||
|
|
||||||
# Skriv til fil
|
# Skriv til fil
|
||||||
if local_path and can_write_dances(local_path):
|
if local_path and can_write_dances(local_path):
|
||||||
|
|||||||
Reference in New Issue
Block a user