Expand Doris Kitchen import, failures, and tag curation
This commit is contained in:
@@ -236,12 +236,118 @@ def coerce_text(value: Any) -> str:
|
||||
return unescape(str(value)).strip()
|
||||
|
||||
|
||||
def normalize_whitespace(value: str) -> str:
|
||||
cleaned = re.sub(r'\s+', ' ', value or '').strip()
|
||||
return re.sub(r'(?<=[.!?])(?=[A-Z])', ' ', cleaned)
|
||||
|
||||
|
||||
def split_paragraphs(value: str) -> list[str]:
|
||||
parts: list[str] = []
|
||||
for chunk in re.split(r'\n\s*\n|\r\n\r\n', value or ''):
|
||||
cleaned = normalize_whitespace(chunk)
|
||||
if cleaned:
|
||||
parts.append(cleaned)
|
||||
return parts
|
||||
|
||||
|
||||
def extract_instruction_steps(value: Any) -> list[str]:
|
||||
steps: list[str] = []
|
||||
|
||||
def visit(node: Any) -> None:
|
||||
if node is None:
|
||||
return
|
||||
if isinstance(node, str):
|
||||
for chunk in re.split(r'\n+', node):
|
||||
cleaned = normalize_whitespace(chunk)
|
||||
if cleaned:
|
||||
steps.append(cleaned)
|
||||
return
|
||||
if isinstance(node, list):
|
||||
for item in node:
|
||||
visit(item)
|
||||
return
|
||||
if isinstance(node, dict):
|
||||
if node.get('@type') == 'HowToSection':
|
||||
name = normalize_whitespace(coerce_text(node.get('name')))
|
||||
if name:
|
||||
steps.append(f'{name}:')
|
||||
visit(node.get('itemListElement'))
|
||||
return
|
||||
if node.get('@type') in {'HowToStep', 'ListItem'}:
|
||||
text = normalize_whitespace(coerce_text(node.get('text') or node.get('name') or node.get('item')))
|
||||
if text:
|
||||
steps.append(text)
|
||||
elif node.get('itemListElement') is not None:
|
||||
visit(node.get('itemListElement'))
|
||||
return
|
||||
if node.get('itemListElement') is not None:
|
||||
visit(node.get('itemListElement'))
|
||||
return
|
||||
text = normalize_whitespace(coerce_text(node.get('text') or node.get('name')))
|
||||
if text:
|
||||
steps.append(text)
|
||||
|
||||
visit(value)
|
||||
deduped: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for step in steps:
|
||||
key = step.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
deduped.append(step)
|
||||
return deduped
|
||||
|
||||
|
||||
def build_description(recipe_node: dict[str, Any]) -> str:
|
||||
intro_parts = split_paragraphs(coerce_text(recipe_node.get('description')))
|
||||
intro = intro_parts[0] if intro_parts else ''
|
||||
notes = intro_parts[1:] if len(intro_parts) > 1 else []
|
||||
steps = extract_instruction_steps(recipe_node.get('recipeInstructions'))
|
||||
parts: list[str] = []
|
||||
if intro:
|
||||
parts.append(intro)
|
||||
if steps:
|
||||
rendered_steps = []
|
||||
step_number = 1
|
||||
for step in steps:
|
||||
if step.endswith(':'):
|
||||
rendered_steps.append(step)
|
||||
else:
|
||||
rendered_steps.append(f'{step_number}. {step}')
|
||||
step_number += 1
|
||||
parts.append('\n'.join(rendered_steps))
|
||||
elif intro_parts[1:]:
|
||||
parts.append('\n'.join(intro_parts[1:]))
|
||||
if notes:
|
||||
parts.append('Notes:\n- ' + '\n- '.join(notes))
|
||||
return '\n\n'.join(part for part in parts if part).strip()
|
||||
|
||||
|
||||
def slugify_tag(value: str) -> str:
|
||||
value = value.strip().lower()
|
||||
value = re.sub(r'[^a-z0-9]+', '-', value)
|
||||
return value.strip('-')[:48]
|
||||
|
||||
|
||||
def extract_image_url(value: Any) -> str | None:
|
||||
if isinstance(value, str):
|
||||
cleaned = value.strip()
|
||||
return cleaned or None
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
found = extract_image_url(item)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
for key in ('url', 'contentUrl'):
|
||||
found = extract_image_url(value.get(key))
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def normalize_keywords(value: Any) -> list[str]:
|
||||
out: list[str] = []
|
||||
if isinstance(value, str):
|
||||
@@ -294,15 +400,7 @@ def split_ingredient(line: str) -> tuple[str, str]:
|
||||
|
||||
def normalize_recipe(url: str, recipe_node: dict[str, Any], title_fallback: str | None = None) -> dict[str, Any]:
|
||||
name = coerce_text(recipe_node.get('name')) or (title_fallback or url)
|
||||
description_parts: list[str] = []
|
||||
desc = coerce_text(recipe_node.get('description'))
|
||||
instructions = recipe_node.get('recipeInstructions')
|
||||
instructions_text = coerce_text(instructions)
|
||||
if desc:
|
||||
description_parts.append(desc)
|
||||
if instructions_text:
|
||||
description_parts.append(instructions_text)
|
||||
description = '\n\n'.join(part for part in description_parts if part).strip()
|
||||
description = build_description(recipe_node)
|
||||
|
||||
ingredients = recipe_node.get('recipeIngredient') or []
|
||||
items = []
|
||||
@@ -348,6 +446,9 @@ def normalize_recipe(url: str, recipe_node: dict[str, Any], title_fallback: str
|
||||
'items': items,
|
||||
'tags': deduped_tags[:20],
|
||||
}
|
||||
image_url = extract_image_url(recipe_node.get('image'))
|
||||
if image_url:
|
||||
payload['photo'] = image_url
|
||||
cook_time = parse_duration_minutes(recipe_node.get('cookTime'))
|
||||
prep_time = parse_duration_minutes(recipe_node.get('prepTime'))
|
||||
total_time = parse_duration_minutes(recipe_node.get('totalTime'))
|
||||
|
||||
Reference in New Issue
Block a user