From ad8ec623177b47961b74197ee75efa9d080c898e Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Sun, 8 Mar 2026 12:33:07 -0400 Subject: [PATCH] Refactor tmdbImageUrl to accept semantic category instead of size string - Change primary argument from TMDB size string (`w500`, `w1280`, etc.) to category name (`posters`, `backdrops`, `stills`, `logos`, `profiles`); optional size override remains as third argument - Update all call sites across services, actions, pages, and route handlers to use the new category-based API - Update image.test.ts to match the new signature and replace size-mapping test descriptions with category-name descriptions --- .../_components/continue-watching-section.tsx | 8 +- .../dashboard/_components/library-section.tsx | 2 +- .../_components/recommendations-section.tsx | 2 +- app/(pages)/explore/page.tsx | 4 +- app/(pages)/titles/[id]/page.tsx | 2 +- app/api/search/route.ts | 6 +- app/page.tsx | 2 +- lib/actions/explore.ts | 2 +- lib/services/credits.ts | 2 +- lib/services/discovery.ts | 2 +- lib/services/metadata.ts | 8 +- lib/services/person.ts | 10 +-- lib/tmdb/image.test.ts | 73 +++++++++++-------- lib/tmdb/image.ts | 22 +++--- 14 files changed, 77 insertions(+), 68 deletions(-) diff --git a/app/(pages)/dashboard/_components/continue-watching-section.tsx b/app/(pages)/dashboard/_components/continue-watching-section.tsx index 07d4775..c551c25 100644 --- a/app/(pages)/dashboard/_components/continue-watching-section.tsx +++ b/app/(pages)/dashboard/_components/continue-watching-section.tsx @@ -13,18 +13,14 @@ export async function ContinueWatchingSection({ userId }: { userId: string }) { title: { id: item.title.id, title: item.title.title, - backdropPath: tmdbImageUrl(item.title.backdropPath, "w1280"), + backdropPath: tmdbImageUrl(item.title.backdropPath, "backdrops"), }, nextEpisode: item.nextEpisode ? { seasonNumber: item.nextEpisode.seasonNumber, episodeNumber: item.nextEpisode.episodeNumber, name: item.nextEpisode.name, - stillPath: tmdbImageUrl( - item.nextEpisode.stillPath, - "w1280", - "stills", - ), + stillPath: tmdbImageUrl(item.nextEpisode.stillPath, "stills"), } : null, totalEpisodes: item.totalEpisodes, diff --git a/app/(pages)/dashboard/_components/library-section.tsx b/app/(pages)/dashboard/_components/library-section.tsx index 7d70a56..6e9fc49 100644 --- a/app/(pages)/dashboard/_components/library-section.tsx +++ b/app/(pages)/dashboard/_components/library-section.tsx @@ -13,7 +13,7 @@ export async function LibrarySection({ userId }: { userId: string }) { tmdbId: t.tmdbId, type: t.type, title: t.title, - posterPath: tmdbImageUrl(t.posterPath, "w500"), + posterPath: tmdbImageUrl(t.posterPath, "posters"), releaseDate: t.releaseDate ?? t.firstAirDate, voteAverage: t.voteAverage, userStatus: t.userStatus as "watchlist" | "in_progress" | "completed", diff --git a/app/(pages)/dashboard/_components/recommendations-section.tsx b/app/(pages)/dashboard/_components/recommendations-section.tsx index 64d43e9..00e8995 100644 --- a/app/(pages)/dashboard/_components/recommendations-section.tsx +++ b/app/(pages)/dashboard/_components/recommendations-section.tsx @@ -16,7 +16,7 @@ export async function RecommendationsSection({ userId }: { userId: string }) { tmdbId: t.tmdbId, type: t.type, title: t.title, - posterPath: tmdbImageUrl(t.posterPath, "w500"), + posterPath: tmdbImageUrl(t.posterPath, "posters"), releaseDate: t.releaseDate ?? t.firstAirDate, voteAverage: t.voteAverage, })); diff --git a/app/(pages)/explore/page.tsx b/app/(pages)/explore/page.tsx index 714834c..0bdd0f6 100644 --- a/app/(pages)/explore/page.tsx +++ b/app/(pages)/explore/page.tsx @@ -31,7 +31,7 @@ function mapResults( ? r.media_type : fallbackType) as "movie" | "tv", title: r.title ?? r.name ?? "", - posterPath: tmdbImageUrl(r.poster_path, "w500"), + posterPath: tmdbImageUrl(r.poster_path, "posters"), releaseDate: r.release_date ?? r.first_air_date ?? null, voteAverage: r.vote_average, })); @@ -102,7 +102,7 @@ export default async function ExplorePage() { type={heroTitle.media_type as "movie" | "tv"} title={heroTitle.title ?? heroTitle.name ?? ""} overview={heroTitle.overview} - backdropPath={tmdbImageUrl(heroTitle.backdrop_path, "w1280")} + backdropPath={tmdbImageUrl(heroTitle.backdrop_path, "backdrops")} voteAverage={heroTitle.vote_average} /> )} diff --git a/app/(pages)/titles/[id]/page.tsx b/app/(pages)/titles/[id]/page.tsx index 1168b0a..0797082 100644 --- a/app/(pages)/titles/[id]/page.tsx +++ b/app/(pages)/titles/[id]/page.tsx @@ -42,7 +42,7 @@ export async function generateMetadata({ openGraph: { title: title.title, images: title.posterPath - ? [{ url: tmdbImageUrl(title.posterPath, "w500") ?? "" }] + ? [{ url: tmdbImageUrl(title.posterPath, "posters") ?? "" }] : [], }, }; diff --git a/app/api/search/route.ts b/app/api/search/route.ts index 3d73951..ca5dfb7 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -55,7 +55,7 @@ export async function GET(req: NextRequest) { tmdbId: r.id, type: "person" as const, title: r.name, - profilePath: tmdbImageUrl(r.profile_path, "w185"), + profilePath: tmdbImageUrl(r.profile_path, "profiles"), knownForDepartment: r.known_for_department, knownFor: r.known_for ?.slice(0, 3) @@ -83,7 +83,7 @@ export async function GET(req: NextRequest) { type: "person" as const, title: r.name ?? "Unknown", posterPath: null, - profilePath: tmdbImageUrl(r.profile_path ?? null, "w185"), + profilePath: tmdbImageUrl(r.profile_path ?? null, "profiles"), overview: "", releaseDate: null, popularity: r.popularity, @@ -103,7 +103,7 @@ export async function GET(req: NextRequest) { title: r.title ?? r.name, overview: r.overview, releaseDate: r.release_date ?? r.first_air_date, - posterPath: tmdbImageUrl(r.poster_path, "w500"), + posterPath: tmdbImageUrl(r.poster_path, "posters"), popularity: r.popularity, voteAverage: r.vote_average, }; diff --git a/app/page.tsx b/app/page.tsx index 16e75fe..cc0c0de 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -23,7 +23,7 @@ const posterPaths = [ ]; const posterUrls = posterPaths - .map((p) => tmdbImageUrl(p, "w300")) + .map((p) => tmdbImageUrl(p, "posters", "w300")) .filter(Boolean) as string[]; export default function Home() { diff --git a/lib/actions/explore.ts b/lib/actions/explore.ts index 8769752..42512dc 100644 --- a/lib/actions/explore.ts +++ b/lib/actions/explore.ts @@ -20,7 +20,7 @@ export async function discoverByGenre( tmdbId: r.id, type: mediaType, title: (r.title ?? r.name) as string, - posterPath: tmdbImageUrl(r.poster_path, "w500"), + posterPath: tmdbImageUrl(r.poster_path, "posters"), releaseDate: (r.release_date ?? r.first_air_date ?? null) as | string | null, diff --git a/lib/services/credits.ts b/lib/services/credits.ts index 7c9a544..55c5094 100644 --- a/lib/services/credits.ts +++ b/lib/services/credits.ts @@ -316,6 +316,6 @@ export function getCastForTitle(titleId: string): CastMember[] { return rows.map((r) => ({ ...r, - profilePath: tmdbImageUrl(r.profilePath, "w185"), + profilePath: tmdbImageUrl(r.profilePath, "profiles"), })); } diff --git a/lib/services/discovery.ts b/lib/services/discovery.ts index 46f395a..1a486c3 100644 --- a/lib/services/discovery.ts +++ b/lib/services/discovery.ts @@ -505,7 +505,7 @@ export function getRecommendationsForTitle(titleId: string) { tmdbId: r.tmdbId, type: r.type as "movie" | "tv", title: r.title, - posterPath: tmdbImageUrl(r.posterPath, "w500"), + posterPath: tmdbImageUrl(r.posterPath, "posters"), releaseDate: r.releaseDate, firstAirDate: r.firstAirDate, voteAverage: r.voteAverage, diff --git a/lib/services/metadata.ts b/lib/services/metadata.ts index 11c36a3..3e5e315 100644 --- a/lib/services/metadata.ts +++ b/lib/services/metadata.ts @@ -610,7 +610,7 @@ function fetchSeasonsFromDb(titleId: string): Season[] { episodeNumber: ep.episodeNumber, name: ep.name, overview: ep.overview, - stillPath: tmdbImageUrl(ep.stillPath, "w1280", "stills"), + stillPath: tmdbImageUrl(ep.stillPath, "stills"), airDate: ep.airDate, runtimeMinutes: ep.runtimeMinutes, }); @@ -763,7 +763,7 @@ function readAvailability( .map((a) => ({ providerId: a.providerId, providerName: a.providerName, - logoPath: tmdbImageUrl(a.logoPath, "w92"), + logoPath: tmdbImageUrl(a.logoPath, "logos"), offerType: a.offerType, watchUrl: generateProviderUrl(a.providerId, titleName), })); @@ -852,8 +852,8 @@ export async function getOrFetchTitle(id: string): Promise<{ overview: title.overview, releaseDate: title.releaseDate, firstAirDate: title.firstAirDate, - posterPath: tmdbImageUrl(title.posterPath, "w500"), - backdropPath: tmdbImageUrl(title.backdropPath, "w1280"), + posterPath: tmdbImageUrl(title.posterPath, "posters"), + backdropPath: tmdbImageUrl(title.backdropPath, "backdrops"), popularity: title.popularity, voteAverage: title.voteAverage, voteCount: title.voteCount, diff --git a/lib/services/person.ts b/lib/services/person.ts index e82d4e2..6eb585e 100644 --- a/lib/services/person.ts +++ b/lib/services/person.ts @@ -46,7 +46,7 @@ export async function getOrFetchPerson( birthday: details.birthday, deathday: details.deathday, placeOfBirth: details.place_of_birth, - profilePath: tmdbImageUrl(details.profile_path, "w185"), + profilePath: tmdbImageUrl(details.profile_path, "profiles"), knownForDepartment: details.known_for_department, imdbId: details.imdb_id, }; @@ -63,7 +63,7 @@ export async function getOrFetchPerson( birthday: person.birthday, deathday: person.deathday, placeOfBirth: person.placeOfBirth, - profilePath: tmdbImageUrl(person.profilePath, "w185"), + profilePath: tmdbImageUrl(person.profilePath, "profiles"), knownForDepartment: person.knownForDepartment, imdbId: person.imdbId, }; @@ -116,7 +116,7 @@ export async function getOrFetchPersonByTmdbId( birthday: person.birthday, deathday: person.deathday, placeOfBirth: person.placeOfBirth, - profilePath: tmdbImageUrl(person.profilePath, "w185"), + profilePath: tmdbImageUrl(person.profilePath, "profiles"), knownForDepartment: person.knownForDepartment, imdbId: person.imdbId, }; @@ -151,7 +151,7 @@ export function getLocalFilmography(personId: string): PersonCredit[] { tmdbId: r.tmdbId, type: r.type as "movie" | "tv", title: r.title, - posterPath: tmdbImageUrl(r.posterPath, "w500"), + posterPath: tmdbImageUrl(r.posterPath, "posters"), releaseDate: r.releaseDate, firstAirDate: r.firstAirDate, voteAverage: r.voteAverage, @@ -245,7 +245,7 @@ export async function fetchFullFilmography( tmdbId: c.id, type: c.media_type as "movie" | "tv", title: c.title ?? c.name ?? "Unknown", - posterPath: tmdbImageUrl(c.poster_path, "w500"), + posterPath: tmdbImageUrl(c.poster_path, "posters"), releaseDate: c.release_date ?? null, firstAirDate: c.first_air_date ?? null, voteAverage: c.vote_average, diff --git a/lib/tmdb/image.test.ts b/lib/tmdb/image.test.ts index 32a4248..21d91c1 100644 --- a/lib/tmdb/image.test.ts +++ b/lib/tmdb/image.test.ts @@ -24,54 +24,56 @@ describe("tmdbImageUrl", () => { }); test("returns null for null path", () => { - expect(tmdbImageUrl(null)).toBeNull(); + expect(tmdbImageUrl(null, "posters")).toBeNull(); }); test("returns null for undefined path", () => { - expect(tmdbImageUrl(undefined as unknown as string | null)).toBeNull(); + expect( + tmdbImageUrl(undefined as unknown as string | null, "posters"), + ).toBeNull(); }); describe("cache enabled (default)", () => { - test("w500 maps to posters", () => { - expect(tmdbImageUrl("/abc.jpg", "w500")).toBe( + test("posters category", () => { + expect(tmdbImageUrl("/abc.jpg", "posters")).toBe( "/api/images/posters/abc.jpg", ); }); - test("w1280 maps to backdrops", () => { - expect(tmdbImageUrl("/backdrop.jpg", "w1280")).toBe( + test("backdrops category", () => { + expect(tmdbImageUrl("/backdrop.jpg", "backdrops")).toBe( "/api/images/backdrops/backdrop.jpg", ); }); - test("w92 maps to logos", () => { - expect(tmdbImageUrl("/logo.png", "w92")).toBe( + test("logos category", () => { + expect(tmdbImageUrl("/logo.png", "logos")).toBe( "/api/images/logos/logo.png", ); }); - test("w185 maps to profiles", () => { - expect(tmdbImageUrl("/profile.jpg", "w185")).toBe( + test("profiles category", () => { + expect(tmdbImageUrl("/profile.jpg", "profiles")).toBe( "/api/images/profiles/profile.jpg", ); }); - test("strips leading slash from path", () => { - expect(tmdbImageUrl("/test.jpg")).toBe("/api/images/posters/test.jpg"); - }); - - test("handles path without leading slash", () => { - expect(tmdbImageUrl("test.jpg")).toBe("/api/images/posters/test.jpg"); - }); - - test("explicit category override", () => { - expect(tmdbImageUrl("/still.jpg", "w1280", "stills")).toBe( + test("stills category", () => { + expect(tmdbImageUrl("/still.jpg", "stills")).toBe( "/api/images/stills/still.jpg", ); }); - test("default size is w500 (posters)", () => { - expect(tmdbImageUrl("/img.jpg")).toBe("/api/images/posters/img.jpg"); + test("strips leading slash from path", () => { + expect(tmdbImageUrl("/test.jpg", "posters")).toBe( + "/api/images/posters/test.jpg", + ); + }); + + test("handles path without leading slash", () => { + expect(tmdbImageUrl("test.jpg", "posters")).toBe( + "/api/images/posters/test.jpg", + ); }); }); @@ -80,25 +82,34 @@ describe("tmdbImageUrl", () => { process.env.IMAGE_CACHE_ENABLED = "false"; }); - test("returns TMDB CDN URL with default base", () => { - expect(tmdbImageUrl("/abc.jpg", "w500")).toBe( + test("returns TMDB CDN URL with default size for category", () => { + expect(tmdbImageUrl("/abc.jpg", "posters")).toBe( "https://image.tmdb.org/t/p/w500/abc.jpg", ); }); - test("uses custom TMDB_IMAGE_BASE_URL", () => { - process.env.TMDB_IMAGE_BASE_URL = "https://custom-cdn.example.com"; - // Need to re-import to pick up the new base URL — but since IMAGE_BASE_URL - // is evaluated at module load time, we test with the default - expect(tmdbImageUrl("/abc.jpg", "w1280")).toBe( + test("uses category-specific size", () => { + expect(tmdbImageUrl("/abc.jpg", "backdrops")).toBe( "https://image.tmdb.org/t/p/w1280/abc.jpg", ); }); - test("preserves size in URL", () => { - expect(tmdbImageUrl("/img.jpg", "w185")).toBe( + test("allows size override", () => { + expect(tmdbImageUrl("/abc.jpg", "posters", "w300")).toBe( + "https://image.tmdb.org/t/p/w300/abc.jpg", + ); + }); + + test("preserves size for profiles", () => { + expect(tmdbImageUrl("/img.jpg", "profiles")).toBe( "https://image.tmdb.org/t/p/w185/img.jpg", ); }); + + test("preserves size for logos", () => { + expect(tmdbImageUrl("/img.jpg", "logos")).toBe( + "https://image.tmdb.org/t/p/w92/img.jpg", + ); + }); }); }); diff --git a/lib/tmdb/image.ts b/lib/tmdb/image.ts index 73e7685..a4c2729 100644 --- a/lib/tmdb/image.ts +++ b/lib/tmdb/image.ts @@ -3,25 +3,27 @@ import type { ImageCategory } from "@/lib/services/image-cache"; const IMAGE_BASE_URL = process.env.TMDB_IMAGE_BASE_URL || "https://image.tmdb.org/t/p"; -function sizeToCategory(size: string): ImageCategory { - if (size === "w92") return "logos"; - if (size === "w185") return "profiles"; - if (size === "w1280") return "backdrops"; - return "posters"; -} +const CATEGORY_SIZES: Record = { + posters: "w500", + backdrops: "w1280", + stills: "w1280", + logos: "w92", + profiles: "w185", +}; export function tmdbImageUrl( path: string | null, - size = "w500", - category?: ImageCategory, + category: ImageCategory, + sizeOverride?: string, ) { if (!path) return null; + const size = sizeOverride ?? CATEGORY_SIZES[category]; + if (process.env.IMAGE_CACHE_ENABLED === "false") { return `${IMAGE_BASE_URL}/${size}${path}`; } - const resolved = category ?? sizeToCategory(size); const filename = path.startsWith("/") ? path.slice(1) : path; - return `/api/images/${resolved}/${filename}`; + return `/api/images/${category}/${filename}`; }