fix(plex): a null progress timestamp must sort a rail row last, not raise
The Continue-watching and Surprise rails sorted on `(ts is not None, ts)`. That key LOOKS null-safe and is not: two rows that both lack a timestamp compare equal on the flag and then compare `None < None`, which raises — so a show whose rollup has no `resumed_at`, or a movie state written before `progress_updated_at` existed, could 500 the rail instead of sorting last. Replaced with an explicit epoch sentinel, and named it beside the other rail constants so the sibling rail E24 adds cannot re-derive the same broken key.
This commit is contained in:
@@ -14,6 +14,8 @@ grid uses, applied to each rail's movie + show query (a rail may legitimately co
|
|||||||
the client hides). Genre-language matches the grid; duration is movie-only.
|
the client hides). Genre-language matches the grid; duration is movie-only.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
from sqlalchemy import and_, func, or_, select
|
from sqlalchemy import and_, func, or_, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -27,12 +29,13 @@ from app.plex.query import (
|
|||||||
LibraryFilters,
|
LibraryFilters,
|
||||||
apply_meta_filters,
|
apply_meta_filters,
|
||||||
apply_show_state_filter,
|
apply_show_state_filter,
|
||||||
|
content_libs,
|
||||||
genres_col,
|
genres_col,
|
||||||
meta_filter_conds,
|
meta_filter_conds,
|
||||||
top_art_keys,
|
top_art_keys,
|
||||||
)
|
)
|
||||||
from app.plex.tmdb import resolve_recs
|
from app.plex.tmdb import resolve_recs
|
||||||
from app.routes.plex._shared import _content_libs, _norm_lang
|
from app.routes.plex._shared import _norm_lang
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/plex", tags=["plex"])
|
router = APIRouter(prefix="/api/plex", tags=["plex"])
|
||||||
|
|
||||||
@@ -40,10 +43,16 @@ RAIL_LIMIT = 20
|
|||||||
GENRE_TILES = 12
|
GENRE_TILES = 12
|
||||||
SURPRISE_PER_KIND = 3 # Surprise-me: at most this many movies + this many shows (6 total)
|
SURPRISE_PER_KIND = 3 # Surprise-me: at most this many movies + this many shows (6 total)
|
||||||
|
|
||||||
|
# Sort sentinel for a row with no timestamp. The `(ts is not None, ts)` key this replaced LOOKS
|
||||||
|
# NULL-safe and is not: two null-timestamped rows compare equal on the flag and then compare
|
||||||
|
# `None < None`, which raises — so a show whose rollup has no `resumed_at`, or a movie state written
|
||||||
|
# before `progress_updated_at` existed, could 500 a rail rather than sort last (E24).
|
||||||
|
_NO_TS = datetime.min.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def _scope_libs(db: Session, scope: str) -> tuple[list[int], list[int]]:
|
def _scope_libs(db: Session, scope: str) -> tuple[list[int], list[int]]:
|
||||||
"""The movie/show library ids visible for this scope (empty list = out of scope)."""
|
"""The movie/show library ids visible for this scope (empty list = out of scope)."""
|
||||||
movie_ids, show_ids = _content_libs(db)
|
movie_ids, show_ids = content_libs(db)
|
||||||
return (
|
return (
|
||||||
movie_ids if scope in ("movie", "both") else [],
|
movie_ids if scope in ("movie", "both") else [],
|
||||||
show_ids if scope in ("show", "both") else [],
|
show_ids if scope in ("show", "both") else [],
|
||||||
@@ -170,7 +179,7 @@ def _recently_watched(
|
|||||||
)
|
)
|
||||||
for ep, st, sh_title, sh_rk in db.execute(eq).all():
|
for ep, st, sh_title, sh_rk in db.execute(eq).all():
|
||||||
rows.append((st.watched_at, leaf_card(ep, st, show_title=sh_title, show_rk=sh_rk)))
|
rows.append((st.watched_at, leaf_card(ep, st, show_title=sh_title, show_rk=sh_rk)))
|
||||||
rows.sort(key=lambda r: (r[0] is not None, r[0]), reverse=True)
|
rows.sort(key=lambda r: r[0] or _NO_TS, reverse=True)
|
||||||
return [card for _, card in rows[:RAIL_LIMIT]]
|
return [card for _, card in rows[:RAIL_LIMIT]]
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +243,7 @@ def _continue_watching(
|
|||||||
for sh, resumed_at in srows:
|
for sh, resumed_at in srows:
|
||||||
rows.append((resumed_at, show_card(sh, "in_progress")))
|
rows.append((resumed_at, show_card(sh, "in_progress")))
|
||||||
|
|
||||||
rows.sort(key=lambda r: (r[0] is not None, r[0]), reverse=True)
|
rows.sort(key=lambda r: r[0] or _NO_TS, reverse=True)
|
||||||
return [card for _, card in rows[:RAIL_LIMIT]]
|
return [card for _, card in rows[:RAIL_LIMIT]]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user