feat(core): use TMDB combined credits for full person filmography

The people.detail endpoint now fetches a person's complete filmography
from TMDB's combined_credits API (cast + crew) instead of only returning
locally-enriched titles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 16:13:43 -04:00
co-authored by Claude Opus 4.6
parent 4ce285f721
commit d0eca7cf32
2 changed files with 37 additions and 9 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
import { ORPCError } from "@orpc/server";
import {
getLocalFilmography,
fetchFullFilmography,
getOrFetchPerson,
getOrFetchPersonByTmdbId,
} from "@sofa/core/person";
@@ -15,7 +15,7 @@ export const detail = os.people.detail
if (!person)
throw new ORPCError("NOT_FOUND", { message: "Person not found" });
const filmography = getLocalFilmography(person.id);
const filmography = await fetchFullFilmography(person.id);
const userStatuses = getUserStatusesByTitleIds(
context.user.id,
filmography.map((c) => c.titleId),
+35 -7
View File
@@ -217,19 +217,29 @@ export async function fetchFullFilmography(
const credits = await getPersonCombinedCredits(person.tmdbId);
// Filter to valid cast entries
// Schema types combined credits cast as movie-only; TV entries also carry
// Schema types combined credits as movie-only; TV entries also carry
// `name` and `first_air_date` at runtime, so widen the type minimally.
type CastEntry = (typeof credits)["cast"] extends (infer E)[] | undefined
? E & { name?: string; first_air_date?: string }
: never;
type CrewEntry = (typeof credits)["crew"] extends (infer E)[] | undefined
? E & { name?: string; first_air_date?: string }
: never;
const validCast = ((credits.cast ?? []) as CastEntry[]).filter(
(c) => c.media_type === "movie" || c.media_type === "tv",
);
if (validCast.length === 0) return [];
const validCrew = ((credits.crew ?? []) as CrewEntry[]).filter(
(c) => c.media_type === "movie" || c.media_type === "tv",
);
if (validCast.length === 0 && validCrew.length === 0) return [];
// Collect all unique TMDB IDs from both cast and crew
const allEntries = [...validCast.map((c) => c), ...validCrew.map((c) => c)];
const tmdbIds = [...new Set(allEntries.map((c) => c.id))];
// Batch prefetch existing titles (1 query)
const tmdbIds = [...new Set(validCast.map((c) => c.id))];
const existingTitles = db
.select({ id: titles.id, tmdbId: titles.tmdbId })
.from(titles)
@@ -240,11 +250,11 @@ export async function fetchFullFilmography(
);
// Batch insert missing titles in a transaction
const newCast = validCast.filter((c) => !titleIdMap.has(c.id));
if (newCast.length > 0) {
const newEntries = allEntries.filter((c) => !titleIdMap.has(c.id));
if (newEntries.length > 0) {
const insertedTmdbIds = new Set<number>();
db.transaction((tx) => {
for (const c of newCast) {
for (const c of newEntries) {
if (insertedTmdbIds.has(c.id)) continue;
insertedTmdbIds.add(c.id);
const row = tx
@@ -304,6 +314,24 @@ export async function fetchFullFilmography(
job: null,
});
}
for (const c of validCrew) {
const tid = titleIdMap.get(c.id);
if (!tid) continue;
results.push({
titleId: tid,
tmdbId: c.id,
type: c.media_type as "movie" | "tv",
title: c.title ?? c.name ?? "Unknown",
posterPath: tmdbImageUrl(c.poster_path ?? null, "posters"),
posterThumbHash: null,
releaseDate: c.release_date ?? null,
firstAirDate: c.first_air_date ?? null,
voteAverage: c.vote_average,
character: null,
department: c.department ?? "Crew",
job: c.job ?? null,
});
}
return results;
}