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:
|
||||
"""A playable-leaf card (movie or episode). Used by playlists, which can mix both kinds."""
|
||||
def _leaf_card(
|
||||
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":
|
||||
card = _episode_card(it, st)
|
||||
card["show_title"] = show_title
|
||||
card["show_id"] = show_rk
|
||||
return card
|
||||
return _movie_card(it, st)
|
||||
|
||||
|
|
@ -639,15 +643,23 @@ def _playlist_item_query(db: Session, pid: int):
|
|||
@router.get("/playlists")
|
||||
def playlists(
|
||||
contains: str | None = None,
|
||||
contains_group: str | None = None,
|
||||
user: User = Depends(current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""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
|
||||
if contains:
|
||||
row = db.query(PlexItem.id).filter_by(rating_key=str(contains)).first()
|
||||
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 = []
|
||||
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
|
||||
|
|
@ -658,8 +670,18 @@ def playlists(
|
|||
db.query(PlexPlaylistItem.id).filter_by(playlist_id=pl.id, item_id=contains_id).first()
|
||||
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)
|
||||
return {"playlists": out}
|
||||
resp: dict = {"playlists": out}
|
||||
if group_ids is not None:
|
||||
resp["group_size"] = len(group_ids)
|
||||
return resp
|
||||
|
||||
|
||||
@router.post("/playlists")
|
||||
|
|
@ -694,10 +716,18 @@ def playlist_detail(pid: int, user: User = Depends(current_user), db: Session =
|
|||
)
|
||||
}
|
||||
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]))
|
||||
}
|
||||
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}
|
||||
|
||||
|
||||
|
|
@ -748,6 +778,65 @@ def playlist_remove_item(pid: int, item_rating_key: str, user: User = Depends(cu
|
|||
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")
|
||||
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)."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue