Add actor/cast information with person pages and search support

Integrate TMDB credits data into the app: cast carousels on title detail
pages, person detail pages with biography and filmography, and person
search results in the command palette. Includes new persons/titleCast
DB tables, profile image caching, and a nightly credits refresh cron job.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 14:36:39 -05:00
co-authored by Claude Opus 4.6
parent 9ecf7bbe3c
commit cd5d773e98
27 changed files with 3957 additions and 44 deletions
+27 -1
View File
@@ -20,12 +20,14 @@ import { tmdbImageUrl } from "@/lib/tmdb/image";
import type { TmdbVideo } from "@/lib/tmdb/types";
import type {
AvailabilityOffer,
CastMember,
Episode,
ResolvedTitle,
Season,
} from "@/lib/types/title";
import { refreshAvailability } from "./availability";
import { extractAndStoreColors, parseColorPalette } from "./colors";
import { getCastForTitle, refreshCredits } from "./credits";
import {
cacheEpisodeStills,
cacheImagesForTitle,
@@ -83,6 +85,9 @@ export async function importTitle(
extractAndStoreColors(existing.id, show.poster_path).catch((err) =>
log.debug("Color extraction failed:", err),
),
refreshCredits(existing.id).catch((err) =>
log.debug("Credits enrichment failed:", err),
),
]);
} else {
refreshAvailability(existing.id).catch((err) =>
@@ -91,6 +96,9 @@ export async function importTitle(
refreshRecommendations(existing.id).catch((err) =>
log.debug("Recommendations enrichment failed:", err),
);
refreshCredits(existing.id).catch((err) =>
log.debug("Credits enrichment failed:", err),
);
}
if (imageCacheEnabled()) {
cacheImagesForTitle(existing.id).catch((err) =>
@@ -140,6 +148,9 @@ export async function importTitle(
extractAndStoreColors(row.id, movie.poster_path).catch((err) =>
log.debug("Color extraction failed:", err),
),
refreshCredits(row.id).catch((err) =>
log.debug("Credits enrichment failed:", err),
),
]);
} else {
refreshAvailability(row.id).catch((err) =>
@@ -151,6 +162,9 @@ export async function importTitle(
extractAndStoreColors(row.id, movie.poster_path).catch((err) =>
log.debug("Color extraction failed:", err),
);
refreshCredits(row.id).catch((err) =>
log.debug("Credits enrichment failed:", err),
);
}
refreshTrailer(row.id).catch((err) =>
log.debug("Trailer enrichment failed:", err),
@@ -197,6 +211,9 @@ export async function importTitle(
extractAndStoreColors(row.id, show.poster_path).catch((err) =>
log.debug("Color extraction failed:", err),
),
refreshCredits(row.id).catch((err) =>
log.debug("Credits enrichment failed:", err),
),
]);
} else {
refreshAvailability(row.id).catch((err) =>
@@ -208,6 +225,9 @@ export async function importTitle(
extractAndStoreColors(row.id, show.poster_path).catch((err) =>
log.debug("Color extraction failed:", err),
);
refreshCredits(row.id).catch((err) =>
log.debug("Credits enrichment failed:", err),
);
}
refreshTrailer(row.id).catch((err) =>
log.debug("Trailer enrichment failed:", err),
@@ -277,6 +297,9 @@ export async function refreshTitle(titleId: string) {
refreshTrailer(updated.id).catch((err) =>
log.debug("Trailer enrichment failed:", err),
);
refreshCredits(updated.id).catch((err) =>
log.debug("Credits enrichment failed:", err),
);
if (imageCacheEnabled()) {
cacheImagesForTitle(updated.id).catch((err) =>
log.debug("Image caching failed:", err),
@@ -516,6 +539,7 @@ export async function getTitleWithChildren(id: string): Promise<{
title: ResolvedTitle;
seasons: Season[];
availability: AvailabilityOffer[];
cast: CastMember[];
} | null> {
let title = db.select().from(titles).where(eq(titles.id, id)).get();
if (!title) return null;
@@ -671,7 +695,9 @@ export async function getTitleWithChildren(id: string): Promise<{
trailerVideoKey: title.trailerVideoKey,
};
return { title: resolvedTitle, seasons: titleSeasons, availability };
const cast = getCastForTitle(id);
return { title: resolvedTitle, seasons: titleSeasons, availability, cast };
}
export function pickBestTrailer(videos: TmdbVideo[]): string | null {