refactor search

This commit is contained in:
castdrian 2023-06-30 12:20:01 +02:00
parent 7c890443e0
commit 95f03db5b2
6 changed files with 45 additions and 162 deletions

View File

@ -19,6 +19,7 @@ import {
} from "./types/justwatch"; } from "./types/justwatch";
import { MWMediaMeta, MWMediaType } from "./types/mw"; import { MWMediaMeta, MWMediaType } from "./types/mw";
import { import {
TMDBContentTypes,
TMDBMediaResult, TMDBMediaResult,
TMDBMovieData, TMDBMovieData,
TMDBSeasonMetaResult, TMDBSeasonMetaResult,
@ -177,7 +178,7 @@ export async function convertLegacyUrl(
const urlParts = url.split("/").slice(2); const urlParts = url.split("/").slice(2);
const [, type, id] = urlParts[0].split("-", 3); const [, type, id] = urlParts[0].split("-", 3);
const mediaType = TMDBMediaToMediaType(type); const mediaType = TMDBMediaToMediaType(type as TMDBContentTypes);
const meta = await getLegacyMetaFromId(mediaType, id); const meta = await getLegacyMetaFromId(mediaType, id);
if (!meta) return undefined; if (!meta) return undefined;

View File

@ -1,11 +1,6 @@
import { SimpleCache } from "@/utils/cache"; import { SimpleCache } from "@/utils/cache";
import { import { formatTMDBMeta, formatTMDBSearchResult, multiSearch } from "./tmdb";
formatTMDBMeta,
formatTMDBSearchResult,
mediaTypeToTMDB,
searchMedia,
} from "./tmdb";
import { MWMediaMeta, MWQuery } from "./types/mw"; import { MWMediaMeta, MWQuery } from "./types/mw";
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>(); const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
@ -16,11 +11,11 @@ cache.initialize();
export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> { export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
if (cache.has(query)) return cache.get(query) as MWMediaMeta[]; if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
const { searchQuery, type } = query; const { searchQuery } = query;
const data = await searchMedia(searchQuery, mediaTypeToTMDB(type)); const data = await multiSearch(searchQuery);
const results = data.results.map((v) => { const results = data.map((v) => {
const formattedResult = formatTMDBSearchResult(v, mediaTypeToTMDB(type)); const formattedResult = formatTMDBSearchResult(v, v.media_type);
return formatTMDBMeta(formattedResult); return formatTMDBMeta(formattedResult);
}); });

View File

@ -11,29 +11,25 @@ import {
TMDBMediaResult, TMDBMediaResult,
TMDBMovieData, TMDBMovieData,
TMDBMovieExternalIds, TMDBMovieExternalIds,
TMDBMovieResponse,
TMDBMovieResult,
TMDBMovieSearchResult, TMDBMovieSearchResult,
TMDBSearchResult, TMDBSearchResult,
TMDBSeason, TMDBSeason,
TMDBSeasonMetaResult, TMDBSeasonMetaResult,
TMDBShowData, TMDBShowData,
TMDBShowExternalIds, TMDBShowExternalIds,
TMDBShowResponse,
TMDBShowResult,
TMDBShowSearchResult, TMDBShowSearchResult,
} from "./types/tmdb"; } from "./types/tmdb";
import { mwFetch } from "../helpers/fetch"; import { mwFetch } from "../helpers/fetch";
export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes { export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes {
if (type === MWMediaType.MOVIE) return "movie"; if (type === MWMediaType.MOVIE) return TMDBContentTypes.MOVIE;
if (type === MWMediaType.SERIES) return "show"; if (type === MWMediaType.SERIES) return TMDBContentTypes.TV;
throw new Error("unsupported type"); throw new Error("unsupported type");
} }
export function TMDBMediaToMediaType(type: string): MWMediaType { export function TMDBMediaToMediaType(type: TMDBContentTypes): MWMediaType {
if (type === "movie") return MWMediaType.MOVIE; if (type === TMDBContentTypes.MOVIE) return MWMediaType.MOVIE;
if (type === "show") return MWMediaType.SERIES; if (type === TMDBContentTypes.TV) return MWMediaType.SERIES;
throw new Error("unsupported type"); throw new Error("unsupported type");
} }
@ -103,7 +99,7 @@ export function decodeTMDBId(
if (prefix !== "tmdb") return null; if (prefix !== "tmdb") return null;
let mediaType; let mediaType;
try { try {
mediaType = TMDBMediaToMediaType(type); mediaType = TMDBMediaToMediaType(type as TMDBContentTypes);
} catch { } catch {
return null; return null;
} }
@ -131,36 +127,6 @@ async function get<T>(url: string, params?: object): Promise<T> {
return res; return res;
} }
export async function searchMedia(
query: string,
type: TMDBContentTypes
): Promise<TMDBMovieResponse | TMDBShowResponse> {
let data;
switch (type) {
case "movie":
data = await get<TMDBMovieResponse>("search/movie", {
query,
include_adult: false,
language: "en-US",
page: 1,
});
break;
case "show":
data = await get<TMDBShowResponse>("search/tv", {
query,
include_adult: false,
language: "en-US",
page: 1,
});
break;
default:
throw new Error("Invalid media type");
}
return data;
}
export async function multiSearch( export async function multiSearch(
query: string query: string
): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> { ): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> {
@ -172,7 +138,9 @@ export async function multiSearch(
}); });
// filter out results that aren't movies or shows // filter out results that aren't movies or shows
const results = data.results.filter( const results = data.results.filter(
(r) => r.media_type === "movie" || r.media_type === "tv" (r) =>
r.media_type === TMDBContentTypes.MOVIE ||
r.media_type === TMDBContentTypes.TV
); );
return results; return results;
} }
@ -183,31 +151,32 @@ export async function generateQuickSearchMediaUrl(
const data = await multiSearch(query); const data = await multiSearch(query);
if (data.length === 0) return undefined; if (data.length === 0) return undefined;
const result = data[0]; const result = data[0];
const type = result.media_type === "movie" ? "movie" : "show"; const title =
const title = result.media_type === "movie" ? result.title : result.name; result.media_type === TMDBContentTypes.MOVIE ? result.title : result.name;
return `/media/${TMDBIdToUrlId( return `/media/${TMDBIdToUrlId(
TMDBMediaToMediaType(type), TMDBMediaToMediaType(result.media_type),
result.id.toString(), result.id.toString(),
title title
)}`; )}`;
} }
// Conditional type which for inferring the return type based on the content type // Conditional type which for inferring the return type based on the content type
type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie" type MediaDetailReturn<T extends TMDBContentTypes> =
? TMDBMovieData T extends TMDBContentTypes.MOVIE
: T extends "show" ? TMDBMovieData
? TMDBShowData : T extends TMDBContentTypes.TV
: never; ? TMDBShowData
: never;
export function getMediaDetails< export function getMediaDetails<
T extends TMDBContentTypes, T extends TMDBContentTypes,
TReturn = MediaDetailReturn<T> TReturn = MediaDetailReturn<T>
>(id: string, type: T): Promise<TReturn> { >(id: string, type: T): Promise<TReturn> {
if (type === "movie") { if (type === TMDBContentTypes.MOVIE) {
return get<TReturn>(`/movie/${id}`); return get<TReturn>(`/movie/${id}`);
} }
if (type === "show") { if (type === TMDBContentTypes.TV) {
return get<TReturn>(`/tv/${id}`); return get<TReturn>(`/tv/${id}`);
} }
throw new Error("Invalid media type"); throw new Error("Invalid media type");
@ -236,10 +205,10 @@ export async function getExternalIds(
let data; let data;
switch (type) { switch (type) {
case "movie": case TMDBContentTypes.MOVIE:
data = await get<TMDBMovieExternalIds>(`/movie/${id}/external_ids`); data = await get<TMDBMovieExternalIds>(`/movie/${id}/external_ids`);
break; break;
case "show": case TMDBContentTypes.TV:
data = await get<TMDBShowExternalIds>(`/tv/${id}/external_ids`); data = await get<TMDBShowExternalIds>(`/tv/${id}/external_ids`);
break; break;
default: default:
@ -263,12 +232,12 @@ export async function getMovieFromExternalId(
} }
export function formatTMDBSearchResult( export function formatTMDBSearchResult(
result: TMDBShowResult | TMDBMovieResult, result: TMDBMovieSearchResult | TMDBShowSearchResult,
mediatype: TMDBContentTypes mediatype: TMDBContentTypes
): TMDBMediaResult { ): TMDBMediaResult {
const type = TMDBMediaToMediaType(mediatype); const type = TMDBMediaToMediaType(mediatype);
if (type === MWMediaType.SERIES) { if (type === MWMediaType.SERIES) {
const show = result as TMDBShowResult; const show = result as TMDBShowSearchResult;
return { return {
title: show.name, title: show.name,
poster: getMediaPoster(show.poster_path), poster: getMediaPoster(show.poster_path),
@ -277,7 +246,8 @@ export function formatTMDBSearchResult(
object_type: mediatype, object_type: mediatype,
}; };
} }
const movie = result as TMDBMovieResult;
const movie = result as TMDBMovieSearchResult;
return { return {
title: movie.title, title: movie.title,

View File

@ -1,4 +1,7 @@
export type TMDBContentTypes = "movie" | "show"; export enum TMDBContentTypes {
MOVIE = "movie",
TV = "tv",
}
export type TMDBSeasonShort = { export type TMDBSeasonShort = {
title: string; title: string;
@ -183,54 +186,6 @@ export interface TMDBEpisodeResult {
}; };
} }
export interface TMDBShowResult {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
origin_country: string[];
original_language: string;
original_name: string;
overview: string;
popularity: number;
poster_path: string | null;
first_air_date: string;
name: string;
vote_average: number;
vote_count: number;
}
export interface TMDBShowResponse {
page: number;
results: TMDBShowResult[];
total_pages: number;
total_results: number;
}
export interface TMDBMovieResult {
adult: boolean;
backdrop_path: string | null;
genre_ids: number[];
id: number;
original_language: string;
original_title: string;
overview: string;
popularity: number;
poster_path: string | null;
release_date: string;
title: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export interface TMDBMovieResponse {
page: number;
results: TMDBMovieResult[];
total_pages: number;
total_results: number;
}
export interface TMDBEpisode { export interface TMDBEpisode {
air_date: string; air_date: string;
episode_number: number; episode_number: number;
@ -316,7 +271,7 @@ export interface TMDBMovieSearchResult {
original_title: string; original_title: string;
overview: string; overview: string;
poster_path: string; poster_path: string;
media_type: "movie"; media_type: TMDBContentTypes.MOVIE;
genre_ids: number[]; genre_ids: number[];
popularity: number; popularity: number;
release_date: string; release_date: string;
@ -334,7 +289,7 @@ export interface TMDBShowSearchResult {
original_name: string; original_name: string;
overview: string; overview: string;
poster_path: string; poster_path: string;
media_type: "tv"; media_type: TMDBContentTypes.TV;
genre_ids: number[]; genre_ids: number[];
popularity: number; popularity: number;
first_air_date: string; first_air_date: string;

View File

@ -1,14 +1,9 @@
import { useState } from "react"; import { MWQuery } from "@/backend/metadata/types/mw";
import { useTranslation } from "react-i18next";
import { MWMediaType, MWQuery } from "@/backend/metadata/types/mw";
import { DropdownButton } from "./buttons/DropdownButton";
import { Icon, Icons } from "./Icon"; import { Icon, Icons } from "./Icon";
import { TextInputControl } from "./text-inputs/TextInputControl"; import { TextInputControl } from "./text-inputs/TextInputControl";
export interface SearchBarProps { export interface SearchBarProps {
buttonText?: string;
placeholder?: string; placeholder?: string;
onChange: (value: MWQuery, force: boolean) => void; onChange: (value: MWQuery, force: boolean) => void;
onUnFocus: () => void; onUnFocus: () => void;
@ -16,9 +11,6 @@ export interface SearchBarProps {
} }
export function SearchBarInput(props: SearchBarProps) { export function SearchBarInput(props: SearchBarProps) {
const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false);
function setSearch(value: string) { function setSearch(value: string) {
props.onChange( props.onChange(
{ {
@ -28,15 +20,6 @@ export function SearchBarInput(props: SearchBarProps) {
false false
); );
} }
function setType(type: string) {
props.onChange(
{
...props.value,
type: type as MWMediaType,
},
true
);
}
return ( return (
<div className="relative flex flex-col rounded-[28px] bg-denim-400 transition-colors focus-within:bg-denim-400 hover:bg-denim-500 sm:flex-row sm:items-center"> <div className="relative flex flex-col rounded-[28px] bg-denim-400 transition-colors focus-within:bg-denim-400 hover:bg-denim-500 sm:flex-row sm:items-center">
@ -51,31 +34,6 @@ export function SearchBarInput(props: SearchBarProps) {
className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-white placeholder-denim-700 focus:outline-none sm:py-4 sm:pr-2" className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-white placeholder-denim-700 focus:outline-none sm:py-4 sm:pr-2"
placeholder={props.placeholder} placeholder={props.placeholder}
/> />
<div className="px-4 py-4 pt-0 sm:px-2 sm:py-2">
<DropdownButton
icon={Icons.SEARCH}
open={dropdownOpen}
setOpen={(val) => setDropdownOpen(val)}
selectedItem={props.value.type}
setSelectedItem={(val) => setType(val)}
options={[
{
id: MWMediaType.MOVIE,
name: t("searchBar.movie"),
icon: Icons.FILM,
},
{
id: MWMediaType.SERIES,
name: t("searchBar.series"),
icon: Icons.CLAPPER_BOARD,
},
]}
onClick={() => setDropdownOpen((old) => !old)}
>
{props.buttonText || t("searchBar.search")}
</DropdownButton>
</div>
</div> </div>
); );
} }

View File

@ -5,6 +5,7 @@ import {
getMovieFromExternalId, getMovieFromExternalId,
} from "@/backend/metadata/tmdb"; } from "@/backend/metadata/tmdb";
import { MWMediaType } from "@/backend/metadata/types/mw"; import { MWMediaType } from "@/backend/metadata/types/mw";
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
import { BookmarkStoreData } from "@/state/bookmark/types"; import { BookmarkStoreData } from "@/state/bookmark/types";
import { isNotNull } from "@/utils/typeguard"; import { isNotNull } from "@/utils/typeguard";
@ -59,7 +60,10 @@ export async function migrateV3Videos(
clone.item.meta.id = migratedId; clone.item.meta.id = migratedId;
if (clone.item.series) { if (clone.item.series) {
const series = clone.item.series; const series = clone.item.series;
const details = await getMediaDetails(migratedId, "show"); const details = await getMediaDetails(
migratedId,
TMDBContentTypes.TV
);
const season = details.seasons.find( const season = details.seasons.find(
(v) => v.season_number === series.season (v) => v.season_number === series.season