Reciply – Rezept-Scraper
Situation
Rezept-Inhalte leben in Social-Media-Reels, doch das eigentliche Rezept steckt in einer Caption, die man scrollen, per Screenshot festhalten und von Hand abtippen muss. Kein Tool machte aus einer Video-URL ein gespeichertes, strukturiertes Rezept, das einem selbst gehört – das Ziel war: eine URL rein, ein strukturiertes Rezept raus, abgelegt in der eigenen Bibliothek.
Aufgabe
Eine Full-Stack-App bauen, die aus einer Instagram-/Facebook-Reel-URL ein sauberes, editierbares, persistiertes Rezept mit Nutzerkonten, Bildern und optionalem Community-Sharing erzeugt – ohne die Datenbank mit unbestätigtem KI-Output zu füllen und robust genug für den Deploy. Die zentrale Design-Vorgabe: Die Extraktion darf nicht persistieren. Das Parsen liefert einen flüchtigen Entwurf, den der:die Nutzer:in bearbeitet und erst dann speichert, sodass sich die DB nie mit KI-Rateraten füllt.
Umsetzung
Die Pipeline ist URL → Scraper → KI-Parser → editierbare Vorschau → Speichern. Ein Caption-First-Scraper ruft yt-dlp auf (--dump-json, ohne Browser) und deckt ~80 % der Reels ab; Whisper-Transkription und Headless-Browser-Fallbacks sind dokumentiert, aber bewusst zurückgestellt, um v1 klein zu halten. Die Captions gehen an Gemini mit einem responseSchema für strukturierten Output, und jedes Ergebnis wird Zod-validiert, bevor es den Client erreicht. Die Auth ist JWT mit kurzlebigen Access Tokens (15 Min) plus rotierenden Refresh Tokens (7 Tage, als SHA-256-Hash gespeichert, als httpOnly-Cookie ausgeliefert) mit Reuse Detection, bcrypt (Cost 12) für Passwörter und einem Ownership-Scoping pro userId bei jedem Lesen, Schreiben und Löschen (404 statt Existenz preiszugeben). Bilder werden zu Cloudinary hochgeladen, wobei die public_id deterministisch aus der gespeicherten URL abgeleitet wird, und ein Community-Feed lässt Nutzer:innen veröffentlichen und als eigenständigen Snapshot „zur Bibliothek hinzufügen“; die Rezept-Identität ist die sourceUrl, sodass Duplikate über App-Level-Guards ein 409 zurückgeben statt über einen riskanten Unique-Index. Teure Routen (extract, community) werden im Memory gecacht, Rate-Limiter decken die globale API plus Extract, Bild-Upload und Refresh ab, die Umgebung wird beim Boot Zod-validiert, Fehler nutzen ein einziges { message }-Envelope, und das async Auto-Forwarding von Express 5 hat ~16 überflüssige try/catch-Blöcke entfernt.
Ergebnis
v1 läuft durchgängig: URL → yt-dlp → Gemini → editierbare Vorschau → MongoDB, mit Auth, Bibliotheken pro Nutzer:in, Favoriten, Bild-Uploads und einem öffentlichen Community-Feed mit Dedup. Es ist deploy-gehärtet – In-Memory-Caching, vollständige Rate-Limiter-Abdeckung, Env-Validierung beim Boot und strikte Ownership-Isolation. Der Umfang bleibt bewusst auf Caption-basierte Extraktion mit dokumentierten Upgrade-Pfaden begrenzt, sodass die Codebasis überschaubar bleibt.
Fazit & Lessons Learned
Der Zuschnitt auf die Caption-First-Leiter und das Aufschreiben – statt Bauen – der Whisper-/Headless-Fallbacks hielt v1 auslieferbar, ohne Türen zu schließen. Extract und Save getrennt zu halten war die entscheidende Architekturentscheidung – die Datenbank enthält nur von Menschen bestätigte Rezepte. App-Level-409-Dedup über die sourceUrl vermied einen Unique-Index und dessen Migrationsrisiko auf Bestandsdaten, und das async Auto-Forwarding von Express 5 hat still eine ganze Klasse Boilerplate gelöscht. Der naheliegende nächste Schritt ist der Tausch der In-Memory-Caches gegen Redis, bereits an der Aufrufstelle markiert.