feat(plex): P2 player — rich full-page HLS/direct player + watch-state
- backend: GET /api/plex/item/{rk} detail (metadata, cast + intro/credit markers
from Plex, episode prev/next, per-user resume+status); POST /item/{rk}/progress
(resume checkpoint, near-end→watched) + /state (new|watched|hidden).
- frontend PlexPlayer.tsx (lazy, pulls hls.js): full-page player over the Plex
module. Direct files stream raw <video>; remux via hls.js on the seek-restart
session — custom controls map the ABSOLUTE timeline over the session offset, and a
seek beyond the loaded region restarts the session at the target. Play/pause/resume
/restart, ±10s seek, prev/next episode (Shift+←/→), volume, windowed/fullscreen,
full keyboard, Skip intro/Skip credits from markers (shaded on the seekbar),
auto-advance + watched-on-end, resume from saved position, 10s progress checkpoints.
- PlexBrowse: card/episode click opens the player as a history subview (browser Back
returns to the grid/show). api client (plexItem/plexSession/plexProgress/plexSetState)
+ types; plex.player.* i18n en/hu/de. hls.js@^1.6.16.
- Verified over HTTP: detail w/ markers+cast+episode-nav, progress persist. Video
playback itself is pending browser UAT. Subtitle/audio track SELECTION deferred.
This commit is contained in:
parent
4e9b18cda9
commit
86b86cb260
9 changed files with 726 additions and 11 deletions
|
|
@ -672,6 +672,37 @@ export interface PlexShowDetail {
|
|||
seasons: PlexSeasonDetail[];
|
||||
}
|
||||
|
||||
export interface PlexMarker {
|
||||
type: "intro" | "credits";
|
||||
start_s: number;
|
||||
end_s: number;
|
||||
}
|
||||
export interface PlexItemDetail {
|
||||
id: string;
|
||||
kind: "movie" | "episode";
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
year?: number | null;
|
||||
duration_seconds: number | null;
|
||||
playable: string; // direct | remux | transcode
|
||||
thumb: string;
|
||||
art: string;
|
||||
cast: string[];
|
||||
markers: PlexMarker[];
|
||||
status: string;
|
||||
position_seconds: number;
|
||||
show_title?: string | null;
|
||||
season_number?: number | null;
|
||||
episode_number?: number | null;
|
||||
prev_id?: string | null;
|
||||
next_id?: string | null;
|
||||
}
|
||||
export interface PlexPlaySession {
|
||||
mode: "direct" | "hls";
|
||||
url: string;
|
||||
start_s: number;
|
||||
}
|
||||
|
||||
// Admin Users & roles page.
|
||||
export interface AdminUserRow {
|
||||
id: number;
|
||||
|
|
@ -1006,6 +1037,23 @@ export const api = {
|
|||
},
|
||||
plexShow: (id: string): Promise<PlexShowDetail> =>
|
||||
req(`/api/plex/show/${encodeURIComponent(id)}`),
|
||||
plexItem: (id: string): Promise<PlexItemDetail> =>
|
||||
req(`/api/plex/item/${encodeURIComponent(id)}`),
|
||||
plexSession: (id: string, start = 0): Promise<PlexPlaySession> =>
|
||||
req(`/api/plex/stream/${encodeURIComponent(id)}/session?start=${Math.max(0, Math.floor(start))}`, {
|
||||
method: "POST",
|
||||
}),
|
||||
plexProgress: (id: string, position_seconds: number, duration_seconds: number): Promise<unknown> =>
|
||||
req(`/api/plex/item/${encodeURIComponent(id)}/progress`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ position_seconds, duration_seconds }),
|
||||
}),
|
||||
plexSetState: (id: string, status: "new" | "watched" | "hidden"): Promise<unknown> =>
|
||||
req(`/api/plex/item/${encodeURIComponent(id)}/state`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ status }),
|
||||
}),
|
||||
plexStreamFileUrl: (id: string): string => `/api/plex/stream/${encodeURIComponent(id)}/file`,
|
||||
plexImageUrl: (id: string, variant?: "thumb" | "art"): string =>
|
||||
`/api/plex/image/${encodeURIComponent(id)}${variant === "art" ? "?variant=art" : ""}`,
|
||||
// --- admin: users & roles ---
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue