refactor(plex): convert browse.py + rollup db.query() to 2.0 select() (wave 2)
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.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
Drives the show/season grid badges and the Resume / Play-from-start / mark-all buttons.
|
Drives the show/season grid badges and the Resume / Play-from-start / mark-all buttons.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy import and_, case, func, or_
|
from sqlalchemy import and_, case, func, or_, select
|
||||||
from sqlalchemy.orm import Session, aliased
|
from sqlalchemy.orm import Session, aliased
|
||||||
|
|
||||||
from app.models import PlexItem, PlexState
|
from app.models import PlexItem, PlexState
|
||||||
@@ -47,7 +47,7 @@ def agg_subquery(db: Session, user_id: int):
|
|||||||
grid by aggregate watch-state and (b) compute each card's badge."""
|
grid by aggregate watch-state and (b) compute each card's badge."""
|
||||||
ps = aliased(PlexState)
|
ps = aliased(PlexState)
|
||||||
return (
|
return (
|
||||||
db.query(
|
select(
|
||||||
PlexItem.show_id.label("show_id"),
|
PlexItem.show_id.label("show_id"),
|
||||||
func.count(PlexItem.id).label("total"),
|
func.count(PlexItem.id).label("total"),
|
||||||
func.count(case((ps.status == "watched", 1))).label("watched"),
|
func.count(case((ps.status == "watched", 1))).label("watched"),
|
||||||
@@ -64,7 +64,7 @@ def agg_subquery(db: Session, user_id: int):
|
|||||||
).label("inprog"),
|
).label("inprog"),
|
||||||
)
|
)
|
||||||
.outerjoin(ps, and_(ps.item_id == PlexItem.id, ps.user_id == user_id))
|
.outerjoin(ps, and_(ps.item_id == PlexItem.id, ps.user_id == user_id))
|
||||||
.filter(PlexItem.kind == "episode", PlexItem.show_id.isnot(None))
|
.where(PlexItem.kind == "episode", PlexItem.show_id.isnot(None))
|
||||||
.group_by(PlexItem.show_id)
|
.group_by(PlexItem.show_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,5 +72,7 @@ def agg_subquery(db: Session, user_id: int):
|
|||||||
def state_map(db: Session, user_id: int, show_ids: list[int]) -> dict[int, str]:
|
def state_map(db: Session, user_id: int, show_ids: list[int]) -> dict[int, str]:
|
||||||
if not show_ids:
|
if not show_ids:
|
||||||
return {}
|
return {}
|
||||||
rows = agg_subquery(db, user_id).filter(PlexItem.show_id.in_(show_ids)).all()
|
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}
|
return {r.show_id: show_status(r.total, r.watched, r.inprog) for r in rows}
|
||||||
|
|||||||
+297
-192
@@ -8,7 +8,20 @@ from datetime import datetime, timedelta, timezone
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import Integer, String, and_, case, cast, func, literal, null, or_
|
from sqlalchemy import (
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
and_,
|
||||||
|
case,
|
||||||
|
cast,
|
||||||
|
delete,
|
||||||
|
func,
|
||||||
|
literal,
|
||||||
|
null,
|
||||||
|
or_,
|
||||||
|
select,
|
||||||
|
update,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import Session, aliased
|
from sqlalchemy.orm import Session, aliased
|
||||||
|
|
||||||
from app import sysconfig
|
from app import sysconfig
|
||||||
@@ -45,20 +58,20 @@ def list_libraries(
|
|||||||
user: User = Depends(current_user), db: Session = Depends(get_db)
|
user: User = Depends(current_user), db: Session = Depends(get_db)
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""The mirrored libraries the user can browse (drives the Plex scope selector)."""
|
"""The mirrored libraries the user can browse (drives the Plex scope selector)."""
|
||||||
libs = (
|
libs = db.execute(
|
||||||
db.query(PlexLibrary).order_by(PlexLibrary.kind.desc(), PlexLibrary.title).all()
|
select(PlexLibrary).order_by(PlexLibrary.kind.desc(), PlexLibrary.title)
|
||||||
)
|
).scalars().all()
|
||||||
out = []
|
out = []
|
||||||
for lib in libs:
|
for lib in libs:
|
||||||
if lib.kind == "movie":
|
if lib.kind == "movie":
|
||||||
count = (
|
count = db.scalar(
|
||||||
db.query(func.count(PlexItem.id))
|
select(func.count(PlexItem.id)).where(
|
||||||
.filter_by(library_id=lib.id, kind="movie")
|
PlexItem.library_id == lib.id, PlexItem.kind == "movie"
|
||||||
.scalar()
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
count = (
|
count = db.scalar(
|
||||||
db.query(func.count(PlexShow.id)).filter_by(library_id=lib.id).scalar()
|
select(func.count(PlexShow.id)).where(PlexShow.library_id == lib.id)
|
||||||
)
|
)
|
||||||
out.append(
|
out.append(
|
||||||
{
|
{
|
||||||
@@ -188,7 +201,7 @@ def _meta_filter_conds(model, p: dict) -> list:
|
|||||||
|
|
||||||
def _apply_meta_filters(q, model, p: dict):
|
def _apply_meta_filters(q, model, p: dict):
|
||||||
conds = _meta_filter_conds(model, p)
|
conds = _meta_filter_conds(model, p)
|
||||||
return q.filter(*conds) if conds else q
|
return q.where(*conds) if conds else q
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -252,7 +265,9 @@ def unified_library(
|
|||||||
episodes come back in a separate `episodes` list (grouped result — the 'Episodes' section)."""
|
episodes come back in a separate `episodes` list (grouped result — the 'Episodes' section)."""
|
||||||
offset = max(0, offset)
|
offset = max(0, offset)
|
||||||
p = f.as_dict()
|
p = f.as_dict()
|
||||||
libs = db.query(PlexLibrary).filter_by(enabled=True).all()
|
libs = db.execute(
|
||||||
|
select(PlexLibrary).where(PlexLibrary.enabled.is_(True))
|
||||||
|
).scalars().all()
|
||||||
movie_lib_ids = [lb.id for lb in libs if lb.kind == "movie"]
|
movie_lib_ids = [lb.id for lb in libs if lb.kind == "movie"]
|
||||||
show_lib_ids = [lb.id for lb in libs if lb.kind == "show"]
|
show_lib_ids = [lb.id for lb in libs if lb.kind == "show"]
|
||||||
want_movies = scope in ("movie", "both") and bool(movie_lib_ids)
|
want_movies = scope in ("movie", "both") and bool(movie_lib_ids)
|
||||||
@@ -276,7 +291,7 @@ def unified_library(
|
|||||||
else_="new",
|
else_="new",
|
||||||
)
|
)
|
||||||
mq = (
|
mq = (
|
||||||
db.query(
|
select(
|
||||||
PlexItem.rating_key.label("rk"),
|
PlexItem.rating_key.label("rk"),
|
||||||
literal("movie").label("kind"),
|
literal("movie").label("kind"),
|
||||||
PlexItem.title.label("title"),
|
PlexItem.title.label("title"),
|
||||||
@@ -296,28 +311,28 @@ def unified_library(
|
|||||||
).label("rank"),
|
).label("rank"),
|
||||||
)
|
)
|
||||||
.outerjoin(st, and_(st.item_id == PlexItem.id, st.user_id == user.id))
|
.outerjoin(st, and_(st.item_id == PlexItem.id, st.user_id == user.id))
|
||||||
.filter(PlexItem.kind == "movie", PlexItem.library_id.in_(movie_lib_ids))
|
.where(PlexItem.kind == "movie", PlexItem.library_id.in_(movie_lib_ids))
|
||||||
)
|
)
|
||||||
if show == "watched":
|
if show == "watched":
|
||||||
mq = mq.filter(st.status == "watched")
|
mq = mq.where(st.status == "watched")
|
||||||
elif show == "in_progress":
|
elif show == "in_progress":
|
||||||
mq = mq.filter(
|
mq = mq.where(
|
||||||
st.position_seconds > 0,
|
st.position_seconds > 0,
|
||||||
or_(st.status.is_(None), st.status != "watched"),
|
or_(st.status.is_(None), st.status != "watched"),
|
||||||
)
|
)
|
||||||
elif show == "unwatched":
|
elif show == "unwatched":
|
||||||
mq = mq.filter(
|
mq = mq.where(
|
||||||
or_(st.status.is_(None), st.status.notin_(["watched", "hidden"]))
|
or_(st.status.is_(None), st.status.notin_(["watched", "hidden"]))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
mq = mq.filter(or_(st.status.is_(None), st.status != "hidden"))
|
mq = mq.where(or_(st.status.is_(None), st.status != "hidden"))
|
||||||
mq = _apply_meta_filters(mq, PlexItem, p)
|
mq = _apply_meta_filters(mq, PlexItem, p)
|
||||||
if f.duration_min is not None:
|
if f.duration_min is not None:
|
||||||
mq = mq.filter(PlexItem.duration_s >= f.duration_min)
|
mq = mq.where(PlexItem.duration_s >= f.duration_min)
|
||||||
if f.duration_max is not None:
|
if f.duration_max is not None:
|
||||||
mq = mq.filter(PlexItem.duration_s <= f.duration_max)
|
mq = mq.where(PlexItem.duration_s <= f.duration_max)
|
||||||
if tsq is not None:
|
if tsq is not None:
|
||||||
mq = mq.filter(PlexItem.search_vector.op("@@")(tsq))
|
mq = mq.where(PlexItem.search_vector.op("@@")(tsq))
|
||||||
selects.append(mq)
|
selects.append(mq)
|
||||||
if want_shows:
|
if want_shows:
|
||||||
agg = rollup.agg_subquery(db, user.id).subquery()
|
agg = rollup.agg_subquery(db, user.id).subquery()
|
||||||
@@ -330,7 +345,7 @@ def unified_library(
|
|||||||
else_="new",
|
else_="new",
|
||||||
)
|
)
|
||||||
sq = (
|
sq = (
|
||||||
db.query(
|
select(
|
||||||
PlexShow.rating_key.label("rk"),
|
PlexShow.rating_key.label("rk"),
|
||||||
literal("show").label("kind"),
|
literal("show").label("kind"),
|
||||||
PlexShow.title.label("title"),
|
PlexShow.title.label("title"),
|
||||||
@@ -350,17 +365,17 @@ def unified_library(
|
|||||||
).label("rank"),
|
).label("rank"),
|
||||||
)
|
)
|
||||||
.outerjoin(agg, agg.c.show_id == PlexShow.id)
|
.outerjoin(agg, agg.c.show_id == PlexShow.id)
|
||||||
.filter(PlexShow.library_id.in_(show_lib_ids))
|
.where(PlexShow.library_id.in_(show_lib_ids))
|
||||||
)
|
)
|
||||||
if show == "watched":
|
if show == "watched":
|
||||||
sq = sq.filter(tot > 0, wat >= tot)
|
sq = sq.where(tot > 0, wat >= tot)
|
||||||
elif show == "in_progress":
|
elif show == "in_progress":
|
||||||
sq = sq.filter(or_(wat > 0, inp > 0), wat < tot)
|
sq = sq.where(or_(wat > 0, inp > 0), wat < tot)
|
||||||
elif show == "unwatched":
|
elif show == "unwatched":
|
||||||
sq = sq.filter(wat == 0, inp == 0)
|
sq = sq.where(wat == 0, inp == 0)
|
||||||
sq = _apply_meta_filters(sq, PlexShow, p)
|
sq = _apply_meta_filters(sq, PlexShow, p)
|
||||||
if tsq is not None:
|
if tsq is not None:
|
||||||
sq = sq.filter(PlexShow.search_vector.op("@@")(tsq))
|
sq = sq.where(PlexShow.search_vector.op("@@")(tsq))
|
||||||
selects.append(sq)
|
selects.append(sq)
|
||||||
|
|
||||||
if not selects:
|
if not selects:
|
||||||
@@ -374,7 +389,7 @@ def unified_library(
|
|||||||
}
|
}
|
||||||
union = selects[0] if len(selects) == 1 else selects[0].union_all(*selects[1:])
|
union = selects[0] if len(selects) == 1 else selects[0].union_all(*selects[1:])
|
||||||
u = union.subquery()
|
u = union.subquery()
|
||||||
total = db.query(func.count()).select_from(u).scalar() or 0
|
total = db.scalar(select(func.count()).select_from(u)) or 0
|
||||||
|
|
||||||
if tsq is not None:
|
if tsq is not None:
|
||||||
order = [u.c.rank.desc()]
|
order = [u.c.rank.desc()]
|
||||||
@@ -388,7 +403,9 @@ def unified_library(
|
|||||||
}.get(sort, u.c.added_at)
|
}.get(sort, u.c.added_at)
|
||||||
order = [(col.asc() if sort_dir == "asc" else col.desc()).nullslast()]
|
order = [(col.asc() if sort_dir == "asc" else col.desc()).nullslast()]
|
||||||
order.append(u.c.rk.desc())
|
order.append(u.c.rk.desc())
|
||||||
rows = db.query(u).order_by(*order).offset(offset).limit(limit).all()
|
rows = db.execute(
|
||||||
|
select(u).order_by(*order).offset(offset).limit(limit)
|
||||||
|
).all()
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
@@ -422,21 +439,20 @@ def unified_library(
|
|||||||
episodes = []
|
episodes = []
|
||||||
if tsq is not None and want_shows:
|
if tsq is not None and want_shows:
|
||||||
ep_st = aliased(PlexState)
|
ep_st = aliased(PlexState)
|
||||||
eps = (
|
eps = db.execute(
|
||||||
db.query(PlexItem, ep_st, PlexShow.title.label("show_title"))
|
select(PlexItem, ep_st, PlexShow.title.label("show_title"))
|
||||||
.outerjoin(
|
.outerjoin(
|
||||||
ep_st, and_(ep_st.item_id == PlexItem.id, ep_st.user_id == user.id)
|
ep_st, and_(ep_st.item_id == PlexItem.id, ep_st.user_id == user.id)
|
||||||
)
|
)
|
||||||
.outerjoin(PlexShow, PlexShow.id == PlexItem.show_id)
|
.outerjoin(PlexShow, PlexShow.id == PlexItem.show_id)
|
||||||
.filter(
|
.where(
|
||||||
PlexItem.kind == "episode",
|
PlexItem.kind == "episode",
|
||||||
PlexItem.library_id.in_(show_lib_ids),
|
PlexItem.library_id.in_(show_lib_ids),
|
||||||
PlexItem.search_vector.op("@@")(tsq),
|
PlexItem.search_vector.op("@@")(tsq),
|
||||||
)
|
)
|
||||||
.order_by(func.ts_rank(PlexItem.search_vector, tsq).desc())
|
.order_by(func.ts_rank(PlexItem.search_vector, tsq).desc())
|
||||||
.limit(24)
|
.limit(24)
|
||||||
.all()
|
).all()
|
||||||
)
|
|
||||||
for e, s, show_title in eps:
|
for e, s, show_title in eps:
|
||||||
card = _episode_card(e, s)
|
card = _episode_card(e, s)
|
||||||
card["show_title"] = show_title
|
card["show_title"] = show_title
|
||||||
@@ -474,7 +490,9 @@ def facets(
|
|||||||
"duration_min": None,
|
"duration_min": None,
|
||||||
"duration_max": None,
|
"duration_max": None,
|
||||||
}
|
}
|
||||||
libs = db.query(PlexLibrary).filter_by(enabled=True).all()
|
libs = db.execute(
|
||||||
|
select(PlexLibrary).where(PlexLibrary.enabled.is_(True))
|
||||||
|
).scalars().all()
|
||||||
movie_ids = (
|
movie_ids = (
|
||||||
[lb.id for lb in libs if lb.kind == "movie"]
|
[lb.id for lb in libs if lb.kind == "movie"]
|
||||||
if scope in ("movie", "both")
|
if scope in ("movie", "both")
|
||||||
@@ -492,30 +510,30 @@ def facets(
|
|||||||
def wsq(query, model, ids, pp, kind_movie, with_dur):
|
def wsq(query, model, ids, pp, kind_movie, with_dur):
|
||||||
"""Apply the (self-excluded) metadata filters + library scope + the per-user watch-state to a
|
"""Apply the (self-excluded) metadata filters + library scope + the per-user watch-state to a
|
||||||
facet aggregation query, so the facets reflect exactly the visible result set."""
|
facet aggregation query, so the facets reflect exactly the visible result set."""
|
||||||
query = query.filter(model.library_id.in_(ids), *_meta_filter_conds(model, pp))
|
query = query.where(model.library_id.in_(ids), *_meta_filter_conds(model, pp))
|
||||||
if kind_movie:
|
if kind_movie:
|
||||||
query = query.filter(model.kind == "movie")
|
query = query.where(model.kind == "movie")
|
||||||
if with_dur and pp.get("duration_min") is not None:
|
if with_dur and pp.get("duration_min") is not None:
|
||||||
query = query.filter(model.duration_s >= pp["duration_min"])
|
query = query.where(model.duration_s >= pp["duration_min"])
|
||||||
if with_dur and pp.get("duration_max") is not None:
|
if with_dur and pp.get("duration_max") is not None:
|
||||||
query = query.filter(model.duration_s <= pp["duration_max"])
|
query = query.where(model.duration_s <= pp["duration_max"])
|
||||||
# Always join watch-state and mirror the grid's status branches (unified_library) exactly —
|
# Always join watch-state and mirror the grid's status branches (unified_library) exactly —
|
||||||
# hidden movies never appear in the grid, so they must not inflate facet counts/bounds either.
|
# hidden movies never appear in the grid, so they must not inflate facet counts/bounds either.
|
||||||
st = aliased(PlexState)
|
st = aliased(PlexState)
|
||||||
query = query.outerjoin(st, and_(st.item_id == model.id, st.user_id == uid))
|
query = query.outerjoin(st, and_(st.item_id == model.id, st.user_id == uid))
|
||||||
if show == "watched":
|
if show == "watched":
|
||||||
query = query.filter(st.status == "watched")
|
query = query.where(st.status == "watched")
|
||||||
elif show == "in_progress":
|
elif show == "in_progress":
|
||||||
query = query.filter(
|
query = query.where(
|
||||||
st.position_seconds > 0,
|
st.position_seconds > 0,
|
||||||
or_(st.status.is_(None), st.status != "watched"),
|
or_(st.status.is_(None), st.status != "watched"),
|
||||||
)
|
)
|
||||||
elif show == "unwatched":
|
elif show == "unwatched":
|
||||||
query = query.filter(
|
query = query.where(
|
||||||
or_(st.status.is_(None), st.status.notin_(["watched", "hidden"]))
|
or_(st.status.is_(None), st.status.notin_(["watched", "hidden"]))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
query = query.filter(or_(st.status.is_(None), st.status != "hidden"))
|
query = query.where(or_(st.status.is_(None), st.status != "hidden"))
|
||||||
else:
|
else:
|
||||||
if show in ("watched", "in_progress", "unwatched"):
|
if show in ("watched", "in_progress", "unwatched"):
|
||||||
agg = rollup.agg_subquery(db, uid).subquery()
|
agg = rollup.agg_subquery(db, uid).subquery()
|
||||||
@@ -524,11 +542,11 @@ def facets(
|
|||||||
wat = func.coalesce(agg.c.watched, 0)
|
wat = func.coalesce(agg.c.watched, 0)
|
||||||
inp = func.coalesce(agg.c.inprog, 0)
|
inp = func.coalesce(agg.c.inprog, 0)
|
||||||
if show == "watched":
|
if show == "watched":
|
||||||
query = query.filter(tot > 0, wat >= tot)
|
query = query.where(tot > 0, wat >= tot)
|
||||||
elif show == "in_progress":
|
elif show == "in_progress":
|
||||||
query = query.filter(or_(wat > 0, inp > 0), wat < tot)
|
query = query.where(or_(wat > 0, inp > 0), wat < tot)
|
||||||
else:
|
else:
|
||||||
query = query.filter(wat == 0, inp == 0)
|
query = query.where(wat == 0, inp == 0)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
genre_counts: dict[str, int] = {}
|
genre_counts: dict[str, int] = {}
|
||||||
@@ -553,66 +571,74 @@ def facets(
|
|||||||
# whole facets endpoint. Mirror the exact guard people.py already applies to the same columns.
|
# whole facets endpoint. Mirror the exact guard people.py already applies to the same columns.
|
||||||
expanded = (
|
expanded = (
|
||||||
wsq(
|
wsq(
|
||||||
db.query(func.jsonb_array_elements_text(model.genres).label("g")),
|
select(func.jsonb_array_elements_text(model.genres).label("g")),
|
||||||
model,
|
model,
|
||||||
ids,
|
ids,
|
||||||
genre_pp,
|
genre_pp,
|
||||||
kind_movie,
|
kind_movie,
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
.filter(func.jsonb_typeof(model.genres) == "array")
|
.where(func.jsonb_typeof(model.genres) == "array")
|
||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
for g, c in db.query(expanded.c.g, func.count()).group_by(expanded.c.g).all():
|
for g, c in db.execute(
|
||||||
|
select(expanded.c.g, func.count()).group_by(expanded.c.g)
|
||||||
|
).all():
|
||||||
genre_counts[g] = genre_counts.get(g, 0) + c
|
genre_counts[g] = genre_counts.get(g, 0) + c
|
||||||
# Content ratings — exclude its own.
|
# Content ratings — exclude its own.
|
||||||
crq = (
|
crq = (
|
||||||
wsq(
|
wsq(
|
||||||
db.query(model.content_rating, func.count()),
|
select(model.content_rating, func.count()),
|
||||||
model,
|
model,
|
||||||
ids,
|
ids,
|
||||||
{**p, "content_ratings": None},
|
{**p, "content_ratings": None},
|
||||||
kind_movie,
|
kind_movie,
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
.filter(model.content_rating.isnot(None))
|
.where(model.content_rating.isnot(None))
|
||||||
.group_by(model.content_rating)
|
.group_by(model.content_rating)
|
||||||
)
|
)
|
||||||
for cr, c in crq.all():
|
for cr, c in db.execute(crq).all():
|
||||||
cr_counts[cr] = cr_counts.get(cr, 0) + c
|
cr_counts[cr] = cr_counts.get(cr, 0) + c
|
||||||
# Year bounds — exclude the year filter.
|
# Year bounds — exclude the year filter.
|
||||||
by = wsq(
|
by = db.execute(
|
||||||
db.query(func.min(model.year), func.max(model.year)),
|
wsq(
|
||||||
model,
|
select(func.min(model.year), func.max(model.year)),
|
||||||
ids,
|
model,
|
||||||
{**p, "year_min": None, "year_max": None},
|
ids,
|
||||||
kind_movie,
|
{**p, "year_min": None, "year_max": None},
|
||||||
True,
|
kind_movie,
|
||||||
|
True,
|
||||||
|
)
|
||||||
).first()
|
).first()
|
||||||
if by[0] is not None:
|
if by[0] is not None:
|
||||||
years.append(by[0])
|
years.append(by[0])
|
||||||
if by[1] is not None:
|
if by[1] is not None:
|
||||||
years.append(by[1])
|
years.append(by[1])
|
||||||
# Rating max — exclude the rating filter.
|
# Rating max — exclude the rating filter.
|
||||||
br = wsq(
|
br = db.scalar(
|
||||||
db.query(func.max(model.rating)),
|
wsq(
|
||||||
model,
|
select(func.max(model.rating)),
|
||||||
ids,
|
model,
|
||||||
{**p, "rating_min": None},
|
ids,
|
||||||
kind_movie,
|
{**p, "rating_min": None},
|
||||||
True,
|
kind_movie,
|
||||||
).scalar()
|
True,
|
||||||
|
)
|
||||||
|
)
|
||||||
if br is not None:
|
if br is not None:
|
||||||
ratings.append(float(br))
|
ratings.append(float(br))
|
||||||
# Duration bounds (movies) — exclude the duration filter.
|
# Duration bounds (movies) — exclude the duration filter.
|
||||||
if kind_movie:
|
if kind_movie:
|
||||||
bd = wsq(
|
bd = db.execute(
|
||||||
db.query(func.min(model.duration_s), func.max(model.duration_s)),
|
wsq(
|
||||||
model,
|
select(func.min(model.duration_s), func.max(model.duration_s)),
|
||||||
ids,
|
model,
|
||||||
p,
|
ids,
|
||||||
kind_movie,
|
p,
|
||||||
False,
|
kind_movie,
|
||||||
|
False,
|
||||||
|
)
|
||||||
).first()
|
).first()
|
||||||
if bd[0] is not None:
|
if bd[0] is not None:
|
||||||
durs.append(bd[0])
|
durs.append(bd[0])
|
||||||
@@ -682,28 +708,39 @@ def collections(
|
|||||||
the UNION across all enabled libraries (the unified-scope sidebar picker, which has no single
|
the UNION across all enabled libraries (the unified-scope sidebar picker, which has no single
|
||||||
library selected)."""
|
library selected)."""
|
||||||
if library is not None:
|
if library is not None:
|
||||||
lib = db.query(PlexLibrary).filter_by(plex_key=str(library)).first()
|
lib = db.execute(
|
||||||
|
select(PlexLibrary).where(PlexLibrary.plex_key == str(library))
|
||||||
|
).scalars().first()
|
||||||
if lib is None:
|
if lib is None:
|
||||||
return {"collections": []}
|
return {"collections": []}
|
||||||
query = db.query(PlexCollection).filter(PlexCollection.library_id == lib.id)
|
query = select(PlexCollection).where(PlexCollection.library_id == lib.id)
|
||||||
else:
|
else:
|
||||||
enabled_ids = [lb.id for lb in db.query(PlexLibrary.id).filter_by(enabled=True)]
|
enabled_ids = list(
|
||||||
|
db.execute(
|
||||||
|
select(PlexLibrary.id).where(PlexLibrary.enabled.is_(True))
|
||||||
|
).scalars()
|
||||||
|
)
|
||||||
if not enabled_ids:
|
if not enabled_ids:
|
||||||
return {"collections": []}
|
return {"collections": []}
|
||||||
query = db.query(PlexCollection).filter(
|
query = select(PlexCollection).where(
|
||||||
PlexCollection.library_id.in_(enabled_ids)
|
PlexCollection.library_id.in_(enabled_ids)
|
||||||
)
|
)
|
||||||
term = (q or "").strip()
|
term = (q or "").strip()
|
||||||
if term:
|
if term:
|
||||||
query = query.filter(PlexCollection.title.ilike(f"%{_like_escape(term)}%"))
|
query = query.where(PlexCollection.title.ilike(f"%{_like_escape(term)}%"))
|
||||||
rows = query.order_by(
|
rows = db.execute(
|
||||||
PlexCollection.child_count.desc().nullslast(), func.lower(PlexCollection.title)
|
query.order_by(
|
||||||
).all()
|
PlexCollection.child_count.desc().nullslast(),
|
||||||
|
func.lower(PlexCollection.title),
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
return {"collections": [_collection_card(c) for c in rows]}
|
return {"collections": [_collection_card(c) for c in rows]}
|
||||||
|
|
||||||
|
|
||||||
def _editable_collection_or_403(db: Session, rating_key: str) -> PlexCollection:
|
def _editable_collection_or_403(db: Session, rating_key: str) -> PlexCollection:
|
||||||
col = db.query(PlexCollection).filter_by(rating_key=str(rating_key)).first()
|
col = db.execute(
|
||||||
|
select(PlexCollection).where(PlexCollection.rating_key == str(rating_key))
|
||||||
|
).scalars().first()
|
||||||
if col is None:
|
if col is None:
|
||||||
raise HTTPException(status_code=404, detail="Unknown collection")
|
raise HTTPException(status_code=404, detail="Unknown collection")
|
||||||
if not _collection_editable(col):
|
if not _collection_editable(col):
|
||||||
@@ -731,7 +768,9 @@ def create_collection(
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400, detail="title and item_rating_key are required"
|
status_code=400, detail="title and item_rating_key are required"
|
||||||
)
|
)
|
||||||
lib = db.query(PlexLibrary).filter_by(plex_key=library).first()
|
lib = db.execute(
|
||||||
|
select(PlexLibrary).where(PlexLibrary.plex_key == library)
|
||||||
|
).scalars().first()
|
||||||
if lib is None:
|
if lib is None:
|
||||||
raise HTTPException(status_code=404, detail="Unknown library")
|
raise HTTPException(status_code=404, detail="Unknown library")
|
||||||
try:
|
try:
|
||||||
@@ -843,7 +882,9 @@ def collection_set_editable(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
"""Mark an existing Plex collection as 'mine to edit' (or unmark). Smart / auto-list collections
|
"""Mark an existing Plex collection as 'mine to edit' (or unmark). Smart / auto-list collections
|
||||||
(IMDb/TMDb/…) can never be made editable — they're regenerated outside Plex."""
|
(IMDb/TMDb/…) can never be made editable — they're regenerated outside Plex."""
|
||||||
col = db.query(PlexCollection).filter_by(rating_key=str(rating_key)).first()
|
col = db.execute(
|
||||||
|
select(PlexCollection).where(PlexCollection.rating_key == str(rating_key))
|
||||||
|
).scalars().first()
|
||||||
if col is None:
|
if col is None:
|
||||||
raise HTTPException(status_code=404, detail="Unknown collection")
|
raise HTTPException(status_code=404, detail="Unknown collection")
|
||||||
want = body.editable
|
want = body.editable
|
||||||
@@ -870,11 +911,11 @@ def _playlist_card(pl: PlexPlaylist, count: int, thumb_rk: str | None) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _playlist_item_query(db: Session, pid: int):
|
def _playlist_item_query(pid: int):
|
||||||
return (
|
return (
|
||||||
db.query(PlexPlaylistItem, PlexItem)
|
select(PlexPlaylistItem, PlexItem)
|
||||||
.join(PlexItem, PlexItem.id == PlexPlaylistItem.item_id)
|
.join(PlexItem, PlexItem.id == PlexPlaylistItem.item_id)
|
||||||
.filter(PlexPlaylistItem.playlist_id == pid)
|
.where(PlexPlaylistItem.playlist_id == pid)
|
||||||
.order_by(PlexPlaylistItem.position, PlexPlaylistItem.id)
|
.order_by(PlexPlaylistItem.position, PlexPlaylistItem.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -892,42 +933,51 @@ def playlists(
|
|||||||
group it already holds) + a top-level `group_size`, for the 'add a whole season/show' dialog."""
|
group it already holds) + a top-level `group_size`, for the 'add a whole season/show' dialog."""
|
||||||
contains_id = None
|
contains_id = None
|
||||||
if contains:
|
if contains:
|
||||||
row = db.query(PlexItem.id).filter_by(rating_key=str(contains)).first()
|
contains_id = db.scalar(
|
||||||
contains_id = row[0] if row else None
|
select(PlexItem.id).where(PlexItem.rating_key == str(contains))
|
||||||
|
)
|
||||||
group_ids: set[int] | None = None
|
group_ids: set[int] | None = None
|
||||||
if contains_group is not None:
|
if contains_group is not None:
|
||||||
rks = [s for s in (contains_group or "").split(",") if s]
|
rks = [s for s in (contains_group or "").split(",") if s]
|
||||||
rows = db.query(PlexItem.id).filter(PlexItem.rating_key.in_(rks or [""])).all()
|
group_ids = set(
|
||||||
group_ids = {r[0] for r in rows}
|
db.execute(
|
||||||
|
select(PlexItem.id).where(PlexItem.rating_key.in_(rks or [""]))
|
||||||
|
).scalars()
|
||||||
|
)
|
||||||
out = []
|
out = []
|
||||||
for pl in (
|
for pl in db.execute(
|
||||||
db.query(PlexPlaylist)
|
select(PlexPlaylist)
|
||||||
.filter_by(user_id=user.id)
|
.where(PlexPlaylist.user_id == user.id)
|
||||||
.order_by(PlexPlaylist.updated_at.desc())
|
.order_by(PlexPlaylist.updated_at.desc())
|
||||||
):
|
).scalars():
|
||||||
count = (
|
count = (
|
||||||
db.query(func.count(PlexPlaylistItem.id))
|
db.scalar(
|
||||||
.filter_by(playlist_id=pl.id)
|
select(func.count(PlexPlaylistItem.id)).where(
|
||||||
.scalar()
|
PlexPlaylistItem.playlist_id == pl.id
|
||||||
|
)
|
||||||
|
)
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
first = _playlist_item_query(db, pl.id).first()
|
first = db.execute(_playlist_item_query(pl.id)).first()
|
||||||
card = _playlist_card(pl, count, first[1].rating_key if first else None)
|
card = _playlist_card(pl, count, first[1].rating_key if first else None)
|
||||||
if contains_id is not None:
|
if contains_id is not None:
|
||||||
card["has_item"] = (
|
card["has_item"] = (
|
||||||
db.query(PlexPlaylistItem.id)
|
db.execute(
|
||||||
.filter_by(playlist_id=pl.id, item_id=contains_id)
|
select(PlexPlaylistItem.id).where(
|
||||||
.first()
|
PlexPlaylistItem.playlist_id == pl.id,
|
||||||
|
PlexPlaylistItem.item_id == contains_id,
|
||||||
|
)
|
||||||
|
).first()
|
||||||
is not None
|
is not None
|
||||||
)
|
)
|
||||||
if group_ids is not None:
|
if group_ids is not None:
|
||||||
card["group_in"] = (
|
card["group_in"] = (
|
||||||
db.query(func.count(PlexPlaylistItem.id))
|
db.scalar(
|
||||||
.filter(
|
select(func.count(PlexPlaylistItem.id)).where(
|
||||||
PlexPlaylistItem.playlist_id == pl.id,
|
PlexPlaylistItem.playlist_id == pl.id,
|
||||||
PlexPlaylistItem.item_id.in_(group_ids or [0]),
|
PlexPlaylistItem.item_id.in_(group_ids or [0]),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.scalar()
|
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
out.append(card)
|
out.append(card)
|
||||||
@@ -958,7 +1008,9 @@ def create_playlist(
|
|||||||
count = 0
|
count = 0
|
||||||
thumb_rk = None
|
thumb_rk = None
|
||||||
if seed:
|
if seed:
|
||||||
it = db.query(PlexItem).filter_by(rating_key=seed).first()
|
it = db.execute(
|
||||||
|
select(PlexItem).where(PlexItem.rating_key == seed)
|
||||||
|
).scalars().first()
|
||||||
if it is not None:
|
if it is not None:
|
||||||
db.add(PlexPlaylistItem(playlist_id=pl.id, item_id=it.id, position=0))
|
db.add(PlexPlaylistItem(playlist_id=pl.id, item_id=it.id, position=0))
|
||||||
count, thumb_rk = 1, it.rating_key
|
count, thumb_rk = 1, it.rating_key
|
||||||
@@ -971,20 +1023,24 @@ def playlist_detail(
|
|||||||
pid: int, user: User = Depends(current_user), db: Session = Depends(get_db)
|
pid: int, user: User = Depends(current_user), db: Session = Depends(get_db)
|
||||||
) -> dict:
|
) -> dict:
|
||||||
pl = _own_playlist_or_404(db, pid, user)
|
pl = _own_playlist_or_404(db, pid, user)
|
||||||
rows = _playlist_item_query(db, pid).all()
|
rows = db.execute(_playlist_item_query(pid)).all()
|
||||||
items = [it for (_pi, it) in rows]
|
items = [it for (_pi, it) in rows]
|
||||||
states = {
|
states = {
|
||||||
s.item_id: s
|
s.item_id: s
|
||||||
for s in db.query(PlexState).filter(
|
for s in db.execute(
|
||||||
PlexState.user_id == user.id,
|
select(PlexState).where(
|
||||||
PlexState.item_id.in_([it.id for it in items] or [0]),
|
PlexState.user_id == user.id,
|
||||||
)
|
PlexState.item_id.in_([it.id for it in items] or [0]),
|
||||||
|
)
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
shows = {
|
shows = {
|
||||||
sh.id: sh
|
sh.id: sh
|
||||||
for sh in db.query(PlexShow).filter(
|
for sh in db.execute(
|
||||||
PlexShow.id.in_([it.show_id for it in items if it.show_id] or [0])
|
select(PlexShow).where(
|
||||||
)
|
PlexShow.id.in_([it.show_id for it in items if it.show_id] or [0])
|
||||||
|
)
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
cards = [
|
cards = [
|
||||||
_leaf_card(
|
_leaf_card(
|
||||||
@@ -1016,10 +1072,14 @@ def rename_playlist(
|
|||||||
pl.title = title
|
pl.title = title
|
||||||
db.commit()
|
db.commit()
|
||||||
count = (
|
count = (
|
||||||
db.query(func.count(PlexPlaylistItem.id)).filter_by(playlist_id=pl.id).scalar()
|
db.scalar(
|
||||||
|
select(func.count(PlexPlaylistItem.id)).where(
|
||||||
|
PlexPlaylistItem.playlist_id == pl.id
|
||||||
|
)
|
||||||
|
)
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
first = _playlist_item_query(db, pl.id).first()
|
first = db.execute(_playlist_item_query(pl.id)).first()
|
||||||
return _playlist_card(pl, count, first[1].rating_key if first else None)
|
return _playlist_card(pl, count, first[1].rating_key if first else None)
|
||||||
|
|
||||||
|
|
||||||
@@ -1045,17 +1105,21 @@ def playlist_add_item(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
pl = _own_playlist_or_404(db, pid, user)
|
pl = _own_playlist_or_404(db, pid, user)
|
||||||
it = db.query(PlexItem).filter_by(rating_key=body.item_rating_key).first()
|
it = db.execute(
|
||||||
|
select(PlexItem).where(PlexItem.rating_key == body.item_rating_key)
|
||||||
|
).scalars().first()
|
||||||
if it is None:
|
if it is None:
|
||||||
raise HTTPException(status_code=404, detail="Unknown item")
|
raise HTTPException(status_code=404, detail="Unknown item")
|
||||||
exists = (
|
exists = db.execute(
|
||||||
db.query(PlexPlaylistItem).filter_by(playlist_id=pid, item_id=it.id).first()
|
select(PlexPlaylistItem).where(
|
||||||
)
|
PlexPlaylistItem.playlist_id == pid, PlexPlaylistItem.item_id == it.id
|
||||||
|
)
|
||||||
|
).scalars().first()
|
||||||
if exists is None:
|
if exists is None:
|
||||||
maxpos = (
|
maxpos = db.scalar(
|
||||||
db.query(func.coalesce(func.max(PlexPlaylistItem.position), -1))
|
select(func.coalesce(func.max(PlexPlaylistItem.position), -1)).where(
|
||||||
.filter_by(playlist_id=pid)
|
PlexPlaylistItem.playlist_id == pid
|
||||||
.scalar()
|
)
|
||||||
)
|
)
|
||||||
db.add(
|
db.add(
|
||||||
PlexPlaylistItem(playlist_id=pid, item_id=it.id, position=int(maxpos) + 1)
|
PlexPlaylistItem(playlist_id=pid, item_id=it.id, position=int(maxpos) + 1)
|
||||||
@@ -1073,9 +1137,15 @@ def playlist_remove_item(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
pl = _own_playlist_or_404(db, pid, user)
|
pl = _own_playlist_or_404(db, pid, user)
|
||||||
it = db.query(PlexItem).filter_by(rating_key=str(item_rating_key)).first()
|
it = db.execute(
|
||||||
|
select(PlexItem).where(PlexItem.rating_key == str(item_rating_key))
|
||||||
|
).scalars().first()
|
||||||
if it is not None:
|
if it is not None:
|
||||||
db.query(PlexPlaylistItem).filter_by(playlist_id=pid, item_id=it.id).delete()
|
db.execute(
|
||||||
|
delete(PlexPlaylistItem).where(
|
||||||
|
PlexPlaylistItem.playlist_id == pid, PlexPlaylistItem.item_id == it.id
|
||||||
|
)
|
||||||
|
)
|
||||||
pl.updated_at = datetime.now(timezone.utc)
|
pl.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
@@ -1092,7 +1162,9 @@ def _items_by_rks(db: Session, rks: list) -> list[PlexItem]:
|
|||||||
return []
|
return []
|
||||||
by_rk = {
|
by_rk = {
|
||||||
it.rating_key: it
|
it.rating_key: it
|
||||||
for it in db.query(PlexItem).filter(PlexItem.rating_key.in_(order))
|
for it in db.execute(
|
||||||
|
select(PlexItem).where(PlexItem.rating_key.in_(order))
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
out: list[PlexItem] = []
|
out: list[PlexItem] = []
|
||||||
@@ -1115,13 +1187,17 @@ def playlist_add_bulk(
|
|||||||
order is preserved for the newly-added ones. Returns how many were added + the new total."""
|
order is preserved for the newly-added ones. Returns how many were added + the new total."""
|
||||||
pl = _own_playlist_or_404(db, pid, user)
|
pl = _own_playlist_or_404(db, pid, user)
|
||||||
items = _items_by_rks(db, body.item_rating_keys)
|
items = _items_by_rks(db, body.item_rating_keys)
|
||||||
existing = {
|
existing = set(
|
||||||
row[0] for row in db.query(PlexPlaylistItem.item_id).filter_by(playlist_id=pid)
|
db.execute(
|
||||||
}
|
select(PlexPlaylistItem.item_id).where(PlexPlaylistItem.playlist_id == pid)
|
||||||
|
).scalars()
|
||||||
|
)
|
||||||
pos = int(
|
pos = int(
|
||||||
db.query(func.coalesce(func.max(PlexPlaylistItem.position), -1))
|
db.scalar(
|
||||||
.filter_by(playlist_id=pid)
|
select(func.coalesce(func.max(PlexPlaylistItem.position), -1)).where(
|
||||||
.scalar()
|
PlexPlaylistItem.playlist_id == pid
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
added = 0
|
added = 0
|
||||||
for it in items:
|
for it in items:
|
||||||
@@ -1135,7 +1211,11 @@ def playlist_add_bulk(
|
|||||||
pl.updated_at = datetime.now(timezone.utc)
|
pl.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
total = (
|
total = (
|
||||||
db.query(func.count(PlexPlaylistItem.id)).filter_by(playlist_id=pid).scalar()
|
db.scalar(
|
||||||
|
select(func.count(PlexPlaylistItem.id)).where(
|
||||||
|
PlexPlaylistItem.playlist_id == pid
|
||||||
|
)
|
||||||
|
)
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
return {"added": added, "item_count": total}
|
return {"added": added, "item_count": total}
|
||||||
@@ -1154,18 +1234,20 @@ def playlist_remove_bulk(
|
|||||||
ids = [it.id for it in _items_by_rks(db, body.item_rating_keys)]
|
ids = [it.id for it in _items_by_rks(db, body.item_rating_keys)]
|
||||||
removed = 0
|
removed = 0
|
||||||
if ids:
|
if ids:
|
||||||
removed = (
|
removed = db.execute(
|
||||||
db.query(PlexPlaylistItem)
|
delete(PlexPlaylistItem).where(
|
||||||
.filter(
|
|
||||||
PlexPlaylistItem.playlist_id == pid, PlexPlaylistItem.item_id.in_(ids)
|
PlexPlaylistItem.playlist_id == pid, PlexPlaylistItem.item_id.in_(ids)
|
||||||
)
|
)
|
||||||
.delete(synchronize_session=False)
|
).rowcount
|
||||||
)
|
|
||||||
if removed:
|
if removed:
|
||||||
pl.updated_at = datetime.now(timezone.utc)
|
pl.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
total = (
|
total = (
|
||||||
db.query(func.count(PlexPlaylistItem.id)).filter_by(playlist_id=pid).scalar()
|
db.scalar(
|
||||||
|
select(func.count(PlexPlaylistItem.id)).where(
|
||||||
|
PlexPlaylistItem.playlist_id == pid
|
||||||
|
)
|
||||||
|
)
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
return {"removed": removed, "item_count": total}
|
return {"removed": removed, "item_count": total}
|
||||||
@@ -1183,13 +1265,19 @@ def reorder_playlist(
|
|||||||
order = body.item_rating_keys
|
order = body.item_rating_keys
|
||||||
id_by_rk = {
|
id_by_rk = {
|
||||||
it.rating_key: it.id
|
it.rating_key: it.id
|
||||||
for it in db.query(PlexItem).filter(PlexItem.rating_key.in_(order or [""]))
|
for it in db.execute(
|
||||||
|
select(PlexItem).where(PlexItem.rating_key.in_(order or [""]))
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
for pos, rk in enumerate(order):
|
for pos, rk in enumerate(order):
|
||||||
iid = id_by_rk.get(rk)
|
iid = id_by_rk.get(rk)
|
||||||
if iid is not None:
|
if iid is not None:
|
||||||
db.query(PlexPlaylistItem).filter_by(playlist_id=pid, item_id=iid).update(
|
db.execute(
|
||||||
{"position": pos}
|
update(PlexPlaylistItem)
|
||||||
|
.where(
|
||||||
|
PlexPlaylistItem.playlist_id == pid, PlexPlaylistItem.item_id == iid
|
||||||
|
)
|
||||||
|
.values(position=pos)
|
||||||
)
|
)
|
||||||
pl.updated_at = datetime.now(timezone.utc)
|
pl.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -1244,30 +1332,32 @@ def show_detail(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""A show's seasons + episodes (the drill-down view) with per-user watch state."""
|
"""A show's seasons + episodes (the drill-down view) with per-user watch state."""
|
||||||
sh = db.query(PlexShow).filter_by(rating_key=str(rating_key)).first()
|
sh = db.execute(
|
||||||
|
select(PlexShow).where(PlexShow.rating_key == str(rating_key))
|
||||||
|
).scalars().first()
|
||||||
if sh is None:
|
if sh is None:
|
||||||
raise HTTPException(status_code=404, detail="Unknown Plex show")
|
raise HTTPException(status_code=404, detail="Unknown Plex show")
|
||||||
seasons = (
|
seasons = db.execute(
|
||||||
db.query(PlexSeason)
|
select(PlexSeason)
|
||||||
.filter_by(show_id=sh.id)
|
.where(PlexSeason.show_id == sh.id)
|
||||||
.order_by(PlexSeason.season_number.asc().nullsfirst())
|
.order_by(PlexSeason.season_number.asc().nullsfirst())
|
||||||
.all()
|
).scalars().all()
|
||||||
)
|
eps = db.execute(
|
||||||
eps = (
|
select(PlexItem)
|
||||||
db.query(PlexItem)
|
.where(PlexItem.show_id == sh.id, PlexItem.kind == "episode")
|
||||||
.filter_by(show_id=sh.id, kind="episode")
|
|
||||||
.order_by(
|
.order_by(
|
||||||
PlexItem.season_number.asc().nullsfirst(),
|
PlexItem.season_number.asc().nullsfirst(),
|
||||||
PlexItem.episode_number.asc().nullsfirst(),
|
PlexItem.episode_number.asc().nullsfirst(),
|
||||||
)
|
)
|
||||||
.all()
|
).scalars().all()
|
||||||
)
|
|
||||||
states = {
|
states = {
|
||||||
s.item_id: s
|
s.item_id: s
|
||||||
for s in db.query(PlexState).filter(
|
for s in db.execute(
|
||||||
PlexState.user_id == user.id,
|
select(PlexState).where(
|
||||||
PlexState.item_id.in_([e.id for e in eps] or [0]),
|
PlexState.user_id == user.id,
|
||||||
)
|
PlexState.item_id.in_([e.id for e in eps] or [0]),
|
||||||
|
)
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
by_season: dict[int | None, list] = {}
|
by_season: dict[int | None, list] = {}
|
||||||
all_cards: list[
|
all_cards: list[
|
||||||
@@ -1362,9 +1452,11 @@ def _related_show_cards(
|
|||||||
return []
|
return []
|
||||||
shows = {
|
shows = {
|
||||||
s.rating_key: s
|
s.rating_key: s
|
||||||
for s in db.query(PlexShow).filter(
|
for s in db.execute(
|
||||||
PlexShow.rating_key.in_(rks[:60]), PlexShow.id != exclude
|
select(PlexShow).where(
|
||||||
)
|
PlexShow.rating_key.in_(rks[:60]), PlexShow.id != exclude
|
||||||
|
)
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
statuses = rollup.state_map(db, user_id, [s.id for s in shows.values()])
|
statuses = rollup.state_map(db, user_id, [s.id for s in shows.values()])
|
||||||
out: list[dict] = []
|
out: list[dict] = []
|
||||||
@@ -1436,37 +1528,41 @@ def people_cards(
|
|||||||
wanted = _csv(names)[:16]
|
wanted = _csv(names)[:16]
|
||||||
if not wanted:
|
if not wanted:
|
||||||
return {"people": []}
|
return {"people": []}
|
||||||
libs = db.query(PlexLibrary).filter_by(enabled=True).all()
|
libs = db.execute(
|
||||||
|
select(PlexLibrary).where(PlexLibrary.enabled.is_(True))
|
||||||
|
).scalars().all()
|
||||||
movie_lib_ids = [lb.id for lb in libs if lb.kind == "movie"]
|
movie_lib_ids = [lb.id for lb in libs if lb.kind == "movie"]
|
||||||
show_lib_ids = [lb.id for lb in libs if lb.kind == "show"]
|
show_lib_ids = [lb.id for lb in libs if lb.kind == "show"]
|
||||||
want_movies = scope in ("movie", "both") and bool(movie_lib_ids)
|
want_movies = scope in ("movie", "both") and bool(movie_lib_ids)
|
||||||
want_shows = scope in ("show", "both") and bool(show_lib_ids)
|
want_shows = scope in ("show", "both") and bool(show_lib_ids)
|
||||||
photos = {
|
photos = {
|
||||||
p.name: p.photo_thumb
|
p.name: p.photo_thumb
|
||||||
for p in db.query(PlexPerson).filter(PlexPerson.name.in_(wanted))
|
for p in db.execute(
|
||||||
|
select(PlexPerson).where(PlexPerson.name.in_(wanted))
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
out = []
|
out = []
|
||||||
for nm in wanted:
|
for nm in wanted:
|
||||||
count = 0
|
count = 0
|
||||||
if want_movies:
|
if want_movies:
|
||||||
count += (
|
count += (
|
||||||
db.query(func.count(PlexItem.id))
|
db.scalar(
|
||||||
.filter(
|
select(func.count(PlexItem.id)).where(
|
||||||
PlexItem.kind == "movie",
|
PlexItem.kind == "movie",
|
||||||
PlexItem.library_id.in_(movie_lib_ids),
|
PlexItem.library_id.in_(movie_lib_ids),
|
||||||
PlexItem.cast_names.contains([nm]),
|
PlexItem.cast_names.contains([nm]),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.scalar()
|
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
if want_shows:
|
if want_shows:
|
||||||
count += (
|
count += (
|
||||||
db.query(func.count(PlexShow.id))
|
db.scalar(
|
||||||
.filter(
|
select(func.count(PlexShow.id)).where(
|
||||||
PlexShow.library_id.in_(show_lib_ids),
|
PlexShow.library_id.in_(show_lib_ids),
|
||||||
PlexShow.cast_names.contains([nm]),
|
PlexShow.cast_names.contains([nm]),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.scalar()
|
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
out.append({"name": nm, "photo": photos.get(nm), "count": count})
|
out.append({"name": nm, "photo": photos.get(nm), "count": count})
|
||||||
@@ -1493,7 +1589,9 @@ def _collection_strips(db: Session, it: PlexItem, user_id: int) -> list[dict]:
|
|||||||
return []
|
return []
|
||||||
cols = {
|
cols = {
|
||||||
c.rating_key: c
|
c.rating_key: c
|
||||||
for c in db.query(PlexCollection).filter(PlexCollection.rating_key.in_(keys))
|
for c in db.execute(
|
||||||
|
select(PlexCollection).where(PlexCollection.rating_key.in_(keys))
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
# Smallest (most specific — e.g. a franchise) first; the big auto-lists (IMDb/TMDb Popular) last.
|
# Smallest (most specific — e.g. a franchise) first; the big auto-lists (IMDb/TMDb Popular) last.
|
||||||
ordered = sorted(
|
ordered = sorted(
|
||||||
@@ -1502,23 +1600,24 @@ def _collection_strips(db: Session, it: PlexItem, user_id: int) -> list[dict]:
|
|||||||
out = []
|
out = []
|
||||||
for col in ordered:
|
for col in ordered:
|
||||||
rk = col.rating_key
|
rk = col.rating_key
|
||||||
members = (
|
members = db.execute(
|
||||||
db.query(PlexItem)
|
select(PlexItem)
|
||||||
.filter(
|
.where(
|
||||||
PlexItem.library_id == it.library_id,
|
PlexItem.library_id == it.library_id,
|
||||||
PlexItem.kind == "movie",
|
PlexItem.kind == "movie",
|
||||||
PlexItem.collection_keys.contains([rk]),
|
PlexItem.collection_keys.contains([rk]),
|
||||||
)
|
)
|
||||||
.order_by(PlexItem.year.asc().nullslast(), func.lower(PlexItem.title))
|
.order_by(PlexItem.year.asc().nullslast(), func.lower(PlexItem.title))
|
||||||
.limit(60)
|
.limit(60)
|
||||||
.all()
|
).scalars().all()
|
||||||
)
|
|
||||||
ids = [m.id for m in members]
|
ids = [m.id for m in members]
|
||||||
states = {
|
states = {
|
||||||
s.item_id: s
|
s.item_id: s
|
||||||
for s in db.query(PlexState).filter(
|
for s in db.execute(
|
||||||
PlexState.user_id == user_id, PlexState.item_id.in_(ids or [0])
|
select(PlexState).where(
|
||||||
)
|
PlexState.user_id == user_id, PlexState.item_id.in_(ids or [0])
|
||||||
|
)
|
||||||
|
).scalars()
|
||||||
}
|
}
|
||||||
out.append(
|
out.append(
|
||||||
{
|
{
|
||||||
@@ -1536,11 +1635,13 @@ def _episode_nav(db: Session, it: PlexItem) -> tuple[str | None, str | None]:
|
|||||||
return None, None
|
return None, None
|
||||||
keys = [
|
keys = [
|
||||||
r[0]
|
r[0]
|
||||||
for r in db.query(PlexItem.rating_key)
|
for r in db.execute(
|
||||||
.filter_by(show_id=it.show_id, kind="episode")
|
select(PlexItem.rating_key)
|
||||||
.order_by(
|
.where(PlexItem.show_id == it.show_id, PlexItem.kind == "episode")
|
||||||
PlexItem.season_number.asc().nullsfirst(),
|
.order_by(
|
||||||
PlexItem.episode_number.asc().nullsfirst(),
|
PlexItem.season_number.asc().nullsfirst(),
|
||||||
|
PlexItem.episode_number.asc().nullsfirst(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
]
|
]
|
||||||
@@ -1562,7 +1663,11 @@ def item_detail(
|
|||||||
"""Everything the player needs: metadata, cast, intro/credit markers (from Plex), episode
|
"""Everything the player needs: metadata, cast, intro/credit markers (from Plex), episode
|
||||||
prev/next, and the per-user resume position + watch status."""
|
prev/next, and the per-user resume position + watch status."""
|
||||||
it = item_or_404(db, rating_key)
|
it = item_or_404(db, rating_key)
|
||||||
st = db.query(PlexState).filter_by(user_id=user.id, item_id=it.id).first()
|
st = db.execute(
|
||||||
|
select(PlexState).where(
|
||||||
|
PlexState.user_id == user.id, PlexState.item_id == it.id
|
||||||
|
)
|
||||||
|
).scalars().first()
|
||||||
|
|
||||||
rich: dict = {}
|
rich: dict = {}
|
||||||
markers: list[dict] = []
|
markers: list[dict] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user