Improved search

search for tmdb id or filter by year
This commit is contained in:
Pas 2025-03-20 10:54:51 -06:00
parent 78b83ed8be
commit b1f3128d07
2 changed files with 160 additions and 46 deletions

View file

@ -4,9 +4,15 @@ import { MediaItem } from "@/utils/mediaTypes";
import {
formatTMDBMetaToMediaItem,
formatTMDBSearchResult,
getMediaDetails,
getMediaPoster,
multiSearch,
} from "./tmdb";
import { MWQuery } from "./types/mw";
import { TMDBContentTypes } from "./types/tmdb";
export interface MWQuery {
searchQuery: string;
}
const cache = new SimpleCache<MWQuery, MediaItem[]>();
cache.setCompare((a, b) => {
@ -14,16 +20,83 @@ cache.setCompare((a, b) => {
});
cache.initialize();
// detect "tmdb:123456" or "tmdb:123456:movie" or "tmdb:123456:tv"
const tmdbIdPattern = /^tmdb:(\d+)(?::(movie|tv))?$/i;
// detect "year:YYYY"
const yearPattern = /(.+?)\s+year:(\d{4})$/i;
export async function searchForMedia(query: MWQuery): Promise<MediaItem[]> {
if (cache.has(query)) return cache.get(query) as MediaItem[];
const { searchQuery } = query;
const data = await multiSearch(searchQuery);
const results = data.map((v) => {
// Check if query is a TMDB ID
const tmdbMatch = searchQuery.match(tmdbIdPattern);
if (tmdbMatch) {
const id = tmdbMatch[1];
const type =
tmdbMatch[2]?.toLowerCase() === "tv"
? TMDBContentTypes.TV
: TMDBContentTypes.MOVIE;
try {
const details = await getMediaDetails(id, type);
if (details) {
// Format the media details to our common format
const mediaResult =
type === TMDBContentTypes.MOVIE
? {
id: details.id,
title: (details as any).title,
poster: getMediaPoster((details as any).poster_path),
object_type: type,
original_release_date: new Date((details as any).release_date),
}
: {
id: details.id,
title: (details as any).name,
poster: getMediaPoster((details as any).poster_path),
object_type: type,
original_release_date: new Date(
(details as any).first_air_date,
),
};
const mediaItem = formatTMDBMetaToMediaItem(mediaResult);
const result = [mediaItem];
cache.set(query, result, 3600);
return result;
}
} catch (error) {
console.error("Error fetching by TMDB ID:", error);
}
}
// year extract logic
let yearValue: string | undefined;
let queryWithoutYear = searchQuery;
const yearMatch = searchQuery.match(yearPattern);
if (yearMatch && yearMatch[2]) {
queryWithoutYear = yearMatch[1].trim();
yearValue = yearMatch[2];
}
// normal search
const data = await multiSearch(queryWithoutYear);
let results = data.map((v) => {
const formattedResult = formatTMDBSearchResult(v, v.media_type);
return formatTMDBMetaToMediaItem(formattedResult);
});
// filter year
if (yearValue) {
results = results.filter((item) => {
const releaseYear = item.release_date?.getFullYear().toString();
return releaseYear === yearValue;
});
}
const movieWithPosters = results.filter((movie) => movie.poster);
const movieWithoutPosters = results.filter((movie) => !movie.poster);

View file

@ -1,5 +1,5 @@
import c from "classnames";
import { forwardRef, useState } from "react";
import { forwardRef, useEffect, useRef, useState } from "react";
import { Flare } from "@/components/utils/Flare";
@ -16,63 +16,104 @@ export interface SearchBarProps {
export const SearchBarInput = forwardRef<HTMLInputElement, SearchBarProps>(
(props, ref) => {
const [focused, setFocused] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [showTooltip, setShowTooltip] = useState(false);
function setSearch(value: string) {
props.onChange(value, true);
}
return (
<Flare.Base
className={c({
"hover:flare-enabled group flex flex-col rounded-[28px] transition-colors sm:flex-row sm:items-center relative":
true,
"bg-search-background": !focused,
"bg-search-focused": focused,
})}
>
<Flare.Light
flareSize={400}
enabled={focused}
className="rounded-[28px]"
backgroundClass={c({
"transition-colors": true,
<div ref={containerRef}>
<Flare.Base
className={c({
"hover:flare-enabled group flex flex-col rounded-[28px] transition-colors sm:flex-row sm:items-center relative":
true,
"bg-search-background": !focused,
"bg-search-focused": focused,
})}
/>
<Flare.Child className="flex flex-1 flex-col">
<div className="pointer-events-none absolute bottom-0 left-5 top-0 flex max-h-14 items-center text-search-icon">
<Icon icon={Icons.SEARCH} />
</div>
<TextInputControl
ref={ref}
onUnFocus={() => {
setFocused(false);
props.onUnFocus();
}}
onFocus={() => setFocused(true)}
onChange={(val) => setSearch(val)}
value={props.value}
className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-search-text placeholder-search-placeholder focus:outline-none sm:py-4 sm:pr-2"
placeholder={props.placeholder}
>
<Flare.Light
flareSize={400}
enabled={focused}
className="rounded-[28px]"
backgroundClass={c({
"transition-colors": true,
"bg-search-background": !focused,
"bg-search-focused": focused,
})}
/>
{props.value.length > 0 && (
<Flare.Child className="flex flex-1 flex-col">
<div
onClick={() => {
props.onUnFocus("");
if (ref && typeof ref !== "function") {
ref.current?.focus();
className="absolute bottom-0 left-5 top-0 flex max-h-14 items-center text-search-icon cursor-pointer z-10"
onClick={(e) => {
e.preventDefault();
setShowTooltip(!showTooltip);
if (ref && typeof ref !== "function" && ref.current) {
ref.current.focus();
}
}}
className="cursor-pointer hover:text-white absolute bottom-0 right-2 top-0 flex justify-center my-auto h-10 w-10 items-center hover:bg-search-hoverBackground active:scale-110 text-search-icon rounded-full transition-[transform,background-color] duration-200"
>
<Icon icon={Icons.X} className="transition-colors duration-200" />
<Icon icon={Icons.SEARCH} />
</div>
)}
</Flare.Child>
</Flare.Base>
<TextInputControl
ref={ref}
onUnFocus={() => {
setFocused(false);
props.onUnFocus();
}}
onFocus={() => setFocused(true)}
onChange={(val) => setSearch(val)}
value={props.value}
className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-search-text placeholder-search-placeholder focus:outline-none sm:py-4 sm:pr-2"
placeholder={props.placeholder}
/>
{showTooltip && (
<div className="py-4">
<p className="font-bold text-sm mb-1 text-search-text">
Advanced Search:
</p>
<div className="space-y-1.5 text-xs text-search-text">
<div>
<p className="mb-0.5">Year search:</p>
<p className="text-type-secondary italic pl-2">
Inception year:2010
</p>
</div>
<div>
<p className="mb-0.5">TMDB ID search:</p>
<p className="text-type-secondary italic pl-2">
tmdb:123456 - For movies
</p>
<p className="text-type-secondary italic pl-2">
tmdb:123456:tv - For TV shows
</p>
</div>
</div>
</div>
)}
{props.value.length > 0 && (
<div
onClick={() => {
props.onUnFocus("");
if (ref && typeof ref !== "function") {
ref.current?.focus();
}
}}
className="cursor-pointer hover:text-white absolute bottom-0 right-2 top-0 flex justify-center my-auto h-10 w-10 items-center hover:bg-search-hoverBackground active:scale-110 text-search-icon rounded-full transition-[transform,background-color] duration-200"
>
<Icon
icon={Icons.X}
className="transition-colors duration-200"
/>
</div>
)}
</Flare.Child>
</Flare.Base>
</div>
);
},
);