feat: add frontmatter and images to 9 recipes without frontmatter (Task 3)
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Black Pepper Tofu (Ottolenghi)
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/black-pepper-tofu-ottolenghi.webp
|
||||||
|
---
|
||||||
|
|
||||||
>> Servings: 4
|
>> Servings: 4
|
||||||
Pour enough @vegetable oil{} into a large #frying pan or wok{} to come 1/4 inch and turn on heat.
|
Pour enough @vegetable oil{} into a large #frying pan or wok{} to come 1/4 inch and turn on heat.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Black Pepper Tofu with Bok Choy
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/black-pepper-tofu-with-bok-choy.webp
|
||||||
|
---
|
||||||
|
|
||||||
>> Servings: 2
|
>> Servings: 2
|
||||||
>> Prep time: 30 minutes
|
>> Prep time: 30 minutes
|
||||||
>> Total time: 30 minutes
|
>> Total time: 30 minutes
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Caramel Cake
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/caramel-cake.webp
|
||||||
|
---
|
||||||
|
|
||||||
>> Servings: 10
|
>> Servings: 10
|
||||||
|
|
||||||
Combine @sour cream{8 ounces} and @milk{1/4 cup}.
|
Combine @sour cream{8 ounces} and @milk{1/4 cup}.
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Caramel Icing
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/caramel-icing.webp
|
||||||
|
---
|
||||||
|
|
||||||
>> Servings: 10
|
>> Servings: 10
|
||||||
|
|
||||||
Sprinkle @sugar{1/2 cup} in a shallow heavy #3½-quart Dutch oven{}.
|
Sprinkle @sugar{1/2 cup} in a shallow heavy #3½-quart Dutch oven{}.
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Italian Sausage & Delicata Rigatoni
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/italian-sausage-and-delicata-rigatoni.webp
|
||||||
|
---
|
||||||
|
|
||||||
>> title: Italian Sausage & Delicata Rigatoni
|
>> title: Italian Sausage & Delicata Rigatoni
|
||||||
>> author: Chef Robin Pridgen
|
>> author: Chef Robin Pridgen
|
||||||
>> servings: 2
|
>> servings: 2
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/tmp/recipe-venv/bin/python3
|
||||||
|
"""Add frontmatter and images to 9 recipes without frontmatter."""
|
||||||
|
|
||||||
|
import re, sys, time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from ddgs import DDGS
|
||||||
|
|
||||||
|
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
|
||||||
|
HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"}
|
||||||
|
|
||||||
|
# (filename, slug, title, source_url_or_empty)
|
||||||
|
recipes = [
|
||||||
|
("Black Pepper Tofu (Ottolenghi).cook", "black-pepper-tofu-ottolenghi", "Black Pepper Tofu (Ottolenghi)", ""),
|
||||||
|
("Black Pepper Tofu with Bok Choy.cook", "black-pepper-tofu-with-bok-choy", "Black Pepper Tofu with Bok Choy", ""),
|
||||||
|
("butternut-turkey-chili.cook", "butternut-turkey-chili", "Butternut & Turkey Chili", ""),
|
||||||
|
("Caramel Cake.cook", "caramel-cake", "Caramel Cake", ""),
|
||||||
|
("Caramel Icing.cook", "caramel-icing", "Caramel Icing", ""),
|
||||||
|
("example-fried-rice.cook", "example-fried-rice", "Fried Rice", ""),
|
||||||
|
("indian-spiced-chicken.cook", "indian-spiced-chicken", "Indian Spiced Chicken", ""),
|
||||||
|
("Italian Sausage and Delicata Rigatoni.cook", "italian-sausage-and-delicata-rigatoni", "Italian Sausage & Delicata Rigatoni", ""),
|
||||||
|
("shrimp-feta.cook", "shrimp-feta", "Orzo with shrimp, tomato and marinated feta", "https://thehappyfoodie.co.uk/recipes/ottolenghis-orzo-with-prawns-tomato-and-marinated-feta/"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_og_image(url: str) -> str | None:
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, headers=HEADERS, timeout=15)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
for meta in soup.find_all("meta"):
|
||||||
|
c = meta.get("content", "")
|
||||||
|
p = (meta.get("property") or "").lower()
|
||||||
|
n = (meta.get("name") or "").lower()
|
||||||
|
if ("og:image" in p or "og:image" in n or "twitter:image" in n) and c:
|
||||||
|
return c
|
||||||
|
# JSON-LD
|
||||||
|
for script in soup.find_all("script", type="application/ld+json"):
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
data = json.loads(script.string)
|
||||||
|
items = data if isinstance(data, list) else [data]
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict) and item.get("@type") == "Recipe":
|
||||||
|
img = item.get("image")
|
||||||
|
if isinstance(img, str): return img
|
||||||
|
if isinstance(img, list):
|
||||||
|
for i in img:
|
||||||
|
if isinstance(i, str): return i
|
||||||
|
if isinstance(i, dict):
|
||||||
|
u = i.get("contentUrl") or i.get("url", "")
|
||||||
|
if u: return u
|
||||||
|
if isinstance(img, dict):
|
||||||
|
u = img.get("contentUrl") or img.get("url", "")
|
||||||
|
if u: return u
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def search_and_scrape(query: str) -> str | None:
|
||||||
|
try:
|
||||||
|
results = list(DDGS().text(query, max_results=5))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
for r in results:
|
||||||
|
url = r.get("href", "")
|
||||||
|
if not url or "pinterest" in url or "facebook" in url:
|
||||||
|
continue
|
||||||
|
img = fetch_og_image(url)
|
||||||
|
if img:
|
||||||
|
return img
|
||||||
|
time.sleep(0.3)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def download_image(url: str, dest: Path) -> bool:
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, headers=HEADERS, timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
ct = resp.headers.get("content-type", "")
|
||||||
|
if not ct.startswith("image/"):
|
||||||
|
return False
|
||||||
|
content = resp.content
|
||||||
|
if len(content) < 5000:
|
||||||
|
return False
|
||||||
|
dest.write_bytes(content)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def add_frontmatter(filepath: Path, slug: str, title: str):
|
||||||
|
url = f"https://roux.roo.lol/recipe-images/{slug}.webp"
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
frontmatter = f"""---
|
||||||
|
title: {title}
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: {url}
|
||||||
|
---
|
||||||
|
|
||||||
|
"""
|
||||||
|
filepath.write_text(frontmatter + content, encoding="utf-8")
|
||||||
|
print(f" Added frontmatter to {filepath.name}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for filename, slug, title, src_url in recipes:
|
||||||
|
fp = WORKDIR / filename
|
||||||
|
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
|
||||||
|
|
||||||
|
print(f"[{slug}] Processing {filename}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Find and download image
|
||||||
|
img_url = None
|
||||||
|
if src_url:
|
||||||
|
print(f" Trying source URL: {src_url}", file=sys.stderr)
|
||||||
|
img_url = fetch_og_image(src_url)
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
if not img_url:
|
||||||
|
print(f" Searching: {title} recipe", file=sys.stderr)
|
||||||
|
img_url = search_and_scrape(f"{title} recipe")
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
if not img_url:
|
||||||
|
print(f" Searching (alt): {title}", file=sys.stderr)
|
||||||
|
img_url = search_and_scrape(title)
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
if img_url:
|
||||||
|
print(f" Found: {img_url[:80]}", file=sys.stderr)
|
||||||
|
if download_image(img_url, dest):
|
||||||
|
print(f" Downloaded ({dest.stat().st_size} bytes)", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f" Download failed, will still add frontmatter", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f" No image found, will still add frontmatter", file=sys.stderr)
|
||||||
|
|
||||||
|
# Add frontmatter (with or without image)
|
||||||
|
add_frontmatter(fp, slug, title)
|
||||||
|
print(f" Done", file=sys.stderr)
|
||||||
|
|
||||||
|
print("\n=== Task 3 complete ===", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Butternut & Turkey Chili
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/butternut-turkey-chili.webp
|
||||||
|
---
|
||||||
|
|
||||||
>> title: Butternut & Turkey Chili
|
>> title: Butternut & Turkey Chili
|
||||||
>> author: Chef Torie Cox
|
>> author: Chef Torie Cox
|
||||||
>> servings: 2
|
>> servings: 2
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Fried Rice
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/example-fried-rice.webp
|
||||||
|
---
|
||||||
|
|
||||||
-- TODO add source
|
-- TODO add source
|
||||||
|
|
||||||
Mix together @oyster sauce{1%tbsp}, @soy sauce{5%tbsp} and @sesame oil{5%tsp}, set aside.
|
Mix together @oyster sauce{1%tbsp}, @soy sauce{5%tbsp} and @sesame oil{5%tsp}, set aside.
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Indian Spiced Chicken
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/indian-spiced-chicken.webp
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
>> source: Gather and Garnish
|
>> source: Gather and Garnish
|
||||||
>> time required: 35 minutes
|
>> time required: 35 minutes
|
||||||
|
|||||||
|
After Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -1,3 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: Orzo with shrimp, tomato and marinated feta
|
||||||
|
description:
|
||||||
|
servings: 1
|
||||||
|
category: uncategorized
|
||||||
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/shrimp-feta.webp
|
||||||
|
---
|
||||||
|
|
||||||
>> title: Orzo with shrimp, tomato and marinated feta
|
>> title: Orzo with shrimp, tomato and marinated feta
|
||||||
>> source: https://thehappyfoodie.co.uk/recipes/ottolenghis-orzo-with-prawns-tomato-and-marinated-feta/
|
>> source: https://thehappyfoodie.co.uk/recipes/ottolenghis-orzo-with-prawns-tomato-and-marinated-feta/
|
||||||
>> servings: 4
|
>> servings: 4
|
||||||
|
|||||||