Completes the plex-package 1.x->2.0 conversion, behaviour-equivalent (full suite 253 passed, 2 skipped; the characterization net locks every path): - routes/plex/browse.py (61 sites): libraries, facets (the wsq builder + jsonb genre unnest), collections, playlists (+ bulk add/remove/reorder), seasons, people/cards, collection strips, episode-nav, item detail, and the unified_library union builder. - plex/rollup.py: agg_subquery now returns a select(); state_map executes it (the atomic unit coupled to unified_library via .subquery()). Query builders -> select(); .filter/.filter_by -> .where; terminals -> db.execute(...).scalars()/.all()/db.scalar(...); bulk .delete()/.update() -> execute(delete()/update()). Zero db.query()/.filter_by remain in the package.
79 lines
2.8 KiB
Python
79 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_, select
|
|
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 (
|
|
select(
|
|
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))
|
|
.where(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 = db.execute(
|
|
agg_subquery(db, user_id).where(PlexItem.show_id.in_(show_ids))
|
|
).all()
|
|
return {r.show_id: show_status(r.total, r.watched, r.inprog) for r in rows}
|