fix Analyse

This commit is contained in:
nadja
2026-08-26 18:29:39 +02:00
parent 797279f641
commit e60da3daf6
9 changed files with 18 additions and 127 deletions

View File

@@ -9,7 +9,3 @@ ADMIN_PASS=DEIN-SICHERES-PASSWORT
# Ollama KI-Modell (optional, Standard: mistral) # Ollama KI-Modell (optional, Standard: mistral)
# OLLAMA_MODEL=mistral # OLLAMA_MODEL=mistral
# Instagram oEmbed Token (optional, für Instagram Reels Extraktion)
# Benötigt ein Facebook App Access Token
# INSTAGRAM_TOKEN=dein-token-hier

View File

@@ -4,7 +4,6 @@ import {
updateRecipe, deleteRecipe, getCategories, getDb updateRecipe, deleteRecipe, getCategories, getDb
} from '../db.js'; } from '../db.js';
import { getTikTokData } from '../services/tiktok.js'; import { getTikTokData } from '../services/tiktok.js';
import { getInstagramData } from '../services/instagram.js';
import { isOllamaAvailable, extractRecipe } from '../services/ai.js'; import { isOllamaAvailable, extractRecipe } from '../services/ai.js';
import { trackEvent } from '../analytics-db.js'; import { trackEvent } from '../analytics-db.js';
@@ -111,43 +110,4 @@ router.post('/from-tiktok', async (req, res) => {
} }
}); });
router.post('/from-instagram', async (req, res) => {
try {
const { url } = req.body;
if (!url) return res.status(400).json({ error: 'URL erforderlich' });
const instagramData = await getInstagramData(url);
const ollamaAvailable = await isOllamaAvailable();
let extracted = null;
let aiStatus = ollamaAvailable ? 'available' : 'unavailable';
let aiMessage = '';
if (ollamaAvailable && instagramData.title) {
try {
extracted = await extractRecipe(instagramData.title);
aiStatus = 'success';
} catch (e) {
console.error('Ollama Extraktion fehlgeschlagen:', e.message);
aiStatus = 'error';
aiMessage = e.message;
}
} else if (!ollamaAvailable) {
aiMessage = 'Ollama-Server nicht erreichbar';
} else if (!instagramData.title) {
aiMessage = 'Kein Titel im Video gefunden';
}
res.json({
instagram: instagramData,
ollama: ollamaAvailable,
aiStatus,
aiMessage,
extracted,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
export default router; export default router;

View File

@@ -1,39 +0,0 @@
const INSTAGRAM_TOKEN = process.env.INSTAGRAM_TOKEN;
export async function getInstagramData(url) {
const reelMatch = url.match(/instagram\.com\/(?:p|reel)\/([A-Za-z0-9_-]+)/);
const usernameMatch = url.match(/instagram\.com\/([^/]+)\//);
if (INSTAGRAM_TOKEN) {
try {
const oembedUrl = `https://graph.facebook.com/v18.0/instagram_oembed?url=${encodeURIComponent(url)}&fields=author_name,thumbnail_url,title,html&access_token=${INSTAGRAM_TOKEN}`;
const response = await fetch(oembedUrl);
if (response.ok) {
const data = await response.json();
return {
title: data.title || '',
author: data.author_name || '',
authorUrl: `https://www.instagram.com/${data.author_name || ''}`,
thumbnail: data.thumbnail_url || '',
embedHtml: data.html || '',
reelId: reelMatch ? reelMatch[1] : null,
};
}
} catch (e) {
console.error('Instagram oEmbed Fehler:', e.message);
}
}
return {
title: '',
author: usernameMatch ? usernameMatch[1] : '',
authorUrl: usernameMatch ? `https://www.instagram.com/${usernameMatch[1]}` : '',
thumbnail: '',
embedHtml: '',
reelId: reelMatch ? reelMatch[1] : null,
};
}
export function isInstagramUrl(url) {
return /instagram\.com\/(p|reel)\//.test(url);
}

View File

@@ -18,7 +18,6 @@ services:
- JWT_SECRET=${JWT_SECRET} - JWT_SECRET=${JWT_SECRET}
- ADMIN_USER=${ADMIN_USER:-admin} - ADMIN_USER=${ADMIN_USER:-admin}
- ADMIN_PASS=${ADMIN_PASS} - ADMIN_PASS=${ADMIN_PASS}
- INSTAGRAM_TOKEN=${INSTAGRAM_TOKEN:-}
volumes: volumes:
- ./data:/app/data - ./data:/app/data
depends_on: depends_on:

View File

@@ -52,13 +52,6 @@ export async function extractFromTikTok(url) {
}); });
} }
export async function extractFromInstagram(url) {
return apiFetch(`${API_BASE}/from-instagram`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
}
export async function fetchCategories() { export async function fetchCategories() {
return apiFetch(`${API_BASE}/categories`); return apiFetch(`${API_BASE}/categories`);

View File

@@ -28,7 +28,7 @@ export default function RecipeCard({ recipe }) {
</p> </p>
)} )}
<div className="flex items-center gap-4 text-xs text-gray-400"> <div className="flex items-center gap-4 text-xs text-gray-400">
{author && <span>@{author} ({source === 'instagram' ? 'IG' : 'TT'})</span>} {author && <span>@{author} (TT)</span>}
<span>{recipe.ingredients?.length || 0} Zutaten</span> <span>{recipe.ingredients?.length || 0} Zutaten</span>
<span>{recipe.steps?.length || 0} Schritte</span> <span>{recipe.steps?.length || 0} Schritte</span>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { extractFromTikTok, extractFromInstagram, saveRecipe } from '../api'; import { extractFromTikTok, saveRecipe } from '../api';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import IngredientList from '../components/IngredientList'; import IngredientList from '../components/IngredientList';
import StepList from '../components/StepList'; import StepList from '../components/StepList';
@@ -8,7 +8,6 @@ import StepList from '../components/StepList';
const CATEGORIES = ['Frühstück', 'Hauptgericht', 'Dessert', 'Snack', 'Getränk', 'Sonstiges']; const CATEGORIES = ['Frühstück', 'Hauptgericht', 'Dessert', 'Snack', 'Getränk', 'Sonstiges'];
function detectSource(url) { function detectSource(url) {
if (/instagram\.com\/(p|reel)\//.test(url)) return 'instagram';
if (/tiktok\.com\//.test(url)) return 'tiktok'; if (/tiktok\.com\//.test(url)) return 'tiktok';
return null; return null;
} }
@@ -37,7 +36,7 @@ export default function AddRecipe() {
if (!url.trim()) return; if (!url.trim()) return;
const detected = detectSource(url); const detected = detectSource(url);
if (!detected) { if (!detected) {
setError('URL muss von TikTok oder Instagram sein.'); setError('URL muss von TikTok sein.');
return; return;
} }
@@ -46,20 +45,11 @@ export default function AddRecipe() {
setAiStatus(null); setAiStatus(null);
setAiMessage(''); setAiMessage('');
try { try {
let data; const data = await extractFromTikTok(url);
if (detected === 'instagram') {
data = await extractFromInstagram(url);
const ig = data.instagram;
setThumbnail(ig.thumbnail);
setSourceAuthor(ig.author);
setSource('instagram');
} else {
data = await extractFromTikTok(url);
const tt = data.tiktok; const tt = data.tiktok;
setThumbnail(tt.thumbnail); setThumbnail(tt.thumbnail);
setSourceAuthor(tt.author); setSourceAuthor(tt.author);
setSource('tiktok'); setSource('tiktok');
}
if (data.extracted) { if (data.extracted) {
setName(data.extracted.name || ''); setName(data.extracted.name || '');
@@ -68,7 +58,7 @@ export default function AddRecipe() {
setIngredients(data.extracted.ingredients || []); setIngredients(data.extracted.ingredients || []);
setSteps(data.extracted.steps || []); setSteps(data.extracted.steps || []);
} else { } else {
setName(detected === 'instagram' ? (data.instagram?.title || '') : (data.tiktok?.title || '')); setName(data.tiktok?.title || '');
} }
setAiStatus(data.aiStatus || (data.extracted ? 'success' : 'unavailable')); setAiStatus(data.aiStatus || (data.extracted ? 'success' : 'unavailable'));
setAiMessage(data.aiMessage || ''); setAiMessage(data.aiMessage || '');
@@ -106,14 +96,14 @@ export default function AddRecipe() {
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6"> <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Video-URL (TikTok oder Instagram Reel) Video-URL (TikTok)
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="url" type="url"
value={url} value={url}
onChange={e => setUrl(e.target.value)} onChange={e => setUrl(e.target.value)}
placeholder="https://www.tiktok.com/@user/video/... oder https://www.instagram.com/reel/..." placeholder="https://www.tiktok.com/@user/video/..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
onKeyDown={e => e.key === 'Enter' && handleExtract()} onKeyDown={e => e.key === 'Enter' && handleExtract()}
/> />

View File

@@ -107,7 +107,7 @@ export default function Home() {
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-400 text-lg mb-2">Keine Rezepte gefunden</p> <p className="text-gray-400 text-lg mb-2">Keine Rezepte gefunden</p>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-sm">
Füge dein erstes Rezept über TikTok- oder Instagram-Links hinzu! Füge dein erstes Rezept über TikTok-Links hinzu!
</p> </p>
</div> </div>
) : ( ) : (

View File

@@ -53,20 +53,12 @@ export default function RecipeDetail() {
let videoId = null; let videoId = null;
let embedUrl = null; let embedUrl = null;
if (sourceUrl) { if (sourceUrl) {
if (sourceType === 'instagram') {
const igMatch = sourceUrl.match(/instagram\.com\/(?:p|reel)\/([A-Za-z0-9_-]+)/);
if (igMatch) {
videoId = igMatch[1];
embedUrl = `https://www.instagram.com/reel/${videoId}/embed/`;
}
} else {
const ttMatch = sourceUrl.match(/video\/(\d+)/); const ttMatch = sourceUrl.match(/video\/(\d+)/);
if (ttMatch) { if (ttMatch) {
videoId = ttMatch[1]; videoId = ttMatch[1];
embedUrl = `https://www.tiktok.com/embed/v2/${videoId}`; embedUrl = `https://www.tiktok.com/embed/v2/${videoId}`;
} }
} }
}
const author = recipe.source_author || recipe.tiktok_author; const author = recipe.source_author || recipe.tiktok_author;
@@ -78,7 +70,7 @@ export default function RecipeDetail() {
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
{embedUrl && ( {embedUrl && (
<div className="w-full" style={{ paddingBottom: sourceType === 'instagram' ? '100%' : '177.78%', position: 'relative' }}> <div className="w-full" style={{ paddingBottom: '177.78%', position: 'relative' }}>
<iframe <iframe
src={embedUrl} src={embedUrl}
className="absolute inset-0 w-full h-full" className="absolute inset-0 w-full h-full"
@@ -153,7 +145,7 @@ export default function RecipeDetail() {
{author && ( {author && (
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
Quelle: @{author} ({sourceType === 'instagram' ? 'Instagram' : 'TikTok'}) Quelle: @{author} (TikTok)
</p> </p>
)} )}