Rettelsaer

This commit is contained in:
2026-04-13 07:23:37 +02:00
parent 45dcedaed4
commit bbd5690d72
22 changed files with 2026 additions and 538 deletions

View File

@@ -289,9 +289,11 @@ class PlaylistPanel(QWidget):
return self._named_playlist_id
def next_playable_idx(self) -> int | None:
"""Find første sang fra toppen der ikke er 'skipped' eller 'played'."""
"""Find første sang fra toppen der ikke er afspillet, sprunget over eller i gang."""
for i in range(len(self._songs)):
if self._statuses[i] not in ("skipped", "played"):
if self._statuses[i] not in ("skipped", "played", "playing"):
if i == self._current_idx and not self._song_ended:
continue
return i
return None
@@ -303,25 +305,42 @@ class PlaylistPanel(QWidget):
self._lbl_autosave.setText("● ikke gemt")
def _autosave(self):
"""Gem til den faste 'Aktiv liste' i SQLite."""
"""Gem til '__aktiv__' OG til den navngivne liste hvis der er én."""
try:
from local.local_db import get_db, create_playlist, add_song_to_playlist
with get_db() as conn:
# Slet den gamle aktive liste
conn.execute(
"DELETE FROM playlists WHERE name=?", (ACTIVE_PLAYLIST_NAME,)
)
# Opret ny
pl_id = create_playlist(ACTIVE_PLAYLIST_NAME)
self._active_playlist_id = pl_id
for i, song in enumerate(self._songs, start=1):
if song.get("id"):
add_song_to_playlist(pl_id, song["id"], position=i)
# Gem også til den navngivne liste
if self._named_playlist_id:
with get_db() as conn:
conn.execute(
"DELETE FROM playlist_songs WHERE playlist_id=?",
(self._named_playlist_id,)
)
for i, song in enumerate(self._songs, start=1):
if song.get("id"):
status = self._statuses[i-1] if i-1 < len(self._statuses) else "pending"
conn.execute(
"INSERT INTO playlist_songs "
"(playlist_id, song_id, position, status, is_workshop, dance_override) "
"VALUES (?,?,?,?,?,?)",
(self._named_playlist_id, song["id"], i, status,
1 if song.get("is_workshop") else 0,
song.get("active_dance") or "")
)
self._lbl_autosave.setText("✓ gemt")
self.playlist_changed.emit()
except Exception as e:
self._lbl_autosave.setText(f"⚠ gemfejl")
pass
self._lbl_autosave.setText("⚠ gemfejl")
def _save_named_playlist_id(self, pl_id: int | None):
"""Gem hvilken navngiven liste der er aktiv — til brug ved næste opstart."""
@@ -374,6 +393,21 @@ class PlaylistPanel(QWidget):
dance_names = [d["name"] for d in dances]
override = row["dance_override"] or ""
active_dance = override if override else (dance_names[0] if dance_names else "")
local_path = row["local_path"]
file_missing = bool(row["file_missing"])
# Forsøg at finde sangen lokalt hvis den mangler
if file_missing or not local_path:
match = conn.execute("""
SELECT local_path FROM songs
WHERE title=? AND artist=? AND file_missing=0
LIMIT 1
""", (row["title"], row["artist"])).fetchone()
if match:
local_path = match["local_path"]
file_missing = False
songs.append({
"id": row["id"],
"title": row["title"],
@@ -381,9 +415,9 @@ class PlaylistPanel(QWidget):
"album": row["album"],
"bpm": row["bpm"],
"duration_sec": row["duration_sec"],
"local_path": row["local_path"],
"local_path": local_path,
"file_format": row["file_format"],
"file_missing": bool(row["file_missing"]),
"file_missing": file_missing,
"dances": dance_names,
"active_dance": active_dance,
"is_workshop": bool(row["is_workshop"]),
@@ -401,15 +435,14 @@ class PlaylistPanel(QWidget):
self._btn_save_current.setToolTip(f"Gem ændringer til '{pl['name']}'")
self._title_label.setText(f"DANSELISTE — {pl['name'].upper()}")
self._lbl_autosave.setText("✓ gendannet")
self._refresh()
# Find næste uafspillede og sæt den klar
# Find næste uafspillede
ni = self.next_playable_idx()
if ni is not None:
self._current_idx = ni
self._refresh()
self.next_song_ready.emit(self._songs[ni])
self._statuses[ni] = "playing"
self._refresh()
return True
except Exception:
pass
@@ -479,10 +512,28 @@ class PlaylistPanel(QWidget):
status = self._statuses[i-1] if i-1 < len(self._statuses) else "pending"
conn.execute(
"INSERT INTO playlist_songs "
"(playlist_id, song_id, position, status) VALUES (?,?,?,?)",
(self._named_playlist_id, song["id"], i, status)
"(playlist_id, song_id, position, status, is_workshop, dance_override) "
"VALUES (?,?,?,?,?,?)",
(self._named_playlist_id, song["id"], i, status,
1 if song.get("is_workshop") else 0,
song.get("active_dance") or "")
)
self._lbl_autosave.setText("✓ gemt")
# Push til server hvis linket med edit-rettighed
if getattr(self, "_can_edit_server", False):
from local.local_db import get_db as _gdb
with _gdb() as c:
meta = c.execute(
"SELECT api_project_id FROM playlists WHERE id=?",
(self._named_playlist_id,)
).fetchone()
if meta and meta["api_project_id"]:
self._push_linked_playlist(
self._named_playlist_id, meta["api_project_id"]
)
self._lbl_autosave.setText("✓ gemt og synkroniseret")
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}")
@@ -495,6 +546,22 @@ class PlaylistPanel(QWidget):
def _load_playlist_by_id(self, pl_id: int, pl_name: str):
try:
from local.local_db import get_db
# Tjek om listen er linket til serveren — pull først
with get_db() as conn:
pl_meta = conn.execute(
"SELECT api_project_id, is_linked, server_permission "
"FROM playlists WHERE id=?", (pl_id,)
).fetchone()
if pl_meta and pl_meta["is_linked"] and pl_meta["api_project_id"]:
self._pull_linked_playlist(pl_id, pl_meta["api_project_id"])
# Opdater gem-knap baseret på rettighed
perm = pl_meta["server_permission"] or "view"
self._named_playlist_id = pl_id
self._can_edit_server = (perm == "edit")
else:
self._can_edit_server = False
with get_db() as conn:
songs_raw = conn.execute("""
SELECT s.*, ps.position, ps.status,
@@ -505,6 +572,7 @@ class PlaylistPanel(QWidget):
""", (pl_id,)).fetchall()
songs = []
statuses = []
repaired = 0
for row in songs_raw:
dances = conn.execute("""
SELECT d.name FROM song_dances sd
@@ -512,29 +580,64 @@ class PlaylistPanel(QWidget):
WHERE sd.song_id=? ORDER BY sd.dance_order
""", (row["id"],)).fetchall()
dance_names = [d["name"] for d in dances]
# dance_override bestemmer hvilken dans der vises
override = row["dance_override"] or ""
active_dance = override if override else (dance_names[0] if dance_names else "")
local_path = row["local_path"]
file_missing = bool(row["file_missing"])
# Forsøg at finde sangen lokalt hvis den mangler
if file_missing or not local_path:
match = conn.execute("""
SELECT local_path FROM songs
WHERE title=? AND artist=? AND file_missing=0
LIMIT 1
""", (row["title"], row["artist"])).fetchone()
if match:
local_path = match["local_path"]
file_missing = False
repaired += 1
songs.append({
"id": row["id"], "title": row["title"],
"artist": row["artist"], "album": row["album"],
"bpm": row["bpm"], "duration_sec": row["duration_sec"],
"local_path": row["local_path"], "file_format": row["file_format"],
"file_missing": bool(row["file_missing"]),
"dances": dance_names,
"id": row["id"],
"title": row["title"],
"artist": row["artist"],
"album": row["album"],
"bpm": row["bpm"],
"duration_sec": row["duration_sec"],
"local_path": local_path,
"file_format": row["file_format"],
"file_missing": file_missing,
"dances": dance_names,
"active_dance": active_dance,
"is_workshop": bool(row["is_workshop"]),
"is_workshop": bool(row["is_workshop"]),
})
statuses.append(row["status"] or "pending")
self._songs = songs
self._statuses = statuses
self._current_idx = -1
self._song_ended = False
self._named_playlist_id = pl_id
self._title_label.setText(f"DANSELISTE — {pl_name.upper()}")
self._lbl_autosave.setText("✓ gendannet")
self._btn_save_current.setEnabled(True)
self._btn_save_current.setToolTip(f"Gem ændringer til '{pl_name}'")
# Vis link-indikator i titlen
is_linked = pl_meta and pl_meta["is_linked"]
perm = pl_meta["server_permission"] if is_linked else "edit"
link_icon = " 🔗" if is_linked else ""
self._title_label.setText(f"DANSELISTE — {pl_name.upper()}{link_icon}")
status_txt = f"✓ indlæst — {repaired} sange fundet lokalt" if repaired else "✓ indlæst"
if is_linked:
status_txt += f" ({perm})"
self._lbl_autosave.setText(status_txt)
# Gem-knap: deaktiver hvis view-only linket liste
can_save = not is_linked or perm == "edit"
self._btn_save_current.setEnabled(can_save)
self._btn_save_current.setToolTip(
f"Gem ændringer til '{pl_name}'" if can_save
else "Du har kun læse-adgang til denne delte liste"
)
self._save_named_playlist_id(pl_id)
self._refresh()
self._trigger_autosave()
@@ -628,6 +731,98 @@ class PlaylistPanel(QWidget):
except Exception:
pass
def _pull_linked_playlist(self, pl_id: int, server_id: str):
"""Hent seneste version af en linket liste fra serveren."""
try:
from ui.settings_dialog import load_settings
from local.local_db import get_db, DB_PATH
s = load_settings()
server_url = s.get("server_url", "").rstrip("/")
# Hent token fra main_window
mw = self.window()
token = getattr(mw, "_api_token", None)
if not token or not server_url:
return
import urllib.request, json
req = urllib.request.Request(
f"{server_url}/sharing/playlists/{server_id}",
headers={"Authorization": f"Bearer {token}"}
)
with urllib.request.urlopen(req, timeout=8) as resp:
pl_data = json.loads(resp.read())
# Opdater lokal liste med server-data
import sqlite3
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
conn.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,))
for song_data in pl_data.get("songs", []):
local = conn.execute(
"SELECT id FROM songs WHERE title=? AND artist=? AND file_missing=0",
(song_data["title"], song_data["artist"])
).fetchone()
if local:
conn.execute(
"INSERT INTO playlist_songs "
"(playlist_id, song_id, position, status, is_workshop, dance_override) "
"VALUES (?,?,?,?,?,?)",
(pl_id, local["id"], song_data["position"],
song_data.get("status", "pending"),
1 if song_data.get("is_workshop") else 0,
song_data.get("dance_override") or "")
)
conn.commit()
conn.close()
except Exception as e:
pass # Offline — brug lokalt cachet version
def _push_linked_playlist(self, pl_id: int, server_id: str):
"""Push ændringer til server for en linket liste."""
try:
from ui.settings_dialog import load_settings
from local.local_db import DB_PATH
s = load_settings()
server_url = s.get("server_url", "").rstrip("/")
mw = self.window()
token = getattr(mw, "_api_token", None)
if not token or not server_url:
return
import sqlite3, json, urllib.request
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
songs = []
for ps in conn.execute(
"SELECT s.title, s.artist, ps.position, ps.status, "
"ps.is_workshop, ps.dance_override "
"FROM playlist_songs ps JOIN songs s ON s.id=ps.song_id "
"WHERE ps.playlist_id=? ORDER BY ps.position", (pl_id,)
).fetchall():
songs.append({
"title": ps["title"],
"artist": ps["artist"],
"position": ps["position"],
"status": ps["status"] or "pending",
"is_workshop": bool(ps["is_workshop"]),
"dance_override": ps["dance_override"] or "",
})
conn.close()
data = json.dumps({"songs": songs}).encode()
req = urllib.request.Request(
f"{server_url}/sharing/playlists/{server_id}/songs",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
},
method="PUT"
)
urllib.request.urlopen(req, timeout=8)
except Exception as e:
pass
def _on_pause_changed(self, seconds: int):
self._pause_seconds = seconds
@@ -642,7 +837,7 @@ class PlaylistPanel(QWidget):
if reply == QMessageBox.StandardButton.Yes:
self._statuses = ["pending"] * len(self._songs)
self._current_idx = -1
self._song_ended = True
self._song_ended = False
try:
from local.local_db import clear_event_state
clear_event_state()
@@ -650,6 +845,12 @@ class PlaylistPanel(QWidget):
pass
self._refresh()
self._scroll_to(0)
# Sæt første sang klar
ni = self.next_playable_idx()
if ni is not None:
self._current_idx = ni
self._refresh()
self.next_song_ready.emit(self._songs[ni])
self.event_started.emit()
# ── Højreklik ─────────────────────────────────────────────────────────────
@@ -718,10 +919,26 @@ class PlaylistPanel(QWidget):
self._list.clear()
played = sum(1 for s in self._statuses if s == "played")
self._lbl_progress.setText(f"{played} / {len(self._songs)} afspillet")
# Find næste uafspillede til blå markering — aldrig samme som current
next_idx = None
if self._current_idx >= 0 and not self._song_ended:
# Sang spiller — vis næste som blå
next_idx = self.next_playable_idx()
elif self._current_idx == -1 or self._song_ended:
# Ingen sang spiller — vis første som blå
next_idx = self.next_playable_idx()
for i, song in enumerate(self._songs):
is_current = (i == self._current_idx and not self._song_ended)
status = "playing" if is_current else self._statuses[i]
icon = self.STATUS_ICON.get(status, " ")
is_next = (i == next_idx and not is_current)
if is_current:
status = "playing"
elif is_next:
status = "next"
else:
status = self._statuses[i]
icon = self.STATUS_ICON.get(status, " ")
# Vis active_dance (override eller første dans) eller alle danse
active = song.get("active_dance", "")
@@ -737,6 +954,9 @@ class PlaylistPanel(QWidget):
if status == "playing":
item.setForeground(QColor(self.STATUS_COLOR["playing"]))
f = item.font(); f.setBold(True); item.setFont(f)
elif status == "next":
item.setForeground(QColor(self.STATUS_COLOR["next"]))
f = item.font(); f.setBold(True); item.setFont(f)
elif status == "played":
item.setForeground(QColor("#2ecc71"))
elif status == "skipped":