""" linked_playlist.py — Håndter linkede server-playlister. Pull ved åbning, push ved gem. """ import json import sqlite3 import urllib.request import urllib.error import logging from pathlib import Path logger = logging.getLogger(__name__) class LinkedPlaylistManager: def __init__(self, db_path: str, server_url: str, token: str): self._db_path = db_path self._server_url = server_url.rstrip("/") self._token = token def _headers(self): return { "Content-Type": "application/json", "Authorization": f"Bearer {self._token}", } def pull(self, playlist_id: int) -> list[dict]: """ Hent seneste version fra serveren og opdater lokal liste. Returnerer sang-liste klar til playlist_panel. """ conn = sqlite3.connect(self._db_path) conn.row_factory = sqlite3.Row pl = conn.execute( "SELECT api_project_id, server_permission FROM playlists WHERE id=?", (playlist_id,) ).fetchone() if not pl or not pl["api_project_id"]: conn.close() return [] # Hent fra server req = urllib.request.Request( f"{self._server_url}/sharing/playlists/{pl['api_project_id']}", headers=self._headers() ) with urllib.request.urlopen(req, timeout=10) as resp: data = json.loads(resp.read()) # Slet eksisterende sange og erstat med server-version conn.execute( "DELETE FROM playlist_songs WHERE playlist_id=?", (playlist_id,) ) songs = [] for song_data in sorted(data.get("songs", []), key=lambda x: x["position"]): # Match lokalt på titel+artist local = conn.execute( "SELECT id, local_path, bpm, duration_sec, file_format, file_missing " "FROM songs WHERE title=? AND artist=? AND file_missing=0 LIMIT 1", (song_data["title"], song_data["artist"]) ).fetchone() if local: conn.execute(""" INSERT OR IGNORE INTO playlist_songs (playlist_id, song_id, position, status, is_workshop, dance_override) VALUES (?,?,?,?,?,?) """, ( playlist_id, local["id"], song_data["position"], song_data["status"], 1 if song_data.get("is_workshop") else 0, song_data.get("dance_override", ""), )) # Hent danse dances = conn.execute(""" SELECT d.name FROM song_dances sd JOIN dances d ON d.id = sd.dance_id WHERE sd.song_id=? ORDER BY sd.dance_order """, (local["id"],)).fetchall() songs.append({ "id": local["id"], "title": song_data["title"], "artist": song_data["artist"], "album": song_data.get("album", ""), "bpm": local["bpm"] or 0, "duration_sec": local["duration_sec"] or 0, "local_path": local["local_path"], "file_format": local["file_format"] or "", "file_missing": False, "dances": [d["name"] for d in dances], "active_dance": song_data.get("dance_override", ""), "is_workshop": bool(song_data.get("is_workshop")), "status": song_data.get("status", "pending"), }) conn.commit() conn.close() return songs def push(self, playlist_id: int): """Push lokal version til serveren.""" conn = sqlite3.connect(self._db_path) conn.row_factory = sqlite3.Row pl = conn.execute( "SELECT api_project_id, server_permission, name FROM playlists WHERE id=?", (playlist_id,) ).fetchone() if not pl or not pl["api_project_id"]: conn.close() raise Exception("Playlisten er ikke linket til serveren") if pl["server_permission"] not in ("edit",): conn.close() raise Exception(f"Du har ikke rettighed til at redigere denne liste (du har: {pl['server_permission']})") # Byg payload til sync/push songs_raw = conn.execute(""" SELECT s.id, s.title, s.artist, s.album, s.bpm, s.duration_sec, s.file_format, 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 """, (playlist_id,)).fetchall() conn.close() from local.sync_manager import SyncManager mgr = SyncManager(self._db_path, self._server_url, self._token) # Byg mini-payload med kun denne playliste song_ids = [row["id"] for row in songs_raw] songs_payload = [] for row in songs_raw: songs_payload.append({ "local_id": str(row["id"]), "title": row["title"] or "", "artist": row["artist"] or "", "album": row["album"] or "", "bpm": row["bpm"] or 0, "duration_sec": row["duration_sec"] or 0, "file_format": row["file_format"] or "", }) pl_payload = [{ "local_id": str(playlist_id), "name": pl["name"], "description": "", "tags": "", "visibility": "shared", "songs": [ { "song_local_id": str(row["id"]), "position": int(row["position"]), "status": row["status"] or "pending", "is_workshop": bool(row["is_workshop"]), "dance_override": row["dance_override"] or "", } for row in songs_raw ] }] result = mgr._post("/sync/push", { "songs": songs_payload, "dances": [], "song_dances": [], "song_alts": [], "playlists": pl_payload, }) return result