feat(plex): add a "Recently watched" rail to Discover (D)

A new Discover rail listing what the user last FULLY watched, newest first: a movie as its
own card, a show as the specific EPISODE watched (carrying its show title via leaf_card). Sits
above Recently added. Movies honour the sidebar filters like the other rails; episodes carry no
filterable metadata so they don't. Clicking a card plays it (the existing onCard path already
routes episodes to the player). Bilingual heading added.

Verified live: the rail renders watched movies + episodes with Watched badges.

Gate: tsc/lint/knip/prettier clean, 423 vitest; backend ruff clean, 13 discover tests (+ a
recently-watched movie+episode assertion).
This commit is contained in:
2026-08-23 04:08:28 +02:00
parent 15f5a5694a
commit c0d869089d
7 changed files with 80 additions and 1 deletions
@@ -77,6 +77,7 @@ const card = (id: string, title: string, type: "movie" | "show" = "movie") => ({
const fullPayload = {
scope: "both",
continue_watching: [card("cw1", "Resume Me")],
recently_watched: [card("rw1", "Watched Me")],
recently_added: [card("ra1", "Just Added")],
genres: [{ value: "Drama", count: 12, art: [] }],
because_you_watched: { anchor: { id: "a1", title: "Anchor" }, items: [card("bw1", "Rec One")] },
@@ -103,6 +104,7 @@ describe("PlexDiscover", () => {
/>,
);
await screen.findByText("plex.discover.continueWatching");
expect(screen.getByText("plex.discover.recentlyWatched")).toBeTruthy();
expect(screen.getByText("plex.discover.recentlyAdded")).toBeTruthy();
expect(screen.getByText("plex.discover.surprise")).toBeTruthy();
expect(screen.getByText("plex.discover.byGenre")).toBeTruthy();
@@ -110,6 +112,7 @@ describe("PlexDiscover", () => {
expect(screen.getByText("plex.discover.becauseWatched:Anchor")).toBeTruthy();
// A card from each rail is present (title rendered in the poster label).
expect(screen.getAllByText("Resume Me").length).toBeGreaterThan(0);
expect(screen.getAllByText("Watched Me").length).toBeGreaterThan(0);
expect(screen.getAllByText("Just Added").length).toBeGreaterThan(0);
expect(screen.getAllByText("Random Pick").length).toBeGreaterThan(0);
expect(screen.getAllByText("Rec One").length).toBeGreaterThan(0);
@@ -215,6 +218,7 @@ describe("PlexDiscover", () => {
plexDiscover.mockResolvedValue({
scope: "both",
continue_watching: [],
recently_watched: [],
recently_added: [],
genres: [],
because_you_watched: null,
+10
View File
@@ -143,6 +143,7 @@ export default function PlexDiscover({
const bw = d.because_you_watched;
const nothing =
d.continue_watching.length === 0 &&
d.recently_watched.length === 0 &&
d.recently_added.length === 0 &&
d.genres.length === 0 &&
surprise.length === 0 &&
@@ -165,6 +166,15 @@ export default function PlexDiscover({
/>
)}
{d.recently_watched.length > 0 && (
<CardRail
title={t("plex.discover.recentlyWatched")}
ariaLabel={t("plex.discover.recentlyWatched")}
cards={d.recently_watched}
{...railProps}
/>
)}
{d.recently_added.length > 0 && (
<CardRail
title={t("plex.discover.recentlyAdded")}