Bedre sync

This commit is contained in:
2026-04-19 19:15:16 +02:00
parent fb7622549c
commit 9e5ddec184
3 changed files with 53 additions and 58 deletions

View File

@@ -76,7 +76,7 @@ class PushPayload(BaseModel):
song_dances: list[SongDanceData] = [] song_dances: list[SongDanceData] = []
song_alts: list[SongAltDanceData] = [] song_alts: list[SongAltDanceData] = []
playlists: list[PlaylistData] = [] playlists: list[PlaylistData] = []
deleted_playlists: list[str] = [] # navne på slettede playlister deleted_playlists: list[str] = [] # server-IDs (api_project_id) på slettede playlister
# ── Push ────────────────────────────────────────────────────────────────────── # ── Push ──────────────────────────────────────────────────────────────────────
@@ -241,8 +241,9 @@ def push(
db.add(proj_song) db.add(proj_song)
# ── Slet playlister der er fjernet lokalt ───────────────────────────────── # ── Slet playlister der er fjernet lokalt ─────────────────────────────────
for name in payload.deleted_playlists: # Klienten sender api_project_id (= server Project.id) som strings
proj = db.query(Project).filter_by(owner_id=me.id, name=name).first() for project_id in payload.deleted_playlists:
proj = db.query(Project).filter_by(id=project_id, owner_id=me.id).first()
if proj: if proj:
db.query(ProjectSong).filter_by(project_id=proj.id).delete() db.query(ProjectSong).filter_by(project_id=proj.id).delete()
db.delete(proj) db.delete(proj)
@@ -378,4 +379,4 @@ def pull(
"shared": shared, "shared": shared,
"my_playlists": my_playlists, "my_playlists": my_playlists,
"song_tags": song_tags, "song_tags": song_tags,
} }

View File

@@ -304,13 +304,16 @@ MIGRATIONS: dict[int, list[str]] = {
12: [ 12: [
# Tabel til at huske slettede playlister — til sync med serveren # Tabel til at huske slettede playlister — til sync med serveren
"""CREATE TABLE IF NOT EXISTS deleted_playlists ( """CREATE TABLE IF NOT EXISTS deleted_playlists (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
deleted_at TEXT NOT NULL DEFAULT (datetime('now')) deleted_at TEXT NOT NULL DEFAULT (datetime('now'))
)""", )""",
], ],
13: [ 13: [
# Tilføj api_project_id så serveren præcist ved hvilken playlist der skal slettes # Soft delete: is_deleted=1 i stedet for at slette rækken.
# Giver sync mulighed for at sende sletningen til serveren.
"""ALTER TABLE playlists ADD COLUMN is_deleted INTEGER NOT NULL DEFAULT 0""",
# api_project_id kolonne på deleted_playlists (hvis den mangler fra migration 12)
"""ALTER TABLE deleted_playlists ADD COLUMN api_project_id TEXT""", """ALTER TABLE deleted_playlists ADD COLUMN api_project_id TEXT""",
], ],
} }
@@ -550,24 +553,6 @@ def create_playlist(name: str, description: str = "", tags: str = "") -> int:
return cur.lastrowid return cur.lastrowid
def delete_playlist(playlist_id: int):
"""
Slet en playliste lokalt og registrér sletningen til næste sync.
Gemmer api_project_id så serveren præcist ved hvad der skal slettes.
"""
with get_db() as conn:
row = conn.execute(
"SELECT name, api_project_id FROM playlists WHERE id=?",
(playlist_id,)
).fetchone()
if row:
conn.execute(
"INSERT INTO deleted_playlists (name, api_project_id) VALUES (?,?)",
(row["name"], row["api_project_id"] or None)
)
conn.execute("DELETE FROM playlists WHERE id=?", (playlist_id,))
def create_linked_playlist(name: str, api_project_id: str, def create_linked_playlist(name: str, api_project_id: str,
permission: str = "view", permission: str = "view",
description: str = "", tags: str = "") -> int: description: str = "", tags: str = "") -> int:
@@ -614,7 +599,7 @@ def get_playlists(tag_filter: str | None = None) -> list[sqlite3.Row]:
SELECT p.*, COUNT(ps.id) as song_count SELECT p.*, COUNT(ps.id) as song_count
FROM playlists p FROM playlists p
LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id
WHERE p.name != ? AND ( WHERE p.name != ? AND p.is_deleted = 0 AND (
p.tags LIKE ? OR p.tags LIKE ? OR p.tags LIKE ? OR p.tags LIKE ? OR
p.tags LIKE ? OR p.tags = ? p.tags LIKE ? OR p.tags = ?
) )
@@ -632,13 +617,27 @@ def get_playlists(tag_filter: str | None = None) -> list[sqlite3.Row]:
SELECT p.*, COUNT(ps.id) as song_count SELECT p.*, COUNT(ps.id) as song_count
FROM playlists p FROM playlists p
LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id
WHERE p.name != ? WHERE p.name != ? AND p.is_deleted = 0
GROUP BY p.id GROUP BY p.id
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
""", ("__aktiv__",)).fetchall() """, ("__aktiv__",)).fetchall()
return rows return rows
def delete_playlist(playlist_id: int):
"""
Soft-slet en playliste: sæt is_deleted=1 i stedet for at fjerne rækken.
Sync kan derefter sende sletningen til serveren via api_project_id.
"""
with get_db() as conn:
conn.execute(
"UPDATE playlists SET is_deleted=1 WHERE id=?",
(playlist_id,)
)
def add_song_to_playlist(playlist_id: int, song_id: str, position: int | None = None) -> int: def add_song_to_playlist(playlist_id: int, song_id: str, position: int | None = None) -> int:
with get_db() as conn: with get_db() as conn:
if position is None: if position is None:
@@ -895,6 +894,4 @@ def get_dance_name_suggestions(prefix: str, limit: int = 20) -> list[str]:
result.append(f"{s['name']} / {s['level_name']}") result.append(f"{s['name']} / {s['level_name']}")
else: else:
result.append(s["name"]) result.append(s["name"])
return result return result

View File

@@ -57,10 +57,12 @@ class SyncManager:
logger.info(f"Push OK: {len(payload['songs'])} sange") logger.info(f"Push OK: {len(payload['songs'])} sange")
result = self._post("/sync/push", payload) result = self._post("/sync/push", payload)
self._save_playlist_ids(result.get("playlist_id_map", {})) self._save_playlist_ids(result.get("playlist_id_map", {}))
# Ryd deleted_playlists nu de er sendt til serveren # Fjern soft-slettede playlister permanent efter succesfuld push
if payload.get("deleted_playlists"): if payload.get("deleted_playlists"):
conn = sqlite3.connect(self._db_path) conn = sqlite3.connect(self._db_path)
conn.execute("DELETE FROM deleted_playlists") conn.execute(
"DELETE FROM playlists WHERE is_deleted=1 AND api_project_id IS NOT NULL"
)
conn.commit() conn.commit()
conn.close() conn.close()
logger.info(f"Push OK: {result.get('songs_synced', '?')} sange synkroniseret") logger.info(f"Push OK: {result.get('songs_synced', '?')} sange synkroniseret")
@@ -116,23 +118,26 @@ class SyncManager:
f"sletter {len(deleted)}: {deleted}") f"sletter {len(deleted)}: {deleted}")
push_result = self._post("/sync/push", payload) push_result = self._post("/sync/push", payload)
self._save_playlist_ids(push_result.get("playlist_id_map", {})) self._save_playlist_ids(push_result.get("playlist_id_map", {}))
logger.info(f"Push svar: {push_result}") logger.info(f"Push svar: status={push_result.get('status')}, "
f"sange={push_result.get('songs_synced', 0)}, "
f"playlister={push_result.get('playlists_synced', 0)}")
# 2. Pull — sletninger er nu gennemført på serveren. # 2. Pull — sletninger er nu gennemført på serveren.
# VIGTIGT: ryd deleted_playlists EFTER pull, så _apply_pull # _apply_pull filtrerer is_deleted=1 rækker fra automatisk.
# stadig kan filtrere de slettede lister fra.
pull_result = self._get("/sync/pull") pull_result = self._get("/sync/pull")
pl_names = [p.get("name") for p in pull_result.get("my_playlists", [])] pl_names = [p.get("name") for p in pull_result.get("my_playlists", [])]
logger.info(f"Pull modtog {len(pl_names)} playlister: {pl_names}") logger.info(f"Pull modtog {len(pl_names)} playlister: {pl_names}")
self._apply_pull(pull_result) self._apply_pull(pull_result)
# Ryd deleted_playlists nu pull er kørt og filtreringen er sket # Fjern soft-slettede playlister permanent nu serveren er opdateret
if deleted: if deleted:
conn = sqlite3.connect(self._db_path) conn = sqlite3.connect(self._db_path)
conn.execute("DELETE FROM deleted_playlists") conn.execute(
"DELETE FROM playlists WHERE is_deleted=1 AND api_project_id IS NOT NULL"
)
conn.commit() conn.commit()
conn.close() conn.close()
logger.info(f"deleted_playlists ryddet efter pull") logger.info(f"Soft-slettede playlister fjernet lokalt efter sync")
pl_count = len(pull_result.get("my_playlists", [])) pl_count = len(pull_result.get("my_playlists", []))
logger.info( logger.info(
@@ -252,13 +257,13 @@ class SyncManager:
# Slettede playlister — skal fjernes fra serveren. # Slettede playlister — skal fjernes fra serveren.
# Serveren forventer en liste af strings (api_project_id). # Serveren forventer en liste af strings (api_project_id).
# Playlister uden api_project_id har aldrig nået serveren — ignorer dem. # Kun playlister der faktisk er nået serveren (har api_project_id).
deleted = [ deleted = [
row["api_project_id"] row["api_project_id"]
for row in conn.execute( for row in conn.execute(
"SELECT api_project_id FROM deleted_playlists" "SELECT api_project_id FROM playlists "
"WHERE is_deleted=1 AND api_project_id IS NOT NULL AND api_project_id != ''"
).fetchall() ).fetchall()
if row["api_project_id"]
] ]
conn.close() conn.close()
@@ -296,19 +301,14 @@ class SyncManager:
d.get("stepsheet_url",""), existing["id"])) d.get("stepsheet_url",""), existing["id"]))
# Importer/opdater egne playlister fra server — server er sandhed # Importer/opdater egne playlister fra server — server er sandhed
# Hent både navne og server-IDs på lokalt slettede playlister # Hent server-IDs på soft-slettede playlister så vi springer dem over
try: deleted_server_ids = {
deleted_rows = conn.execute( row["api_project_id"]
"SELECT name, api_project_id FROM deleted_playlists" for row in conn.execute(
"SELECT api_project_id FROM playlists "
"WHERE is_deleted=1 AND api_project_id IS NOT NULL"
).fetchall() ).fetchall()
deleted_server_ids = { }
row["api_project_id"] for row in deleted_rows
if row["api_project_id"]
}
deleted_names = {row["name"] for row in deleted_rows}
except Exception:
deleted_server_ids = set()
deleted_names = set()
for pl in data.get("my_playlists", []): for pl in data.get("my_playlists", []):
server_id = pl.get("server_id") server_id = pl.get("server_id")
@@ -316,12 +316,9 @@ class SyncManager:
if not server_id or not name: if not server_id or not name:
continue continue
# Spring over hvis listen er markeret som slettet lokalt # Spring over hvis listen er soft-slettet lokalt
# Tjek på server-ID (præcist) og navn (fallback)
if server_id in deleted_server_ids: if server_id in deleted_server_ids:
continue continue
if name in deleted_names and not server_id:
continue
existing = conn.execute( existing = conn.execute(
"SELECT id FROM playlists WHERE api_project_id=?", (server_id,) "SELECT id FROM playlists WHERE api_project_id=?", (server_id,)