2026-07-11 03:10:02 +02:00
|
|
|
import { useState } from "react";
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import { useQueryClient } from "@tanstack/react-query";
|
2026-07-11 03:10:02 +02:00
|
|
|
import { Check, ExternalLink, FolderPlus, ListPlus, Play, RotateCcw, Star, X } from "lucide-react";
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
import { api, type PlexFilters, type PlexItemDetail } from "../lib/api";
|
2026-07-11 03:10:02 +02:00
|
|
|
import { DetailCustomizeMenu, Filterable, PrefToggle, useArtBackdrop, useDetailPrefs } from "../lib/plexDetailUi";
|
2026-07-06 17:33:16 +02:00
|
|
|
import PlexCollectionEditor from "./PlexCollectionEditor";
|
feat(plex): Playlists Phase 1 — per-user ordered watch-lists (Siftlode-native)
Personal ordered lists of Plex items, kept in Siftlode's own DB (works for users
without a Plex account, like watch-state) — the "your own lists" counterpart to
shared collections. Plex-direction sync is a later phase (plex_rating_key
reserved).
Backend: migration 0048 (plex_playlists + plex_playlist_items with position),
PlexPlaylist/PlexPlaylistItem models, and per-user CRUD endpoints under
/api/plex/playlists (list [+?contains for the add dialog], create [seeded],
detail [ordered cards], rename, delete, add/remove item, reorder). _leaf_card
handles the movie/episode mix. Frontend: "Add to playlist" dialog from the movie
info page (all users), a Playlists section in PlexSidebar (list + create),
PlexPlaylistView (reorder up/down, remove, rename, delete, Play all), and
PlexPlayer gained an optional `queue` so play-through follows the list order
(prev/next + auto-advance). i18n en/hu/de. Verified end-to-end on localdev
(backend CRUD + the create→add→view→play-through UI flow).
2026-07-06 19:05:12 +02:00
|
|
|
import PlexPlaylistAdd from "./PlexPlaylistAdd";
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
|
|
|
|
|
// The rich "media info" surface for a Plex item — poster/art, IMDb score + link, content rating,
|
|
|
|
|
// genres, director, cast (with photos), summary. Reused in TWO places: a full-page view opened from
|
|
|
|
|
// a card's "i" button (variant="page", with a big Play/Resume + watch controls), and a lean overlay
|
|
|
|
|
// over the running player (variant="overlay", opened with "i"). Two GUI elements are user-toggleable
|
|
|
|
|
// personal preferences, persisted in users.preferences: the faint art backdrop and the cast row.
|
|
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
detail: PlexItemDetail;
|
|
|
|
|
variant: "page" | "overlay";
|
|
|
|
|
onPlay?: () => void; // page: start/resume playback
|
|
|
|
|
onClose?: () => void; // overlay: dismiss
|
|
|
|
|
onStateChange?: () => void; // after a watch-state change, let the opener refresh
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
// When provided, metadata (year/genre/director/cast/studio/rating) becomes clickable and sets
|
|
|
|
|
// the corresponding Plex filter (then the opener navigates to the filtered grid).
|
|
|
|
|
onFilter?: (patch: Partial<PlexFilters>) => void;
|
2026-07-06 02:25:48 +02:00
|
|
|
// Play a sibling title from a collection strip (opens its player).
|
|
|
|
|
onPlayItem?: (id: string) => void;
|
2026-07-06 17:33:16 +02:00
|
|
|
// The item's library key — enables the admin "Manage collections" editor (page variant, movies).
|
|
|
|
|
library?: string;
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function fmtDur(s?: number | null): string {
|
|
|
|
|
if (!s) return "";
|
|
|
|
|
const h = Math.floor(s / 3600);
|
|
|
|
|
const m = Math.floor((s % 3600) / 60);
|
|
|
|
|
return h ? `${h}h ${m}m` : `${m}m`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 02:25:48 +02:00
|
|
|
export default function PlexInfo({
|
|
|
|
|
detail,
|
|
|
|
|
variant,
|
|
|
|
|
onPlay,
|
|
|
|
|
onClose,
|
|
|
|
|
onStateChange,
|
|
|
|
|
onFilter,
|
|
|
|
|
onPlayItem,
|
2026-07-06 17:33:16 +02:00
|
|
|
library,
|
2026-07-06 02:25:48 +02:00
|
|
|
}: Props) {
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
|
2026-07-11 03:10:02 +02:00
|
|
|
// Personal display prefs (default ON), shared with the series detail pages via useDetailPrefs:
|
|
|
|
|
// artBg/showCast + their toggles and the raw savePref persister. The collection-strip source hiding
|
|
|
|
|
// is PlexInfo-specific state, persisted through the same savePref.
|
|
|
|
|
const { artBg, showCast, toggleArtBg, toggleCast, savePref } = useDetailPrefs();
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
const prefs = (qc.getQueryData<{ preferences?: Record<string, unknown> }>(["me"])?.preferences ??
|
2026-07-11 03:10:02 +02:00
|
|
|
{}) as { plexHiddenStripSources?: string[] };
|
2026-07-06 06:50:03 +02:00
|
|
|
// Which collection-strip source buckets the user has HIDDEN (default: none → all shown). Toggled
|
|
|
|
|
// per type in the customize menu (Collections / IMDb / TMDb / …), only for sources present here.
|
|
|
|
|
const [hiddenSources, setHiddenSources] = useState<string[]>(
|
|
|
|
|
Array.isArray(prefs.plexHiddenStripSources) ? prefs.plexHiddenStripSources : [],
|
|
|
|
|
);
|
|
|
|
|
const toggleSource = (src: string) => {
|
|
|
|
|
setHiddenSources((cur) => {
|
|
|
|
|
const next = cur.includes(src) ? cur.filter((s) => s !== src) : [...cur, src];
|
|
|
|
|
savePref({ plexHiddenStripSources: next });
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
};
|
2026-07-06 17:33:16 +02:00
|
|
|
const isAdmin = (qc.getQueryData<{ role?: string }>(["me"])?.role) === "admin";
|
|
|
|
|
const [editorOpen, setEditorOpen] = useState(false);
|
feat(plex): Playlists Phase 1 — per-user ordered watch-lists (Siftlode-native)
Personal ordered lists of Plex items, kept in Siftlode's own DB (works for users
without a Plex account, like watch-state) — the "your own lists" counterpart to
shared collections. Plex-direction sync is a later phase (plex_rating_key
reserved).
Backend: migration 0048 (plex_playlists + plex_playlist_items with position),
PlexPlaylist/PlexPlaylistItem models, and per-user CRUD endpoints under
/api/plex/playlists (list [+?contains for the add dialog], create [seeded],
detail [ordered cards], rename, delete, add/remove item, reorder). _leaf_card
handles the movie/episode mix. Frontend: "Add to playlist" dialog from the movie
info page (all users), a Playlists section in PlexSidebar (list + create),
PlexPlaylistView (reorder up/down, remove, rename, delete, Play all), and
PlexPlayer gained an optional `queue` so play-through follows the list order
(prev/next + auto-advance). i18n en/hu/de. Verified end-to-end on localdev
(backend CRUD + the create→add→view→play-through UI flow).
2026-07-06 19:05:12 +02:00
|
|
|
const [playlistOpen, setPlaylistOpen] = useState(false);
|
2026-07-06 06:50:03 +02:00
|
|
|
|
2026-07-11 03:10:02 +02:00
|
|
|
// Faint art backdrop, HTPC-style (page variant only) — the shared hook paints the item's art as a
|
|
|
|
|
// fixed background on <main> and clears it on unmount / when disabled / in the overlay variant.
|
|
|
|
|
useArtBackdrop(detail.art, variant !== "overlay" && artBg);
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
|
|
|
|
|
const inProgress = (detail.position_seconds ?? 0) > 0 && detail.status !== "watched";
|
|
|
|
|
const setState = async (status: "new" | "watched") => {
|
|
|
|
|
await api.plexSetState(detail.id, status).catch(() => {});
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["plex-item", detail.id] });
|
feat(plex): unify movies + shows into one cross-library browser
Collapse the separate Movie/Show library sections into ONE unified library with a
shared search + shared filter sidebar and a Movies/Shows/Both scope selector.
Backend: new GET /api/plex/library — a cross-library UNION of movies (plex_items) and
shows (plex_shows) as one mixed, paginated, sorted feed, scoped movie|show|both, with
the shared filters (extracted into _apply_meta_filters, DRY), FTS search, and per-user
watch-state (a show's state = the aggregate of its episodes). On a search that also
matches episodes, matching episodes come back in a separate 'episodes' list (the grouped
'Episodes' section — Proposal 3). /facets is now scope-aware (merged across the scope's
libraries). /item and /show now return their library section key (for the admin
collection editor, since there's no single library prop in the unified view).
Frontend: PlexSidebar's library picker -> a scope selector (Both/Movies/Shows); facets +
browse follow the scope (App's plexLib repurposed to a validated scope, default both).
PlexBrowse uses the unified endpoint, renders a mixed Titles grid + an Episodes section
on search. Poster cards are generalized: a hover Play/Resume overlay on every card, a
clickable title (not just the poster), and a movie/show type tag. The quick watched
toggle is now optimistic so it reliably flips BOTH ways (fixes the movie card that could
mark but not un-mark). Cast/crew members and the show hero's meta (year/rating/genre/
content-rating) are clickable filters; clicking a person widens the scope to Both so the
result is a mixed movie+show feed. i18n plex.filter.scope*/plex.unified.* (en/hu/de).
Still pending from the polish list (next pass): season-card quick toggles (watched +
add-to-playlist), per-episode watched toggle, and the full glassy art-bg + hide-cast
customize menu on the series pages.
2026-07-11 00:15:49 +02:00
|
|
|
qc.invalidateQueries({ queryKey: ["plex-library"] });
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
qc.invalidateQueries({ queryKey: ["plex-show"] });
|
|
|
|
|
onStateChange?.();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const cast = showCast ? detail.cast : [];
|
|
|
|
|
const overlay = variant === "overlay";
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
const mutedCls = overlay ? "text-white/70" : "text-muted";
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
|
2026-07-06 06:50:03 +02:00
|
|
|
// Distinct strip-source buckets present on THIS item, in strip order — one toggle each.
|
|
|
|
|
const presentSources = (detail.collections ?? []).reduce<string[]>((acc, c) => {
|
|
|
|
|
if (!acc.includes(c.source)) acc.push(c.source);
|
|
|
|
|
return acc;
|
|
|
|
|
}, []);
|
|
|
|
|
const sourceLabel = (src: string) =>
|
|
|
|
|
t(`plex.info.stripSource.${src}`, { defaultValue: src.toUpperCase() });
|
|
|
|
|
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
return (
|
|
|
|
|
<div className={overlay ? "relative w-full max-w-3xl" : "relative"}>
|
2026-07-06 06:50:03 +02:00
|
|
|
<div className={overlay ? "p-5" : "flex flex-col gap-4 p-3 sm:p-4"}>
|
|
|
|
|
{/* Hero block: floats as a frosted glass panel over the fixed art backdrop (page variant). */}
|
|
|
|
|
<div
|
|
|
|
|
className={overlay ? "" : "glass rounded-2xl p-4 sm:p-6"}
|
|
|
|
|
style={overlay ? undefined : { background: "color-mix(in srgb, var(--surface) 55%, transparent)" }}
|
|
|
|
|
>
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
{/* Header: close (overlay) / customize */}
|
|
|
|
|
<div className="mb-4 flex items-start gap-3">
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<h1 className={overlay ? "text-lg font-bold text-white" : "text-2xl font-bold"}>
|
|
|
|
|
{detail.kind === "episode" && detail.show_title ? detail.show_title : detail.title}
|
|
|
|
|
</h1>
|
|
|
|
|
{detail.kind === "episode" && (
|
|
|
|
|
<p className={`text-sm ${overlay ? "text-white/70" : "text-muted"}`}>
|
|
|
|
|
S{detail.season_number}·E{detail.episode_number} — {detail.title}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-07-11 03:10:02 +02:00
|
|
|
<DetailCustomizeMenu
|
|
|
|
|
overlay={overlay}
|
|
|
|
|
artBg={artBg}
|
|
|
|
|
onToggleArtBg={toggleArtBg}
|
|
|
|
|
showCast={showCast}
|
|
|
|
|
onToggleCast={toggleCast}
|
|
|
|
|
hasCast={detail.cast.length > 0}
|
|
|
|
|
extra={presentSources.map((src) => (
|
|
|
|
|
<PrefToggle
|
|
|
|
|
key={src}
|
|
|
|
|
label={sourceLabel(src)}
|
|
|
|
|
on={!hiddenSources.includes(src)}
|
|
|
|
|
onClick={() => toggleSource(src)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
/>
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
{overlay && onClose && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
className="shrink-0 p-1.5 rounded-lg text-white/80 hover:bg-white/15"
|
|
|
|
|
aria-label={t("plex.player.back")}
|
|
|
|
|
>
|
|
|
|
|
<X className="w-5 h-5" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-4 sm:gap-6">
|
|
|
|
|
{/* Poster */}
|
|
|
|
|
<img
|
|
|
|
|
src={detail.thumb}
|
|
|
|
|
alt=""
|
|
|
|
|
className={`shrink-0 rounded-xl border border-border object-cover ${overlay ? "w-24 sm:w-28" : "w-32 sm:w-44"} aspect-[2/3]`}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div className={`min-w-0 flex-1 ${overlay ? "text-white/90" : ""}`}>
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
{/* Meta line + IMDb rating/link (each value can set the matching filter). */}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
{detail.year != null && (
|
|
|
|
|
<Filterable
|
|
|
|
|
className={mutedCls}
|
|
|
|
|
title={onFilter ? t("plex.info.filterYear", { year: detail.year }) : undefined}
|
|
|
|
|
onClick={onFilter ? () => onFilter({ yearMin: detail.year, yearMax: detail.year }) : undefined}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
>
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
{detail.year}
|
|
|
|
|
</Filterable>
|
|
|
|
|
)}
|
|
|
|
|
{detail.content_rating && (
|
|
|
|
|
<Filterable
|
|
|
|
|
className={mutedCls}
|
|
|
|
|
onClick={onFilter ? () => onFilter({ contentRatings: [detail.content_rating!] }) : undefined}
|
|
|
|
|
>
|
|
|
|
|
{detail.content_rating}
|
|
|
|
|
</Filterable>
|
|
|
|
|
)}
|
|
|
|
|
{detail.duration_seconds ? <span className={mutedCls}>{fmtDur(detail.duration_seconds)}</span> : null}
|
|
|
|
|
{detail.studio && (
|
|
|
|
|
<Filterable
|
|
|
|
|
className={mutedCls}
|
2026-07-06 00:15:31 +02:00
|
|
|
onClick={onFilter ? () => onFilter({ studios: [detail.studio!] }) : undefined}
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
>
|
|
|
|
|
{detail.studio}
|
|
|
|
|
</Filterable>
|
|
|
|
|
)}
|
|
|
|
|
{detail.imdb_rating != null && (
|
|
|
|
|
<span className="inline-flex items-center gap-1 rounded-md bg-amber-400/15 px-1.5 py-0.5 font-semibold text-amber-500">
|
|
|
|
|
<button
|
|
|
|
|
onClick={onFilter ? () => onFilter({ ratingMin: Math.floor(detail.imdb_rating!) }) : undefined}
|
|
|
|
|
className={`inline-flex items-center gap-1 ${onFilter ? "cursor-pointer hover:opacity-80" : "cursor-default"}`}
|
|
|
|
|
title={onFilter ? t("plex.info.filterRating", { n: Math.floor(detail.imdb_rating!) }) : undefined}
|
|
|
|
|
>
|
|
|
|
|
<Star className="w-3.5 h-3.5 fill-current" />
|
|
|
|
|
{detail.imdb_rating.toFixed(1)}
|
|
|
|
|
</button>
|
|
|
|
|
{detail.imdb_url && (
|
|
|
|
|
<a href={detail.imdb_url} target="_blank" rel="noopener noreferrer" title={t("plex.info.openImdb")}>
|
|
|
|
|
<ExternalLink className="w-3 h-3 opacity-70 hover:opacity-100" />
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{detail.tagline && (
|
|
|
|
|
<p className={`mt-2 text-sm italic ${overlay ? "text-white/60" : "text-muted"}`}>
|
|
|
|
|
{detail.tagline}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{detail.genres.length > 0 && (
|
|
|
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
|
|
|
{detail.genres.map((g) => (
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
<Filterable
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
key={g}
|
|
|
|
|
className={`rounded-full px-2 py-0.5 text-xs ${overlay ? "bg-white/10" : "bg-surface text-muted"}`}
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
onClick={onFilter ? () => onFilter({ genres: [g] }) : undefined}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
>
|
|
|
|
|
{g}
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
</Filterable>
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{detail.directors.length > 0 && (
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
<p className={`mt-2 text-sm ${mutedCls}`}>
|
|
|
|
|
{t("plex.info.director")}:{" "}
|
|
|
|
|
{detail.directors.map((d, i) => (
|
|
|
|
|
<span key={d}>
|
|
|
|
|
<Filterable
|
|
|
|
|
className="font-medium"
|
2026-07-06 00:15:31 +02:00
|
|
|
onClick={onFilter ? () => onFilter({ directors: [d] }) : undefined}
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
>
|
|
|
|
|
{d}
|
|
|
|
|
</Filterable>
|
|
|
|
|
{i < detail.directors.length - 1 ? ", " : ""}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{detail.summary && (
|
|
|
|
|
<p className={`mt-3 text-sm leading-relaxed ${overlay ? "text-white/85 line-clamp-5" : ""}`}>
|
|
|
|
|
{detail.summary}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Page actions: Play/Resume + watch controls */}
|
|
|
|
|
{!overlay && (
|
|
|
|
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
|
|
|
{onPlay && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={onPlay}
|
|
|
|
|
className="inline-flex items-center gap-2 rounded-xl bg-accent px-4 py-2 font-medium text-white hover:opacity-90"
|
|
|
|
|
>
|
|
|
|
|
<Play className="w-5 h-5 fill-current" />
|
|
|
|
|
{inProgress ? t("plex.resume") : t("plex.play")}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{detail.status === "watched" ? (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setState("new")}
|
2026-07-06 06:50:03 +02:00
|
|
|
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
>
|
|
|
|
|
<RotateCcw className="w-4 h-4" />
|
|
|
|
|
{t("plex.info.markUnwatched")}
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setState("watched")}
|
2026-07-06 06:50:03 +02:00
|
|
|
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
>
|
|
|
|
|
<Check className="w-4 h-4" />
|
|
|
|
|
{t("plex.info.markWatched")}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{inProgress && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setState("new")}
|
2026-07-06 06:50:03 +02:00
|
|
|
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
>
|
|
|
|
|
<RotateCcw className="w-4 h-4" />
|
|
|
|
|
{t("plex.info.clearResume")}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
feat(plex): Playlists Phase 1 — per-user ordered watch-lists (Siftlode-native)
Personal ordered lists of Plex items, kept in Siftlode's own DB (works for users
without a Plex account, like watch-state) — the "your own lists" counterpart to
shared collections. Plex-direction sync is a later phase (plex_rating_key
reserved).
Backend: migration 0048 (plex_playlists + plex_playlist_items with position),
PlexPlaylist/PlexPlaylistItem models, and per-user CRUD endpoints under
/api/plex/playlists (list [+?contains for the add dialog], create [seeded],
detail [ordered cards], rename, delete, add/remove item, reorder). _leaf_card
handles the movie/episode mix. Frontend: "Add to playlist" dialog from the movie
info page (all users), a Playlists section in PlexSidebar (list + create),
PlexPlaylistView (reorder up/down, remove, rename, delete, Play all), and
PlexPlayer gained an optional `queue` so play-through follows the list order
(prev/next + auto-advance). i18n en/hu/de. Verified end-to-end on localdev
(backend CRUD + the create→add→view→play-through UI flow).
2026-07-06 19:05:12 +02:00
|
|
|
{detail.kind === "movie" && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setPlaylistOpen(true)}
|
|
|
|
|
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
|
|
|
|
>
|
|
|
|
|
<ListPlus className="w-4 h-4" />
|
|
|
|
|
{t("plex.playlistAdd.manage")}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-07-06 17:33:16 +02:00
|
|
|
{isAdmin && detail.kind === "movie" && library && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setEditorOpen(true)}
|
|
|
|
|
className="glass-card glass-hover inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm hover:text-accent"
|
|
|
|
|
>
|
|
|
|
|
<FolderPlus className="w-4 h-4" />
|
|
|
|
|
{t("plex.collEditor.manage")}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-07-06 06:50:03 +02:00
|
|
|
</div>
|
|
|
|
|
{/* End hero panel. */}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
|
|
|
|
|
{/* Cast row (toggleable), circular photos. */}
|
|
|
|
|
{cast.length > 0 && (
|
2026-07-06 06:50:03 +02:00
|
|
|
<div
|
|
|
|
|
className={overlay ? "mt-5" : "glass-card rounded-2xl p-4 sm:p-5"}
|
|
|
|
|
style={overlay ? undefined : { background: "color-mix(in srgb, var(--card) 55%, transparent)" }}
|
|
|
|
|
>
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
<h2 className={`mb-2 text-sm font-semibold ${overlay ? "text-white/80" : ""}`}>
|
|
|
|
|
{t("plex.info.cast")}
|
|
|
|
|
</h2>
|
|
|
|
|
<div className="flex gap-3 overflow-x-auto pb-2">
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
{cast.map((c, i) => {
|
|
|
|
|
const inner = (
|
|
|
|
|
<>
|
|
|
|
|
<div
|
|
|
|
|
className={`mx-auto mb-1 h-20 w-20 overflow-hidden rounded-full border ${
|
|
|
|
|
overlay ? "border-white/15 bg-white/10" : "border-border bg-surface"
|
|
|
|
|
} ${onFilter ? "group-hover/cast:border-accent" : ""}`}
|
|
|
|
|
>
|
|
|
|
|
{c.thumb ? (
|
|
|
|
|
<img src={c.thumb} alt="" className="h-full w-full object-cover" />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="grid h-full w-full place-items-center text-lg opacity-40">
|
|
|
|
|
{c.name.charAt(0)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={`truncate text-xs font-medium ${overlay ? "text-white/90" : ""} ${
|
|
|
|
|
onFilter ? "group-hover/cast:text-accent" : ""
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{c.name}
|
|
|
|
|
</div>
|
|
|
|
|
{c.role && (
|
|
|
|
|
<div className={`truncate text-[11px] ${overlay ? "text-white/55" : "text-muted"}`}>
|
|
|
|
|
{c.role}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
</div>
|
|
|
|
|
)}
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
return onFilter ? (
|
|
|
|
|
<button
|
|
|
|
|
key={i}
|
2026-07-06 00:15:31 +02:00
|
|
|
onClick={() => onFilter({ actors: [c.name] })}
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
title={t("plex.info.filterActor", { name: c.name })}
|
|
|
|
|
className="group/cast w-20 shrink-0 text-center"
|
|
|
|
|
>
|
|
|
|
|
{inner}
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<div key={i} className="w-20 shrink-0 text-center">
|
|
|
|
|
{inner}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
</div>
|
feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).
Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
|
|
|
);
|
|
|
|
|
})}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-07-06 02:25:48 +02:00
|
|
|
|
2026-07-06 06:50:03 +02:00
|
|
|
{/* Collection strips — the other titles in this movie's collection(s), directly playable.
|
|
|
|
|
Each source bucket (Collections / IMDb / TMDb / smart / …) is independently hideable. */}
|
|
|
|
|
{detail.collections
|
|
|
|
|
?.filter((col) => !hiddenSources.includes(col.source))
|
|
|
|
|
.map((col) => (
|
|
|
|
|
<div
|
|
|
|
|
key={col.id}
|
|
|
|
|
className={overlay ? "mt-6" : "glass-card rounded-2xl p-4 sm:p-5"}
|
|
|
|
|
style={overlay ? undefined : { background: "color-mix(in srgb, var(--card) 55%, transparent)" }}
|
|
|
|
|
>
|
2026-07-06 02:25:48 +02:00
|
|
|
<div className="mb-2 flex items-center justify-between gap-3">
|
|
|
|
|
<h2 className={`text-sm font-semibold ${overlay ? "text-white/80" : ""}`}>{col.title}</h2>
|
|
|
|
|
{onFilter && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onFilter({ collection: col.id, collectionTitle: col.title })}
|
|
|
|
|
className="shrink-0 text-xs text-muted hover:text-accent"
|
|
|
|
|
>
|
|
|
|
|
{t("plex.info.browseCollection")}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
|
|
|
{col.items.map((m) => (
|
|
|
|
|
<button
|
|
|
|
|
key={m.id}
|
|
|
|
|
onClick={() => onPlayItem?.(m.id)}
|
|
|
|
|
disabled={!onPlayItem}
|
|
|
|
|
className="group/col w-28 shrink-0 text-left"
|
|
|
|
|
>
|
|
|
|
|
<div className="relative aspect-[2/3] overflow-hidden rounded-lg border border-border bg-surface">
|
|
|
|
|
<img
|
|
|
|
|
src={m.thumb}
|
|
|
|
|
alt=""
|
|
|
|
|
className="h-full w-full object-cover transition group-hover/col:scale-105"
|
|
|
|
|
/>
|
|
|
|
|
{m.status === "watched" && (
|
|
|
|
|
<span className="absolute right-1 top-1 rounded bg-black/70 px-1 text-[9px] text-white">✓</span>
|
|
|
|
|
)}
|
|
|
|
|
{onPlayItem && (
|
|
|
|
|
<div className="absolute inset-0 grid place-items-center bg-black/40 opacity-0 transition group-hover/col:opacity-100">
|
|
|
|
|
<Play className="h-8 w-8 text-white" fill="currentColor" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{(m.position_seconds ?? 0) > 0 && m.status !== "watched" && m.duration_seconds ? (
|
|
|
|
|
<div className="absolute inset-x-0 bottom-0 h-0.5 bg-black/40">
|
|
|
|
|
<div
|
|
|
|
|
className="h-full bg-accent"
|
|
|
|
|
style={{ width: `${Math.min(100, ((m.position_seconds ?? 0) / m.duration_seconds) * 100)}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`mt-1 truncate text-xs font-medium ${overlay ? "text-white/90" : ""}`}>
|
|
|
|
|
{m.title}
|
|
|
|
|
</div>
|
|
|
|
|
{m.year ? <div className="text-[11px] text-muted">{m.year}</div> : null}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
</div>
|
2026-07-06 17:33:16 +02:00
|
|
|
{editorOpen && library && (
|
|
|
|
|
<PlexCollectionEditor
|
|
|
|
|
item={{ id: detail.id, title: detail.title }}
|
|
|
|
|
library={library}
|
|
|
|
|
memberOf={(detail.collections ?? []).map((c) => c.id)}
|
|
|
|
|
onClose={() => setEditorOpen(false)}
|
|
|
|
|
onChanged={() => onStateChange?.()}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
feat(plex): Playlists Phase 1 — per-user ordered watch-lists (Siftlode-native)
Personal ordered lists of Plex items, kept in Siftlode's own DB (works for users
without a Plex account, like watch-state) — the "your own lists" counterpart to
shared collections. Plex-direction sync is a later phase (plex_rating_key
reserved).
Backend: migration 0048 (plex_playlists + plex_playlist_items with position),
PlexPlaylist/PlexPlaylistItem models, and per-user CRUD endpoints under
/api/plex/playlists (list [+?contains for the add dialog], create [seeded],
detail [ordered cards], rename, delete, add/remove item, reorder). _leaf_card
handles the movie/episode mix. Frontend: "Add to playlist" dialog from the movie
info page (all users), a Playlists section in PlexSidebar (list + create),
PlexPlaylistView (reorder up/down, remove, rename, delete, Play all), and
PlexPlayer gained an optional `queue` so play-through follows the list order
(prev/next + auto-advance). i18n en/hu/de. Verified end-to-end on localdev
(backend CRUD + the create→add→view→play-through UI flow).
2026-07-06 19:05:12 +02:00
|
|
|
{playlistOpen && (
|
2026-07-06 22:14:56 +02:00
|
|
|
<PlexPlaylistAdd
|
|
|
|
|
target={{ kind: "single", ratingKey: detail.id, title: detail.title }}
|
|
|
|
|
onClose={() => setPlaylistOpen(false)}
|
|
|
|
|
/>
|
feat(plex): Playlists Phase 1 — per-user ordered watch-lists (Siftlode-native)
Personal ordered lists of Plex items, kept in Siftlode's own DB (works for users
without a Plex account, like watch-state) — the "your own lists" counterpart to
shared collections. Plex-direction sync is a later phase (plex_rating_key
reserved).
Backend: migration 0048 (plex_playlists + plex_playlist_items with position),
PlexPlaylist/PlexPlaylistItem models, and per-user CRUD endpoints under
/api/plex/playlists (list [+?contains for the add dialog], create [seeded],
detail [ordered cards], rename, delete, add/remove item, reorder). _leaf_card
handles the movie/episode mix. Frontend: "Add to playlist" dialog from the movie
info page (all users), a Playlists section in PlexSidebar (list + create),
PlexPlaylistView (reorder up/down, remove, rename, delete, Play all), and
PlexPlayer gained an optional `queue` so play-through follows the list order
(prev/next + auto-advance). i18n en/hu/de. Verified end-to-end on localdev
(backend CRUD + the create→add→view→play-through UI flow).
2026-07-06 19:05:12 +02:00
|
|
|
)}
|
feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).
Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|