126 lines
4 KiB
Python
126 lines
4 KiB
Python
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from datetime import timedelta
|
|
import re
|
|
from pathvalidate import validate_filename
|
|
import os
|
|
from tqdm import tqdm
|
|
|
|
include_title = False
|
|
output_dir = "recipes-md"
|
|
|
|
|
|
def valid_duration(duration):
|
|
if not duration:
|
|
return False
|
|
if duration == "0":
|
|
return False
|
|
return True
|
|
|
|
def valid_description(description):
|
|
if not description:
|
|
return False
|
|
return True
|
|
|
|
def parse_duration(duration):
|
|
if not duration:
|
|
return ""
|
|
match = re.match(r"PT(?:(\d+)H)?(?:(\d+)M)?", duration)
|
|
if not match:
|
|
return duration
|
|
hours, minutes = match.groups()
|
|
hours = f"{int(hours)}h " if hours else ""
|
|
minutes = f"{int(minutes)}m" if minutes else ""
|
|
return f"{hours}{minutes}".strip()
|
|
|
|
def format_recipe_to_markdown(recipe):
|
|
md = []
|
|
|
|
# Title (may be implicit by filename)
|
|
if include_title:
|
|
md.append(f"# {recipe.get('name', 'Untitled Recipe')}")
|
|
|
|
# Description
|
|
if valid_description(recipe.get('description')):
|
|
md.append(f"\n_{recipe['description']}_\n")
|
|
|
|
# Details
|
|
details_parts = []
|
|
if valid_duration(recipe.get('prepTime')):
|
|
details_parts.append(f"Prep time: {parse_duration(recipe['prepTime'])}")
|
|
if valid_duration(recipe.get('cookTime')):
|
|
details_parts.append(f"Cook time: {parse_duration(recipe['cookTime'])}")
|
|
if valid_duration(recipe.get('totalTime')):
|
|
details_parts.append(f"Total time: {parse_duration(recipe['totalTime'])}")
|
|
if 'recipeYield' in recipe:
|
|
details_parts.append(f"Portionen: {recipe['recipeYield']}")
|
|
if details_parts:
|
|
for p in details_parts:
|
|
md.append(f"* {p}\n")
|
|
|
|
# Ingredients
|
|
if 'recipeIngredient' in recipe:
|
|
md.append("\n## Zutaten")
|
|
for ingredient in recipe['recipeIngredient']:
|
|
md.append(f"- {ingredient}")
|
|
|
|
# Instructions
|
|
if 'recipeInstructions' in recipe:
|
|
md.append("\n## Zubereitung")
|
|
instructions = recipe['recipeInstructions']
|
|
if isinstance(instructions, list):
|
|
for i, step in enumerate(instructions, 1):
|
|
assert isinstance(step, dict)
|
|
text = step.get('text', '').strip()
|
|
|
|
# "ingredient-only" steps shouldn't exist, but they do
|
|
if not text:
|
|
continue
|
|
|
|
md.append(f"{i}.\n{text}")
|
|
else:
|
|
for i, line in enumerate(instructions.strip().split('\n'), 1):
|
|
if line.strip():
|
|
md.append(f"{i}. {line.strip()}")
|
|
|
|
# Nutrition
|
|
if 'nutrition' in recipe:
|
|
md.append("\n## Nutrition")
|
|
for key, value in recipe['nutrition'].items():
|
|
if key != "@type":
|
|
md.append(f"- **{key.replace('_', ' ').capitalize()}**: {value}")
|
|
|
|
return '\n'.join(md)
|
|
|
|
def main(json_file):
|
|
with open(json_file, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
# If the JSON-LD is embedded in @graph or a list
|
|
# if isinstance(data, list):
|
|
# recipe = next((item for item in data if item.get('@type') == 'Recipe'), data[0])
|
|
# elif '@graph' in data:
|
|
# recipe = next((item for item in data['@graph'] if item.get('@type') == 'Recipe'), data['@graph'][0])
|
|
if isinstance(data, list) and data[0].get('@type') == 'Recipe':
|
|
recipes = data
|
|
elif data.get('@type') == 'Recipe':
|
|
recipes = [data]
|
|
else:
|
|
print("No Recipe object found in JSON.")
|
|
return
|
|
|
|
for recipe in tqdm(recipes):
|
|
markdown_fn = f"{recipe.get('name', 'Untitled Recipe')}.md"
|
|
validate_filename(markdown_fn) # XXX does this check directory traversal?
|
|
markdown = format_recipe_to_markdown(recipe)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
with open(os.path.join(output_dir, markdown_fn), "w") as f:
|
|
f.write(markdown)
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 2:
|
|
print("Usage: python recipe_to_md.py <path_to_recipe_json>")
|
|
else:
|
|
main(sys.argv[1])
|
|
|