feat(import): enhance image URL extraction and logging during recipe import

This commit is contained in:
Nils-Johan Gynther
2026-04-22 22:08:05 +02:00
parent 28606d7abd
commit 71bc162015
8 changed files with 163 additions and 21 deletions
@@ -77,7 +77,41 @@ export class GenericRecipeParser extends RecipeParser {
private extractOgImage(html: string): string | undefined {
const match = html.match(/<meta[^>]+property="og:image"[^>]+content="([^"]+)"/i)
|| html.match(/<meta[^>]+content="([^"]+)"[^>]+property="og:image"/i);
return match ? match[1].trim() : undefined;
return match ? this.decodeHtmlEntities(match[1].trim()) : undefined;
}
private decodeHtmlEntities(value: string): string {
return value
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
}
private extractImageValue(image: any): string | undefined {
if (!image) return undefined;
if (typeof image === 'string') return image;
if (Array.isArray(image)) {
for (const item of image) {
const extracted = this.extractImageValue(item);
if (extracted) return extracted;
}
return undefined;
}
if (typeof image === 'object') {
return (
image.url ||
image['@id'] ||
image.contentUrl ||
image.thumbnailUrl ||
this.extractImageValue(image.image)
);
}
return undefined;
}
private extractFromJsonLd(recipe: any, ogImage?: string): ParsedRecipe {
@@ -86,14 +120,9 @@ export class GenericRecipeParser extends RecipeParser {
// Extrahera bildurl från JSON-LD
let imageUrl: string | undefined = ogImage;
if (recipe.image) {
if (typeof recipe.image === 'string') {
imageUrl = recipe.image;
} else if (Array.isArray(recipe.image) && recipe.image.length > 0) {
imageUrl = typeof recipe.image[0] === 'string' ? recipe.image[0] : recipe.image[0]?.url;
} else if (recipe.image?.url) {
imageUrl = recipe.image.url;
}
const extractedImage = this.extractImageValue(recipe.image);
if (extractedImage) {
imageUrl = this.decodeHtmlEntities(extractedImage);
}
const ingredients: Array<{ quantity: number; unit: string; name: string; note?: string }> = [];
+38 -9
View File
@@ -76,7 +76,41 @@ export class IcaRecipeParser extends RecipeParser {
private extractOgImage(html: string): string | undefined {
const match = html.match(/<meta[^>]+property="og:image"[^>]+content="([^"]+)"/i)
|| html.match(/<meta[^>]+content="([^"]+)"[^>]+property="og:image"/i);
return match ? match[1].trim() : undefined;
return match ? this.decodeHtmlEntities(match[1].trim()) : undefined;
}
private decodeHtmlEntities(value: string): string {
return value
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
}
private extractImageValue(image: any): string | undefined {
if (!image) return undefined;
if (typeof image === 'string') return image;
if (Array.isArray(image)) {
for (const item of image) {
const extracted = this.extractImageValue(item);
if (extracted) return extracted;
}
return undefined;
}
if (typeof image === 'object') {
return (
image.url ||
image['@id'] ||
image.contentUrl ||
image.thumbnailUrl ||
this.extractImageValue(image.image)
);
}
return undefined;
}
private extractFromJsonLd(recipe: any, ogImage?: string): ParsedRecipe {
@@ -88,14 +122,9 @@ export class IcaRecipeParser extends RecipeParser {
// Extrahera bildurl från JSON-LD (kan vara sträng eller array)
let imageUrl: string | undefined = ogImage;
if (recipe.image) {
if (typeof recipe.image === 'string') {
imageUrl = recipe.image;
} else if (Array.isArray(recipe.image) && recipe.image.length > 0) {
imageUrl = typeof recipe.image[0] === 'string' ? recipe.image[0] : recipe.image[0]?.url;
} else if (recipe.image?.url) {
imageUrl = recipe.image.url;
}
const extractedImage = this.extractImageValue(recipe.image);
if (extractedImage) {
imageUrl = this.decodeHtmlEntities(extractedImage);
}
// Extrahera ingredienser
@@ -320,7 +320,9 @@ export class QuickImportService {
let imageUrl: string | undefined;
let imageWarning: string | undefined;
if (recipe.imageUrl) {
this.logger.log(`Bildkandidat från parser: ${recipe.imageUrl}`);
const normalizedImageUrl = this.normalizeImageUrl(recipe.imageUrl, url);
this.logger.log(`Normaliserad bild-URL: ${normalizedImageUrl ?? 'null'}`);
if (!normalizedImageUrl) {
imageWarning = 'Receptbild kunde inte tolkas till en giltig URL.';
this.logger.warn(
@@ -331,7 +333,9 @@ export class QuickImportService {
imageUrl = await downloadAndOptimizeImage(normalizedImageUrl, IMAGE_DEST_DIR);
this.logger.log(`Bild optimerad och sparad: ${imageUrl}`);
} catch (imgErr) {
imageWarning = 'Receptbild kunde inte laddas ner.';
// Fallback: behåll extern URL så klienten ändå kan visa bild.
imageUrl = normalizedImageUrl;
imageWarning = 'Receptbild kunde inte laddas ner lokalt; extern URL används.';
this.logger.warn(
`Kunde inte ladda ner bild: ${imgErr} (källa: ${normalizedImageUrl})`,
);
+12 -2
View File
@@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
@@ -27,6 +27,8 @@ interface ParsedRecipe {
@Injectable()
export class RecipesService {
private readonly logger = new Logger(RecipesService.name);
constructor(private readonly prisma: PrismaService) {}
async getInventoryPreview(id: number) {
@@ -301,17 +303,25 @@ export class RecipesService {
}
async create(createRecipeDto: CreateRecipeDto, userId: number) {
this.logger.log(
`[create] Incoming imageUrl from client: ${createRecipeDto.imageUrl ?? 'null'}`,
);
// Om imageUrl är en extern URL — ladda ner och optimera
let imageUrl: string | null = createRecipeDto.imageUrl || null;
if (imageUrl && imageUrl.startsWith('http')) {
const externalImageUrl = imageUrl;
try {
imageUrl = await downloadAndOptimizeImage(imageUrl, IMAGE_DEST_DIR);
} catch (err) {
console.warn('[RecipesService] Kunde inte ladda ner receptbild:', err);
imageUrl = null;
// Behåll extern URL som fallback så bild fortfarande visas.
imageUrl = externalImageUrl;
}
}
this.logger.log(`[create] Final imageUrl persisted to DB: ${imageUrl ?? 'null'}`);
const recipe = await this.prisma.recipe.create({
data: {
name: createRecipeDto.name,