feat(plex): playlist bulk add/remove + group-aware playlist cards
Add POST /playlists/{id}/items/bulk and /items/remove-bulk so a whole
season or show can be added/removed in one call (input order preserved,
duplicates skipped). Extend GET /playlists with contains_group=<rk,rk,…>
returning per-playlist group_in + a top-level group_size for the bulk
add-to-playlist dialog. Include the show's rating_key (show_id) on
playlist episode cards so the client can group a playlist by show/season.
This commit is contained in:
parent
22f36d9d7f
commit
e6b22f971a
1 changed files with 95 additions and 6 deletions
|
|
@ -186,11 +186,15 @@ def _episode_card(e: PlexItem, st: PlexState | None) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _leaf_card(it: PlexItem, st: PlexState | None, show_title: str | None = None) -> dict:
|
def _leaf_card(
|
||||||
"""A playable-leaf card (movie or episode). Used by playlists, which can mix both kinds."""
|
it: PlexItem, st: PlexState | None, show_title: str | None = None, show_rk: str | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""A playable-leaf card (movie or episode). Used by playlists, which can mix both kinds.
|
||||||
|
`show_rk` (the show's rating_key) lets the client group a playlist's episodes by show/season."""
|
||||||
if it.kind == "episode":
|
if it.kind == "episode":
|
||||||
card = _episode_card(it, st)
|
card = _episode_card(it, st)
|
||||||
card["show_title"] = show_title
|
card["show_title"] = show_title
|
||||||
|
card["show_id"] = show_rk
|
||||||
return card
|
return card
|
||||||
return _movie_card(it, st)
|
return _movie_card(it, st)
|
||||||
|
|
||||||
|
|
@ -639,15 +643,23 @@ def _playlist_item_query(db: Session, pid: int):
|
||||||
@router.get("/playlists")
|
@router.get("/playlists")
|
||||||
def playlists(
|
def playlists(
|
||||||
contains: str | None = None,
|
contains: str | None = None,
|
||||||
|
contains_group: str | None = None,
|
||||||
user: User = Depends(current_user),
|
user: User = Depends(current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""The current user's playlists (newest-touched first), each with an item count + a cover thumb.
|
"""The current user's playlists (newest-touched first), each with an item count + a cover thumb.
|
||||||
`contains`=an item rating_key adds `has_item` per playlist (for the 'add to playlist' dialog)."""
|
`contains`=an item rating_key adds `has_item` per playlist (for the single 'add to playlist' dialog).
|
||||||
|
`contains_group`=a comma-separated list of rating_keys adds `group_in` per playlist (how many of the
|
||||||
|
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()
|
row = db.query(PlexItem.id).filter_by(rating_key=str(contains)).first()
|
||||||
contains_id = row[0] if row else None
|
contains_id = row[0] if row else None
|
||||||
|
group_ids: set[int] | None = None
|
||||||
|
if contains_group is not None:
|
||||||
|
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 = {r[0] for r in rows}
|
||||||
out = []
|
out = []
|
||||||
for pl in db.query(PlexPlaylist).filter_by(user_id=user.id).order_by(PlexPlaylist.updated_at.desc()):
|
for pl in db.query(PlexPlaylist).filter_by(user_id=user.id).order_by(PlexPlaylist.updated_at.desc()):
|
||||||
count = db.query(func.count(PlexPlaylistItem.id)).filter_by(playlist_id=pl.id).scalar() or 0
|
count = db.query(func.count(PlexPlaylistItem.id)).filter_by(playlist_id=pl.id).scalar() or 0
|
||||||
|
|
@ -658,8 +670,18 @@ def playlists(
|
||||||
db.query(PlexPlaylistItem.id).filter_by(playlist_id=pl.id, item_id=contains_id).first()
|
db.query(PlexPlaylistItem.id).filter_by(playlist_id=pl.id, item_id=contains_id).first()
|
||||||
is not None
|
is not None
|
||||||
)
|
)
|
||||||
|
if group_ids is not None:
|
||||||
|
card["group_in"] = (
|
||||||
|
db.query(func.count(PlexPlaylistItem.id))
|
||||||
|
.filter(PlexPlaylistItem.playlist_id == pl.id, PlexPlaylistItem.item_id.in_(group_ids or [0]))
|
||||||
|
.scalar()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
out.append(card)
|
out.append(card)
|
||||||
return {"playlists": out}
|
resp: dict = {"playlists": out}
|
||||||
|
if group_ids is not None:
|
||||||
|
resp["group_size"] = len(group_ids)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@router.post("/playlists")
|
@router.post("/playlists")
|
||||||
|
|
@ -694,10 +716,18 @@ def playlist_detail(pid: int, user: User = Depends(current_user), db: Session =
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
shows = {
|
shows = {
|
||||||
sh.id: sh.title
|
sh.id: sh
|
||||||
for sh in db.query(PlexShow).filter(PlexShow.id.in_([it.show_id for it in items if it.show_id] or [0]))
|
for sh in db.query(PlexShow).filter(PlexShow.id.in_([it.show_id for it in items if it.show_id] or [0]))
|
||||||
}
|
}
|
||||||
cards = [_leaf_card(it, states.get(it.id), shows.get(it.show_id)) for it in items]
|
cards = [
|
||||||
|
_leaf_card(
|
||||||
|
it,
|
||||||
|
states.get(it.id),
|
||||||
|
shows[it.show_id].title if it.show_id in shows else None,
|
||||||
|
shows[it.show_id].rating_key if it.show_id in shows else None,
|
||||||
|
)
|
||||||
|
for it in items
|
||||||
|
]
|
||||||
return {"id": pl.id, "title": pl.title, "items": cards}
|
return {"id": pl.id, "title": pl.title, "items": cards}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -748,6 +778,65 @@ def playlist_remove_item(pid: int, item_rating_key: str, user: User = Depends(cu
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
def _items_by_rks(db: Session, rks: list) -> list[PlexItem]:
|
||||||
|
"""Resolve rating_keys to PlexItems, preserving the input order (drops unknown keys)."""
|
||||||
|
order = [str(k) for k in rks if str(k)]
|
||||||
|
if not order:
|
||||||
|
return []
|
||||||
|
by_rk = {it.rating_key: it for it in db.query(PlexItem).filter(PlexItem.rating_key.in_(order))}
|
||||||
|
seen: set[str] = set()
|
||||||
|
out: list[PlexItem] = []
|
||||||
|
for rk in order:
|
||||||
|
it = by_rk.get(rk)
|
||||||
|
if it is not None and rk not in seen:
|
||||||
|
seen.add(rk)
|
||||||
|
out.append(it)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/playlists/{pid}/items/bulk")
|
||||||
|
def playlist_add_bulk(pid: int, payload: dict, user: User = Depends(current_user), db: Session = Depends(get_db)) -> dict:
|
||||||
|
"""Append many items at once (a whole season/show). Already-present items are skipped; the input
|
||||||
|
order is preserved for the newly-added ones. Returns how many were added + the new total."""
|
||||||
|
pl = _own_playlist_or_404(db, pid, user)
|
||||||
|
items = _items_by_rks(db, payload.get("item_rating_keys") or [])
|
||||||
|
existing = {row[0] for row in db.query(PlexPlaylistItem.item_id).filter_by(playlist_id=pid)}
|
||||||
|
pos = int(db.query(func.coalesce(func.max(PlexPlaylistItem.position), -1)).filter_by(playlist_id=pid).scalar())
|
||||||
|
added = 0
|
||||||
|
for it in items:
|
||||||
|
if it.id in existing:
|
||||||
|
continue
|
||||||
|
pos += 1
|
||||||
|
db.add(PlexPlaylistItem(playlist_id=pid, item_id=it.id, position=pos))
|
||||||
|
existing.add(it.id)
|
||||||
|
added += 1
|
||||||
|
if added:
|
||||||
|
pl.updated_at = datetime.now(timezone.utc)
|
||||||
|
db.commit()
|
||||||
|
total = db.query(func.count(PlexPlaylistItem.id)).filter_by(playlist_id=pid).scalar() or 0
|
||||||
|
return {"added": added, "item_count": total}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/playlists/{pid}/items/remove-bulk")
|
||||||
|
def playlist_remove_bulk(pid: int, payload: dict, user: User = Depends(current_user), db: Session = Depends(get_db)) -> dict:
|
||||||
|
"""Remove many items at once (a whole season/show, or a multi-select). Returns how many were
|
||||||
|
removed + the new total."""
|
||||||
|
pl = _own_playlist_or_404(db, pid, user)
|
||||||
|
ids = [it.id for it in _items_by_rks(db, payload.get("item_rating_keys") or [])]
|
||||||
|
removed = 0
|
||||||
|
if ids:
|
||||||
|
removed = (
|
||||||
|
db.query(PlexPlaylistItem)
|
||||||
|
.filter(PlexPlaylistItem.playlist_id == pid, PlexPlaylistItem.item_id.in_(ids))
|
||||||
|
.delete(synchronize_session=False)
|
||||||
|
)
|
||||||
|
if removed:
|
||||||
|
pl.updated_at = datetime.now(timezone.utc)
|
||||||
|
db.commit()
|
||||||
|
total = db.query(func.count(PlexPlaylistItem.id)).filter_by(playlist_id=pid).scalar() or 0
|
||||||
|
return {"removed": removed, "item_count": total}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/playlists/{pid}/order")
|
@router.put("/playlists/{pid}/order")
|
||||||
def reorder_playlist(pid: int, payload: dict, user: User = Depends(current_user), db: Session = Depends(get_db)) -> dict:
|
def reorder_playlist(pid: int, payload: dict, user: User = Depends(current_user), db: Session = Depends(get_db)) -> dict:
|
||||||
"""Set the playlist order from an explicit list of item rating_keys (0-based positions)."""
|
"""Set the playlist order from an explicit list of item rating_keys (0-based positions)."""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue