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:
+1
-16
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user