mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
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:
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user