fixes qodo stuff

This commit is contained in:
Duplicake-fyi 2026-03-03 00:42:06 +00:00
parent ba4c73360f
commit c47cd0616d
5 changed files with 84 additions and 24 deletions

View file

@ -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) => {

View file

@ -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(() => {

View file

@ -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;

View file

@ -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 (

View file

@ -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; );
});
} }
/* /*