diff --git a/linedance-api/app/routers/sync.py b/linedance-api/app/routers/sync.py index d8295e3b..c91c389e 100644 --- a/linedance-api/app/routers/sync.py +++ b/linedance-api/app/routers/sync.py @@ -71,11 +71,12 @@ class PlaylistData(BaseModel): songs: list[PlaylistSongData] = [] class PushPayload(BaseModel): - songs: list[SongData] = [] - dances: list[DanceData] = [] - song_dances: list[SongDanceData] = [] - song_alts: list[SongAltDanceData] = [] - playlists: list[PlaylistData] = [] + songs: list[SongData] = [] + dances: list[DanceData] = [] + song_dances: list[SongDanceData] = [] + song_alts: list[SongAltDanceData] = [] + playlists: list[PlaylistData] = [] + deleted_playlists: list[str] = [] # navne på slettede playlister # ── Push ────────────────────────────────────────────────────────────────────── @@ -206,8 +207,9 @@ def push( if existing: existing.description = pl.description existing.visibility = pl.visibility - # Slet og geninsert sange - db.query(ProjectSong).filter_by(project_id=existing.id).delete() + # Opdater kun sange hvis push faktisk har sange med + if pl.songs: + db.query(ProjectSong).filter_by(project_id=existing.id).delete() project = existing else: project = Project( @@ -238,6 +240,13 @@ def push( ) db.add(proj_song) + # ── Slet playlister der er fjernet lokalt ───────────────────────────────── + for name in payload.deleted_playlists: + proj = db.query(Project).filter_by(owner_id=me.id, name=name).first() + if proj: + db.query(ProjectSong).filter_by(project_id=proj.id).delete() + db.delete(proj) + db.commit() return { diff --git a/linedance-app/local/local_db.py b/linedance-app/local/local_db.py index 6f9de5d8..20f1e05d 100644 --- a/linedance-app/local/local_db.py +++ b/linedance-app/local/local_db.py @@ -301,6 +301,14 @@ MIGRATIONS: dict[int, list[str]] = { "INSERT INTO dance_levels (sort_order, name, description) VALUES (90, 'High Intermediate', 'Stærk intermediate')", "INSERT INTO dance_levels (sort_order, name, description) VALUES (99, 'Advanced', 'Fuld beherskelse af trin og teknik')", ], + 12: [ + # Tabel til at huske slettede playlister — til sync med serveren + """CREATE TABLE IF NOT EXISTS deleted_playlists ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + deleted_at TEXT NOT NULL DEFAULT (datetime('now')) + )""", + ], } diff --git a/linedance-app/local/sync_manager.py b/linedance-app/local/sync_manager.py index 70da8ff6..90ba7913 100644 --- a/linedance-app/local/sync_manager.py +++ b/linedance-app/local/sync_manager.py @@ -57,6 +57,12 @@ class SyncManager: logger.info(f"Push OK: {len(payload['songs'])} sange") result = self._post("/sync/push", payload) self._save_playlist_ids(result.get("playlist_id_map", {})) + # Ryd deleted_playlists nu de er sendt til serveren + if payload.get("deleted_playlists"): + conn = sqlite3.connect(self._db_path) + conn.execute("DELETE FROM deleted_playlists") + conn.commit() + conn.close() logger.info(f"Push OK: {result.get('songs_synced', '?')} sange synkroniseret") if on_done: on_done(result) @@ -99,19 +105,24 @@ class SyncManager: threading.Thread(target=_run, daemon=True).start() def push_and_pull(self, on_done=None, on_error=None): - """Push og derefter pull i samme tråd.""" + """Pull FØR push — server er sandhed for playlister.""" def _run(): try: + # 1. Pull FØR push — hent server-data ned lokalt + pull_result = self._get("/sync/pull") + self._apply_pull(pull_result) + + # 2. Push lokal data op (sange, danse, dans-tags) + # — playlister der kom fra serveren pushes IKKE payload = self._build_push_payload() push_result = self._post("/sync/push", payload) - pull_result = self._get("/sync/pull") + pl_count = len(pull_result.get("my_playlists", [])) logger.info( f"Sync OK — {len(payload['songs'])} sange, " f"{len(payload['playlists'])} playlister, " f"{pl_count} server-playlister" ) - self._apply_pull(pull_result) if on_done: on_done({"push": push_result, "pull": pull_result}) except Exception as e: @@ -190,11 +201,11 @@ class SyncManager: "note": row["note"] or "", }) - # Playlister (kun navngivne — ikke __aktiv__) + # Playlister (kun lokalt oprettede — IKKE dem der kom fra serveren) playlists = [] for pl in conn.execute( - "SELECT id, name, description, tags FROM playlists " - "WHERE name != '__aktiv__'" + "SELECT id, name, description, tags, api_project_id FROM playlists " + "WHERE name != '__aktiv__' AND (api_project_id IS NULL OR api_project_id = '')" ).fetchall(): pl_songs = [] for ps in conn.execute(""" @@ -222,13 +233,21 @@ class SyncManager: "songs": pl_songs, }) + # Slettede playlister — skal fjernes fra serveren + deleted = [ + row["name"] for row in conn.execute( + "SELECT name FROM deleted_playlists" + ).fetchall() + ] + conn.close() return { - "songs": songs, - "dances": dances, - "song_dances": song_dances, - "song_alts": song_alts, - "playlists": playlists, + "songs": songs, + "dances": dances, + "song_dances": song_dances, + "song_alts": song_alts, + "playlists": playlists, + "deleted_playlists": deleted, } # ── Anvend pull ─────────────────────────────────────────────────────────── @@ -255,42 +274,44 @@ class SyncManager: """, (d.get("choreographer",""), d.get("video_url",""), d.get("stepsheet_url",""), existing["id"])) - # Importer egne playlister fra server hvis de ikke findes lokalt + # Importer/opdater egne playlister fra server — server er sandhed for pl in data.get("my_playlists", []): server_id = pl.get("server_id") name = pl.get("name", "") if not server_id or not name: continue - # Tjek om listen allerede eksisterer lokalt existing = conn.execute( "SELECT id FROM playlists WHERE api_project_id=?", (server_id,) ).fetchone() + if existing: - continue # Allerede importeret — spring over + pl_id = existing["id"] + # Opdater navn hvis det er ændret på serveren + conn.execute( + "UPDATE playlists SET name=? WHERE id=?", (name, pl_id) + ) + else: + cur = conn.execute( + "INSERT INTO playlists (name, description, api_project_id, is_linked, server_permission) " + "VALUES (?,?,?,1,'edit')", + (name, pl.get("description",""), server_id) + ) + pl_id = cur.lastrowid - # Opret liste - cur = conn.execute( - "INSERT INTO playlists (name, description, api_project_id, is_linked, server_permission) " - "VALUES (?,?,?,1,'edit')", - (name, pl.get("description",""), server_id) - ) - pl_id = cur.lastrowid - - # Indsæt sange — opret dem lokalt hvis de ikke findes endnu + # Genindlæs sange fra serveren — server er sandhed + conn.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,)) position = 1 for song_data in pl.get("songs", []): title = song_data.get("title", "") artist = song_data.get("artist", "") if not title: continue - # Find sangen lokalt local = conn.execute( "SELECT id FROM songs WHERE title=? AND artist=? LIMIT 1", (title, artist) ).fetchone() if not local: - # Opret som file_missing=1 — kobles til rigtig fil ved næste scan import uuid new_id = str(uuid.uuid4()) conn.execute( diff --git a/linedance-app/ui/main_window.py b/linedance-app/ui/main_window.py index 862aa180..0ba5b15d 100644 --- a/linedance-app/ui/main_window.py +++ b/linedance-app/ui/main_window.py @@ -372,6 +372,12 @@ class MainWindow(QMainWindow): self._sync_debounce.setInterval(5000) self._sync_debounce.timeout.connect(self._auto_sync) + # Periodisk sync — kører hvert 10. minut + self._sync_periodic = QTimer(self) + self._sync_periodic.setInterval(10 * 60 * 1000) + self._sync_periodic.timeout.connect(self._manual_sync) + self._sync_periodic.start() + self._library_panel = LibraryPanel() self._library_panel.set_preview_player(self._preview_player) diff --git a/linedance-app/ui/playlist_manager.py b/linedance-app/ui/playlist_manager.py index 8e4a3b55..678d665e 100644 --- a/linedance-app/ui/playlist_manager.py +++ b/linedance-app/ui/playlist_manager.py @@ -180,6 +180,10 @@ class PlaylistManagerDialog(QDialog): try: from local.local_db import get_db with get_db() as conn: + conn.execute( + "INSERT INTO deleted_playlists (name) " + "SELECT name FROM playlists WHERE id=?", (pl["id"],) + ) conn.execute("DELETE FROM playlists WHERE id=?", (pl["id"],)) self._load_saved_playlists() except Exception as e: