Bedre sync
This commit is contained in:
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -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,)
|
||||||
|
|||||||
Reference in New Issue
Block a user