You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
overseerr/server/lib/search.ts

217 lines
5.4 KiB

import TheMovieDb from '@server/api/themoviedb';
import type {
TmdbMovieDetails,
TmdbMovieResult,
TmdbPersonDetails,
TmdbPersonResult,
TmdbSearchMovieResponse,
TmdbSearchMultiResponse,
TmdbSearchTvResponse,
TmdbTvDetails,
TmdbTvResult,
} from '@server/api/themoviedb/interfaces';
import {
mapMovieDetailsToResult,
mapPersonDetailsToResult,
mapTvDetailsToResult,
} from '@server/models/Search';
import {
isMovie,
isMovieDetails,
isTvDetails,
} from '@server/utils/typeHelpers';
interface SearchProvider {
pattern: RegExp;
search: ({
id,
language,
query,
}: {
id: string;
language?: string;
query?: string;
}) => Promise<TmdbSearchMultiResponse>;
}
const searchProviders: SearchProvider[] = [];
export const findSearchProvider = (
query: string
): SearchProvider | undefined => {
return searchProviders.find((provider) => provider.pattern.test(query));
};
searchProviders.push({
pattern: new RegExp(/(?<=tmdb:)\d+/),
search: async ({ id, language }) => {
const tmdb = new TheMovieDb();
const moviePromise = tmdb.getMovie({ movieId: parseInt(id), language });
const tvShowPromise = tmdb.getTvShow({ tvId: parseInt(id), language });
const personPromise = tmdb.getPerson({ personId: parseInt(id), language });
const responses = await Promise.allSettled([
moviePromise,
tvShowPromise,
personPromise,
]);
const successfulResponses = responses.filter(
(r) => r.status === 'fulfilled'
) as
| (
| PromiseFulfilledResult<TmdbMovieDetails>
| PromiseFulfilledResult<TmdbTvDetails>
| PromiseFulfilledResult<TmdbPersonDetails>
)[];
const results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[] = [];
if (successfulResponses.length) {
results.push(
...successfulResponses.map((r) => {
if (isMovieDetails(r.value)) {
return mapMovieDetailsToResult(r.value);
} else if (isTvDetails(r.value)) {
return mapTvDetailsToResult(r.value);
} else {
return mapPersonDetailsToResult(r.value);
}
})
);
}
return {
page: 1,
total_pages: 1,
total_results: results.length,
results,
};
},
});
searchProviders.push({
pattern: new RegExp(/(?<=imdb:)(tt|nm)\d+/),
search: async ({ id, language }) => {
const tmdb = new TheMovieDb();
const responses = await tmdb.getByExternalId({
externalId: id,
type: 'imdb',
language,
});
const results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[] = [];
// set the media_type here since searching by external id doesn't return it
results.push(
...(responses.movie_results.map((movie) => ({
...movie,
media_type: 'movie',
})) as TmdbMovieResult[]),
...(responses.tv_results.map((tv) => ({
...tv,
media_type: 'tv',
})) as TmdbTvResult[]),
...(responses.person_results.map((person) => ({
...person,
media_type: 'person',
})) as TmdbPersonResult[])
);
return {
page: 1,
total_pages: 1,
total_results: results.length,
results,
};
},
});
searchProviders.push({
pattern: new RegExp(/(?<=tvdb:)\d+/),
search: async ({ id, language }) => {
const tmdb = new TheMovieDb();
const responses = await tmdb.getByExternalId({
externalId: parseInt(id),
type: 'tvdb',
language,
});
const results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[] = [];
// set the media_type here since searching by external id doesn't return it
results.push(
...(responses.movie_results.map((movie) => ({
...movie,
media_type: 'movie',
})) as TmdbMovieResult[]),
...(responses.tv_results.map((tv) => ({
...tv,
media_type: 'tv',
})) as TmdbTvResult[]),
...(responses.person_results.map((person) => ({
...person,
media_type: 'person',
})) as TmdbPersonResult[])
);
return {
page: 1,
total_pages: 1,
total_results: results.length,
results,
};
},
});
searchProviders.push({
pattern: new RegExp(/(?<=year:)\d{4}/),
search: async ({ id: year, query }) => {
const tmdb = new TheMovieDb();
const moviesPromise = tmdb.searchMovies({
query: query?.replace(new RegExp(/year:\d{4}/), '') ?? '',
year: parseInt(year),
});
const tvShowsPromise = tmdb.searchTvShows({
query: query?.replace(new RegExp(/year:\d{4}/), '') ?? '',
year: parseInt(year),
});
const responses = await Promise.allSettled([moviesPromise, tvShowsPromise]);
const successfulResponses = responses.filter(
(r) => r.status === 'fulfilled'
) as
| (
| PromiseFulfilledResult<TmdbSearchMovieResponse>
| PromiseFulfilledResult<TmdbSearchTvResponse>
)[];
const results: (TmdbMovieResult | TmdbTvResult)[] = [];
if (successfulResponses.length) {
successfulResponses.forEach((response) => {
response.value.results.forEach((result) =>
// set the media_type here since the search endpoints don't return it
results.push(
isMovie(result)
? { ...result, media_type: 'movie' }
: { ...result, media_type: 'tv' }
)
);
});
}
return {
page: 1,
total_pages: 1,
total_results: results.length,
results,
};
},
});