Add phase 5: link import, Instagram fallback and share target

- SSRF-safe fetcher: public IPs only, connects to the checked address,
  redirects re-validated, size/time limits, honest user agent
- schema.org/Recipe JSON-LD extraction (graphs, HowToSections, ISO
  durations, yields, keywords, images) without extra dependencies;
  AI fallback on plain page text when no structured recipe exists
- Instagram: one login-free request to the public embed page; when
  Instagram withholds the caption, the import page explains how to paste
  the text or import a screenshot instead
- Suggested source photo can be downloaded and attached on save
- Text import works without AI (text becomes the description)
- Installable PWA with manifest, icons and Android share target
- Offline tests for SSRF rules, parsers, Instagram and import views

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Kyle Dammann
2026-10-30 21:53:28 +01:00
co-authored by Claude Opus 5.5
parent 1bf8ef8b65
commit 60785e2437
22 changed files with 1081 additions and 16 deletions
+8 -1
View File
@@ -59,6 +59,9 @@ class RecipeForm(forms.ModelForm):
)
source_url = forms.URLField(label="Quelle (URL)", required=False, max_length=1000, assume_scheme="https")
photos = photo_field()
# Link import: photo suggested by the source page, downloaded only when the user keeps the tick.
import_image_url = forms.URLField(required=False, max_length=2000, widget=forms.HiddenInput, assume_scheme="https")
use_import_image = forms.BooleanField(label="Bild der Quelle als Foto übernehmen", required=False, initial=True)
class Meta:
model = Recipe
@@ -74,7 +77,8 @@ class RecipeForm(forms.ModelForm):
}
field_order = ["title", "description", "servings", "servings_label", "prep_minutes", "cook_minutes",
"ingredients_text", "steps_text", "tags_text", "photos", "author_notes", "source_url", "visibility"]
"ingredients_text", "steps_text", "tags_text", "use_import_image", "photos", "author_notes",
"source_url", "visibility"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -85,6 +89,9 @@ class RecipeForm(forms.ModelForm):
self.initial.setdefault("ingredients_text", parsing.ingredients_to_text(recipe.ingredients.all()))
self.initial.setdefault("steps_text", parsing.steps_to_text([s.text for s in recipe.steps.all()]))
self.initial.setdefault("tags_text", ", ".join(t.name for t in recipe.tags.all()))
if recipe.pk or not (self.initial.get("import_image_url") or self.data.get("import_image_url")):
del self.fields["import_image_url"]
del self.fields["use_import_image"]
if recipe.pk:
remaining = settings.IMAGE_MAX_PER_RECIPE - recipe.images.count()
self.fields["photos"].label = "Weitere Fotos"