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:
@@ -13,6 +13,7 @@ describe("PLEX_SUB_URL encode", () => {
|
||||
expect(U.encode({ kind: "info", id: "m9" })).toBe("info:m9");
|
||||
expect(U.encode({ kind: "person", name: "Jane Doe" })).toBe("person:Jane Doe");
|
||||
expect(U.encode({ kind: "playlist", id: 12 })).toBe("playlist:12");
|
||||
expect(U.encode({ kind: "collection", id: "c5" })).toBe("collection:c5");
|
||||
});
|
||||
|
||||
it("does NOT mirror the root Discover landing or the ephemeral player (encode → null)", () => {
|
||||
@@ -31,6 +32,7 @@ describe("PLEX_SUB_URL round-trip", () => {
|
||||
{ kind: "info", id: "m9" },
|
||||
{ kind: "person", name: "Jane Doe" },
|
||||
{ kind: "playlist", id: 12 },
|
||||
{ kind: "collection", id: "c5" },
|
||||
];
|
||||
it.each(cases)("encode→decode is identity for %o", (view) => {
|
||||
const raw = U.encode(view);
|
||||
@@ -60,10 +62,11 @@ describe("PLEX_SUB_URL decode guards", () => {
|
||||
expect(U.decode("season:s1:se2")).toEqual({ kind: "season", showId: "s1", seasonId: "se2" });
|
||||
});
|
||||
|
||||
it("requires a non-empty arg for show / info / person", () => {
|
||||
it("requires a non-empty arg for show / info / person / collection", () => {
|
||||
expect(U.decode("show:")).toBeNull();
|
||||
expect(U.decode("info:")).toBeNull();
|
||||
expect(U.decode("person:")).toBeNull();
|
||||
expect(U.decode("collection:")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects an unknown kind or garbage", () => {
|
||||
|
||||
Reference in New Issue
Block a user