Files
kochwas/src/lib/server/recipes/importer.ts

74 lines
2.3 KiB
TypeScript
Raw Normal View History

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<Recipe> {
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 };
}