Rettelsaer
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user