feat(plex): collections — sync, browse, filter, and info-page strips (Phase 1, read)
Backend (migration 0047_plex_collections): a plex_collections table mirrors every Plex collection (card metadata + smart flag + an editable flag reserved for Phase 2); membership is stored as GIN-indexed collection_keys on member movies (plex_items) and shows (plex_shows). The background sync (plex_sync) now fetches each library's collections + their children and rebuilds membership — so ALL reads are local (Plex's dual-language collection queries are slow; this trades a ~4-min background sync for instant reads). New /api/plex/collections endpoint; /browse gains a combinable filter; item_detail returns the movie's collection 'strips' (sibling titles as playable cards, smallest/most-specific collection first) — all pure local lookups. Frontend: PlexSidebar gains a searchable Collection picker + an active-collection chip; PlexInfo renders the collection strips (playable posters + 'Browse collection' → sets the filter); the collection is part of PlexFilters (persisted). i18n en/hu/de. Phase 2 (create/edit collections with write-back to Plex) is separate.
This commit is contained in:
parent
0385b13a28
commit
418a874851
14 changed files with 386 additions and 15 deletions
|
|
@ -714,6 +714,16 @@ export interface PlexItemDetail {
|
|||
episode_number?: number | null;
|
||||
prev_id?: string | null;
|
||||
next_id?: string | null;
|
||||
collections?: { id: string; title: string; items: PlexCard[] }[];
|
||||
}
|
||||
export interface PlexCollection {
|
||||
id: string;
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
thumb?: string | null;
|
||||
child_count?: number | null;
|
||||
smart: boolean;
|
||||
editable: boolean;
|
||||
}
|
||||
export interface PlexCastMember {
|
||||
name: string;
|
||||
|
|
@ -734,6 +744,8 @@ export interface PlexFilters {
|
|||
directors: string[]; // AND — titles featuring ALL selected
|
||||
actors: string[]; // AND — titles featuring ALL selected
|
||||
studios: string[]; // OR — from any selected studio
|
||||
collection?: string | null; // a single Plex collection (rating_key)
|
||||
collectionTitle?: string | null; // its title, for the active-filter chip
|
||||
sortDir?: "asc" | "desc"; // sort direction (default desc)
|
||||
}
|
||||
export const EMPTY_PLEX_FILTERS: PlexFilters = {
|
||||
|
|
@ -753,7 +765,8 @@ export function plexFilterCount(f: PlexFilters): number {
|
|||
(f.addedWithin ? 1 : 0) +
|
||||
f.directors.length +
|
||||
f.actors.length +
|
||||
f.studios.length
|
||||
f.studios.length +
|
||||
(f.collection ? 1 : 0)
|
||||
);
|
||||
}
|
||||
export interface PlexPerson {
|
||||
|
|
@ -1122,6 +1135,7 @@ export const api = {
|
|||
if (f.directors?.length) u.set("directors", f.directors.join(","));
|
||||
if (f.actors?.length) u.set("actors", f.actors.join(","));
|
||||
if (f.studios?.length) u.set("studios", f.studios.join(","));
|
||||
if (f.collection) u.set("collection", f.collection);
|
||||
if (f.sortDir === "asc") u.set("sort_dir", "asc");
|
||||
}
|
||||
return req(`/api/plex/browse?${u.toString()}`);
|
||||
|
|
@ -1130,6 +1144,8 @@ export const api = {
|
|||
req(`/api/plex/facets?library=${encodeURIComponent(library)}`),
|
||||
plexPeople: (library: string, q: string): Promise<{ people: PlexPerson[] }> =>
|
||||
req(`/api/plex/people?library=${encodeURIComponent(library)}&q=${encodeURIComponent(q)}`),
|
||||
plexCollections: (library: string, q?: string): Promise<{ collections: PlexCollection[] }> =>
|
||||
req(`/api/plex/collections?library=${encodeURIComponent(library)}${q ? `&q=${encodeURIComponent(q)}` : ""}`),
|
||||
plexShow: (id: string): Promise<PlexShowDetail> =>
|
||||
req(`/api/plex/show/${encodeURIComponent(id)}`),
|
||||
plexItem: (id: string): Promise<PlexItemDetail> =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue