feat: structured ingredients with unit/food resolution

Scraper returns {quantity, unit, food, extra} dicts instead of flat
strings. UI shows 4-column ingredient editor. Mealie client resolves
unit/food IDs via API (creates missing ones automatically).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 08:33:24 +01:00
parent 274aaeb0b6
commit cb669f1861
3 changed files with 194 additions and 40 deletions
+21 -26
View File
@@ -26,7 +26,7 @@ def scrape(url: str) -> dict:
"title": str,
"description": str,
"image_url": str | None,
"ingredients": [str, ...],
"ingredients": [{"quantity": str, "unit": str, "food": str, "extra": str}, ...],
"instructions": [str, ...],
"original_url": str,
}
@@ -65,41 +65,33 @@ def _parse_mindmegette(soup: BeautifulSoup, url: str) -> dict:
ing_container = soup.find("div", class_="ingredients")
if ing_container:
for row in ing_container.find_all("div", class_="ingredients-meta"):
parts = []
# Actual HTML: <strong>qty</strong> <span>unit</span>
# <a class="ingredients-link">name</a>
# <a class="ingredients-link">name</a> <small>(extra)</small>
qty_el = row.find("strong")
# Unit: first plain <span> (not one with a specific class like
# "ingredients-checkbox" etc.)
unit_el = None
for sp in row.find_all("span"):
if not sp.get("class"):
unit_el = sp
break
name_el = row.find("a", class_="ingredients-link")
# Extra info: <small>(darált)</small> or <span class="extra">
extra_el = row.find("small") or row.find("span", class_="extra")
if qty_el:
parts.append(_text(qty_el))
if unit_el:
parts.append(_text(unit_el))
if name_el:
parts.append(_text(name_el))
if extra_el:
extra = _text(extra_el)
if extra:
# Wrap in parens if not already
if not extra.startswith("("):
extra = f"({extra})"
parts.append(extra)
qty = _text(qty_el)
unit = _text(unit_el)
food = _text(name_el)
extra = _text(extra_el).strip("() ")
line = " ".join(p for p in parts if p)
if not line:
# Fallback: grab whole row text with spaces between elements
line = row.get_text(separator=" ", strip=True)
if line:
ingredients.append(line)
if not food:
# Fallback: grab whole row text
food = row.get_text(separator=" ", strip=True)
if food:
ingredients.append({
"quantity": qty,
"unit": unit,
"food": food,
"extra": extra,
})
# --- Instructions ---
instructions = []
@@ -150,7 +142,10 @@ def _parse_generic(soup: BeautifulSoup, url: str) -> dict:
if isinstance(data, list):
data = data[0]
if data.get("@type") == "Recipe":
ingredients = data.get("recipeIngredient", [])
for line in data.get("recipeIngredient", []):
ingredients.append({
"quantity": "", "unit": "", "food": line, "extra": "",
})
raw_instructions = data.get("recipeInstructions", [])
for item in raw_instructions:
if isinstance(item, str):