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
+40 -2
View File
@@ -2,18 +2,31 @@ import { mkdir, rename } from "node:fs/promises";
import path from "node:path";
import { eq } from "drizzle-orm";
import { db } from "@/lib/db/client";
import { availabilityOffers, episodes, seasons, titles } from "@/lib/db/schema";
import {
availabilityOffers,
episodes,
persons,
seasons,
titleCast,
titles,
} from "@/lib/db/schema";
import { createLogger } from "@/lib/logger";
const log = createLogger("image-cache");
export type ImageCategory = "posters" | "backdrops" | "stills" | "logos";
export type ImageCategory =
| "posters"
| "backdrops"
| "stills"
| "logos"
| "profiles";
const CATEGORY_SIZES: Record<ImageCategory, string> = {
posters: "w500",
backdrops: "w1280",
stills: "w1280",
logos: "w92",
profiles: "w185",
};
const IMAGE_BASE_URL =
@@ -218,3 +231,28 @@ export async function cacheProviderLogos(titleId: string) {
}
await Promise.allSettled(tasks);
}
export async function cacheProfilePhotos(titleId: string) {
const castRows = db
.select({ profilePath: persons.profilePath })
.from(titleCast)
.innerJoin(persons, eq(titleCast.personId, persons.id))
.where(eq(titleCast.titleId, titleId))
.all();
const tasks: Promise<unknown>[] = [];
const seen = new Set<string>();
for (const row of castRows) {
if (row.profilePath) {
const basename = path.basename(row.profilePath);
if (!seen.has(basename) && !(await isImageCached("profiles", basename))) {
seen.add(basename);
tasks.push(downloadAndCacheImage(row.profilePath, "profiles"));
}
}
}
if (tasks.length > 0) {
log.debug(`Caching ${tasks.length} profile photos for title ${titleId}`);
}
await Promise.allSettled(tasks);
}