feat(playlists): derive dirty from a synced-state fingerprint

Store a SHA-256 of each playlist's name + ordered video ids as last synced from /
pushed to YouTube (migration 0013, backfilled for already-clean linked playlists).
'dirty' is now recomputed on every edit by comparing the current fingerprint to that
baseline instead of being stickily set true. So manually restoring the original order
(or undo back to it) clears dirty on its own — the Reset to YouTube button disappears
and no YouTube read is needed. Baseline is set on read-sync, repull (reset), and a
clean push; a missing baseline counts as dirty (unknown YT state). Verified the
migration's fingerprint matches the app's runtime fingerprint exactly.
This commit is contained in:
npeter83 2026-06-15 23:00:56 +02:00
parent a2ad555ef3
commit 6330ac3184
4 changed files with 106 additions and 14 deletions

View file

@ -13,6 +13,7 @@ from app.routes.feed import _saved_expr, _serialize
from app.sync.playlists import (
plan_push,
push_playlist,
recompute_dirty,
repull_playlist,
sync_user_playlists,
)
@ -28,14 +29,6 @@ def _own_playlist(db: Session, user: User, playlist_id: int) -> Playlist:
return pl
def _mark_dirty(pl: Playlist) -> None:
"""Flag a YouTube-linked playlist as having unpushed local edits, so the UI can offer
a 'Sync to YouTube' and the read-sync won't clobber them. Covers both exported local
playlists and edited YouTube mirrors; no-op for un-linked lists and Watch later."""
if pl.kind != "watch_later" and pl.yt_playlist_id:
pl.dirty = True
def _summary(
db: Session,
pl: Playlist,
@ -399,7 +392,7 @@ def rename_playlist(
raise HTTPException(status_code=400, detail="name cannot be empty")
if name != pl.name:
pl.name = name
_mark_dirty(pl)
recompute_dirty(db, pl)
db.commit()
count = db.scalar(
select(func.count()).where(PlaylistItem.playlist_id == pl.id)
@ -463,7 +456,7 @@ def add_item(
db.add(
PlaylistItem(playlist_id=pl.id, video_id=video_id, position=next_pos)
)
_mark_dirty(pl)
recompute_dirty(db, pl)
db.commit()
return {"playlist_id": pl.id, "video_id": video_id}
@ -483,7 +476,7 @@ def remove_item(
)
if item is not None:
db.delete(item)
_mark_dirty(pl)
recompute_dirty(db, pl)
db.commit()
return {"playlist_id": pl.id, "video_id": video_id}
@ -509,6 +502,6 @@ def reorder_items(
if it is not None:
it.position = pos
pos += 1
_mark_dirty(pl)
recompute_dirty(db, pl)
db.commit()
return {"playlist_id": pl.id, "count": pos}