From f7810ba33d602c87aa0033a6f2af9671c2df3f1e Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 24 Feb 2026 09:02:53 +0100 Subject: [PATCH] feat: duplicate detection + original URL in description - Add original recipe URL to Mealie description (appended after blank line) - Check for duplicate recipes on scrape (match orgURL or URL in description) - Show warning with link to existing recipe if duplicate found - User can still import anyway (warning only, not blocking) Co-Authored-By: Claude Opus 4.6 --- app/main.py | 14 ++++++++++++- app/mealie.py | 43 +++++++++++++++++++++++++++++++++++++-- app/templates/base.html | 1 + app/templates/import.html | 9 +++++++- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index 3915719..09f8e60 100644 --- a/app/main.py +++ b/app/main.py @@ -81,7 +81,19 @@ def scrape_url(): return jsonify({"ok": False, "error": "Nincs URL megadva."}) try: data = scrape(url) - return jsonify({"ok": True, "data": data}) + + # Check for duplicate in Mealie + duplicate = None + cfg = config.load() + if cfg.get("mealie_url") and cfg.get("mealie_api_key"): + try: + client = MealieClient(cfg["mealie_url"], cfg["mealie_api_key"], + api_url=config.MEALIE_INTERNAL_URL) + duplicate = client.find_duplicate(url, data.get("title", "")) + except Exception: + pass # non-fatal + + return jsonify({"ok": True, "data": data, "duplicate": duplicate}) except Exception as exc: return jsonify({"ok": False, "error": str(exc), "trace": traceback.format_exc()}) diff --git a/app/mealie.py b/app/mealie.py index 90094e7..f31419c 100644 --- a/app/mealie.py +++ b/app/mealie.py @@ -29,6 +29,40 @@ class MealieClient: r.raise_for_status() return r.json() + def find_duplicate(self, url: str, title: str = "") -> dict | None: + """Check if a recipe with this original URL already exists. + + Searches by URL in orgURL and description fields. + Returns {"slug": ..., "name": ..., "url": ...} or None. + """ + if not url: + return None + # Search by title to find candidates (Mealie search matches name) + search_term = title or url.split("/")[-1].replace("-", " ") + r = self.session.get( + f"{self.api_url}/api/recipes", + params={"search": search_term, "perPage": 50}, + timeout=10, + ) + if not r.ok: + return None + for item in r.json().get("items", []): + detail = self.session.get( + f"{self.api_url}/api/recipes/{item['slug']}", timeout=10 + ) + if not detail.ok: + continue + data = detail.json() + org = data.get("orgURL", "") or "" + desc = data.get("description", "") or "" + if org == url or url in desc: + return { + "slug": data["slug"], + "name": data.get("name", data["slug"]), + "url": f"{self.base_url}/g/home/r/{data['slug']}", + } + return None + def create_recipe(self, recipe: dict) -> str: """Create a recipe in Mealie from a scraper result dict. @@ -175,12 +209,17 @@ class MealieClient: "ingredientReferences": [], }) + description = recipe.get("description", "") + original_url = recipe.get("original_url", "") + if original_url and original_url not in description: + description = f"{description}\n\n{original_url}".strip() + return { "name": recipe["title"], - "description": recipe.get("description", ""), + "description": description, "recipeIngredient": ingredients, "recipeInstructions": instructions, - "orgURL": recipe.get("original_url", ""), + "orgURL": original_url, "recipeYield": "", } diff --git a/app/templates/base.html b/app/templates/base.html index 4457b81..bebf582 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -162,6 +162,7 @@ .text-dim { color: var(--text-dim); } .text-success { color: var(--success); } .text-danger { color: var(--danger); } + .text-warning { color: var(--warning); } .flex { display: flex; gap: 0.75rem; align-items: center; } .flex-wrap { flex-wrap: wrap; } .grow { flex: 1; } diff --git a/app/templates/import.html b/app/templates/import.html index d89efb6..fd44881 100644 --- a/app/templates/import.html +++ b/app/templates/import.html @@ -232,7 +232,14 @@ async function scrapeRecipe() { currentRecipe = data.data; populatePreview(currentRecipe); document.getElementById('previewCard').classList.add('visible'); - status.innerHTML = '✓ Beolvasva'; + + if (data.duplicate) { + status.innerHTML = '⚠ Ez a recept már létezik Mealie-ben: ' + + '' + + escHtml(data.duplicate.name) + ''; + } else { + status.innerHTML = '✓ Beolvasva'; + } } catch (e) { status.innerHTML = 'Hálózati hiba: ' + e.message + ''; }