Files
memorium/lib/search.ts
2025-11-05 00:42:53 +01:00

114 lines
3.0 KiB
TypeScript

import { resources } from "@lib/resources.ts";
import fuzzysort from "fuzzysort";
import { extractHashTags } from "@lib/string.ts";
import { listResources } from "./marka/index.ts";
import { GenericResource } from "./marka/schema.ts";
import { parseRating } from "./helpers.ts";
type ResourceType = keyof typeof resources;
type SearchParams = {
q: string;
types?: string[];
tags?: string[];
rating?: number;
authors?: string[];
};
export function parseResourceUrl(_url: string | URL): SearchParams | undefined {
try {
const url = typeof _url === "string" ? new URL(_url) : _url;
let query = url.searchParams.get("q") || "*";
if (!(typeof query === "string")) {
return undefined;
}
query = decodeURIComponent(query);
const hashTags = extractHashTags(query);
for (const tag of hashTags) {
query = query.replace("#" + tag, "");
}
return {
q: query,
types: url.searchParams.get("type")?.split(",") as ResourceType[] ||
undefined,
tags: hashTags,
rating: url.searchParams.has("rating")
? parseInt(url.searchParams.get("rating")!)
: undefined,
};
} catch (_err) {
return undefined;
}
}
const isResource = (
item: GenericResource | boolean | undefined,
): item is GenericResource => {
return !!item;
};
export async function searchResource(
{ q, tags = [], types, rating }: SearchParams,
): Promise<GenericResource[]> {
const resources = (await Promise.all([
(!types || types.includes("movies")) && listResources("movies"),
(!types || types.includes("series")) && listResources("series"),
(!types || types.includes("articles")) && listResources("articles"),
(!types || types.includes("recipes")) && listResources("recipes"),
])).flat().filter(isResource);
console.log({ types, rating, tags, q });
const results: Record<string, GenericResource> = {};
for (const resource of resources) {
if (
!(resource.name in results) &&
tags?.length && resource.content.keywords?.length &&
tags.every((t) => resource.content.keywords?.includes(t))
) {
results[resource.name] = resource;
}
// Select not-rated resources
if (
rating === 0 &&
resource.content?.reviewRating?.ratingValue === undefined
) {
results[resource.name] = resource;
}
if (
!(resource.name in results) &&
typeof rating == "number" &&
resource.content.reviewRating?.ratingValue &&
parseRating(resource.content.reviewRating.ratingValue) >= rating
) {
results[resource.name] = resource;
}
}
if (q.length && q !== "*") {
q = decodeURIComponent(q);
const fuzzyResult = fuzzysort.go(q, resources, {
keys: [
"name",
"content.articleBody",
"content.reviewBody",
"content.name",
"content.description",
"content.author.name",
],
threshold: 0.3,
});
for (const result of fuzzyResult) {
results[result.obj.name] = result.obj;
}
}
return Object.values(results);
}