feat(plex): Collection info page — hero, play/shuffle, and write-through editing (B)

Clicking a collection tile now opens a dedicated Collection info page (like the Plex-web
one) instead of just applying it as a filter. New GET /collections/{rk} returns the hero
aggregates (year range, most-restrictive member cert, total runtime) + the members ordered
by release year then title; a new PlexCollectionView renders the hero (art/title/meta/
summary+More) and the member grid, with a Play-all and Shuffle over the playable leaves
(reusing the player's existing queue + auto-advance).

For a non-external, admin-enabled collection the page offers rename / delete / add-title
(a search picker) / remove-item, all through the pre-existing admin endpoints that write
THROUGH to Plex (each edit awaits and re-fetches — no optimism, since a Plex write can 422).
An eligible collection shows "Enable editing" first (the DB editable flag). Read-only ones
(smart / imdb / tmdb / … auto-lists) are marked with a lock on both the info page AND the
browse tile, up front.

New `{ kind: "collection" }` sub-view wired through plexSubUrl (bookmarkable ?v=collection:rk),
PlexBrowse (render branch, Backspace back-nav, tile handler), with a round-trip test.

Verified live: tile→page, hero + chronological members (James Bond 1962→2021, PG-13, 55h),
Play/Shuffle, Enable editing → edit controls + remove-X, the add search (marks already-in
titles "Added"), and the read-only lock on external collections. Real Plex mutations wired
but not executed against the live account.

Gate: tsc/lint/knip/prettier clean, 424 vitest; backend ruff clean, 6 collections tests
(+ a detail-hero/ordering assertion).
This commit is contained in:
2026-08-23 04:40:09 +02:00
parent c0d869089d
commit 5d99a5f253
10 changed files with 593 additions and 8 deletions
@@ -94,6 +94,29 @@ def test_collection_cards_carry_montage_art_and_total_runtime(db, monkeypatch):
assert card["total_runtime_s"] == 5400 # 3600 + 1800
def test_collection_detail_hero_and_ordered_members(db, monkeypatch):
# The Collection info page: hero aggregates (year range, most-restrictive cert, total runtime) +
# members ordered by release year then title.
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
db.add(lib)
db.flush()
db.add(PlexCollection(rating_key="A", library_id=lib.id, title="Coll A", summary="S", child_count=2))
common = dict(library_id=lib.id, kind="movie", collection_keys=["A"])
db.add_all([
PlexItem(rating_key="later", title="Later", year=1996, duration_s=3600, content_rating="PG", **common),
PlexItem(rating_key="early", title="Early", year=1992, duration_s=1800, content_rating="G", **common),
])
user = User(email="cd@example.com")
db.add(user)
db.commit()
d = _get(db, user, monkeypatch, "/api/plex/collections/A").json()
assert d["title"] == "Coll A" and d["summary"] == "S"
assert d["year_min"] == 1992 and d["year_max"] == 1996
assert d["content_rating"] == "PG" # most restrictive of {G, PG}
assert d["total_runtime_s"] == 5400
assert [i["id"] for i in d["items"]] == ["early", "later"] # release-year order
def test_show_collection_gets_montage_art_from_shows(db, monkeypatch):
# A show-library collection must get its montage from PlexShow members (else it renders blank) — the
# A3 fix that unions movie + show art. Runtime stays 0 (shows have no single duration).