diff --git a/app/database.py b/app/database.py index 5a45706..a1daa06 100755 --- a/app/database.py +++ b/app/database.py @@ -85,6 +85,21 @@ def clean_source_label(source: Optional[str]) -> str: return src +def clean_instructions_text(text: Optional[str]) -> str: + """Entfernt vorangestellte Schrittnummern (z. B. '1. ', 'Schritt 1: ') aus Zubereitungszeilen.""" + if not text: + return "" + import re + cleaned_lines = [] + for line in str(text).splitlines(): + line_s = line.strip() + if not line_s: + continue + cleaned = re.sub(r'^(?:Schritt\s*\d+[:.]?|\d+[\.\)\:])\s*', '', line_s, flags=re.IGNORECASE) + cleaned_lines.append(cleaned) + return "\n".join(cleaned_lines) + + def guess_category_for_ingredient(name: str) -> str: """Bestimmt automatisch die passende Supermarkt-Kategorie anhand deutscher Zutaten-Keywords.""" if not name: @@ -713,6 +728,8 @@ def add_recipe( # Fallback to mapped chapter number if possible chapter_number = CHAPTER_MAP.get(category, 1) + instructions = clean_instructions_text(instructions) + res = execute_query( """ INSERT INTO recipes (title, category, chapter_number, effort, prep_time, cook_time, portions, has_portion_table, ingredients, instructions, notes, source, image_url, printed_at) @@ -755,6 +772,8 @@ def update_recipe( if not existing: raise KeyError(f"Rezept #{recipe_id} nicht gefunden.") + instructions = clean_instructions_text(instructions) + use_portion_table = int(bool(has_portion_table)) if has_portion_table is not None else existing.get("has_portion_table", 0) if image_url is not None: diff --git a/app/main.py b/app/main.py index f95068b..2bfea00 100755 --- a/app/main.py +++ b/app/main.py @@ -130,6 +130,14 @@ async def auth_middleware(request: Request, call_next): app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") templates = Jinja2Templates(directory=TEMPLATES_DIR) +def strip_step_number_filter(text: str) -> str: + if not text: + return "" + import re + return re.sub(r'^(?:Schritt\s*\d+[:.]?|\d+[\.\)\:])\s*', '', str(text).strip(), flags=re.IGNORECASE) + +templates.env.filters["strip_step_number"] = strip_step_number_filter + # Mapping Kochbuch-Kapitel -> Zufallsgenerator-Kategorien CHAPTER_TO_DISH_CAT = { "Suppen & Eintöpfe": "Suppen", diff --git a/app/templates/cookbook.html b/app/templates/cookbook.html index e68c160..b2304f7 100755 --- a/app/templates/cookbook.html +++ b/app/templates/cookbook.html @@ -803,7 +803,7 @@ {% if step.strip() %}
{{ loop.index }} -
{{ step.strip() }}
+
{{ step.strip() | strip_step_number }}
{% endif %} {% endfor %} diff --git a/app/templates/recipe_detail.html b/app/templates/recipe_detail.html index 14ee2c6..d37f0ab 100755 --- a/app/templates/recipe_detail.html +++ b/app/templates/recipe_detail.html @@ -181,7 +181,7 @@ {% if step.strip() %}
{{ loop.index }} -
{{ step.strip() }}
+
{{ step.strip() | strip_step_number }}
{% endif %} {% endfor %} diff --git a/scripts/preview_strip_numbers.py b/scripts/preview_strip_numbers.py new file mode 100644 index 0000000..ce728b6 --- /dev/null +++ b/scripts/preview_strip_numbers.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +import sys +import re + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.database import get_connection + +sys.stdout.reconfigure(encoding='utf-8') + +conn = get_connection() +cur = conn.cursor() +cur.execute("SELECT id, title, instructions FROM recipes ORDER BY id") +recipes = cur.fetchall() + +def clean_instructions(text: str) -> str: + cleaned_lines = [] + for line in text.split("\n"): + line_s = line.strip() + if not line_s: + continue + cleaned = re.sub(r'^(?:Schritt\s*)?\d+[\.\)\:\-]?\s*', '', line_s, flags=re.IGNORECASE) + cleaned_lines.append(cleaned) + return "\n".join(cleaned_lines) + +print(f"Total recipes to preview: {len(recipes)}") +for r in recipes[:10]: + lines_before = [l.strip() for l in r['instructions'].split('\n') if l.strip()] + lines_after = [l.strip() for l in clean_instructions(r['instructions']).split('\n') if l.strip()] + print(f"\n=== ID {r['id']}: {r['title']} ===") + for b, a in zip(lines_before[:2], lines_after[:2]): + print(f" VORHER: {b[:65]}") + print(f" NACHHER: {a[:65]}") + +conn.close() diff --git a/scripts/strip_step_numbers_db.py b/scripts/strip_step_numbers_db.py new file mode 100644 index 0000000..a27e169 --- /dev/null +++ b/scripts/strip_step_numbers_db.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Strip leading step numbers from instructions in MariaDB to avoid duplicate numbers in the UI. +""" +import os +import sys +import re + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from app.database import get_connection + +sys.stdout.reconfigure(encoding='utf-8') + +STEP_REGEX = re.compile(r'^(?:Schritt\s*\d+[:.]?|\d+[\.\)\:])\s*', re.IGNORECASE) + +def clean_instructions_text(text: str) -> str: + if not text: + return "" + cleaned_lines = [] + for line in text.split("\n"): + line_s = line.strip() + if not line_s: + continue + cleaned = STEP_REGEX.sub('', line_s) + cleaned_lines.append(cleaned) + return "\n".join(cleaned_lines) + +def main(): + conn = get_connection() + cur = conn.cursor() + cur.execute("SELECT id, title, instructions FROM recipes ORDER BY id") + recipes = cur.fetchall() + + updated_count = 0 + total_lines_cleaned = 0 + + for r in recipes: + original = r['instructions'] or "" + cleaned = clean_instructions_text(original) + if cleaned != original: + cur.execute("UPDATE recipes SET instructions = %s WHERE id = %s", (cleaned, r['id'])) + updated_count += 1 + # count cleaned lines + for orig_line in original.split("\n"): + if STEP_REGEX.match(orig_line.strip()): + total_lines_cleaned += 1 + + conn.commit() + conn.close() + + print(f"Successfully cleaned instructions across {updated_count} of {len(recipes)} recipes.") + print(f"Total step number prefixes removed: {total_lines_cleaned}.") + +if __name__ == "__main__": + main() diff --git a/scripts/test_endpoints.py b/scripts/test_endpoints.py index 615f3b9..d97be65 100644 --- a/scripts/test_endpoints.py +++ b/scripts/test_endpoints.py @@ -33,6 +33,14 @@ def test_routes(): assert res.status_code == 200, f"Recipe 1 failed: {res.status_code}" print("✓ Recipe detail (/cookbook/recipe/1) OK") + # Verify no double numbers in /cookbook + import re + res_cb = client.get("/cookbook") + pattern = re.compile(r'
\s*((?:Schritt\s*\d+[:.]?|\d+[\.\)\:])\s*[^<]*)', re.IGNORECASE) + matches = pattern.findall(res_cb.text) + assert len(matches) == 0, f"Found {len(matches)} double numbers in /cookbook: {matches[:3]}" + print("✓ Zero double numbers in rendered /cookbook steps confirmed!") + print("\nAll route tests passed successfully!") if __name__ == "__main__":