Files
siftlode/backend/app/plex/rollup.py
T
peter f484de1008 refactor(plex): address /code-review on the R12 S1 split
Cleanup only, no behaviour change (ruff clean + pytest 193):
- imports hoisted above `router =` and grouped/sorted (isort) across the split
  files; ruff-format pass restores the 2-blank-line PEP8 spacing the generator
  collapsed to 1.
- the shared on-disk cache root moves to a neutral app/plex/cache.py, so
  subtitles.py no longer imports it from imgcache.py (drops the cross-import).
- the former plex.py module docstring's access-model note (read endpoints open
  to any authed user; admin endpoints test/sync) is restored in the package
  __init__.py.
2026-08-11 21:46:43 +02:00

77 lines
2.8 KiB
Python

"""Roll a show's episodes up into a single watch-state (new | in_progress | watched) + on-deck.
Drives the show/season grid badges and the Resume / Play-from-start / mark-all buttons.
"""
from sqlalchemy import and_, case, func, or_
from sqlalchemy.orm import Session, aliased
from app.models import PlexItem, PlexState
def show_status(total: int, watched: int, inprog: int) -> str:
"""Roll a show's episodes up into a single watch state: fully watched, partially started, or new."""
if total and watched >= total:
return "watched"
if (watched or 0) > 0 or (inprog or 0) > 0:
return "in_progress"
return "new"
def rollup(cards: list[dict]) -> dict:
"""From ORDERED episode cards → the aggregate watch status, the "on deck" episode to Resume (the
last in-progress one, else the first unwatched), the first episode (for Play from the start), and
the episode count. Drives the show/season Resume + Play + mark-all buttons."""
total = len(cards)
watched = sum(1 for c in cards if c.get("status") == "watched")
inprog = [
c
for c in cards
if (c.get("position_seconds") or 0) > 0 and c.get("status") != "watched"
]
resume = None
if inprog:
resume = inprog[-1] # continue the latest-started episode
else:
resume = next((c for c in cards if c.get("status") != "watched"), None)
return {
"status": show_status(total, watched, len(inprog)),
"resume": resume,
"first": cards[0] if cards else None,
"episode_count": total,
}
def agg_subquery(db: Session, user_id: int):
"""Per-show episode counts for a user: total, watched, in-progress. Reused to (a) filter the show
grid by aggregate watch-state and (b) compute each card's badge."""
ps = aliased(PlexState)
return (
db.query(
PlexItem.show_id.label("show_id"),
func.count(PlexItem.id).label("total"),
func.count(case((ps.status == "watched", 1))).label("watched"),
func.count(
case(
(
and_(
ps.position_seconds > 0,
or_(ps.status.is_(None), ps.status != "watched"),
),
1,
)
)
).label("inprog"),
)
.outerjoin(ps, and_(ps.item_id == PlexItem.id, ps.user_id == user_id))
.filter(PlexItem.kind == "episode", PlexItem.show_id.isnot(None))
.group_by(PlexItem.show_id)
)
def state_map(db: Session, user_id: int, show_ids: list[int]) -> dict[int, str]:
if not show_ids:
return {}
rows = agg_subquery(db, user_id).filter(PlexItem.show_id.in_(show_ids)).all()
return {r.show_id: show_status(r.total, r.watched, r.inprog) for r in rows}