import { Handlers } from "$fresh/server.ts"; import { AccessDeniedError, BadRequestError } from "@lib/errors.ts"; import { createStreamResponse, isValidUrl } from "@lib/helpers.ts"; import * as openai from "@lib/openai.ts"; import { createLogger } from "@lib/log/index.ts"; import recipeSchema from "@lib/recipeSchema.ts"; import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; import { safeFileName } from "@lib/string.ts"; import { parseJsonLdToRecipeSchema } from "./parseJsonLd.ts"; import z from "zod"; import { createResource } from "@lib/marka/index.ts"; import { webScrape } from "@lib/webScraper.ts"; import { Defuddle } from "defuddle/node"; import { RecipeResource } from "@lib/marka/schema.ts"; const log = createLogger("api/article"); async function processCreateRecipeFromUrl( { fetchUrl, streamResponse }: { fetchUrl: string; streamResponse: ReturnType; }, ) { log.info("create article from url", { url: fetchUrl }); streamResponse.enqueue("downloading article"); const doc = await webScrape(fetchUrl, streamResponse); const result = await Defuddle(doc, fetchUrl, { markdown: true, }); streamResponse.enqueue("download success"); const jsonLds = Array.from( doc?.querySelectorAll( "script[type='application/ld+json']", ), ) as unknown as HTMLScriptElement[]; let recipe: z.infer | undefined = undefined; if (jsonLds.length > 0) { for (const jsonLd of jsonLds) { recipe = parseJsonLdToRecipeSchema(jsonLd.textContent || ""); if (recipe) break; } } if (!recipe) { recipe = await openai.extractRecipe(result.content); } const id = safeFileName(recipe?.name || ""); if (!recipe) { streamResponse.enqueue("failed to parse recipe"); streamResponse.cancel(); return; } const newRecipe: RecipeResource["content"] = { ...recipe, _type: "Recipe", totalTime: recipe?.totalTime ? `${recipe?.totalTime?.toString()} minutes` : undefined, url: fetchUrl, }; if (newRecipe?.image && newRecipe.image.length > 5) { const extension = fileExtension(new URL(newRecipe.image).pathname); const finalPath = `resources/recipes/images/${safeFileName(id) }_cover.${extension}`; streamResponse.enqueue("downloading image"); try { streamResponse.enqueue("downloading image"); const res = await fetch(newRecipe.image); streamResponse.enqueue("saving image"); const buffer = await res.arrayBuffer(); await createResource(finalPath, buffer); newRecipe.image = finalPath; } catch (err) { console.log("Failed to save image", err); } } streamResponse.enqueue("finished processing, creating file"); await createResource(`recipes/${id}.md`, newRecipe); streamResponse.enqueue("id: " + id); } export const handler: Handlers = { GET(req, ctx) { const session = ctx.state.session; if (!session) { throw new AccessDeniedError(); } const url = new URL(req.url); const fetchUrl = url.searchParams.get("url"); if (!fetchUrl || !isValidUrl(fetchUrl)) { throw new BadRequestError(); } const streamResponse = createStreamResponse(); processCreateRecipeFromUrl({ fetchUrl, streamResponse }).then((article) => { log.debug("created article from link", { article }); }).catch((err) => { streamResponse.enqueue(`error creating recipe: ${err}`); log.error(err); }).finally(() => { streamResponse.cancel(); }); return streamResponse.response; }, };