A season marked watched, a pull from Plex and a YouTube resume point made their first row by a read then an add, and failed on the unique key when a first checkpoint or the watched mark made it in between. They go through the same insert-then-read as the checkpoint (the resume point is one upsert); a pull that meets a local change still waiting for its push leaves it. Folding duplicate Watch later lists moved their videos but not a Home pin, which the cascade then deleted; the first pin of a dropped copy moves onto the kept list. Marking one notice read trimmed before its own flag was flushed, so the trim left one past the cap. The route test now crosses the cap and reads the result from a session of its own.
71 lines
2.5 KiB
Python
71 lines
2.5 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), a Home pin
|
|
of one of them moves to it (unless it is pinned itself; of several, the first on the shelf), then
|
|
they go — a pin left on them goes with them, as the kept list already holds one.
|
|
"""
|
|
|
|
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"""
|
|
UPDATE comfort_pins c SET playlist_id = m.keep_id
|
|
FROM (
|
|
SELECT DISTINCT ON (e.keep_id) p.id AS pin_id, e.keep_id
|
|
FROM comfort_pins p
|
|
JOIN ({_EXTRA}) e ON e.id = p.playlist_id
|
|
WHERE NOT EXISTS (SELECT 1 FROM comfort_pins k WHERE k.playlist_id = e.keep_id)
|
|
ORDER BY e.keep_id, p.position, p.id
|
|
) m
|
|
WHERE c.id = m.pin_id
|
|
"""
|
|
)
|
|
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")
|