mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
fixes qodo stuff
This commit is contained in:
parent
ba4c73360f
commit
c47cd0616d
5 changed files with 84 additions and 24 deletions
|
|
@ -40,13 +40,18 @@ function normalizeQuery(input: string): string {
|
||||||
|
|
||||||
function getLenientQueries(searchQuery: string): string[] {
|
function getLenientQueries(searchQuery: string): string[] {
|
||||||
const base = searchQuery.trim();
|
const base = searchQuery.trim();
|
||||||
|
if (base.length < 3) return [base];
|
||||||
|
|
||||||
const normalized = normalizeQuery(base);
|
const normalized = normalizeQuery(base);
|
||||||
const withoutTrailingYear = base.replace(trailingYearPattern, "").trim();
|
const withoutTrailingYear = base.replace(trailingYearPattern, "").trim();
|
||||||
const normalizedWithoutYear = normalizeQuery(withoutTrailingYear);
|
const normalizedWithoutYear = normalizeQuery(withoutTrailingYear);
|
||||||
|
|
||||||
return [
|
const variants = [
|
||||||
...new Set([base, normalized, withoutTrailingYear, normalizedWithoutYear]),
|
...new Set([base, normalized, withoutTrailingYear, normalizedWithoutYear]),
|
||||||
].filter((q) => q.length > 0);
|
].filter((q) => q.length > 0);
|
||||||
|
|
||||||
|
// Keep fanout small to avoid TMDB rate-limit pressure.
|
||||||
|
return variants.slice(0, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dedupeTMDBResults(
|
function dedupeTMDBResults(
|
||||||
|
|
@ -141,10 +146,24 @@ export async function searchForMedia(query: MWQuery): Promise<MediaItem[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryVariants = getLenientQueries(searchQuery);
|
const queryVariants = getLenientQueries(searchQuery);
|
||||||
const resultSets = await Promise.all(
|
const settledResults = await Promise.allSettled(
|
||||||
queryVariants.map((q) => multiSearch(q)),
|
queryVariants.map((q) => multiSearch(q)),
|
||||||
);
|
);
|
||||||
const data = dedupeTMDBResults(resultSets.flat());
|
const fulfilledResults = settledResults
|
||||||
|
.filter(
|
||||||
|
(
|
||||||
|
result,
|
||||||
|
): result is PromiseFulfilledResult<
|
||||||
|
(TMDBMovieSearchResult | TMDBShowSearchResult)[]
|
||||||
|
> => result.status === "fulfilled",
|
||||||
|
)
|
||||||
|
.map((result) => result.value);
|
||||||
|
|
||||||
|
if (fulfilledResults.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = dedupeTMDBResults(fulfilledResults.flat());
|
||||||
const rankedData = rankTMDBResultsFuzzy(data, searchQuery);
|
const rankedData = rankTMDBResultsFuzzy(data, searchQuery);
|
||||||
|
|
||||||
const results = rankedData.map((v) => {
|
const results = rankedData.map((v) => {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export const SearchBarInput = forwardRef<HTMLInputElement, SearchBarProps>(
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
function setSearch(value: string) {
|
function setSearch(value: string) {
|
||||||
props.onChange(value, true);
|
props.onChange(value, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ export function useSearchQuery(): [
|
||||||
const updateParams = (inp: string, commitToUrl = false) => {
|
const updateParams = (inp: string, commitToUrl = false) => {
|
||||||
setSearch(inp);
|
setSearch(inp);
|
||||||
if (!commitToUrl) return;
|
if (!commitToUrl) return;
|
||||||
|
const current = decode(params.query);
|
||||||
|
if (inp === current) return;
|
||||||
if (inp.length === 0) {
|
if (inp.length === 0) {
|
||||||
navigate("/", { replace: true });
|
navigate("/", { replace: true });
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAsyncFn } from "react-use";
|
|
||||||
|
|
||||||
import { searchForMedia } from "@/backend/metadata/search";
|
import { searchForMedia } from "@/backend/metadata/search";
|
||||||
import { MWQuery } from "@/backend/metadata/types/mw";
|
import { MWQuery } from "@/backend/metadata/types/mw";
|
||||||
|
|
@ -10,6 +9,7 @@ import { Icons } from "@/components/Icon";
|
||||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||||
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
import { Button } from "@/pages/About";
|
import { Button } from "@/pages/About";
|
||||||
import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart";
|
import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart";
|
||||||
import { MediaItem } from "@/utils/mediaTypes";
|
import { MediaItem } from "@/utils/mediaTypes";
|
||||||
|
|
@ -67,20 +67,47 @@ export function SearchListPart({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [results, setResults] = useState<MediaItem[]>([]);
|
const [results, setResults] = useState<MediaItem[]>([]);
|
||||||
const [state, exec] = useAsyncFn((query: MWQuery) => searchForMedia(query));
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
const requestIdRef = useRef(0);
|
||||||
|
const debouncedSearchQuery = useDebounce(searchQuery, 300);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function runSearch(query: MWQuery) {
|
async function runSearch(query: MWQuery, requestId: number) {
|
||||||
const searchResults = await exec(query);
|
setLoading(true);
|
||||||
if (!searchResults) return;
|
setFailed(false);
|
||||||
setResults(searchResults);
|
|
||||||
|
let nextResults: MediaItem[] = [];
|
||||||
|
let didFail = false;
|
||||||
|
try {
|
||||||
|
nextResults = (await searchForMedia(query)) ?? [];
|
||||||
|
} catch {
|
||||||
|
didFail = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore stale responses from older requests.
|
||||||
|
if (requestIdRef.current !== requestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFailed(didFail);
|
||||||
|
if (!didFail) setResults(nextResults);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchQuery !== "") runSearch({ searchQuery });
|
if (debouncedSearchQuery === "") {
|
||||||
}, [searchQuery, exec]);
|
setResults([]);
|
||||||
|
setLoading(false);
|
||||||
|
setFailed(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.loading) return <SearchLoadingPart />;
|
requestIdRef.current += 1;
|
||||||
if (state.error) return <SearchSuffix failed />;
|
runSearch({ searchQuery: debouncedSearchQuery }, requestIdRef.current);
|
||||||
|
}, [debouncedSearchQuery]);
|
||||||
|
|
||||||
|
if (loading) return <SearchLoadingPart />;
|
||||||
|
if (failed) return <SearchSuffix failed />;
|
||||||
if (!results) return null;
|
if (!results) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,23 @@ export class SimpleCache<Key, Value> {
|
||||||
|
|
||||||
protected _storage: { key: Key; value: Value; expiry: Date }[] = [];
|
protected _storage: { key: Key; value: Value; expiry: Date }[] = [];
|
||||||
|
|
||||||
|
private static isExpired(entry: { expiry: Date }): boolean {
|
||||||
|
return entry.expiry.getTime() <= Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private pruneExpired(): void {
|
||||||
|
this._storage = this._storage.filter(
|
||||||
|
(entry) => !SimpleCache.isExpired(entry),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** initialize store, will start the interval
|
** initialize store, will start the interval
|
||||||
*/
|
*/
|
||||||
public initialize(): void {
|
public initialize(): void {
|
||||||
if (this._interval) throw new Error("cache is already initialized");
|
if (this._interval) throw new Error("cache is already initialized");
|
||||||
this._interval = setInterval(() => {
|
this._interval = setInterval(() => {
|
||||||
const now = new Date();
|
this.pruneExpired();
|
||||||
this._storage.filter((val) => {
|
|
||||||
if (val.expiry < now) return false; // remove if expiry date is in the past
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}, this.INTERVAL_MS);
|
}, this.INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,6 +32,7 @@ export class SimpleCache<Key, Value> {
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
if (this._interval) clearInterval(this._interval);
|
if (this._interval) clearInterval(this._interval);
|
||||||
|
this._interval = null;
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,10 +55,15 @@ export class SimpleCache<Key, Value> {
|
||||||
*/
|
*/
|
||||||
public get(key: Key): Value | undefined {
|
public get(key: Key): Value | undefined {
|
||||||
if (!this._compare) throw new Error("Compare function not set");
|
if (!this._compare) throw new Error("Compare function not set");
|
||||||
|
this.pruneExpired();
|
||||||
const foundValue = this._storage.find(
|
const foundValue = this._storage.find(
|
||||||
(item) => this._compare && this._compare(item.key, key),
|
(item) => this._compare && this._compare(item.key, key),
|
||||||
);
|
);
|
||||||
if (!foundValue) return undefined;
|
if (!foundValue) return undefined;
|
||||||
|
if (SimpleCache.isExpired(foundValue)) {
|
||||||
|
this.remove(key);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return foundValue.value;
|
return foundValue.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,6 +72,7 @@ export class SimpleCache<Key, Value> {
|
||||||
*/
|
*/
|
||||||
public set(key: Key, value: Value, expirySeconds: number): void {
|
public set(key: Key, value: Value, expirySeconds: number): void {
|
||||||
if (!this._compare) throw new Error("Compare function not set");
|
if (!this._compare) throw new Error("Compare function not set");
|
||||||
|
this.pruneExpired();
|
||||||
const foundValue = this._storage.find(
|
const foundValue = this._storage.find(
|
||||||
(item) => this._compare && this._compare(item.key, key),
|
(item) => this._compare && this._compare(item.key, key),
|
||||||
);
|
);
|
||||||
|
|
@ -86,10 +99,9 @@ export class SimpleCache<Key, Value> {
|
||||||
*/
|
*/
|
||||||
public remove(key: Key): void {
|
public remove(key: Key): void {
|
||||||
if (!this._compare) throw new Error("Compare function not set");
|
if (!this._compare) throw new Error("Compare function not set");
|
||||||
this._storage.filter((val) => {
|
this._storage = this._storage.filter(
|
||||||
if (this._compare && this._compare(val.key, key)) return false; // remove if compare is success
|
(val) => !(this._compare && this._compare(val.key, key)),
|
||||||
return true;
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue