import type Database from 'better-sqlite3'; import type { Recipe } from '$lib/types'; import { fetchText } from '../http'; import { extractRecipeFromHtml } from '../parsers/json-ld-recipe'; import { downloadImage } from '../images/image-downloader'; import { getRecipeById, getRecipeIdBySourceUrl, insertRecipe } from './repository'; export class ImporterError extends Error { constructor( public readonly code: | 'INVALID_URL' | 'FETCH_FAILED' | 'NO_RECIPE_FOUND', message: string ) { super(message); this.name = 'ImporterError'; } } function hostnameOrThrow(url: string): string { try { return new URL(url).hostname.toLowerCase(); } catch { throw new ImporterError('INVALID_URL', `Not a valid URL: ${url}`); } } // Manuelle URL-Importe sind absichtlich NICHT mehr auf die allowed_domain- // Whitelist beschränkt — der User pastet bewusst eine URL und erwartet, // dass der Import klappt. Die Whitelist bleibt für die Web-Suche (searxng) // relevant, weil dort ein breites Crawl-Feld eingeschränkt werden soll. export async function previewRecipe(_db: Database.Database, url: string): Promise { const host = hostnameOrThrow(url); let html: string; try { html = await fetchText(url); } catch (e) { throw new ImporterError('FETCH_FAILED', (e as Error).message); } const recipe = extractRecipeFromHtml(html); if (!recipe) { throw new ImporterError('NO_RECIPE_FOUND', 'No schema.org/Recipe JSON-LD on page'); } recipe.source_url = url; recipe.source_domain = host.replace(/^www\./, ''); return recipe; } export async function importRecipe( db: Database.Database, imageDir: string, url: string ): Promise<{ id: number; duplicate: boolean; recipe: Recipe }> { const existingId = getRecipeIdBySourceUrl(db, url); if (existingId !== null) { const recipe = getRecipeById(db, existingId); if (recipe) return { id: existingId, duplicate: true, recipe }; } const recipe = await previewRecipe(db, url); let imageFilename: string | null = null; if (recipe.image_path) { imageFilename = await downloadImage(recipe.image_path, imageDir); } recipe.image_path = imageFilename; const id = insertRecipe(db, recipe); const persisted = getRecipeById(db, id); return { id, duplicate: false, recipe: persisted ?? recipe }; }