From 8a4c879213cd9a00498e021565e831c96f76a5e0 Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Mon, 20 Apr 2026 01:41:24 +0200 Subject: [PATCH] Synk virker --- linedance-app/local/local_db.py | 18 ++- linedance-app/local/sync_manager.py | 8 +- linedance-app/ui/playlist_browser.py | 12 +- linedance-app/ui/playlist_panel.py | 223 ++++++++++++++++----------- 4 files changed, 163 insertions(+), 98 deletions(-) diff --git a/linedance-app/local/local_db.py b/linedance-app/local/local_db.py index ef17f5c5..647975e8 100644 --- a/linedance-app/local/local_db.py +++ b/linedance-app/local/local_db.py @@ -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( diff --git a/linedance-app/local/sync_manager.py b/linedance-app/local/sync_manager.py index 2cbfa430..a5d2e35d 100644 --- a/linedance-app/local/sync_manager.py +++ b/linedance-app/local/sync_manager.py @@ -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 diff --git a/linedance-app/ui/playlist_browser.py b/linedance-app/ui/playlist_browser.py index e5360a68..15172595 100644 --- a/linedance-app/ui/playlist_browser.py +++ b/linedance-app/ui/playlist_browser.py @@ -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: @@ -515,4 +519,4 @@ class PlaylistBrowserDialog(QDialog): f"'{name}' er nu linket til server-listen.\n" f"Du har rettighed til at {perm_text} listen.\n\n" f"{matched} af {len(pl_data.get('songs', []))} sange fundet lokalt." - ) + ) \ No newline at end of file diff --git a/linedance-app/ui/playlist_panel.py b/linedance-app/ui/playlist_panel.py index 885918b6..41ed4892 100644 --- a/linedance-app/ui/playlist_panel.py +++ b/linedance-app/ui/playlist_panel.py @@ -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"