fix Analyse
This commit is contained in:
@@ -9,7 +9,3 @@ ADMIN_PASS=DEIN-SICHERES-PASSWORT
|
||||
|
||||
# Ollama KI-Modell (optional, Standard: 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
|
||||
} from '../db.js';
|
||||
import { getTikTokData } from '../services/tiktok.js';
|
||||
import { getInstagramData } from '../services/instagram.js';
|
||||
import { isOllamaAvailable, extractRecipe } from '../services/ai.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;
|
||||
|
||||
@@ -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}
|
||||
- ADMIN_USER=${ADMIN_USER:-admin}
|
||||
- ADMIN_PASS=${ADMIN_PASS}
|
||||
- INSTAGRAM_TOKEN=${INSTAGRAM_TOKEN:-}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
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() {
|
||||
return apiFetch(`${API_BASE}/categories`);
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function RecipeCard({ recipe }) {
|
||||
</p>
|
||||
)}
|
||||
<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.steps?.length || 0} Schritte</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { extractFromTikTok, extractFromInstagram, saveRecipe } from '../api';
|
||||
import { extractFromTikTok, saveRecipe } from '../api';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import IngredientList from '../components/IngredientList';
|
||||
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'];
|
||||
|
||||
function detectSource(url) {
|
||||
if (/instagram\.com\/(p|reel)\//.test(url)) return 'instagram';
|
||||
if (/tiktok\.com\//.test(url)) return 'tiktok';
|
||||
return null;
|
||||
}
|
||||
@@ -37,7 +36,7 @@ export default function AddRecipe() {
|
||||
if (!url.trim()) return;
|
||||
const detected = detectSource(url);
|
||||
if (!detected) {
|
||||
setError('URL muss von TikTok oder Instagram sein.');
|
||||
setError('URL muss von TikTok sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,20 +45,11 @@ export default function AddRecipe() {
|
||||
setAiStatus(null);
|
||||
setAiMessage('');
|
||||
try {
|
||||
let data;
|
||||
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 data = await extractFromTikTok(url);
|
||||
const tt = data.tiktok;
|
||||
setThumbnail(tt.thumbnail);
|
||||
setSourceAuthor(tt.author);
|
||||
setSource('tiktok');
|
||||
}
|
||||
|
||||
if (data.extracted) {
|
||||
setName(data.extracted.name || '');
|
||||
@@ -68,7 +58,7 @@ export default function AddRecipe() {
|
||||
setIngredients(data.extracted.ingredients || []);
|
||||
setSteps(data.extracted.steps || []);
|
||||
} else {
|
||||
setName(detected === 'instagram' ? (data.instagram?.title || '') : (data.tiktok?.title || ''));
|
||||
setName(data.tiktok?.title || '');
|
||||
}
|
||||
setAiStatus(data.aiStatus || (data.extracted ? 'success' : 'unavailable'));
|
||||
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">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Video-URL (TikTok oder Instagram Reel)
|
||||
Video-URL (TikTok)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
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"
|
||||
onKeyDown={e => e.key === 'Enter' && handleExtract()}
|
||||
/>
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function Home() {
|
||||
<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-sm">
|
||||
Füge dein erstes Rezept über TikTok- oder Instagram-Links hinzu!
|
||||
Füge dein erstes Rezept über TikTok-Links hinzu!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -53,20 +53,12 @@ export default function RecipeDetail() {
|
||||
let videoId = null;
|
||||
let embedUrl = null;
|
||||
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+)/);
|
||||
if (ttMatch) {
|
||||
videoId = ttMatch[1];
|
||||
embedUrl = `https://www.tiktok.com/embed/v2/${videoId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
{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
|
||||
src={embedUrl}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
@@ -153,7 +145,7 @@ export default function RecipeDetail() {
|
||||
|
||||
{author && (
|
||||
<p className="text-xs text-gray-400">
|
||||
Quelle: @{author} ({sourceType === 'instagram' ? 'Instagram' : 'TikTok'})
|
||||
Quelle: @{author} (TikTok)
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user