fix Analyse
This commit is contained in:
@@ -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
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user