feat: url scraper to recipe
This commit is contained in:
103
routes/api/recipes/create/parseJsonLd.ts
Normal file
103
routes/api/recipes/create/parseJsonLd.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import recipeSchema from "@lib/recipeSchema.ts";
|
||||
import { parseIngredient } from "@lib/parseIngredient.ts";
|
||||
|
||||
export function parseJsonLdToRecipeSchema(jsonLdContent: string) {
|
||||
try {
|
||||
let data = JSON.parse(jsonLdContent);
|
||||
|
||||
const image = data.image;
|
||||
|
||||
// Handle nested data inside `mainEntity`
|
||||
if (data["mainEntity"]) {
|
||||
data = data["mainEntity"];
|
||||
}
|
||||
|
||||
// Ensure it's a valid Recipe type
|
||||
if (
|
||||
typeof data !== "object" || !data["@type"] || data["@type"] !== "Recipe"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map and parse ingredients into the new schema
|
||||
const ingredients = (data.recipeIngredient || []).map(
|
||||
parseIngredient,
|
||||
);
|
||||
|
||||
const instructions = Array.isArray(data.recipeInstructions)
|
||||
? data.recipeInstructions.map((instr) => {
|
||||
if (typeof instr === "string") return instr;
|
||||
if (typeof instr === "object" && instr.text) return instr.text;
|
||||
return "";
|
||||
}).filter((instr) => instr.trim() !== "")
|
||||
: [];
|
||||
|
||||
// Parse servings
|
||||
const servings = parseServings(data.recipeYield);
|
||||
|
||||
// Parse times
|
||||
const prepTime = parseDuration(data.prepTime);
|
||||
const cookTime = parseDuration(data.cookTime);
|
||||
const totalTime = parseDuration(data.totalTime);
|
||||
|
||||
// Extract tags
|
||||
const tags = data.keywords
|
||||
? Array.isArray(data.keywords)
|
||||
? data.keywords
|
||||
: data.keywords.split(",").map((tag: string) => tag.trim())
|
||||
: [];
|
||||
|
||||
// Build the recipe object
|
||||
const recipe = {
|
||||
title: data.name || "Unnamed Recipe",
|
||||
image: pickImage(image || data.image || ""),
|
||||
author: Array.isArray(data.author)
|
||||
? data.author.map((a: any) => a.name).join(", ")
|
||||
: data.author?.name || "",
|
||||
description: data.description || "",
|
||||
ingredients,
|
||||
instructions,
|
||||
servings,
|
||||
prepTime,
|
||||
cookTime,
|
||||
totalTime,
|
||||
tags,
|
||||
notes: data.notes || [],
|
||||
};
|
||||
|
||||
// Validate against the schema
|
||||
return recipeSchema.parse(recipe);
|
||||
} catch (error) {
|
||||
console.error("Invalid JSON-LD content or parsing error:", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function pickImage(images: string | string[]): string {
|
||||
if (Array.isArray(images)) {
|
||||
return images[0];
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
function parseServings(servingsData: any): number {
|
||||
if (typeof servingsData === "string") {
|
||||
const match = servingsData.match(/\d+/);
|
||||
return match ? parseInt(match[0], 10) : 1;
|
||||
}
|
||||
if (typeof servingsData === "number") {
|
||||
return servingsData;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function parseDuration(duration: string | undefined): number {
|
||||
if (!duration) return 0;
|
||||
|
||||
// Matches ISO 8601 durations (e.g., "PT30M" -> 30 minutes)
|
||||
const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?/);
|
||||
const hours = match?.[1] ? parseInt(match[1], 10) : 0;
|
||||
const minutes = match?.[2] ? parseInt(match[2], 10) : 0;
|
||||
|
||||
return hours * 60 + minutes;
|
||||
}
|
Reference in New Issue
Block a user