#!/usr/bin/env python3 """Scrape og:image from recipe source URLs, download images, update .cook files.""" import csv import os import re import sys import time from pathlib import Path import requests from bs4 import BeautifulSoup WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images") HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", } def get_source_url(filepath: Path) -> str | None: """Extract the source URL from a .cook file's YAML frontmatter.""" content = filepath.read_text(encoding="utf-8") match = re.search(r"^source:\s*(.+)$", content, re.MULTILINE) if not match: return None val = match.group(1).strip() # Only return actual URLs if val.startswith("http://") or val.startswith("https://"): return val return None def fetch_og_image(url: str, timeout: int = 20) -> str | None: """Fetch a page and extract og:image URL.""" try: resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True) resp.raise_for_status() except requests.RequestException as e: print(f" FETCH FAILED: {e}", file=sys.stderr) return None soup = BeautifulSoup(resp.text, "html.parser") # Try og:image meta tag for meta in soup.find_all("meta"): if meta.get("property") == "og:image" and meta.get("content"): return meta["content"] if meta.get("name") == "og:image" and meta.get("content"): return meta["content"] # Try twitter:image for meta in soup.find_all("meta"): if meta.get("name") == "twitter:image" and meta.get("content"): return meta["content"] # Try finding the first large image (common in recipe sites) for img in soup.find_all("img"): src = img.get("src") or "" if "logo" in src.lower() or "avatar" in src.lower() or "icon" in src.lower(): continue if src.startswith("http") and any(kw in src.lower() for kw in ["upload", "wp-content", "images"]): return src return None def download_image(url: str, dest: Path) -> bool: """Download an image from URL to destination path.""" try: resp = requests.get(url, headers=HEADERS, timeout=30, stream=True) resp.raise_for_status() dest.write_bytes(resp.content) return True except requests.RequestException as e: print(f" DOWNLOAD FAILED: {e}", file=sys.stderr) return False def add_image_field(filepath: Path, slug: str): """Add or update the image field in a .cook file's YAML frontmatter.""" url = f"https://roux.roo.lol/recipe-images/{slug}.webp" content = filepath.read_text(encoding="utf-8") # If image field already exists, replace it if re.search(r"^image:", content, re.MULTILINE): content = re.sub(r"^image:.*$", f"image: {url}", content, count=1, flags=re.MULTILINE) else: # Insert before closing --- content = re.sub(r"^---$", f"image: {url}\n---", content, count=1, flags=re.MULTILINE) filepath.write_text(content, encoding="utf-8") print(f" Updated image field: {filepath.name}", file=sys.stderr) def process_recipe(filepath: Path, slug: str) -> bool: """Process a single recipe: find image, download, update frontmatter.""" dest = WORKDIR / "recipe-images" / f"{slug}.webp" # Skip if already downloaded if dest.exists(): print(f"[{slug}] Image already exists, skipping download", file=sys.stderr) add_image_field(filepath, slug) return True # Try to get source URL and scrape image src_url = get_source_url(filepath) if src_url: print(f"[{slug}] Scraping {src_url}", file=sys.stderr) img_url = fetch_og_image(src_url) if img_url: print(f" Found: {img_url}", file=sys.stderr) if download_image(img_url, dest): print(f" Downloaded to {dest.name}", file=sys.stderr) add_image_field(filepath, slug) return True else: print(f" Download failed", file=sys.stderr) return False def main(): csv_file = WORKDIR / "docs" / "no-image-has-fm.csv" if not csv_file.exists(): print(f"CSV not found: {csv_file}", file=sys.stderr) sys.exit(1) recipes = [] with open(csv_file) as f: reader = csv.reader(f, delimiter="|") for row in reader: if len(row) >= 2: recipes.append((row[0].strip(), row[1].strip())) print(f"Processing {len(recipes)} recipes...", file=sys.stderr) success = [] failed = [] for filename, slug in recipes: filepath = WORKDIR / filename if not filepath.exists(): print(f"[{slug}] File not found: {filepath}", file=sys.stderr) failed.append(slug) continue ok = process_recipe(filepath, slug) if ok: success.append(slug) else: failed.append(slug) # Be polite to servers time.sleep(0.5) print(f"\n=== Results ===", file=sys.stderr) print(f"Success: {len(success)}", file=sys.stderr) print(f"Failed: {len(failed)}", file=sys.stderr) if failed: print(f"Failed slugs: {', '.join(failed)}", file=sys.stderr) # Write failed slugs to file for retry if failed: with open(WORKDIR / "docs" / "failed-images.csv", "w") as f: for slug in failed: f.write(f"{slug}\n") if __name__ == "__main__": main()