mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-26 18:21:54 +01:00
refactor search
This commit is contained in:
parent
7c890443e0
commit
95f03db5b2
@ -19,6 +19,7 @@ import {
|
||||
} from "./types/justwatch";
|
||||
import { MWMediaMeta, MWMediaType } from "./types/mw";
|
||||
import {
|
||||
TMDBContentTypes,
|
||||
TMDBMediaResult,
|
||||
TMDBMovieData,
|
||||
TMDBSeasonMetaResult,
|
||||
@ -177,7 +178,7 @@ export async function convertLegacyUrl(
|
||||
const urlParts = url.split("/").slice(2);
|
||||
const [, type, id] = urlParts[0].split("-", 3);
|
||||
|
||||
const mediaType = TMDBMediaToMediaType(type);
|
||||
const mediaType = TMDBMediaToMediaType(type as TMDBContentTypes);
|
||||
const meta = await getLegacyMetaFromId(mediaType, id);
|
||||
|
||||
if (!meta) return undefined;
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { SimpleCache } from "@/utils/cache";
|
||||
|
||||
import {
|
||||
formatTMDBMeta,
|
||||
formatTMDBSearchResult,
|
||||
mediaTypeToTMDB,
|
||||
searchMedia,
|
||||
} from "./tmdb";
|
||||
import { formatTMDBMeta, formatTMDBSearchResult, multiSearch } from "./tmdb";
|
||||
import { MWMediaMeta, MWQuery } from "./types/mw";
|
||||
|
||||
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
|
||||
@ -16,11 +11,11 @@ cache.initialize();
|
||||
|
||||
export async function searchForMedia(query: MWQuery): Promise<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 results = data.results.map((v) => {
|
||||
const formattedResult = formatTMDBSearchResult(v, mediaTypeToTMDB(type));
|
||||
const data = await multiSearch(searchQuery);
|
||||
const results = data.map((v) => {
|
||||
const formattedResult = formatTMDBSearchResult(v, v.media_type);
|
||||
return formatTMDBMeta(formattedResult);
|
||||
});
|
||||
|
||||
|
@ -11,29 +11,25 @@ import {
|
||||
TMDBMediaResult,
|
||||
TMDBMovieData,
|
||||
TMDBMovieExternalIds,
|
||||
TMDBMovieResponse,
|
||||
TMDBMovieResult,
|
||||
TMDBMovieSearchResult,
|
||||
TMDBSearchResult,
|
||||
TMDBSeason,
|
||||
TMDBSeasonMetaResult,
|
||||
TMDBShowData,
|
||||
TMDBShowExternalIds,
|
||||
TMDBShowResponse,
|
||||
TMDBShowResult,
|
||||
TMDBShowSearchResult,
|
||||
} from "./types/tmdb";
|
||||
import { mwFetch } from "../helpers/fetch";
|
||||
|
||||
export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes {
|
||||
if (type === MWMediaType.MOVIE) return "movie";
|
||||
if (type === MWMediaType.SERIES) return "show";
|
||||
if (type === MWMediaType.MOVIE) return TMDBContentTypes.MOVIE;
|
||||
if (type === MWMediaType.SERIES) return TMDBContentTypes.TV;
|
||||
throw new Error("unsupported type");
|
||||
}
|
||||
|
||||
export function TMDBMediaToMediaType(type: string): MWMediaType {
|
||||
if (type === "movie") return MWMediaType.MOVIE;
|
||||
if (type === "show") return MWMediaType.SERIES;
|
||||
export function TMDBMediaToMediaType(type: TMDBContentTypes): MWMediaType {
|
||||
if (type === TMDBContentTypes.MOVIE) return MWMediaType.MOVIE;
|
||||
if (type === TMDBContentTypes.TV) return MWMediaType.SERIES;
|
||||
throw new Error("unsupported type");
|
||||
}
|
||||
|
||||
@ -103,7 +99,7 @@ export function decodeTMDBId(
|
||||
if (prefix !== "tmdb") return null;
|
||||
let mediaType;
|
||||
try {
|
||||
mediaType = TMDBMediaToMediaType(type);
|
||||
mediaType = TMDBMediaToMediaType(type as TMDBContentTypes);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@ -131,36 +127,6 @@ async function get<T>(url: string, params?: object): Promise<T> {
|
||||
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(
|
||||
query: string
|
||||
): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> {
|
||||
@ -172,7 +138,9 @@ export async function multiSearch(
|
||||
});
|
||||
// filter out results that aren't movies or shows
|
||||
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;
|
||||
}
|
||||
@ -183,31 +151,32 @@ export async function generateQuickSearchMediaUrl(
|
||||
const data = await multiSearch(query);
|
||||
if (data.length === 0) return undefined;
|
||||
const result = data[0];
|
||||
const type = result.media_type === "movie" ? "movie" : "show";
|
||||
const title = result.media_type === "movie" ? result.title : result.name;
|
||||
const title =
|
||||
result.media_type === TMDBContentTypes.MOVIE ? result.title : result.name;
|
||||
|
||||
return `/media/${TMDBIdToUrlId(
|
||||
TMDBMediaToMediaType(type),
|
||||
TMDBMediaToMediaType(result.media_type),
|
||||
result.id.toString(),
|
||||
title
|
||||
)}`;
|
||||
}
|
||||
|
||||
// Conditional type which for inferring the return type based on the content type
|
||||
type MediaDetailReturn<T extends TMDBContentTypes> = T extends "movie"
|
||||
? TMDBMovieData
|
||||
: T extends "show"
|
||||
? TMDBShowData
|
||||
: never;
|
||||
type MediaDetailReturn<T extends TMDBContentTypes> =
|
||||
T extends TMDBContentTypes.MOVIE
|
||||
? TMDBMovieData
|
||||
: T extends TMDBContentTypes.TV
|
||||
? TMDBShowData
|
||||
: never;
|
||||
|
||||
export function getMediaDetails<
|
||||
T extends TMDBContentTypes,
|
||||
TReturn = MediaDetailReturn<T>
|
||||
>(id: string, type: T): Promise<TReturn> {
|
||||
if (type === "movie") {
|
||||
if (type === TMDBContentTypes.MOVIE) {
|
||||
return get<TReturn>(`/movie/${id}`);
|
||||
}
|
||||
if (type === "show") {
|
||||
if (type === TMDBContentTypes.TV) {
|
||||
return get<TReturn>(`/tv/${id}`);
|
||||
}
|
||||
throw new Error("Invalid media type");
|
||||
@ -236,10 +205,10 @@ export async function getExternalIds(
|
||||
let data;
|
||||
|
||||
switch (type) {
|
||||
case "movie":
|
||||
case TMDBContentTypes.MOVIE:
|
||||
data = await get<TMDBMovieExternalIds>(`/movie/${id}/external_ids`);
|
||||
break;
|
||||
case "show":
|
||||
case TMDBContentTypes.TV:
|
||||
data = await get<TMDBShowExternalIds>(`/tv/${id}/external_ids`);
|
||||
break;
|
||||
default:
|
||||
@ -263,12 +232,12 @@ export async function getMovieFromExternalId(
|
||||
}
|
||||
|
||||
export function formatTMDBSearchResult(
|
||||
result: TMDBShowResult | TMDBMovieResult,
|
||||
result: TMDBMovieSearchResult | TMDBShowSearchResult,
|
||||
mediatype: TMDBContentTypes
|
||||
): TMDBMediaResult {
|
||||
const type = TMDBMediaToMediaType(mediatype);
|
||||
if (type === MWMediaType.SERIES) {
|
||||
const show = result as TMDBShowResult;
|
||||
const show = result as TMDBShowSearchResult;
|
||||
return {
|
||||
title: show.name,
|
||||
poster: getMediaPoster(show.poster_path),
|
||||
@ -277,7 +246,8 @@ export function formatTMDBSearchResult(
|
||||
object_type: mediatype,
|
||||
};
|
||||
}
|
||||
const movie = result as TMDBMovieResult;
|
||||
|
||||
const movie = result as TMDBMovieSearchResult;
|
||||
|
||||
return {
|
||||
title: movie.title,
|
||||
|
@ -1,4 +1,7 @@
|
||||
export type TMDBContentTypes = "movie" | "show";
|
||||
export enum TMDBContentTypes {
|
||||
MOVIE = "movie",
|
||||
TV = "tv",
|
||||
}
|
||||
|
||||
export type TMDBSeasonShort = {
|
||||
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 {
|
||||
air_date: string;
|
||||
episode_number: number;
|
||||
@ -316,7 +271,7 @@ export interface TMDBMovieSearchResult {
|
||||
original_title: string;
|
||||
overview: string;
|
||||
poster_path: string;
|
||||
media_type: "movie";
|
||||
media_type: TMDBContentTypes.MOVIE;
|
||||
genre_ids: number[];
|
||||
popularity: number;
|
||||
release_date: string;
|
||||
@ -334,7 +289,7 @@ export interface TMDBShowSearchResult {
|
||||
original_name: string;
|
||||
overview: string;
|
||||
poster_path: string;
|
||||
media_type: "tv";
|
||||
media_type: TMDBContentTypes.TV;
|
||||
genre_ids: number[];
|
||||
popularity: number;
|
||||
first_air_date: string;
|
||||
|
@ -1,14 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MWQuery } from "@/backend/metadata/types/mw";
|
||||
|
||||
import { MWMediaType, MWQuery } from "@/backend/metadata/types/mw";
|
||||
|
||||
import { DropdownButton } from "./buttons/DropdownButton";
|
||||
import { Icon, Icons } from "./Icon";
|
||||
import { TextInputControl } from "./text-inputs/TextInputControl";
|
||||
|
||||
export interface SearchBarProps {
|
||||
buttonText?: string;
|
||||
placeholder?: string;
|
||||
onChange: (value: MWQuery, force: boolean) => void;
|
||||
onUnFocus: () => void;
|
||||
@ -16,9 +11,6 @@ export interface SearchBarProps {
|
||||
}
|
||||
|
||||
export function SearchBarInput(props: SearchBarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
function setSearch(value: string) {
|
||||
props.onChange(
|
||||
{
|
||||
@ -28,15 +20,6 @@ export function SearchBarInput(props: SearchBarProps) {
|
||||
false
|
||||
);
|
||||
}
|
||||
function setType(type: string) {
|
||||
props.onChange(
|
||||
{
|
||||
...props.value,
|
||||
type: type as MWMediaType,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
@ -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"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
getMovieFromExternalId,
|
||||
} from "@/backend/metadata/tmdb";
|
||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
||||
import { BookmarkStoreData } from "@/state/bookmark/types";
|
||||
import { isNotNull } from "@/utils/typeguard";
|
||||
|
||||
@ -59,7 +60,10 @@ export async function migrateV3Videos(
|
||||
clone.item.meta.id = migratedId;
|
||||
if (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(
|
||||
(v) => v.season_number === series.season
|
||||
|
Loading…
Reference in New Issue
Block a user