feat(plex): P1 frontend — Plex feed source, browse/search, show drill-down
- PlexBrowse.tsx: self-contained Plex mode (library scope tabs, own search + sort, poster-card grid with watch state + playability tint, paged; show → seasons/episodes drill-down). Playback announces (P2 wires the real player). - Feed source dropdown gains a 'Plex' option (gated on me.plex_enabled); App swaps <Feed> for <PlexBrowse> when librarySource==='plex' so the video-feed hooks don't run in Plex mode. me exposes plex_enabled. - api client (plexLibraries/plexBrowse/plexShow/plexImageUrl) + types; librarySource union + share-URL restore gain 'plex'; new plex i18n namespace en/hu/de. - Verified over HTTP (authed): libraries, browse+FTS, image proxy 200 image/jpeg.
This commit is contained in:
parent
63c6e782c8
commit
62636319b1
12 changed files with 452 additions and 5 deletions
|
|
@ -13,6 +13,7 @@ export interface Me {
|
|||
has_google: boolean;
|
||||
has_password: boolean;
|
||||
google_enabled: boolean;
|
||||
plex_enabled: boolean;
|
||||
can_read: boolean;
|
||||
can_write: boolean;
|
||||
pending_invites: number;
|
||||
|
|
@ -179,7 +180,7 @@ export interface FeedFilters {
|
|||
show: string;
|
||||
// Library (scope "all") only — provenance filter: "organic" (default) hides search-
|
||||
// discovered videos, "all" mixes them in, "search" shows only them. Ignored for "my".
|
||||
librarySource?: "organic" | "all" | "search";
|
||||
librarySource?: "organic" | "all" | "search" | "plex";
|
||||
channelId?: string;
|
||||
channelName?: string;
|
||||
maxAgeDays?: number;
|
||||
|
|
@ -623,6 +624,54 @@ export interface PlexTestResult {
|
|||
sections: PlexSection[];
|
||||
}
|
||||
|
||||
// Plex browse/search/drill-down (the mirrored catalog rendered as feed-style cards).
|
||||
export interface PlexLibrary {
|
||||
key: string;
|
||||
title: string;
|
||||
kind: "movie" | "show";
|
||||
count: number;
|
||||
}
|
||||
export interface PlexCard {
|
||||
id: string;
|
||||
type: "movie" | "show" | "episode";
|
||||
title: string;
|
||||
year?: number | null;
|
||||
duration_seconds?: number | null;
|
||||
thumb: string;
|
||||
playable?: string; // direct | remux | transcode
|
||||
status?: string; // new | watched | hidden
|
||||
position_seconds?: number;
|
||||
season_count?: number | null; // show
|
||||
season_number?: number | null; // episode
|
||||
episode_number?: number | null; // episode
|
||||
summary?: string | null; // episode
|
||||
}
|
||||
export interface PlexBrowseResult {
|
||||
kind: "movie" | "show";
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
items: PlexCard[];
|
||||
}
|
||||
export interface PlexSeasonDetail {
|
||||
id: string;
|
||||
season_number: number | null;
|
||||
title: string;
|
||||
thumb: string;
|
||||
episodes: PlexCard[];
|
||||
}
|
||||
export interface PlexShowDetail {
|
||||
show: {
|
||||
id: string;
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
year?: number | null;
|
||||
thumb: string;
|
||||
art: string;
|
||||
};
|
||||
seasons: PlexSeasonDetail[];
|
||||
}
|
||||
|
||||
// Admin Users & roles page.
|
||||
export interface AdminUserRow {
|
||||
id: number;
|
||||
|
|
@ -936,6 +985,27 @@ export const api = {
|
|||
testEmail: (): Promise<{ sent: boolean; to: string }> =>
|
||||
req("/api/admin/config/test-email", { method: "POST" }),
|
||||
testPlex: (): Promise<PlexTestResult> => req("/api/plex/test", { method: "POST" }),
|
||||
syncPlex: (): Promise<Record<string, unknown>> => req("/api/plex/sync", { method: "POST" }),
|
||||
plexLibraries: (): Promise<{ enabled: boolean; libraries: PlexLibrary[] }> =>
|
||||
req("/api/plex/libraries"),
|
||||
plexBrowse: (p: {
|
||||
library: string;
|
||||
q?: string;
|
||||
sort?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}): Promise<PlexBrowseResult> => {
|
||||
const u = new URLSearchParams({ library: p.library });
|
||||
if (p.q) u.set("q", p.q);
|
||||
if (p.sort) u.set("sort", p.sort);
|
||||
if (p.offset) u.set("offset", String(p.offset));
|
||||
if (p.limit) u.set("limit", String(p.limit));
|
||||
return req(`/api/plex/browse?${u.toString()}`);
|
||||
},
|
||||
plexShow: (id: string): Promise<PlexShowDetail> =>
|
||||
req(`/api/plex/show/${encodeURIComponent(id)}`),
|
||||
plexImageUrl: (id: string, variant?: "thumb" | "art"): string =>
|
||||
`/api/plex/image/${encodeURIComponent(id)}${variant === "art" ? "?variant=art" : ""}`,
|
||||
// --- admin: users & roles ---
|
||||
adminUsers: (): Promise<AdminUserRow[]> => req("/api/admin/users"),
|
||||
setUserRole: (id: number, role: "user" | "admin"): Promise<AdminUserRow> =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue