From 167cd1f4bbc2b7e8a8e1e5cf4922d0158894973d Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Sun, 30 Nov 2025 21:56:21 +0100 Subject: [PATCH] with thumbnail! --- .gitignore | 1 + src/lib/server/tandoor.ts | 54 +++++++++++++++++++++++++++++-- src/routes/api/extract/+server.ts | 49 ++++++++++++++++++++++------ src/routes/api/tandoor/+server.ts | 14 ++++++-- src/routes/share/+page.svelte | 48 ++++++++++++++++++++++++++- 5 files changed, 152 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 3b462cb..20cf0e8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* +debug_page.txt diff --git a/src/lib/server/tandoor.ts b/src/lib/server/tandoor.ts index e8cf854..00b4e43 100644 --- a/src/lib/server/tandoor.ts +++ b/src/lib/server/tandoor.ts @@ -60,6 +60,7 @@ interface ExtractedRecipe { unit: string; }> | null; steps: string[] | null; + image?: string | null; } /** @@ -279,7 +280,7 @@ function buildTandoorRecipeDTO(recipe: ExtractedRecipe): TandoorRecipeDTO { */ export async function uploadRecipeWithIngredientsDTO( recipe: ExtractedRecipe -): Promise<{ success: boolean; recipeId?: number; error?: string }> { +): Promise<{ success: boolean; recipeId?: number; imageUrl?: string; error?: string }> { try { // Validate token const token = tandoorConfig.token; @@ -317,7 +318,8 @@ export async function uploadRecipeWithIngredientsDTO( return { success: true, - recipeId: createdRecipe.id + recipeId: createdRecipe.id, + imageUrl: recipe.image || undefined }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; @@ -328,3 +330,51 @@ export async function uploadRecipeWithIngredientsDTO( }; } } + +/** + * Uploads an image to a Tandoor recipe + */ +export async function uploadRecipeImage( + recipeId: number, + imageUrl: string +): Promise<{ success: boolean; error?: string }> { + try { + const token = tandoorConfig.token; + if (!token) { + return { success: false, error: 'TANDOOR_TOKEN not set' }; + } + + console.log('Uploading image for recipe ID:', recipeId, 'URL:', imageUrl.substring(0, 50)); + + // Convert base64 data URL to Blob for multipart upload + const response = await fetch(imageUrl); + const imageBlob = await response.blob(); + + // Use image field with multipart form data (Tandoor's binary upload support) + const formData = new FormData(); + formData.append('image', imageBlob, 'recipe-image.jpg'); + + // Upload to Tandoor + const uploadResponse = await fetch( + `${tandoorConfig.serverUrl}/api/recipe/${recipeId}/image/`, + { + method: 'PUT', + headers: { 'Authorization': `Bearer ${token}` }, + body: formData + } + ); + + if (!uploadResponse.ok) { + console.warn(`Image upload returned ${uploadResponse.status}`); + return { success: false, error: `Upload failed: ${uploadResponse.statusText}` }; + } + + console.log('Image uploaded successfully'); + return { success: true }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + console.warn(`Image upload failed: ${errorMsg}`); + // Don't fail recipe creation if image fails + return { success: false, error: errorMsg }; + } +} diff --git a/src/routes/api/extract/+server.ts b/src/routes/api/extract/+server.ts index 53428e5..93fb43c 100644 --- a/src/routes/api/extract/+server.ts +++ b/src/routes/api/extract/+server.ts @@ -3,8 +3,8 @@ import { createLLM } from '$lib/server/llm'; import { json } from '@sveltejs/kit'; import fs, { writeFileSync } from 'fs'; import { zodResponseFormat } from 'openai/helpers/zod'; -import { z } from 'zod'; import path from 'path'; +import { z } from 'zod'; const RecipeSchema = z.object({ name: z.string(), @@ -15,7 +15,8 @@ const RecipeSchema = z.object({ amount: z.string(), unit: z.string() })).nullable(), - steps: z.array(z.string()).nullable() + steps: z.array(z.string()).nullable(), + image: z.string().nullable().optional() }); @@ -34,18 +35,43 @@ export async function POST({ request }) { const context = await createBrowserContext(authPath); const page = await context.newPage(); + + // Set a fixed viewport size (Instagram feed width) + await page.setViewportSize({ width: 1080, height: 1920 }); + let bodyText = ''; + let thumbnail: string | null = null; try { await page.goto(url, { waitUntil: 'domcontentloaded' }); - // Extract HTML from the page bodyText = (await page.evaluate(() => document.body.innerText)).replace(/^(?:.*\n){6}/, '').split('More posts from')[0].trim(); - - // Cleaning steps - // 1. Remove @tags and #hashtags bodyText = bodyText.replace(/@\w+/g, '').replace(/#\w+/g, ''); - + writeFileSync(path.resolve('debug_page.txt'), bodyText); // Save for debugging, overwriting if exists + const videoBounds = await page.evaluate(() => { + const video = document.querySelector('video'); + if (!video) return null; + const rect = video.getBoundingClientRect(); + return { + x: Math.max(0, rect.left), + y: Math.max(0, rect.top), + width: Math.min(rect.width, window.innerWidth), + height: Math.min(rect.height, window.innerHeight) + }; + }); + + if (videoBounds && videoBounds.width > 0 && videoBounds.height > 0) { + const screenshotBuffer = await page.screenshot({ + type: 'jpeg', + quality: 85, + clip: videoBounds + }); + thumbnail = `data:image/jpeg;base64,${screenshotBuffer.toString('base64')}`; + } else { + console.warn('Video element not found or has no size, taking full page screenshot'); + const screenshotBuffer = await page.screenshot({ type: 'jpeg', quality: 85 }); + thumbnail = `data:image/jpeg;base64,${screenshotBuffer.toString('base64')}`; + } } catch (e) { console.error('Scraping error:', e); return json({ error: 'Failed to scrape URL' }, { status: 500 }); @@ -122,10 +148,15 @@ Extract ONLY what's explicitly in the text. Be accurate and literal. } else { recipe.description = `Link: ${url}`; } + + // Add thumbnail to recipe + if (thumbnail) { + recipe.image = thumbnail; + } - return json({ recipe }); + return json({ recipe, bodyText }); } catch (e) { console.error('LLM error:', e); - return json({ error: 'Failed to parse recipe' }, { status: 500 }); + return json({ error: 'Failed to parse recipe', bodyText }, { status: 500 }); } } \ No newline at end of file diff --git a/src/routes/api/tandoor/+server.ts b/src/routes/api/tandoor/+server.ts index c2f0d8b..2489b00 100644 --- a/src/routes/api/tandoor/+server.ts +++ b/src/routes/api/tandoor/+server.ts @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit'; -import { uploadRecipeWithIngredientsDTO } from '$lib/server/tandoor'; +import { uploadRecipeWithIngredientsDTO, uploadRecipeImage } from '$lib/server/tandoor'; export async function POST({ request }) { const { recipe } = await request.json(); @@ -15,10 +15,20 @@ export async function POST({ request }) { return json({ error: result.error || 'Failed to upload recipe' }, { status: 500 }); } + // Upload image if available + let imageStatus = null; + if (result.recipeId && result.imageUrl) { + imageStatus = await uploadRecipeImage(result.recipeId, result.imageUrl); + if (!imageStatus.success) { + console.warn('Image upload failed, but recipe created:', imageStatus.error); + } + } + return json({ success: true, message: 'Recipe successfully imported to Tandoor', - recipeId: result.recipeId + recipeId: result.recipeId, + imageUpload: imageStatus?.success ? 'successful' : 'failed' }); } catch (error) { console.error('Tandoor upload error:', error); diff --git a/src/routes/share/+page.svelte b/src/routes/share/+page.svelte index b16509b..79c7340 100644 --- a/src/routes/share/+page.svelte +++ b/src/routes/share/+page.svelte @@ -4,6 +4,7 @@ let status = $state('idle'); let logs = $state([]); let recipe = $state(null); + let bodyText = $state(''); let tandoorEnabled = $state(false); let tandoorImporting = $state(false); let tandoorError = $state(null); @@ -51,10 +52,12 @@ if (data.recipe) { recipe = data.recipe; + bodyText = data.bodyText || ''; status = 'done'; logs = [...logs, 'Recipe extraction successful']; } else { - logs = [...logs, 'Error: ' + JSON.stringify(data)]; + bodyText = data.bodyText || ''; + logs = [...logs, 'Error: ' + (data.error || JSON.stringify(data))]; status = 'error'; } } catch(e) { @@ -63,6 +66,14 @@ } } + async function retry() { + recipe = null; + bodyText = ''; + status = 'idle'; + logs = [...logs, 'Retrying extraction...']; + await process(); + } + async function importToTandoor() { if (!recipe) return; @@ -116,11 +127,21 @@
Extracting data...
{/if} + {#if bodyText} +
+ 📝 View Extracted Text +
+ {bodyText} +
+
+ {/if} {#if recipe}

{recipe.name}

{recipe.description}

Servings: {recipe.servings}

+ +

Ingredients

    {#each recipe.ingredients as ing} @@ -151,6 +172,31 @@
{/if} + + + + {/if} + + {#if status === 'error' && bodyText} +
+

Extraction Error - Raw Text Available

+
+ 📝 View Extracted Text +
+ {bodyText} +
+
+
{/if}