Synk virker

This commit is contained in:
2026-04-20 01:41:24 +02:00
parent f92af40dd7
commit 8a4c879213
4 changed files with 163 additions and 98 deletions

View File

@@ -419,13 +419,13 @@ def get_all_songs_with_files(limit: int = 5000) -> list:
# ── Playlister ────────────────────────────────────────────────────────────────
def create_playlist(name: str, description: str = "") -> str:
def create_playlist(name: str, description: str = "", tags: str = "") -> str:
import uuid as _uuid
pl_id = str(_uuid.uuid4())
with get_db() as conn:
conn.execute(
"INSERT INTO playlists (id, name, description) VALUES (?,?,?)",
(pl_id, name, description)
"INSERT INTO playlists (id, name, description, tags) VALUES (?,?,?,?)",
(pl_id, name, description, tags)
)
return pl_id
@@ -434,7 +434,9 @@ def get_playlists(tag_filter: str | None = None) -> list:
with get_db() as conn:
if tag_filter:
return conn.execute("""
SELECT p.*, COUNT(ps.id) as song_count
SELECT p.id, p.name, p.description, p.tags, p.api_project_id,
p.is_linked, p.server_permission, p.is_deleted, p.created_at,
COUNT(ps.position) as song_count
FROM playlists p
LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id
WHERE p.name != '__aktiv__' AND p.is_deleted = 0
@@ -444,7 +446,9 @@ def get_playlists(tag_filter: str | None = None) -> list:
f"%, {tag_filter}", tag_filter)).fetchall()
else:
return conn.execute("""
SELECT p.*, COUNT(ps.id) as song_count
SELECT p.id, p.name, p.description, p.tags, p.api_project_id,
p.is_linked, p.server_permission, p.is_deleted, p.created_at,
COUNT(ps.position) as song_count
FROM playlists p
LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id
WHERE p.name != '__aktiv__' AND p.is_deleted = 0
@@ -506,6 +510,10 @@ def add_song_to_playlist(playlist_id: str, song_id: str,
# ── Dans-tags ─────────────────────────────────────────────────────────────────
def get_all_playlist_tags() -> list[str]:
return get_playlist_tags()
def get_playlist_tags() -> list[str]:
with get_db() as conn:
rows = conn.execute(

View File

@@ -14,10 +14,12 @@ logger = logging.getLogger(__name__)
class SyncManager:
def __init__(self, api_url: str, db_path: str):
self._api_url = api_url.rstrip("/")
def __init__(self, api_url: str = "", db_path: str = "",
server_url: str = "", token: str | None = None):
# Støt både api_url og server_url som parameter-navn
self._api_url = (api_url or server_url).rstrip("/")
self._db_path = db_path
self._token: str | None = None
self._token: str | None = token
def set_token(self, token: str):
self._token = token

View File

@@ -19,7 +19,7 @@ from PyQt6.QtGui import QColor
class PlaylistBrowserDialog(QDialog):
"""Kombineret gem/hent dialog til danselister."""
playlist_selected = pyqtSignal(int, str) # playlist_id, name
playlist_selected = pyqtSignal(str, str) # playlist_id, name
sync_requested = pyqtSignal() # bed main_window om at køre sync
def __init__(self, mode: str = "load", current_songs: list = None,
@@ -315,7 +315,9 @@ class PlaylistBrowserDialog(QDialog):
)
for i, song in enumerate(self._current_songs, start=1):
if song.get("id"):
add_song_to_playlist(pl_id, song["id"], position=i)
add_song_to_playlist(pl_id, song["id"],
file_id=song.get("file_id"),
position=i)
self.playlist_selected.emit(pl_id, name)
self.accept()
except Exception as e:
@@ -327,7 +329,9 @@ class PlaylistBrowserDialog(QDialog):
pl_id = create_playlist(name, tags=tags)
for i, song in enumerate(self._current_songs, start=1):
if song.get("id"):
add_song_to_playlist(pl_id, song["id"], position=i)
add_song_to_playlist(pl_id, song["id"],
file_id=song.get("file_id"),
position=i)
self.playlist_selected.emit(pl_id, name)
self.accept()
except Exception as e:

View File

@@ -50,8 +50,8 @@ class PlaylistPanel(QWidget):
self._statuses: list[str] = []
self._current_idx = -1
self._song_ended = False
self._active_playlist_id: int | None = None
self._named_playlist_id: int | None = None # den indlæste/gemte navngivne liste
self._active_playlist_id: str | None = None
self._named_playlist_id: str | None = None # den indlæste/gemte navngivne liste
self._build_ui()
self.setAcceptDrops(True)
# Autogem-timer — venter 800ms efter sidst ændring
@@ -317,7 +317,7 @@ class PlaylistPanel(QWidget):
pass
return False
def get_named_playlist_id(self) -> int | None:
def get_named_playlist_id(self) -> str | None:
return self._named_playlist_id
def next_playable_idx(self) -> int | None:
@@ -360,11 +360,14 @@ class PlaylistPanel(QWidget):
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"
import uuid as _uuid
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,
"(id, playlist_id, song_id, file_id, position, status, is_workshop, dance_override) "
"VALUES (?,?,?,?,?,?,?,?)",
(str(_uuid.uuid4()), self._named_playlist_id,
song["id"], song.get("file_id"),
i, status,
1 if song.get("is_workshop") else 0,
song.get("active_dance") or "")
)
@@ -374,7 +377,7 @@ class PlaylistPanel(QWidget):
except Exception as e:
self._lbl_autosave.setText("⚠ gemfejl")
def _save_named_playlist_id(self, pl_id: int | None):
def _save_named_playlist_id(self, pl_id: str | None):
"""Gem hvilken navngiven liste der er aktiv — til brug ved næste opstart."""
from PyQt6.QtCore import QSettings
s = QSettings("LineDance", "Player")
@@ -388,7 +391,7 @@ class PlaylistPanel(QWidget):
try:
from PyQt6.QtCore import QSettings
s = QSettings("LineDance", "Player")
pl_id = s.value("session/named_playlist_id", None, type=int)
pl_id = s.value("session/named_playlist_id", None, type=str)
if not pl_id:
return False
@@ -410,11 +413,14 @@ class PlaylistPanel(QWidget):
# file_missing betyder bare at filen ikke er på denne maskine
songs_raw = conn.execute("""
SELECT s.id, s.title, s.artist, s.album,
s.bpm, s.duration_sec, s.file_format,
s.local_path, s.file_missing,
s.bpm, s.duration_sec,
ps.file_id,
f.local_path, f.file_format,
COALESCE(f.file_missing, 1) as file_missing,
ps.position, ps.status, ps.is_workshop, ps.dance_override
FROM playlist_songs ps
JOIN songs s ON s.id = ps.song_id
LEFT JOIN files f ON f.id = ps.file_id
WHERE ps.playlist_id=? ORDER BY ps.position
""", (pl_id,)).fetchall()
@@ -433,13 +439,13 @@ class PlaylistPanel(QWidget):
local_path = row["local_path"] or ""
file_missing = bool(row["file_missing"])
# Forsøg at finde filen lokalt hvis den mangler på denne maskine
# Forsøg at finde en anden fil lokalt hvis den specifikke 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()
match = conn.execute(
"SELECT f.local_path FROM files f "
"WHERE f.song_id=? AND f.file_missing=0 LIMIT 1",
(row["id"],)
).fetchone()
if match:
local_path = match["local_path"]
file_missing = False
@@ -451,6 +457,7 @@ class PlaylistPanel(QWidget):
"album": row["album"] or "",
"bpm": row["bpm"] or 0,
"duration_sec": row["duration_sec"] or 0,
"file_id": row["file_id"] if "file_id" in row.keys() else None,
"local_path": local_path,
"file_format": row["file_format"] or "",
"file_missing": file_missing,
@@ -551,11 +558,14 @@ class PlaylistPanel(QWidget):
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"
import uuid as _uuid
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,
"(id, playlist_id, song_id, file_id, position, status, is_workshop, dance_override) "
"VALUES (?,?,?,?,?,?,?,?)",
(str(_uuid.uuid4()), self._named_playlist_id,
song["id"], song.get("file_id"),
i, status,
1 if song.get("is_workshop") else 0,
song.get("active_dance") or "")
)
@@ -589,7 +599,7 @@ class PlaylistPanel(QWidget):
dialog.sync_requested.connect(self._request_sync)
dialog.exec()
def _load_playlist_by_id(self, pl_id: int, pl_name: str):
def _load_playlist_by_id(self, pl_id: str, pl_name: str):
try:
from local.local_db import get_db
@@ -612,11 +622,14 @@ class PlaylistPanel(QWidget):
# JOIN songs — sangen er altid i songs tabellen (oprettet ved pull med file_missing=1)
songs_raw = conn.execute("""
SELECT s.id, s.title, s.artist, s.album,
s.bpm, s.duration_sec, s.file_format,
s.local_path, s.file_missing,
s.bpm, s.duration_sec,
ps.file_id,
f.local_path, f.file_format,
COALESCE(f.file_missing, 1) as file_missing,
ps.position, ps.status, ps.is_workshop, ps.dance_override
FROM playlist_songs ps
JOIN songs s ON s.id = ps.song_id
LEFT JOIN files f ON f.id = ps.file_id
WHERE ps.playlist_id=? ORDER BY ps.position
""", (pl_id,)).fetchall()
songs = []
@@ -625,7 +638,7 @@ class PlaylistPanel(QWidget):
for row in songs_raw:
dances = conn.execute("""
SELECT d.name FROM song_dances sd
JOIN dances d ON d.id = sad.dance_id
JOIN dances d ON d.id = sd.dance_id
WHERE sd.song_id=? ORDER BY sd.dance_order
""", (row["id"],)).fetchall()
dance_names = [d["name"] for d in dances]
@@ -635,13 +648,13 @@ class PlaylistPanel(QWidget):
local_path = row["local_path"] or ""
file_missing = bool(row["file_missing"])
# Forsøg at finde filen lokalt hvis den mangler på denne maskine
# Forsøg at finde en anden fil lokalt hvis den specifikke 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()
match = conn.execute(
"SELECT f.local_path FROM files f "
"WHERE f.song_id=? AND f.file_missing=0 LIMIT 1",
(row["id"],)
).fetchone()
if match:
local_path = match["local_path"]
file_missing = False
@@ -654,6 +667,7 @@ class PlaylistPanel(QWidget):
"album": row["album"] or "",
"bpm": row["bpm"] or 0,
"duration_sec": row["duration_sec"] or 0,
"file_id": row["file_id"] if "file_id" in row.keys() else None,
"local_path": local_path,
"file_format": row["file_format"] or "",
"file_missing": file_missing,
@@ -798,53 +812,91 @@ 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
def _pull_linked_playlist(self, pl_id: str, server_id: str):
"""Hent seneste version af en linket liste fra serveren — i baggrundstråd."""
import threading
import uuid
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())
def _do_pull():
try:
from ui.settings_dialog import load_settings
from local.local_db import DB_PATH
import sqlite3, urllib.request, json
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
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())
conn = sqlite3.connect(str(DB_PATH), timeout=10)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,))
position = 1
for song_data in pl_data.get("songs", []):
title = song_data.get("title", "")
artist = song_data.get("artist", "")
if not title:
continue
# Find sang via titel+artist
local = conn.execute(
"SELECT s.id FROM songs s "
"JOIN files f ON f.song_id = s.id AND f.file_missing=0 "
"WHERE s.title=? AND s.artist=? LIMIT 1",
(title, artist)
).fetchone()
if not local:
# Sang mangler lokalt — opret som missing
local = conn.execute(
"SELECT id FROM songs WHERE title=? AND artist=? LIMIT 1",
(title, artist)
).fetchone()
if not local:
new_id = str(uuid.uuid4())
conn.execute(
"INSERT INTO songs (id, title, artist) VALUES (?,?,?)",
(new_id, title, artist)
)
song_id = new_id
else:
song_id = local["id"]
# Find fil
file_row = conn.execute(
"SELECT id FROM files WHERE song_id=? AND file_missing=0 LIMIT 1",
(song_id,)
).fetchone()
file_id = file_row["id"] if file_row else None
# 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"),
"(id, playlist_id, song_id, file_id, position, status, is_workshop, dance_override) "
"VALUES (?,?,?,?,?,?,?,?)",
(str(uuid.uuid4()), pl_id, song_id, file_id,
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
position += 1
def _push_linked_playlist(self, pl_id: int, server_id: str):
conn.commit()
conn.close()
except Exception:
pass # Offline — brug lokalt cachet version
threading.Thread(target=_do_pull, daemon=True).start()
def _push_linked_playlist(self, pl_id: str, server_id: str):
"""Push ændringer til server for en linket liste."""
try:
from ui.settings_dialog import load_settings
@@ -969,42 +1021,41 @@ class PlaylistPanel(QWidget):
with get_db() as conn:
for song in self._songs:
path = song.get("local_path", "")
# Grøn — eksisterer og tilgængeligt
# Grøn — filen eksisterer lokalt
if path and Path(path).exists():
song["availability"] = "green"
song["file_missing"] = False
# Opdater songs tabellen
# Opdater files tabellen
conn.execute(
"UPDATE songs SET file_missing=0, local_path=? WHERE id=?",
(path, song["id"])
"UPDATE files SET file_missing=0 WHERE local_path=?",
(path,)
)
continue
# Forsøg auto-match via titel+artist
# Forsøg auto-match via titel+artist i files tabellen
title = song.get("title", "")
artist = song.get("artist", "")
match = conn.execute("""
SELECT id, local_path FROM songs
WHERE title=? AND artist=? AND file_missing=0
AND local_path IS NOT NULL AND local_path != ''
SELECT f.id as file_id, f.local_path, s.id as song_id
FROM files f
JOIN songs s ON s.id = f.song_id
WHERE s.title=? AND s.artist=? AND f.file_missing=0
AND f.local_path IS NOT NULL AND f.local_path != ''
LIMIT 1
""", (title, artist)).fetchone()
if match and Path(match["local_path"]).exists():
song["local_path"] = match["local_path"]
song["id"] = match["id"]
song["file_id"] = match["file_id"]
song["availability"] = "green"
song["file_missing"] = False
# Opdater playlist_songs til at pege på den fundne sang
# Opdater playlist_songs til at pege på den fundne fil
if self._named_playlist_id:
conn.execute("""
UPDATE playlist_songs SET song_id=?
WHERE playlist_id=? AND song_id=(
SELECT id FROM songs
WHERE title=? AND artist=?
LIMIT 1
)
""", (match["id"], self._named_playlist_id, title, artist))
conn.execute(
"UPDATE playlist_songs SET file_id=? "
"WHERE playlist_id=? AND song_id=?",
(match["file_id"], self._named_playlist_id, song["id"])
)
else:
song["availability"] = "red"