The first checkpoint of an item and the final flush beside it both read no state and both inserted; the loser failed on uq_plex_user_item. Two first saves each made a Watch later list. Both are created by an ON CONFLICT DO NOTHING insert and then read; a partial unique index holds one Watch later per user, and the migration folds an existing second list into the first. Plex keeps one per-item state map (rollup.item_state_map).
56 lines
1.9 KiB
Python
56 lines
1.9 KiB
Python
"""One Watch later list per user, held by the database
|
|
|
|
Revision ID: 0112_one_watch_later
|
|
Revises: 0111_audit_request_id
|
|
Create Date: 2026-10-20
|
|
|
|
Watch later was made on a user's first save by a read then an insert, so two first saves at once
|
|
each made a list, and the reader then picked between them at random. A partial unique index holds
|
|
"one per user" now. An instance that already has a second list keeps the oldest: the others' videos
|
|
move into it (after its own, in their order; one it already holds is not added twice), then they go.
|
|
"""
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "0112_one_watch_later"
|
|
down_revision: str | None = "0111_audit_request_id"
|
|
branch_labels: str | None = None
|
|
depends_on: str | None = None
|
|
|
|
_EXTRA = """
|
|
SELECT p.id, k.keep_id
|
|
FROM playlists p
|
|
JOIN (
|
|
SELECT user_id, min(id) AS keep_id FROM playlists WHERE kind = 'watch_later' GROUP BY user_id
|
|
) k ON k.user_id = p.user_id
|
|
WHERE p.kind = 'watch_later' AND p.id <> k.keep_id
|
|
"""
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.execute(
|
|
f"""
|
|
INSERT INTO playlist_items (playlist_id, video_id, position, added_at)
|
|
SELECT e.keep_id, i.video_id,
|
|
(SELECT coalesce(max(position), -1) FROM playlist_items WHERE playlist_id = e.keep_id)
|
|
+ row_number() OVER (PARTITION BY e.keep_id ORDER BY i.playlist_id, i.position, i.id),
|
|
i.added_at
|
|
FROM playlist_items i
|
|
JOIN ({_EXTRA}) e ON e.id = i.playlist_id
|
|
ON CONFLICT ON CONSTRAINT uq_playlist_video DO NOTHING
|
|
"""
|
|
)
|
|
op.execute(f"DELETE FROM playlists WHERE id IN (SELECT id FROM ({_EXTRA}) e)")
|
|
op.create_index(
|
|
"uq_playlist_watch_later",
|
|
"playlists",
|
|
["user_id"],
|
|
unique=True,
|
|
postgresql_where=sa.text("kind = 'watch_later'"),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("uq_playlist_watch_later", table_name="playlists")
|