65 lines
2.1 KiB
Python
65 lines
2.1 KiB
Python
|
|
"""record a synced-state fingerprint per playlist
|
||
|
|
|
||
|
|
Revision ID: 0013_playlist_fingerprint
|
||
|
|
Revises: 0012_watch_later
|
||
|
|
Create Date: 2026-06-15
|
||
|
|
|
||
|
|
Adds playlists.synced_fingerprint — a SHA-256 of the playlist's name + ordered video ids
|
||
|
|
as last synced from / pushed to YouTube. `dirty` is then derived by comparing the current
|
||
|
|
fingerprint to this baseline, so manually restoring the original order clears dirty without
|
||
|
|
a YouTube read. Backfills the baseline for already-clean linked playlists (the current
|
||
|
|
state == the synced state for them); dirty ones are left NULL so they stay dirty until the
|
||
|
|
next real sync establishes a baseline.
|
||
|
|
"""
|
||
|
|
import hashlib
|
||
|
|
from typing import Sequence, Union
|
||
|
|
|
||
|
|
import sqlalchemy as sa
|
||
|
|
from alembic import op
|
||
|
|
|
||
|
|
revision: str = "0013_playlist_fingerprint"
|
||
|
|
down_revision: Union[str, None] = "0012_watch_later"
|
||
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
||
|
|
depends_on: Union[str, Sequence[str], None] = None
|
||
|
|
|
||
|
|
|
||
|
|
def _fingerprint(name: str, ids: list[str]) -> str:
|
||
|
|
h = hashlib.sha256()
|
||
|
|
h.update((name or "").encode())
|
||
|
|
h.update(b"\n")
|
||
|
|
h.update(",".join(ids).encode())
|
||
|
|
return h.hexdigest()
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
op.add_column(
|
||
|
|
"playlists", sa.Column("synced_fingerprint", sa.String(), nullable=True)
|
||
|
|
)
|
||
|
|
bind = op.get_bind()
|
||
|
|
# Backfill clean, YouTube-linked playlists: their current state IS the synced state.
|
||
|
|
rows = bind.execute(
|
||
|
|
sa.text(
|
||
|
|
"SELECT id, name FROM playlists "
|
||
|
|
"WHERE yt_playlist_id IS NOT NULL AND dirty = false"
|
||
|
|
)
|
||
|
|
).fetchall()
|
||
|
|
for pid, name in rows:
|
||
|
|
ids = [
|
||
|
|
r[0]
|
||
|
|
for r in bind.execute(
|
||
|
|
sa.text(
|
||
|
|
"SELECT video_id FROM playlist_items "
|
||
|
|
"WHERE playlist_id = :pid ORDER BY position"
|
||
|
|
),
|
||
|
|
{"pid": pid},
|
||
|
|
).fetchall()
|
||
|
|
]
|
||
|
|
bind.execute(
|
||
|
|
sa.text("UPDATE playlists SET synced_fingerprint = :fp WHERE id = :pid"),
|
||
|
|
{"fp": _fingerprint(name, ids), "pid": pid},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def downgrade() -> None:
|
||
|
|
op.drop_column("playlists", "synced_fingerprint")
|