feat: initial refactor to use marka as backend

This commit is contained in:
Max Richter
2025-10-28 20:15:23 +01:00
parent 0beb3b1071
commit f680b5f832
39 changed files with 245 additions and 1012 deletions

View File

@@ -1,149 +0,0 @@
import {
createDocument,
getDocument,
getDocuments,
transformDocument,
} from "@lib/documents.ts";
import { Root } from "https://esm.sh/remark-frontmatter@4.0.1";
import { GenericResource } from "@lib/types.ts";
import { parseRating } from "@lib/helpers.ts";
import { isLocalImage } from "@lib/string.ts";
import { SILVERBULLET_SERVER } from "@lib/env.ts";
import { imageTable } from "@lib/db/schema.ts";
import { db } from "@lib/db/sqlite.ts";
import { eq } from "drizzle-orm/sql";
import { createCache } from "@lib/cache.ts";
export async function addThumbnailToResource<T extends GenericResource>(
res: T,
): Promise<T> {
if (!res?.meta?.image) return res;
const imageUrl = isLocalImage(res.meta.image)
? `${SILVERBULLET_SERVER}/${res.meta.image}`
: res.meta.image;
const image = await db.select().from(imageTable)
.where(eq(imageTable.url, imageUrl))
.limit(1)
.then((images) => images[0]);
if (image) {
return {
...res,
meta: {
...res.meta,
average: image.average,
thumbnail: image.blurhash,
},
};
}
return res;
}
type SortType = "rating" | "date" | "name" | "author";
function sortFunction<T extends GenericResource>(sortType: SortType) {
return (a: T, b: T) => {
switch (sortType) {
case "rating":
return parseRating(a.meta?.rating || 0) >
parseRating(b.meta?.rating || 0)
? -1
: 1;
case "date":
return (a.meta?.date || 0) > (b.meta?.date || 0) ? -1 : 1;
case "name":
return a.name.localeCompare(b.name);
case "author":
return a.meta?.author?.localeCompare(b.meta?.author || "") || 0;
default:
return 0;
}
};
}
export function createCrud<T extends GenericResource>(
{ prefix, parse, render, hasThumbnails = false }: {
prefix: string;
hasThumbnails?: boolean;
render?: (doc: T) => string;
parse: (doc: string, id: string) => T;
},
) {
const cache = createCache<T>(`crud/${prefix}`, { expires: 60 * 1000 });
function pathFromId(id: string) {
return `${prefix}${id.replaceAll(":", "")}.md`;
}
async function read(id: string) {
const path = pathFromId(id);
const content = await getDocument(path);
if (!content) {
return;
}
let parsed = parse(content, id);
if (hasThumbnails) {
parsed = await addThumbnailToResource(parsed);
}
const doc = { ...parsed, content };
return doc;
}
function create(id: string, content: string | ArrayBuffer | T) {
const path = pathFromId(id);
cache.set("all", undefined);
if (
typeof content === "string" || content instanceof ArrayBuffer
) {
return createDocument(path, content);
}
if (render) {
const rendered = render(content);
return createDocument(path, rendered);
}
throw new Error("No renderer defined for " + prefix + " CRUD");
}
async function update(id: string, updater: (r: Root) => Root) {
const path = pathFromId(id);
const content = await getDocument(path);
if (!content) {
return;
}
const newDoc = transformDocument(content, updater);
await createDocument(path, newDoc);
}
async function readAll({ sort = "rating" }: { sort?: SortType } = {}) {
if (cache.has("all")) {
return cache.get("all") as unknown as T[];
}
const allDocuments = await getDocuments();
const parsed = (await Promise.all(
allDocuments.filter((d) => {
return d.name.startsWith(prefix) &&
d.contentType === "text/markdown" &&
!d.name.endsWith("index.md");
}).map((doc) => {
const id = doc.name.replace(prefix, "").replace(/\.md$/, "");
return read(id);
}),
)).sort(sortFunction<T>(sort)).filter((v) => !!v);
return parsed;
}
return {
read,
readAll,
create,
update,
};
}

View File

@@ -1,5 +1,5 @@
import { drizzle } from "drizzle-orm/libsql/node";
import { DATA_DIR } from "@lib/env.ts";
import { drizzle } from "drizzle-orm/libsql";
import path from "node:path";
const DB_FILE = "file:" + path.resolve(DATA_DIR, "db.sqlite");

View File

@@ -1,19 +1,7 @@
import { unified } from "https://esm.sh/unified@10.1.2";
import { render } from "gfm";
import "https://esm.sh/prismjs@1.29.0/components/prism-typescript?no-check";
import "https://esm.sh/prismjs@1.29.0/components/prism-bash?no-check";
import "https://esm.sh/prismjs@1.29.0/components/prism-rust?no-check";
import remarkParse from "https://esm.sh/remark-parse@10.0.2";
import remarkStringify from "https://esm.sh/remark-stringify@10.0.3";
import remarkFrontmatter, {
Root,
} from "https://esm.sh/remark-frontmatter@4.0.1";
import { SILVERBULLET_SERVER } from "@lib/env.ts";
import { fixRenderedMarkdown } from "@lib/helpers.ts";
import { createLogger } from "@lib/log/index.ts";
import { db } from "@lib/db/sqlite.ts";
import { documentTable } from "@lib/db/schema.ts";
import { eq } from "drizzle-orm/sql";
export type Document = {
name: string;
@@ -24,114 +12,6 @@ export type Document = {
perm: string;
};
const log = createLogger("documents");
export async function getDocuments(): Promise<Document[]> {
let documents = await db.select().from(documentTable).all();
if (documents.length) return documents;
const headers = new Headers();
headers.append("Accept", "application/json");
headers.append("X-Sync-Mode", "true");
log.debug("fetching all documents");
const response = await fetch(`${SILVERBULLET_SERVER}/index.json`, {
headers: headers,
});
documents = await response.json();
await db.delete(documentTable);
await db.insert(documentTable).values(documents);
return documents;
}
export function createDocument(
name: string,
content: string | ArrayBuffer,
mediaType?: string,
) {
const headers = new Headers();
if (mediaType) {
headers.append("Content-Type", mediaType);
}
log.info("creating document", { name });
if (typeof content === "string") {
updateDocument(name, content).catch(log.error);
}
return fetch(SILVERBULLET_SERVER + "/" + name, {
body: content,
method: "PUT",
headers,
});
}
async function fetchDocument(name: string) {
log.debug("fetching document", { name });
const headers = new Headers();
headers.append("X-Sync-Mode", "true");
const response = await fetch(SILVERBULLET_SERVER + "/" + name, { headers });
if (response.status === 404) {
return;
}
return response.text();
}
export async function getDocument(name: string): Promise<string | undefined> {
const documents = await db.select().from(documentTable).where(
eq(documentTable.name, name),
).limit(1);
// This updates the document in the background
fetchDocument(name).then((content) => {
if (content) {
updateDocument(name, content);
} else {
db.delete(documentTable).where(eq(documentTable.name, name));
}
}).catch(
log.error,
);
if (documents[0]?.content) return documents[0].content;
const text = await fetchDocument(name);
if (!text) {
db.delete(documentTable).where(eq(documentTable.name, name));
return;
}
await updateDocument(name, text);
return text;
}
export function updateDocument(name: string, content: string) {
return db.update(documentTable).set({
content,
}).where(eq(documentTable.name, name)).run();
}
export function transformDocument(input: string, cb: (r: Root) => Root) {
const out = unified()
.use(remarkParse)
.use(remarkFrontmatter, ["yaml"])
.use(() => (tree) => {
return cb(tree);
})
.use(remarkStringify)
.processSync(input);
return fixRenderedMarkdown(String(out));
}
export function parseDocument(doc: string) {
return unified()
.use(remarkParse)
.use(remarkFrontmatter, ["yaml", "toml"])
.parse(doc);
}
function removeFrontmatter(doc: string) {
if (doc.trim().startsWith("---")) {
return doc.trim().split("---").filter((s) => s.length).slice(1).join("---");
@@ -160,42 +40,15 @@ export function removeImage(doc: string, imageUrl?: string) {
export function renderMarkdown(doc: string) {
return render(removeFrontmatter(doc), {
baseUrl: SILVERBULLET_SERVER,
baseUrl: "https://max-richter.dev",
allowMath: true,
});
}
export type ParsedDocument = ReturnType<typeof parseDocument>;
export type DocumentChild = ParsedDocument["children"][number];
export function findRangeOfChildren(children: DocumentChild[]) {
const firstChild = children[0];
const lastChild = children.length > 1
? children[children.length - 1]
: firstChild;
const start = firstChild.position?.start.offset;
const end = lastChild.position?.end.offset;
if (typeof start !== "number" || typeof end !== "number") return;
return [start, end];
}
export function getTextOfRange(children: DocumentChild[], text: string) {
if (!children || children.length === 0) {
return;
}
const range = findRangeOfChildren(children);
if (!range) return;
return text.substring(range[0], range[1]);
}
export function getTextOfChild(child: DocumentChild): string | undefined {
if ("value" in child) return child.value;
if ("children" in child) {
return getTextOfChild(child.children[0]);
}
return;
export function createDocument(
path: string,
entry: string,
mimetype = "image/jpeg",
) {
console.log("creating", { path, entry, mimetype });
}

View File

@@ -4,7 +4,6 @@ export const PROXY_SERVER = Deno.env.get("PROXY_SERVER");
export const PROXY_USERNAME = Deno.env.get("PROXY_USERNAME");
export const PROXY_PASSWORD = Deno.env.get("PROXY_PASSWORD");
export const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER");
export const TMDB_API_KEY = Deno.env.get("TMDB_API_KEY");
export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY");
@@ -15,8 +14,6 @@ export const GITEA_CLIENT_ID = Deno.env.get("GITEA_CLIENT_ID")!;
export const GITEA_CLIENT_SECRET = Deno.env.get("GITEA_CLIENT_SECRET");
export const GITEA_REDIRECT_URL = Deno.env.get("GITEA_REDIRECT_URL");
export const DATABASE_URL = Deno.env.get("DATABASE_URL") || "dev.db";
const duration = Deno.env.get("SESSION_DURATION");
export const SESSION_DURATION = duration ? +duration : (60 * 60 * 24);

View File

@@ -103,7 +103,7 @@ export function debounce<T extends (...args: Parameters<T>) => void>(
export function parseRating(rating: string | number) {
if (typeof rating === "string") {
return [...rating.matchAll(/⭐/)].length;
return [...rating.matchAll(/⭐/g)].length;
}
return rating;
}

View File

@@ -17,25 +17,31 @@ export const IngredientGroupSchema = z.object({
export type IngredientGroup = z.infer<typeof IngredientGroupSchema>;
const recipeSchema = z.object({
title: z.string().describe(
"Title of the Recipe, without the name of the website or author",
),
image: z.string().describe("URL of the main image of the recipe"),
author: z.string().describe("author of the Recipe (optional)"),
description: z.string().describe("Optional, short description of the recipe"),
ingredients: z.array(z.union([IngredientSchema, IngredientGroupSchema]))
.describe("List of ingredients"),
instructions: z.array(z.string()).describe("List of instructions"),
servings: z.number().describe("Amount of Portions"),
prepTime: z.number().describe("Preparation time in minutes"),
cookTime: z.number().describe("Cooking time in minutes"),
totalTime: z.number().describe("Total time in minutes"),
tags: z.array(z.string()).describe(
"List of tags (e.g., ['vegan', 'dessert'])",
),
notes: z.array(z.string()).describe("Optional notes about the recipe"),
name: z.string(),
content: z.object({
_type: z.literal("Recipe"),
name: z.string().describe(
"Title of the Recipe, without the name of the website or author",
),
description: z.string().describe(
"Optional, short description of the recipe",
),
image: z.string().describe("URL of the main image of the recipe"),
author: z.object({
_type: z.literal("Person"),
name: z.string().describe("author of the Recipe (optional)"),
}),
recipeEngredient: z.array(z.string())
.describe("List of ingredients"),
recipeInstructions: z.array(z.string()).describe("List of instructions"),
recipeYield: z.number().describe("Amount of Portions"),
prepTime: z.number().describe("Preparation time in minutes"),
cookTime: z.number().describe("Cooking time in minutes"),
}),
});
export type Recipe = z.infer<typeof recipeSchema>;
const noRecipeSchema = z.object({
errorMessages: z.array(z.string()).describe(
"List of error messages, if no recipe was found",
@@ -46,12 +52,13 @@ export const recipeResponseSchema = z.union([recipeSchema, noRecipeSchema]);
export function isValidRecipe(
recipe:
| { ingredients?: unknown[]; instructions?: string[]; name?: string }
| Recipe
| null
| undefined,
) {
return recipe?.ingredients?.length && recipe.ingredients.length > 1 &&
recipe?.instructions?.length &&
return recipe?.content?.recipeIngredient?.length &&
recipe?.content?.recipeIngredient.length > 1 &&
recipe?.content?.recipeInstructions?.length &&
recipe.name?.length;
}

View File

@@ -1,9 +1,3 @@
import { parseDocument } from "@lib/documents.ts";
import { parse, stringify } from "@std/yaml";
import { createCrud } from "@lib/crud.ts";
import { extractHashTags, formatDate } from "@lib/string.ts";
import { fixRenderedMarkdown } from "@lib/helpers.ts";
export type Article = {
id: string;
type: "article";
@@ -21,88 +15,3 @@ export type Article = {
rating?: number;
};
};
function renderArticle(article: Article) {
const meta = article.meta;
if ("date" in meta) {
meta.date = formatDate(meta.date);
}
return fixRenderedMarkdown(`${meta
? `---
${stringify(meta)}
---`
: `---
---`
}
# ${article.name}
${article.tags.map((t) => `#${t}`).join(" ")}
${article.content}
`);
}
function parseArticle(original: string, id: string): Article {
const doc = parseDocument(original);
let meta = {} as Article["meta"];
let name = "";
const range = [Infinity, -Infinity];
for (const child of doc.children) {
if (child.type === "yaml") {
try {
meta = parse(child.value) as Article["meta"];
} catch (err) {
console.log("Error parsing YAML", err);
console.log("YAML:", child.value);
}
if (meta["rating"] && typeof meta["rating"] === "string") {
meta.rating = [...meta.rating?.matchAll("⭐")].length;
}
continue;
}
if (
child.type === "heading" && child.depth === 1 && !name &&
child.children.length === 1 && child.children[0].type === "text"
) {
name = child.children[0].value;
continue;
}
if (name) {
const start = child.position?.start.offset || Infinity;
const end = child.position?.end.offset || -Infinity;
if (start < range[0]) range[0] = start;
if (end > range[1]) range[1] = end;
}
}
let content = original.slice(range[0], range[1]);
const tags = extractHashTags(content);
for (const tag of tags) {
content = content.replace("#" + tag, "");
}
return {
type: "article",
id,
name,
tags,
content,
meta,
};
}
const crud = createCrud<Article>({
prefix: "Media/articles/",
parse: parseArticle,
render: renderArticle,
hasThumbnails: true,
});
export const getAllArticles = crud.readAll;
export const getArticle = crud.read;
export const createArticle = crud.create;

View File

@@ -1,9 +1,3 @@
import { parseDocument } from "@lib/documents.ts";
import { parse, stringify } from "yaml";
import { createCrud } from "@lib/crud.ts";
import { extractHashTags, formatDate } from "@lib/string.ts";
import { fixRenderedMarkdown } from "@lib/helpers.ts";
export type Movie = {
id: string;
name: string;
@@ -21,103 +15,3 @@ export type Movie = {
rating: number;
};
};
export function renderMovie(movie: Movie) {
const meta = movie.meta;
if ("date" in meta && typeof meta.date !== "string") {
meta.date = formatDate(meta.date) as unknown as Date;
}
delete meta.thumbnail;
delete meta.average;
const movieImage = `![](${movie.meta.image})`;
return fixRenderedMarkdown(`${
meta
? `---
${stringify(meta)}
---`
: `---
---`
}
# ${movie.name}
${
// So we do not add a new image to the description everytime we render
(movie.meta.image && !movie.description.includes(movieImage))
? movieImage
: ""}
${movie.tags.map((t) => `#${t}`).join(" ")}
${movie.description}
`);
}
export function parseMovie(original: string, id: string): Movie {
const doc = parseDocument(original);
let meta = {} as Movie["meta"];
let name = "";
const range = [Infinity, -Infinity];
for (const child of doc.children) {
if (child.type === "yaml") {
try {
meta = (parse(child.value) || {}) as Movie["meta"];
} catch (_) {
// ignore here
}
if (meta["rating"] && typeof meta["rating"] === "string") {
meta.rating = [...meta.rating?.matchAll("⭐")].length;
}
continue;
}
if (
child.type === "heading" && child.depth === 1 && !name &&
child.children.length === 1 && child.children[0].type === "text"
) {
name = child.children[0].value;
continue;
}
if (name) {
const start = child.position?.start.offset || Infinity;
const end = child.position?.end.offset || -Infinity;
if (start < range[0]) range[0] = start;
if (end > range[1]) range[1] = end;
}
}
let description = original.slice(range[0], range[1]);
const tags = extractHashTags(description);
for (const tag of tags) {
description = description.replace("#" + tag, "");
}
return {
type: "movie",
id,
name,
tags,
description,
meta,
};
}
const crud = createCrud<Movie>({
prefix: "Media/movies/",
parse: parseMovie,
render: renderMovie,
hasThumbnails: true,
});
export const getMovie = async (id: string) => {
const movie = await crud.read(id);
return movie;
};
export const getAllMovies = crud.readAll;
export const createMovie = crud.create;

View File

@@ -1,15 +1,3 @@
import {
type DocumentChild,
getTextOfRange,
parseDocument,
} from "@lib/documents.ts";
import { parse, stringify } from "yaml";
import { createCrud } from "@lib/crud.ts";
import { extractHashTags } from "@lib/string.ts";
import { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts";
import { fixRenderedMarkdown } from "@lib/helpers.ts";
import { parseIngredients } from "@lib/parseIngredient.ts";
export type Recipe = {
type: "recipe";
id: string;
@@ -31,179 +19,3 @@ export type Recipe = {
thumbnail?: string;
};
};
function extractSteps(
content: string,
seperator: RegExp = /\n(?=\d+\.)/g,
): string[] {
const steps = content.split(seperator).map((step) => {
const match = step.match(/^(\d+)\.\s*(.*)/);
if (match) return match[2];
return step;
}).filter((step) => !!step);
return steps as string[];
}
export function parseRecipe(original: string, id: string): Recipe {
const doc = parseDocument(original);
let name = "";
let meta: Recipe["meta"] = {};
const groups: DocumentChild[][] = [];
let group: DocumentChild[] = [];
for (const child of doc.children) {
if (child.type === "yaml") {
try {
meta = parse(child.value) as Recipe["meta"];
} catch (err) {
console.log("Error parsing YAML", err);
}
continue;
}
if (
child.type === "heading" && child.depth === 1 && !name &&
child.children.length === 1 && child.children[0].type === "text"
) {
name = child.children[0].value;
continue;
}
if (child.type === "thematicBreak") {
groups.push(group);
group = [];
continue;
}
group.push(child);
}
if (group.length) {
groups.push(group);
}
let description = getTextOfRange(groups[0], original);
let ingredientsText = getTextOfRange(groups[1], original);
if (ingredientsText) {
ingredientsText = ingredientsText.replace(/#+\s?Ingredients?/, "");
} else {
ingredientsText = "";
}
const ingredients = parseIngredients(ingredientsText);
const instructionText = getTextOfRange(groups[2], original);
let instructions = extractSteps(instructionText || "");
if (instructions.length <= 1) {
const d = extractSteps(instructionText || "", /\n/g);
if (d.length > instructions.length) {
instructions = d;
}
}
const tags = extractHashTags(description || "");
if (description) {
for (const tag of tags) {
description = description.replace("#" + tag, "");
}
}
return {
type: "recipe",
id,
meta,
name,
tags,
markdown: original,
notes: getTextOfRange(groups[3], original)?.split("\n"),
description,
ingredients,
instructions,
};
}
function filterUndefinedFromObject<T extends { [key: string]: unknown }>(
obj: T,
) {
return Object.fromEntries(
Object.entries(obj).filter(([_, v]) => v !== undefined),
);
}
export function renderRecipe(recipe: Recipe) {
const meta = filterUndefinedFromObject(recipe.meta || {});
// Clean up meta properties
delete meta.thumbnail;
delete meta.average;
const recipeImage = meta.image ? `![](${meta.image})` : "";
// Format ingredient groups and standalone ingredients
const ingredients = recipe.ingredients
.map((item) => {
if ("items" in item) {
return `\n*${item.name}*\n${
item.items
.map((ing) => {
if (ing.quantity && ing.unit) {
return `- **${ing.quantity.trim() || ""}${
ing.unit.trim() || ""
}** ${ing.name}`;
}
return `- ${ing.name}`;
})
.join("\n")
}`;
}
if (item.quantity && item.unit) {
return `- **${item.quantity?.trim() || ""}${
item.unit?.trim() || ""
}** ${item.name}`;
}
if (item.quantity) {
return `- **${item.quantity}** ${item.name}`;
}
return `- ${item.name}`;
})
.join("\n");
// Format instructions as a numbered list
const instructions = recipe.instructions
? recipe.instructions.map((step, i) => `${i + 1}. ${step}`).join("\n")
: "";
// Render the final markdown
return fixRenderedMarkdown(`${
Object.keys(meta).length
? `---
${stringify(meta)}
---`
: `---
---`
}
# ${recipe.name}
${recipe.meta?.image ? recipeImage : ""}
${recipe.tags.map((t) => `#${t.replaceAll(" ", "-")}`).join(" ")}
${recipe.description || ""}
---
${ingredients ? `## Ingredients\n\n${ingredients}\n\n---\n` : ""}
${instructions ? `${instructions}\n\n---` : ""}
${recipe.notes?.length ? `\n${recipe.notes.join("\n")}` : ""}
`);
}
const crud = createCrud<Recipe>({
prefix: `Recipes/`,
parse: parseRecipe,
render: renderRecipe,
hasThumbnails: true,
});
export const getAllRecipes = crud.readAll;
export const getRecipe = crud.read;
export const updateRecipe = crud.update;
export const createRecipe = crud.create;

View File

@@ -1,9 +1,3 @@
import { parseDocument } from "@lib/documents.ts";
import { parse, stringify } from "yaml";
import { createCrud } from "@lib/crud.ts";
import { extractHashTags, formatDate } from "@lib/string.ts";
import { fixRenderedMarkdown } from "@lib/helpers.ts";
export type Series = {
id: string;
name: string;
@@ -21,99 +15,3 @@ export type Series = {
done?: boolean;
};
};
function renderSeries(series: Series) {
const meta = series.meta;
if ("date" in meta) {
meta.date = formatDate(meta.date);
}
delete meta.thumbnail;
delete meta.average;
const movieImage = `![](${series.meta.image})`;
return fixRenderedMarkdown(`${
meta
? `---
${stringify(meta)}
---`
: `---
---`
}
# ${series.name}
${
// So we do not add a new image to the description everytime we render
(series.meta.image && !series.description.includes(movieImage))
? movieImage
: ""}
${series.tags.map((t) => `#${t}`).join(" ")}
${series.description}
`);
}
export function parseSeries(original: string, id: string): Series {
const doc = parseDocument(original);
let meta = {} as Series["meta"];
let name = "";
const range = [Infinity, -Infinity];
for (const child of doc.children) {
if (child.type === "yaml") {
try {
meta = (parse(child.value) || {}) as Series["meta"];
} catch (_) {
// ignore here
}
if (meta["rating"] && typeof meta["rating"] === "string") {
meta.rating = [...meta.rating?.matchAll("⭐")].length;
}
continue;
}
if (
child.type === "heading" && child.depth === 1 && !name &&
child.children.length === 1 && child.children[0].type === "text"
) {
name = child.children[0].value;
continue;
}
if (name) {
const start = child.position?.start.offset || Infinity;
const end = child.position?.end.offset || -Infinity;
if (start < range[0]) range[0] = start;
if (end > range[1]) range[1] = end;
}
}
let description = original.slice(range[0], range[1]);
const tags = extractHashTags(description);
for (const tag of tags) {
description = description.replace("#" + tag, "");
}
return {
type: "series",
id,
name,
tags,
description,
meta,
};
}
const crud = createCrud<Series>({
prefix: "Media/series/",
parse: parseSeries,
render: renderSeries,
hasThumbnails: true,
});
export const getSeries = crud.read;
export const getAllSeries = crud.readAll;
export const createSeries = crud.create;

View File

@@ -30,3 +30,14 @@ export const resources = {
prefix: "Media/series/",
},
} as const;
export async function fetchResource(resource: string) {
try {
const response = await fetch(
`https://marka.max-richter.dev/resources/${resource}`,
);
return response.json();
} catch (_e) {
return [];
}
}

View File

@@ -2,10 +2,11 @@ import { resources } from "@lib/resources.ts";
import fuzzysort from "npm:fuzzysort";
import { GenericResource } from "@lib/types.ts";
import { extractHashTags } from "@lib/string.ts";
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
import { Article, getAllArticles } from "@lib/resource/articles.ts";
import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts";
import { getAllSeries, Series } from "@lib/resource/series.ts";
import { Movie } from "@lib/resource/movies.ts";
import { Article } from "@lib/resource/articles.ts";
import { Recipe } from "@lib/resource/recipes.ts";
import { Series } from "@lib/resource/series.ts";
import { fetchResource } from "./resources.ts";
type ResourceType = keyof typeof resources;
@@ -56,10 +57,10 @@ export async function searchResource(
{ q, tags = [], types, rating }: SearchParams,
): Promise<GenericResource[]> {
const resources = (await Promise.all([
(!types || types.includes("movie")) && getAllMovies(),
(!types || types.includes("series")) && getAllSeries(),
(!types || types.includes("article")) && getAllArticles(),
(!types || types.includes("recipe")) && getAllRecipes(),
(!types || types.includes("movie")) && fetchResource("movies"),
(!types || types.includes("series")) && fetchResource("series"),
(!types || types.includes("article")) && fetchResource("articles"),
(!types || types.includes("recipe")) && fetchResource("recipes"),
])).flat().filter(isResource);
const results: Record<string, GenericResource> = {};