Created
May 4, 2025 08:08
-
-
Save janbalaz/c28ad40828789d8a520c32be5c48a31e to your computer and use it in GitHub Desktop.
MealMaster v7.07 to Tandoor recipes format
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Vibe-coded version of formatter from MealMaster v7.07 to Tandoor recipes format. | |
""" | |
import re | |
import json | |
import os | |
import time | |
import zipfile | |
from datetime import datetime | |
from typing import List, Dict, Any | |
from pathlib import Path | |
def parse_meal_master_file(filename: str) -> List[str]: | |
"""Read the file and split it into individual recipes.""" | |
with open(filename, 'r', encoding='utf-8') as f: | |
content = f.read() | |
# Split content into individual recipes | |
recipes = content.split('---------- Recipe via Meal-Master (tm) v7.07') | |
# Remove empty entries | |
return [r.strip() for r in recipes if r.strip()] | |
def parse_recipe_header(recipe_text: str) -> Dict[str, Any]: | |
"""Parse the recipe header (title, categories, servings).""" | |
title_match = re.search(r'Title:\s*(.*?)\n', recipe_text) | |
categories_match = re.search(r'Categories:\s*(.*?)\n', recipe_text) | |
servings_match = re.search(r'Servings:\s*(\d+)', recipe_text) | |
title = title_match.group(1).strip() if title_match else "" | |
categories = [cat.strip() for cat in categories_match.group(1).split(',')] if categories_match else [] | |
servings = int(servings_match.group(1)) if servings_match else 1 | |
return { | |
"title": title, | |
"categories": categories, | |
"servings": servings | |
} | |
def parse_ingredients(recipe_text: str) -> List[Dict[str, Any]]: | |
"""Parse the ingredients section.""" | |
ingredients = [] | |
# Find the ingredients section (between header and instructions) | |
lines = recipe_text.split('\n') | |
ingredient_lines = [] | |
for line in lines: | |
if re.match(r'\s+\d+(/\d+)?\s+[a-zA-Z]', line): | |
ingredient_lines.append(line) | |
for line in ingredient_lines: | |
# Parse amount, unit, and ingredient name | |
match = re.match(r'\s*(\d+(/\d+)?)\s+([a-zA-Z]+)\s+(.*)', line) | |
if match: | |
amount_str, unit, name = match.group(1), match.group(3), match.group(4) | |
# Convert fractions to decimal | |
if '/' in amount_str: | |
num, denom = map(int, amount_str.split('/')) | |
amount = num / denom | |
else: | |
amount = float(amount_str) | |
ingredients.append({ | |
"food": { | |
"name": name.lower().strip(), | |
"plural_name": None, | |
"ignore_shopping": False, | |
"supermarket_category": None | |
}, | |
"unit": { | |
"name": unit.lower(), | |
"plural_name": None, | |
"description": None | |
}, | |
"amount": amount, | |
"note": "", | |
"order": 0, | |
"is_header": False, | |
"no_amount": False, | |
"always_use_plural_unit": False, | |
"always_use_plural_food": False | |
}) | |
return ingredients | |
def parse_instructions(recipe_text: str) -> str: | |
"""Parse the instructions section.""" | |
# Find the first line that doesn't match ingredient pattern | |
lines = recipe_text.split('\n') | |
start_idx = 0 | |
for i, line in enumerate(lines): | |
if not re.match(r'\s*\d+(/\d+)?\s+[a-zA-Z]', line) and line.strip(): | |
start_idx = i | |
break | |
return '\n'.join(lines[start_idx:]).strip() | |
def create_tandoor_recipe(recipe_text: str) -> Dict[str, Any]: | |
"""Convert a single recipe to Tandoor format.""" | |
header = parse_recipe_header(recipe_text) | |
ingredients = parse_ingredients(recipe_text) | |
instructions = parse_instructions(recipe_text) | |
# Create keywords from categories | |
keywords = [] | |
timestamp = datetime.now().isoformat() | |
for category in header["categories"]: | |
keywords.append({ | |
"name": category.strip(), | |
"description": "", | |
"created_at": timestamp, | |
"updated_at": timestamp | |
}) | |
return { | |
"name": header["title"], | |
"description": None, | |
"keywords": keywords, | |
"steps": [{ | |
"name": "", | |
"instruction": instructions, | |
"ingredients": ingredients, | |
"time": 0, | |
"order": 0, | |
"show_as_header": True, | |
"show_ingredients_table": True | |
}], | |
"working_time": 0, | |
"waiting_time": 0, | |
"internal": True, | |
"nutrition": None, | |
"servings": header["servings"], | |
"servings_text": "", | |
"source_url": None | |
} | |
def create_export_structure(recipes: List[Dict[str, Any]]) -> str: | |
""" | |
Create export directory structure and save recipes as individual zip files. | |
Returns the export directory name. | |
""" | |
# Create export directory with current date | |
current_date = datetime.now().strftime('%Y-%m-%d') | |
export_dir = f"export_{current_date}" | |
# Create directory if it doesn't exist | |
os.makedirs(export_dir, exist_ok=True) | |
# Save each recipe as a separate zip file | |
for recipe in recipes: | |
# Create timestamp for zip file name | |
timestamp = int(time.time()) | |
zip_filename = f"{timestamp}.zip" | |
zip_path = os.path.join(export_dir, zip_filename) | |
# Create zip file containing recipe.json | |
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: | |
# Convert recipe to JSON string | |
recipe_json = json.dumps(recipe, indent=4) | |
# Add recipe.json to zip file | |
zf.writestr('recipe.json', recipe_json) | |
# Small delay to ensure unique timestamps | |
time.sleep(1) | |
return export_dir | |
def create_final_archive(export_dir: str): | |
"""Create a zip archive of the export directory.""" | |
with zipfile.ZipFile(f"{export_dir}.zip", 'w', zipfile.ZIP_DEFLATED) as zf: | |
# Walk through the export directory | |
for root, _, files in os.walk(export_dir): | |
for file in files: | |
file_path = os.path.join(root, file) | |
# Add file to zip with relative path | |
arcname = os.path.relpath(file_path, start=os.path.dirname(export_dir)) | |
zf.write(file_path, arcname) | |
def convert_recipes(input_file: str): | |
"""Convert all recipes from input file to individual Tandoor format files.""" | |
# Read and parse recipes | |
recipe_texts = parse_meal_master_file(input_file) | |
# Convert each recipe to Tandoor format | |
tandoor_recipes = [] | |
for recipe_text in recipe_texts: | |
tandoor_recipe = create_tandoor_recipe(recipe_text) | |
tandoor_recipes.append(tandoor_recipe) | |
# Create export structure | |
export_dir = create_export_structure(tandoor_recipes) | |
# Create final archive | |
create_final_archive(export_dir) | |
print(f"Export completed. Files are in {export_dir}/ and {export_dir}.zip") | |
if __name__ == "__main__": | |
convert_recipes("input.mmf") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment