refactor(web): drop cast from title detail

DESIGN.md §9.6 no longer carries cast (#163). Removes the cast
sections, the MetadataCastMember schema and field from both metadata
responses, and every .cast-* rule. arr-meta keeps CastMember and the
credits append; the TMDB call is unchanged.
This commit is contained in:
Miguel Palhas
2026-08-24 12:34:00 +01:00
parent 4d5cc120d1
commit 7d4ddb7331
7 changed files with 3 additions and 206 deletions
+1 -16
View File
@@ -203,8 +203,7 @@
metadata response's own key (no second call); TMDB, IMDb and Rotten
Tomatoes sit beside it — RT is a search URL built here, never a resolved
page. LIBRARY controls follow on their own row: wanted, blocked, root,
then search indexers and remove. CAST is the top 10 billed, each photo
linking out to that person's TMDB page. ON DISK lists imported files
then search indexers and remove. ON DISK lists imported files
with ffprobe attribute tags and any §5.7 waiver, worded honestly. The
§9.3 deck closes the page unchanged — buckets, chips, score heat and
rejection rules untouched; /movies/{id}/releases lands here with the
@@ -271,13 +270,6 @@
<div class="remove-panel" id="remove-panel" hidden></div>
</section>
<section class="deck-group" id="movie-cast" hidden aria-labelledby="label-cast">
<header class="deck-head">
<h3 class="deck-label" id="label-cast">cast</h3>
</header>
<ul class="cast-grid" id="rows-cast"></ul>
</section>
<section class="deck-group" id="movie-files" hidden aria-labelledby="label-disk">
<header class="deck-head">
<h3 class="deck-label" id="label-disk">on disk</h3>
@@ -428,13 +420,6 @@
</div>
</section>
<section class="deck-group" id="series-cast" hidden aria-labelledby="label-series-cast">
<header class="deck-head">
<h3 class="deck-label" id="label-series-cast">cast</h3>
</header>
<ul class="cast-grid" id="rows-series-cast"></ul>
</section>
<ul class="deck-rows seasons" id="rows-seasons"></ul>
</main>
-62
View File
@@ -12,13 +12,11 @@ import {
formatRating,
formatVoteCount,
imdbLink,
type MetadataCastMember,
movieMetadata,
resolveTrailer,
rottenTomatoesSearch,
tmdbImage,
tmdbMovieLink,
tmdbPersonLink,
tmdbSeriesLink,
tvdbLink,
updateMovie,
@@ -1288,46 +1286,6 @@ function externalLink(label: string, href: string): HTMLAnchorElement {
return link;
}
/** One top-billed cast member, linking out to their TMDB page (§9.6). */
function castItem(member: MetadataCastMember): HTMLLIElement {
const item = document.createElement("li");
item.className = "cast-item";
const link = document.createElement("a");
link.className = "cast-link";
link.href = tmdbPersonLink(member.tmdb_id);
link.target = "_blank";
link.rel = "noreferrer";
const photo = document.createElement("span");
photo.className = "cast-photo";
const image = tmdbImage(member.profile_path, "w185");
if (image !== null) {
const img = document.createElement("img");
img.src = image;
img.alt = "";
img.loading = "lazy";
// hotlinked art can 404 (§9.6); fall back to the same empty frame a
// person with no photo gets, rather than a broken-image glyph.
img.addEventListener("error", () => {
img.remove();
photo.classList.add("cast-photo-none");
});
photo.append(img);
} else {
photo.classList.add("cast-photo-none");
}
const name = document.createElement("span");
name.className = "cast-name";
name.textContent = member.name;
const character = document.createElement("span");
character.className = "cast-character readout dim";
character.textContent = member.character;
link.setAttribute("aria-label", `${member.name} as ${member.character} — on TMDB`);
link.append(photo, name, character);
link.title = `${member.name}${member.character}`;
item.append(link);
return item;
}
function movieMain(board: HTMLElement, views: HideableView[]): MovieView {
const view = must<HTMLElement>("#movie");
const deckEl = must<HTMLElement>("#deck");
@@ -1349,8 +1307,6 @@ function movieMain(board: HTMLElement, views: HideableView[]): MovieView {
const sweep = must<HTMLButtonElement>("#movie-sweep");
const remove = must<HTMLButtonElement>("#movie-remove");
const removeWrap = must<HTMLElement>("#remove-panel");
const castSection = must<HTMLElement>("#movie-cast");
const castRows = must<HTMLUListElement>("#rows-cast");
const filesSection = must<HTMLElement>("#movie-files");
const diskCount = must<HTMLElement>("#count-disk");
const diskRows = must<HTMLUListElement>("#rows-disk");
@@ -1589,8 +1545,6 @@ function movieMain(board: HTMLElement, views: HideableView[]): MovieView {
taglineEl.hidden = true;
overviewEl.hidden = true;
actionsEl.replaceChildren();
castSection.hidden = true;
castRows.replaceChildren();
}
async function loadMetadata(id: number, ticket: number) {
@@ -1660,12 +1614,6 @@ function movieMain(board: HTMLElement, views: HideableView[]): MovieView {
externalLink("rotten tomatoes", rottenTomatoesSearch(current.title, current.year)),
);
}
if (detail.cast.length > 0 && current !== null) {
castSection.hidden = false;
for (const member of detail.cast) {
castRows.append(castItem(member));
}
}
}
/* ---- on disk: ffprobe truth and honest waivers (§5.6, §5.7) ---- */
@@ -3033,8 +2981,6 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
const taglineEl = must<HTMLElement>("#series-tagline");
const overviewEl = must<HTMLElement>("#series-overview");
const actionsEl = must<HTMLElement>("#series-actions");
const castSection = must<HTMLElement>("#series-cast");
const castRows = must<HTMLUListElement>("#rows-series-cast");
const statusEl = must<HTMLElement>("#series-status");
const seasonsList = must<HTMLUListElement>("#rows-seasons");
@@ -3104,8 +3050,6 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
taglineEl.hidden = true;
overviewEl.hidden = true;
actionsEl.replaceChildren();
castSection.hidden = true;
castRows.replaceChildren();
}
async function loadMetadata(id: number, ticket: number) {
@@ -3184,12 +3128,6 @@ function seriesMain(board: HTMLElement, tvDeck: TvReleasesView, views: HideableV
externalLink("rotten tomatoes", rottenTomatoesSearch(series.title, series.year)),
);
}
if (detail.cast.length > 0) {
castSection.hidden = false;
for (const member of detail.cast) {
castRows.append(castItem(member));
}
}
}
function seasonCountsChip(season: ApiSeason): HTMLSpanElement {
-14
View File
@@ -4,15 +4,6 @@
import { type ActionOutcome, errorDetail } from "./releases";
/** One of the top-billed cast members (§9.6). */
export interface MetadataCastMember {
tmdb_id: number;
name: string;
character: string;
/** Path fragment exactly as TMDB sends it; the browser composes the URL. */
profile_path: string | null;
}
/** The one trailer worth showing, resolved by the §9.6 rule in arr-meta. */
export interface MetadataTrailer {
youtube_key: string;
@@ -35,7 +26,6 @@ export interface MovieMetadata {
vote_count: number;
homepage: string | null;
imdb_id: string | null;
cast: MetadataCastMember[];
trailer: MetadataTrailer | null;
}
@@ -129,10 +119,6 @@ export function tvdbLink(tvdbId: number): string {
return `https://www.thetvdb.com/dereferrer/series/${tvdbId}`;
}
export function tmdbPersonLink(tmdbId: number): string {
return `https://www.themoviedb.org/person/${tmdbId}`;
}
export function imdbLink(imdbId: string): string {
return `https://www.imdb.com/title/${imdbId}/`;
}
+1 -2
View File
@@ -2,7 +2,7 @@
// release schemas — same reasoning as search.ts: the generated client
// (src/api/) is uncommitted, so CI's tsc cannot see it.
import type { MetadataCastMember, MetadataTrailer } from "./movie";
import type { MetadataTrailer } from "./movie";
import type { ActionOutcome, MovieRelease, ReleasesOutcome, WaiveOutcome } from "./releases";
import { errorDetail, probedAttributeTags, waiverOverride } from "./releases";
@@ -259,7 +259,6 @@ export interface SeriesMetadata {
homepage: string | null;
/** §9.6 links out to TVDB for series; absent when the id is unknown. */
tvdb_id: number | null;
cast: MetadataCastMember[];
trailer: MetadataTrailer | null;
}
-77
View File
@@ -1679,79 +1679,6 @@ body {
flex: 1;
}
/* cast: top billed, each linking out to TMDB (§9.6) */
.cast-grid {
margin: 0;
padding: var(--space-3) var(--space-1) 0;
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
gap: var(--space-4) var(--space-2);
}
.cast-link {
display: grid;
gap: var(--space-1);
justify-items: start;
color: inherit;
text-decoration: none;
min-width: 0;
}
.cast-link:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.cast-photo {
width: 100%;
aspect-ratio: 2 / 3;
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-raised);
transition: border-color 150ms var(--ease-out);
}
.cast-photo img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* no profile photo: dead air, not a broken frame */
.cast-photo-none::before {
content: "";
display: block;
width: 40%;
aspect-ratio: 1;
margin: 30% auto 0;
border-radius: 50%;
border: 1px solid var(--line);
}
.cast-link:hover .cast-photo,
.cast-link:focus-visible .cast-photo {
border-color: var(--accent);
}
.cast-name {
font-size: var(--text-xs);
color: var(--ink);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
}
.cast-character {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
}
/* on-disk rows share the deck's rel grammar: name leads, chips trail */
.disk-name {
min-width: 0;
@@ -1876,10 +1803,6 @@ body {
flex: 1;
}
.cast-grid {
grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));
}
.board {
padding: var(--space-6) var(--space-4);
}