feat(plex): add a movie or show to a collection from its card menu

"Add to collection…" opens a panel beside the card: the title's library's
manual collections tick it in or out, and a name + Enter creates a new one with
the title in it. It used to take the Collections page, a new collection, its
name and a title search, then another search per title. A lean endpoint lists
the collections with whether each holds the title. Admin only, as on the
title's page.

Also: a collection seeded with a show is made as a show collection (Plex type
2). Measured: as a movie one it came out empty and refused the show after.
This commit is contained in:
2026-10-08 03:09:55 +02:00
parent 75093458c9
commit dae0b80f6a
13 changed files with 421 additions and 15 deletions
+7 -3
View File
@@ -167,12 +167,16 @@ class PlexClient:
keys = ",".join(str(k) for k in rating_keys)
return f"server://{self.machine_id}/com.plexapp.plugins.library/library/metadata/{keys}"
def create_collection(self, section_key: str, title: str, item_rating_key: str) -> dict:
"""Create a (dumb) collection seeded with one item; returns its Metadata (incl. ratingKey)."""
def create_collection(
self, section_key: str, title: str, item_rating_key: str, item_type: int = 1
) -> dict:
"""Create a (dumb) collection seeded with one item; returns its Metadata (incl. ratingKey).
`item_type` is the Plex metadata type of its members — 1 movie, 2 show. Measured: a collection
made as type 1 in a TV section came out EMPTY with a show seed, and refused the show after."""
uri = quote(self._metadata_uri(item_rating_key), safe="")
mc = self._write(
"POST",
f"/library/collections?type=1&smart=0&sectionId={section_key}"
f"/library/collections?type={item_type}&smart=0&sectionId={section_key}"
f"&title={quote(title, safe='')}&uri={uri}",
)
items = mc.get("Metadata", []) or []
+38 -2
View File
@@ -176,6 +176,40 @@ def collections(
}
@router.get("/collections/for/{item_rating_key}")
def collections_for_item(
item_rating_key: str,
user: User = Depends(current_user),
db: Session = Depends(get_db),
) -> dict:
"""What a movie or show card's "Add to collection" lists: the editable collections of the title's
library (a collection lives in one library, and only a manual one can take a title — the same
`can_edit` the editor offers), each saying whether the title is in it already, by name."""
movie = by_rating_key(db, PlexItem, item_rating_key)
title: PlexItem | PlexShow | None = (
movie if movie is not None and movie.kind == "movie" else None
) or by_rating_key(db, PlexShow, item_rating_key)
if title is None:
raise not_found("item")
member_of = set(title.collection_keys or [])
rows = (
db.execute(
select(PlexCollection)
.where(PlexCollection.library_id == title.library_id)
.order_by(collated(PlexCollection.title, lang_for_user(user)))
)
.scalars()
.all()
)
return {
"collections": [
{**_collection_card(c), "contains": c.rating_key in member_of}
for c in rows
if _collection_editable(c)
]
}
class Member(NamedTuple):
"""One collection member flattened to everything the Sort control can order by. The page's two
legs are different tables (movies and shows), so there is no single query to ORDER BY — the
@@ -407,13 +441,14 @@ def create_collection(
seed = body.item_rating_key
if not title or not seed:
raise refused(400, "collection_fields_required")
seed_show = by_rating_key(db, PlexShow, seed)
if library:
lib = (
db.execute(select(PlexLibrary).where(PlexLibrary.plex_key == library)).scalars().first()
)
else:
# The "New collection" flow only knows the seed title → derive its library.
seed_item = by_rating_key(db, PlexItem, seed) or by_rating_key(db, PlexShow, seed)
seed_item = by_rating_key(db, PlexItem, seed) or seed_show
lib = db.get(PlexLibrary, seed_item.library_id) if seed_item else None
if lib is None:
raise not_found("library")
@@ -421,7 +456,8 @@ def create_collection(
wrote = False
try:
with PlexClient(db) as plex:
new = plex.create_collection(lib.plex_key, title, seed)
# A show collection is made as one (Plex type 2): as a movie one it stayed empty.
new = plex.create_collection(lib.plex_key, title, seed, 2 if seed_show else 1)
wrote = True
col = plex_sync.resync_collection(db, plex, lib, str(new.get("ratingKey")))
col.editable = True