feat: tag management — scrape, edit, search existing, import to Mealie/Tandoor

- Scraper extracts tags from mindmegette.hu (<a class="tag">) and schema.org keywords
- Tag editor UI with removable chips, search/autocomplete for existing tags, custom add
- Mealie: auto-create tags via POST /api/organizers/tags, include in recipe PATCH
- Tandoor: include keywords in recipe POST (auto-created by name)
- New GET /tags endpoint returns existing tags from both services for search

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 12:42:54 +01:00
parent 458b1e362a
commit bbd0889471
6 changed files with 314 additions and 8 deletions
+40 -1
View File
@@ -63,10 +63,22 @@ class MealieClient:
}
return None
def list_tags(self) -> list[dict]:
"""Return all tags as [{name, slug, id}]."""
r = self.session.get(
f"{self.api_url}/api/organizers/tags",
params={"page": 1, "perPage": -1},
timeout=10,
)
if not r.ok:
return []
return [{"name": t["name"], "slug": t["slug"], "id": t["id"]}
for t in r.json().get("items", [])]
def create_recipe(self, recipe: dict) -> str:
"""Create a recipe in Mealie from a scraper result dict.
*recipe* keys: title, description, image_url, ingredients, instructions, original_url.
*recipe* keys: title, description, image_url, ingredients, instructions, tags, original_url.
Returns the recipe slug.
"""
# Step 1: create stub
@@ -80,6 +92,12 @@ class MealieClient:
# Step 2: build full payload and PATCH
payload = self._build_payload(recipe)
# Step 2b: resolve tags (create if needed, get {id, name, slug})
tag_names = recipe.get("tags", [])
if tag_names:
payload["tags"] = self._ensure_tags(tag_names)
r = self.session.patch(
f"{self.api_url}/api/recipes/{slug}",
json=payload,
@@ -173,6 +191,27 @@ class MealieClient:
return entry
return None
def _ensure_tags(self, tag_names: list[str]) -> list[dict]:
"""Create tags that don't exist yet, return [{id, name, slug}] for all."""
existing = {t["name"].lower(): t for t in self.list_tags()}
result = []
for name in tag_names:
key = name.lower()
if key in existing:
result.append(existing[key])
else:
r = self.session.post(
f"{self.api_url}/api/organizers/tags",
json={"name": name},
timeout=10,
)
if r.ok:
t = r.json()
tag = {"id": t["id"], "name": t["name"], "slug": t["slug"]}
existing[key] = tag
result.append(tag)
return result
# ------------------------------------------------------------------
# Internal
# ------------------------------------------------------------------