mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-19 11:22:04 +00:00
Improved search
search for tmdb id or filter by year
This commit is contained in:
parent
78b83ed8be
commit
b1f3128d07
2 changed files with 160 additions and 46 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue