feat(search): find titles by IMDb ID

Search queries that look like an IMDb ID (e.g. tt0133093) are now resolved
through TMDB's find endpoint, respecting the movie/tv type filter.

Supersedes #60.

Co-authored-by: Gonçalo Fonseca <gcare.fonseca99@outlook.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed 2026-10-05 03:57:00 -04:00
1 parent 0f82de55cb
commit b63fbce60d
2 files changed
+116 -2

No files matched your search

+91
View File
@@ -332,3 +332,94 @@ describe("discover.browse", () => {
); );
}); });
}); });
describe("search by IMDb ID", () => {
const found = { movie_results: [movie], tv_results: [show] };
test("resolves an IMDb ID through the find endpoint instead of text search", async () => {
tmdb.findByExternalId.mockResolvedValue(found);
const result = await call(
implementedRouter.discover.search,
{ query: "tt0133093", page: 1 },
ctx,
);
expect(tmdb.findByExternalId).toHaveBeenCalledWith("tt0133093", "imdb_id");
expect(tmdb.searchMulti).not.toHaveBeenCalled();
expect(result.results).toHaveLength(2);
expect(result.results[0]).toMatchObject({ tmdbId: 603, type: "movie", title: "The Matrix" });
expect(result.results[0]?.id).toBeTruthy();
expect(result.results[1]).toMatchObject({
tmdbId: 1399,
type: "tv",
title: "Game of Thrones",
});
expect(result.results[1]?.id).toBeTruthy();
expect(result.page).toBe(1);
expect(result.totalPages).toBe(1);
expect(result.totalResults).toBe(2);
});
test("lowercases the IMDb ID", async () => {
tmdb.findByExternalId.mockResolvedValue(found);
await call(implementedRouter.discover.search, { query: "TT0133093", page: 1 }, ctx);
expect(tmdb.findByExternalId).toHaveBeenCalledWith("tt0133093", "imdb_id");
});
test("respects the type filter", async () => {
tmdb.findByExternalId.mockResolvedValue(found);
const result = await call(
implementedRouter.discover.search,
{ query: "tt0133093", type: "movie", page: 1 },
ctx,
);
expect(result.results).toHaveLength(1);
expect(result.results[0]).toMatchObject({ tmdbId: 603, type: "movie" });
});
test("returns no pages when the ID matches nothing", async () => {
tmdb.findByExternalId.mockResolvedValue({ movie_results: [], tv_results: [] });
const result = await call(
implementedRouter.discover.search,
{ query: "tt0133093", page: 1 },
ctx,
);
expect(result.results).toEqual([]);
expect(result.totalPages).toBe(0);
expect(result.totalResults).toBe(0);
});
test("treats a too-short ID as a regular text query", async () => {
tmdb.searchMulti.mockResolvedValue({ page: 1, total_pages: 1, total_results: 0, results: [] });
await call(implementedRouter.discover.search, { query: "tt123", page: 1 }, ctx);
expect(tmdb.searchMulti).toHaveBeenCalledWith("tt123", 1);
expect(tmdb.findByExternalId).not.toHaveBeenCalled();
});
test("person searches never use the find endpoint", async () => {
tmdb.searchPerson.mockResolvedValue({
page: 1,
total_pages: 1,
total_results: 0,
results: [],
});
await call(
implementedRouter.discover.search,
{ query: "tt0133093", type: "person", page: 1 },
ctx,
);
expect(tmdb.searchPerson).toHaveBeenCalledWith("tt0133093", 1);
expect(tmdb.findByExternalId).not.toHaveBeenCalled();
});
});
+25 -2
View File
@@ -6,6 +6,7 @@ import type { DiscoverInput } from "@sofa/api/schemas";
import { WATCH_REGION } from "@sofa/config"; import { WATCH_REGION } from "@sofa/config";
import { import {
discover, discover,
findByExternalId,
getGenres, getGenres,
getPopular, getPopular,
getTrending, getTrending,
@@ -281,6 +282,27 @@ async function searchPeople(query: string, requestedPage: number) {
}; };
} }
// IMDb IDs (e.g. tt0133093) are resolved via TMDB's find endpoint instead of text search.
const IMDB_ID_PATTERN = /^tt\d{7,10}$/i;
async function findByImdbId(imdbId: string, type: "movie" | "tv" | null): Promise<TmdbPage> {
const found = await findByExternalId(imdbId.toLowerCase(), "imdb_id");
const results = [
...(type === "tv"
? []
: (found.movie_results ?? []).map((r) => Object.assign({}, r, { media_type: "movie" }))),
...(type === "movie"
? []
: (found.tv_results ?? []).map((r) => Object.assign({}, r, { media_type: "tv" }))),
];
return {
page: 1,
total_pages: results.length > 0 ? 1 : 0,
total_results: results.length,
results,
};
}
export async function searchCatalog(input: { export async function searchCatalog(input: {
query: string; query: string;
type?: SearchType | null; type?: SearchType | null;
@@ -296,8 +318,9 @@ export async function searchCatalog(input: {
return searchPeople(query, input.page); return searchPeople(query, input.page);
} }
const raw: TmdbPage = const raw: TmdbPage = IMDB_ID_PATTERN.test(query)
type === "movie" ? await findByImdbId(query, type)
: type === "movie"
? await searchMovies(query, input.page) ? await searchMovies(query, input.page)
: type === "tv" : type === "tv"
? await searchTv(query, input.page) ? await searchTv(query, input.page)