merge: feature/recipe-images into main
Add image metadata to all 78 .cook recipe files: - Download and relink 19 existing images to roux.roo.lol - Find and download images for 50 recipes with frontmatter - Add frontmatter and images for 9 recipes without frontmatter - All images converted to proper WebP format
@@ -16,6 +16,7 @@ tags:
|
|||||||
|
|
||||||
- french
|
- french
|
||||||
|
|
||||||
|
image: https://roux.roo.lol/recipe-images/alain-ducase-bread.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Apple Cider Vinegar Dressing
|
|||||||
description: This homemade apple cider vinaigrette is SO much better than any store bought dressing! Made with 7 simple ingredients, it's tangy, healthy, and easy to make.
|
description: This homemade apple cider vinaigrette is SO much better than any store bought dressing! Made with 7 simple ingredients, it's tangy, healthy, and easy to make.
|
||||||
servings: 6
|
servings: 6
|
||||||
source: https://www.loveandlemons.com/apple-cider-vinegar-dressing/
|
source: https://www.loveandlemons.com/apple-cider-vinegar-dressing/
|
||||||
image: https://kitchen.roo.lol/data/images/14663763-8d52-4e31-a4e8-36a7f3c6c229.webp
|
image: https://roux.roo.lol/recipe-images/apple-cider-vinegar-dressing.webp
|
||||||
thumbnail: https://kitchen.roo.lol/data/images/thumbnails/14663763-8d52-4e31-a4e8-36a7f3c6c229.webp
|
thumbnail: https://kitchen.roo.lol/data/images/thumbnails/14663763-8d52-4e31-a4e8-36a7f3c6c229.webp
|
||||||
date_created: 2024-11-03
|
date_created: 2024-11-03
|
||||||
date_modified: 2024-11-03
|
date_modified: 2024-11-03
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ tags:
|
|||||||
|
|
||||||
- pesto
|
- pesto
|
||||||
|
|
||||||
|
image: https://roux.roo.lol/recipe-images/arugula-pesto-chicken-with-pecans-roasted-butternut.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ tags:
|
|||||||
- arugula
|
- arugula
|
||||||
- pesto
|
- pesto
|
||||||
- wheat berries
|
- wheat berries
|
||||||
|
image: https://roux.roo.lol/recipe-images/arugula-pesto-wheat-berries-recipe.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Place the @cooked wheat berries{3%cups} in a large mixing bowl and set them aside so they can come up to room temperature if you've had them in the refrigerator.
|
Place the @cooked wheat berries{3%cups} in a large mixing bowl and set them aside so they can come up to room temperature if you've had them in the refrigerator.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ tags:
|
|||||||
- salad
|
- salad
|
||||||
description: A fresh spring asparagus salad with peas, radishes, feta, pine nuts, and herbs, tossed with lemon and olive oil and finished with creamy avocado dressing.
|
description: A fresh spring asparagus salad with peas, radishes, feta, pine nuts, and herbs, tossed with lemon and olive oil and finished with creamy avocado dressing.
|
||||||
source: Love and Lemons
|
source: Love and Lemons
|
||||||
|
image: https://roux.roo.lol/recipe-images/asparagus-salad.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Bring a large #pot of well-salted @water{} to a boil and prepare a #bowl of @ice water{}.
|
Bring a large #pot of well-salted @water{} to a boil and prepare a #bowl of @ice water{}.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ tags:
|
|||||||
- pasta
|
- pasta
|
||||||
- italian
|
- italian
|
||||||
- quick
|
- quick
|
||||||
|
image: https://roux.roo.lol/recipe-images/authentic-cacio-e-pepe-recipe.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a #pot, bring @water{} to a boil. Lightly salt the water when it comes to a boil.
|
In a #pot, bring @water{} to a boil. Lightly salt the water when it comes to a boil.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ tags:
|
|||||||
- butternut squash
|
- butternut squash
|
||||||
- chili
|
- chili
|
||||||
- turkey
|
- turkey
|
||||||
|
image: https://roux.roo.lol/recipe-images/butternut-turkey-chili-with-sweet-peppers-spiced-pepitas-queso-fresco.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Heat ~{1%Tbsp} @olive oil{} in a #pot over medium heat.
|
Heat ~{1%Tbsp} @olive oil{} in a #pot over medium heat.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ tags:
|
|||||||
- mexican
|
- mexican
|
||||||
- peppers
|
- peppers
|
||||||
- vegetarian
|
- vegetarian
|
||||||
|
image: https://roux.roo.lol/recipe-images/baked-chile-relleno-with-black-beans.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat the #oven to high broil with a rack near the broiler.
|
Preheat the #oven to high broil with a rack near the broiler.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ tags:
|
|||||||
- rice
|
- rice
|
||||||
- tofu
|
- tofu
|
||||||
- vegetarian
|
- vegetarian
|
||||||
|
image: https://roux.roo.lol/recipe-images/bang-bang-tofu.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Drain @tofu{1%lb}, pat dry, and place on a plate lined with several paper towels or a clean dish towel to dry for ~{15%minutes}.
|
Drain @tofu{1%lb}, pat dry, and place on a plate lined with several paper towels or a clean dish towel to dry for ~{15%minutes}.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ tags:
|
|||||||
- chicken
|
- chicken
|
||||||
- biscuits
|
- biscuits
|
||||||
- okra
|
- okra
|
||||||
|
image: https://roux.roo.lol/recipe-images/barbecue-chicken-with-crispy-pan-fried-okra-cheddar-biscuits.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat the #oven to 400°F and lightly grease a #baking sheet.
|
Preheat the #oven to 400°F and lightly grease a #baking sheet.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ tags:
|
|||||||
- avocado
|
- avocado
|
||||||
- summer
|
- summer
|
||||||
- healthy
|
- healthy
|
||||||
|
image: https://roux.roo.lol/recipe-images/black-bean-and-corn-salad.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a large #mixing bowl{}, whisk together the @lime juice{1/4%cup}, @avocado oil{3%tablespoons}, @garlic{1%clove}, grated, @cumin{1/2%teaspoon}, @sea salt{3/4%teaspoon}, and @black pepper{}.
|
In a large #mixing bowl{}, whisk together the @lime juice{1/4%cup}, @avocado oil{3%tablespoons}, @garlic{1%clove}, grated, @cumin{1/2%teaspoon}, @sea salt{3/4%teaspoon}, and @black pepper{}.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ tags: caesar salad,garnish-and-gather,seafood,shrimp and grits,southern
|
|||||||
source: Unknown
|
source: Unknown
|
||||||
description: Southern-style shrimp and grits with smoked gouda, served with a charred scallion kale caesar salad
|
description: Southern-style shrimp and grits with smoked gouda, served with a charred scallion kale caesar salad
|
||||||
prep_time: 25 minutes
|
prep_time: 25 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/blackened-shrimp-and-gouda-grits.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a #pot{}, bring 3 cups @water{} & @salt{1/2 teaspoon} to a boil.
|
In a #pot{}, bring 3 cups @water{} & @salt{1/2 teaspoon} to a boil.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 6
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://cooking.nytimes.com/recipes/1020094-tofu-makhani-indian-butter-tofu
|
source: https://cooking.nytimes.com/recipes/1020094-tofu-makhani-indian-butter-tofu
|
||||||
description: This recipe is a vegetarian riff on butter chicken, or murgh makhani, a classic Indian dish in which chicken simmers in a rich, dairy-laden tomato sauce that's seasoned with heady spices like turmeric, cumin, garam masala and cardamom. This weeknight-friendly version has a toned-down ingredient list, and the tofu doesn't require hours of marinating, which means you can get everything on the table in under an hour if you start the rice as the tomatoes are cooking down. Heavy cream gives this dish a rich finish, but full-fat coconut milk makes a fine substitute. Whatever you do, don't substitute anything for butter or ghee (sorry, vegans) — it's signature ingredient.
|
description: This recipe is a vegetarian riff on butter chicken, or murgh makhani, a classic Indian dish in which chicken simmers in a rich, dairy-laden tomato sauce that's seasoned with heady spices like turmeric, cumin, garam masala and cardamom. This weeknight-friendly version has a toned-down ingredient list, and the tofu doesn't require hours of marinating, which means you can get everything on the table in under an hour if you start the rice as the tomatoes are cooking down. Heavy cream gives this dish a rich finish, but full-fat coconut milk makes a fine substitute. Whatever you do, don't substitute anything for butter or ghee (sorry, vegans) — it's signature ingredient.
|
||||||
image: https://kitchen.roo.lol/data/images/99596aa7-0678-40ce-8020-f2ae9308be6a.webp
|
image: https://roux.roo.lol/recipe-images/butter-tofu.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a large #bowl{}, whisk @fresh lemon juice{2 tablespoons}, @ground cumin{1 tablespoon} and 1 heaping tablespoon @ground turmeric{} with 1 tablespoon @water{} to make a thin paste. Drain and pat @tofu{2 (14-ounce) packages, firm} dry then cut it into 1-inch cubes. Add @tofu{} to the @turmeric{} marinade, gently stirring to coat. Set aside at room temperature while you prepare the remaining ingredients, or cover and refrigerate for up to 1 day.
|
In a large #bowl{}, whisk @fresh lemon juice{2 tablespoons}, @ground cumin{1 tablespoon} and 1 heaping tablespoon @ground turmeric{} with 1 tablespoon @water{} to make a thin paste. Drain and pat @tofu{2 (14-ounce) packages, firm} dry then cut it into 1-inch cubes. Add @tofu{} to the @turmeric{} marinade, gently stirring to coat. Set aside at room temperature while you prepare the remaining ingredients, or cover and refrigerate for up to 1 day.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ tags:
|
|||||||
- butternut squash
|
- butternut squash
|
||||||
- chili
|
- chili
|
||||||
- turkey
|
- turkey
|
||||||
|
image: https://roux.roo.lol/recipe-images/butternut-turkey-chili-2.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Heat ~{1%Tbsp} @olive oil{} in a #pot over medium heat.
|
Heat ~{1%Tbsp} @olive oil{} in a #pot over medium heat.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Cabbage Salad
|
|||||||
servings: 6
|
servings: 6
|
||||||
category: Salad
|
category: Salad
|
||||||
source: https://www.loveandlemons.com/cabbage-salad/
|
source: https://www.loveandlemons.com/cabbage-salad/
|
||||||
|
image: https://roux.roo.lol/recipe-images/cabbage-salad.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a large bowl, toss together @green cabbage{4%cup}, @red cabbage{4%cup}, @red bell pepper{1%medium}, @carrots{1%cup}(julienned), @cilantro{1/2%cup}(chopped).
|
In a large bowl, toss together @green cabbage{4%cup}, @red cabbage{4%cup}, @red bell pepper{1%medium}, @carrots{1%cup}(julienned), @cilantro{1/2%cup}(chopped).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 4
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://therecipecritic.com/cajun-shrimp-and-sausage-vegetable-foil-packets/
|
source: https://therecipecritic.com/cajun-shrimp-and-sausage-vegetable-foil-packets/
|
||||||
description: Cajun Shrimp and Sausage Foil Packets are loaded with shrimp, sausage, summer vegetables and tossed in cajun spices. These are quick and easy and packed with big flavor!
|
description: Cajun Shrimp and Sausage Foil Packets are loaded with shrimp, sausage, summer vegetables and tossed in cajun spices. These are quick and easy and packed with big flavor!
|
||||||
image: https://kitchen.roo.lol/data/images/9f6c7d9f-c6d7-4ea3-bad7-dcffe1187cba.webp
|
image: https://roux.roo.lol/recipe-images/cajun-shrimp-and-sausage-vegetable-foil-packets.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a large #bowl{} add @shrimp{1 pound, large, peeled and deveined}, @sausage{14 ounce, pork or chicken, sliced}, @zucchini{2 medium sized, sliced}, @yellow squash{2 medium sized, sliced}, @asparagus{½ bunch, sliced into thirds}, @red bell pepper{2, chopped into chunks} and @salt{} and @pepper{}. Add @olive oil{3 Tablespoons} and @cajun seasoning{3 Tablespoons} and toss until coated.
|
In a large #bowl{} add @shrimp{1 pound, large, peeled and deveined}, @sausage{14 ounce, pork or chicken, sliced}, @zucchini{2 medium sized, sliced}, @yellow squash{2 medium sized, sliced}, @asparagus{½ bunch, sliced into thirds}, @red bell pepper{2, chopped into chunks} and @salt{} and @pepper{}. Add @olive oil{3 Tablespoons} and @cajun seasoning{3 Tablespoons} and toss until coated.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Cajun Spice Mix (for pecans, oysters, etc)
|
|||||||
description: A Leonard-family recipe for spice mix for seasoning Cajun Pecans or Oysters
|
description: A Leonard-family recipe for spice mix for seasoning Cajun Pecans or Oysters
|
||||||
servings: 9
|
servings: 9
|
||||||
source: Mealie
|
source: Mealie
|
||||||
image: https://kitchen.roo.lol/data/images/c0540c17-29a5-4fe3-b038-1b421037c6e8.webp
|
image: https://roux.roo.lol/recipe-images/cajun-spice-mix-for-pecans-oysters-etc.webp
|
||||||
thumbnail: https://kitchen.roo.lol/data/images/thumbnails/c0540c17-29a5-4fe3-b038-1b421037c6e8.webp
|
thumbnail: https://kitchen.roo.lol/data/images/thumbnails/c0540c17-29a5-4fe3-b038-1b421037c6e8.webp
|
||||||
date_created: 2024-11-03
|
date_created: 2024-11-03
|
||||||
date_modified: 2024-11-03
|
date_modified: 2024-11-03
|
||||||
|
|||||||
@@ -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{}.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ servings: 6
|
|||||||
category: side
|
category: side
|
||||||
source: OCR
|
source: OCR
|
||||||
tags: salad, indian, vegan, gluten-free, instant-pot, chickpea
|
tags: salad, indian, vegan, gluten-free, instant-pot, chickpea
|
||||||
|
image: https://roux.roo.lol/recipe-images/chana-chickpea-salad.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In the inner cooking pot of the #Instant Pot, combine the @chickpea{1%can}, @bay leaf{2}, and @water{4%cups}.
|
In the inner cooking pot of the #Instant Pot, combine the @chickpea{1%can}, @bay leaf{2}, and @water{4%cups}.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 6
|
|||||||
category: side
|
category: side
|
||||||
source: OCR
|
source: OCR
|
||||||
description: I think my mom lied to me about this chana salaad. She said it was junk food so I would eat it, but now that I'm older I know better. After all, it's chickpeas and vegetables! But I'm glad my mom said whatever it took at time to get me to eat it, because I love this salad. It is perfect for summer and is easy to make. Serve it as a side salad or as a main dish for a vegan meal. Adjust cayenne as desired for your heat preference. SERVES 6
|
description: I think my mom lied to me about this chana salaad. She said it was junk food so I would eat it, but now that I'm older I know better. After all, it's chickpeas and vegetables! But I'm glad my mom said whatever it took at time to get me to eat it, because I love this salad. It is perfect for summer and is easy to make. Serve it as a side salad or as a main dish for a vegan meal. Adjust cayenne as desired for your heat preference. SERVES 6
|
||||||
image: https://kitchen.roo.lol/data/images/bd956abd-bbc8-4b7c-87e1-8c7ca70f804d.webp
|
image: https://roux.roo.lol/recipe-images/chana-salad.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In inner cooking pot of #InstantPot, combine @chickpeas{1 can, 15-16 ounces}, @bay leaves{}, and @water{}.
|
In inner cooking pot of #InstantPot, combine @chickpeas{1 can, 15-16 ounces}, @bay leaves{}, and @water{}.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ source: garnish-and-gather
|
|||||||
description:
|
description:
|
||||||
prep_time: 35 minutesnutes
|
prep_time: 35 minutesnutes
|
||||||
totalTime: PT35M
|
totalTime: PT35M
|
||||||
|
image: https://roux.roo.lol/recipe-images/chicken-and-pumpkin-gnocchi-with-ricotta.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Remove any large stems from @spinach{2 1/2 cups} and roughly chop.
|
Remove any large stems from @spinach{2 1/2 cups} and roughly chop.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ source: Unknown
|
|||||||
description: A vegetarian curry with chickpeas, cauliflower, and coconut milk, served with spiced oil drizzle
|
description: A vegetarian curry with chickpeas, cauliflower, and coconut milk, served with spiced oil drizzle
|
||||||
prep_time: 20 minutes
|
prep_time: 20 minutes
|
||||||
cook_time: 20 minutes
|
cook_time: 20 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/chickpea-butter-curry.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a small #pot{}, bring 1 1/2 cups @water{} & @basmati rice{3/4 cup} to a boil, then cover, reduce heat to low, and cook ~{12-15 minutes} or until @rice{} is tender.
|
In a small #pot{}, bring 1 1/2 cups @water{} & @basmati rice{3/4 cup} to a boil, then cover, reduce heat to low, and cook ~{12-15 minutes} or until @rice{} is tender.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Chickpea Butter Curry
|
|||||||
servings: 2
|
servings: 2
|
||||||
category: Main
|
category: Main
|
||||||
source: Rebel Recipes
|
source: Rebel Recipes
|
||||||
|
image: https://roux.roo.lol/recipe-images/chickpea-butter-curry-2.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a small pot, bring 1 1/2 cups water and @basmati rice{3/4%cup} to a boil, then cover, reduce heat to low, and cook 12-15 minutes or until rice is tender.
|
In a small pot, bring 1 1/2 cups water and @basmati rice{3/4%cup} to a boil, then cover, reduce heat to low, and cook 12-15 minutes or until rice is tender.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ source: Unknown
|
|||||||
description: Shrimp in orange sauce with brussels sprouts and carrots served over scallion rice
|
description: Shrimp in orange sauce with brussels sprouts and carrots served over scallion rice
|
||||||
prep_time: 15 minutes
|
prep_time: 15 minutes
|
||||||
cook_time: 15 minutes
|
cook_time: 15 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/chinese-orange-shrimp.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a #pot{}, bring 1 1/2 cups @water{} & @salt{1/4 teaspoon} to a boil.
|
In a #pot{}, bring 1 1/2 cups @water{} & @salt{1/4 teaspoon} to a boil.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Chocolate Mousse Cake with Raspberries
|
|||||||
servings: 24
|
servings: 24
|
||||||
category: Desserts
|
category: Desserts
|
||||||
source: King Arthur Baking
|
source: King Arthur Baking
|
||||||
|
image: https://roux.roo.lol/recipe-images/chocolate-mousse-cake-with-raspberries.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat the oven to 325°F. Lightly grease two 8" round pans (or adjust for your pan mix) and line with parchment.
|
Preheat the oven to 325°F. Lightly grease two 8" round pans (or adjust for your pan mix) and line with parchment.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ source: Unknown
|
|||||||
description:
|
description:
|
||||||
prep_time: 20 minutes
|
prep_time: 20 minutes
|
||||||
totalTime: PT20M
|
totalTime: PT20M
|
||||||
|
image: https://roux.roo.lol/recipe-images/cilantro-pesto-shrimp-over-orzo.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Juice HALF of @lemon{1} and cut remainder into wedges.
|
Juice HALF of @lemon{1} and cut remainder into wedges.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Crispy Roasted Chickpeas (Garbanzo Beans)
|
|||||||
servings: 2
|
servings: 2
|
||||||
category: Snack
|
category: Snack
|
||||||
source: Unknown
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/crispy-roasted-chickpeas-garbanzo-beans.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat the oven to 400°F.
|
Preheat the oven to 400°F.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 4
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://cooking.nytimes.com/recipes/1021200-crispy-tofu-with-cashews-and-blistered-snap-peas?campaign_id=9&emc=edit_nn_20230415&instance_id=90280&nl=the-morning®i_id=123169868&segment_id=130511&te=1&user_id=c5863897ec3230a42bc044f493123ca2
|
source: https://cooking.nytimes.com/recipes/1021200-crispy-tofu-with-cashews-and-blistered-snap-peas?campaign_id=9&emc=edit_nn_20230415&instance_id=90280&nl=the-morning®i_id=123169868&segment_id=130511&te=1&user_id=c5863897ec3230a42bc044f493123ca2
|
||||||
description: A ginger and coconut milk reduction can coat pretty much anything that browns nicely on its own. Here, it's pieces of pan-seared tofu, but small morsels of chicken and pork will work just as well. The soy and teaspoons of molasses give sauce a little caramelization, and a little shine and gloss. For a fresh side, add some blistered snap peas, tossed with sliced scallions, a little mint and a splash of rice vinegar. Snow peas, green beans, broccoli or asparagus? If it's fresh and green, it'll work just fine.
|
description: A ginger and coconut milk reduction can coat pretty much anything that browns nicely on its own. Here, it's pieces of pan-seared tofu, but small morsels of chicken and pork will work just as well. The soy and teaspoons of molasses give sauce a little caramelization, and a little shine and gloss. For a fresh side, add some blistered snap peas, tossed with sliced scallions, a little mint and a splash of rice vinegar. Snow peas, green beans, broccoli or asparagus? If it's fresh and green, it'll work just fine.
|
||||||
image: https://kitchen.roo.lol/data/images/9fe7eb34-2b1c-4dba-88e8-eb4ba4abea8e.webp
|
image: https://roux.roo.lol/recipe-images/crispy-tofu-with-cashews-and-blistered-snap-peas.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Slice @tofu{1 (14-ounce) block, firm or extra-firm, drained} in half horizontally, and leave on #paper towels{} to dry any excess liquid.
|
Slice @tofu{1 (14-ounce) block, firm or extra-firm, drained} in half horizontally, and leave on #paper towels{} to dry any excess liquid.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Crispy Roasted Chickpeas (Garbanzo Beans)
|
|||||||
servings: 2
|
servings: 2
|
||||||
category: Snack
|
category: Snack
|
||||||
source: Unknown
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/crispy-roasted-chickpeas-garbanzo-beans-2.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat the oven to 400°F.
|
Preheat the oven to 400°F.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ tags:
|
|||||||
- soup
|
- soup
|
||||||
description: A slow-simmered dried fava bean soup with roasted tomatoes, garlic, and onion, finished with mint and cilantro and topped with a tangy quick-pickled guajillo chile mixture.
|
description: A slow-simmered dried fava bean soup with roasted tomatoes, garlic, and onion, finished with mint and cilantro and topped with a tangy quick-pickled guajillo chile mixture.
|
||||||
source: 101 Cookbooks (adapted from Rick Bayless’s Mexican Kitchen)
|
source: 101 Cookbooks (adapted from Rick Bayless’s Mexican Kitchen)
|
||||||
|
image: https://roux.roo.lol/recipe-images/dried-fava-soup.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Make sure the @hulled dried fava bean{1%lb} are clean and free of dirt, then rinse them well.
|
Make sure the @hulled dried fava bean{1%lb} are clean and free of dirt, then rinse them well.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: EASY LEMON ROSEMARY WHITE BEAN SOUP
|
|||||||
servings: 4
|
servings: 4
|
||||||
category: Soup
|
category: Soup
|
||||||
source: Simple Veganista
|
source: Simple Veganista
|
||||||
|
image: https://roux.roo.lol/recipe-images/easy-lemon-rosemary-white-bean-soup.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Saute @olive oil{2%tbsp} in a large pot. Add @onion{1%small}, @carrots{3%large} and @celery{2 stalks}; cook for 7 minutes until softened.
|
Saute @olive oil{2%tbsp} in a large pot. Add @onion{1%small}, @carrots{3%large} and @celery{2 stalks}; cook for 7 minutes until softened.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 6
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://www.loveandlemons.com/vegetarian-chili-recipe/
|
source: https://www.loveandlemons.com/vegetarian-chili-recipe/
|
||||||
description: You'll LOVE this easy vegetarian chili recipe! It's gluten-free, and without yogurt or sour cream on top, it's also vegan.
|
description: You'll LOVE this easy vegetarian chili recipe! It's gluten-free, and without yogurt or sour cream on top, it's also vegan.
|
||||||
image: https://kitchen.roo.lol/data/images/61187b69-c326-4ad4-aa03-f3eb7c0b1806.webp
|
image: https://roux.roo.lol/recipe-images/easy-vegetarian-chili.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Heat @extra-virgin olive oil{2 tablespoons} in a large #pot{} over medium heat. Add @yellow onion{1 small, chopped}, a few pinches of @salt{} and @pepper{}, and stir. Cook until @onion{} is translucent, ~{5 minutes}, then add @garlic{2 cloves, minced} and @red bell pepper{1, diced}. Stir and cook until soft, ~{5 to 8 minutes}, turning heat down as needed.
|
Heat @extra-virgin olive oil{2 tablespoons} in a large #pot{} over medium heat. Add @yellow onion{1 small, chopped}, a few pinches of @salt{} and @pepper{}, and stir. Cook until @onion{} is translucent, ~{5 minutes}, then add @garlic{2 cloves, minced} and @red bell pepper{1, diced}. Stir and cook until soft, ~{5 to 8 minutes}, turning heat down as needed.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: EASY LEMON ROSEMARY WHITE BEAN SOUP
|
|||||||
servings: 4
|
servings: 4
|
||||||
category: Soup
|
category: Soup
|
||||||
source: Simple Veganista
|
source: Simple Veganista
|
||||||
|
image: https://roux.roo.lol/recipe-images/easy-lemon-rosemary-white-bean-soup-2.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Saute @olive oil{2%tbsp} in a large pot. Add @onion{1%small}, @carrots{3%large} and @celery{2 stalks}; cook for 7 minutes until softened.
|
Saute @olive oil{2%tbsp} in a large pot. Add @onion{1%small}, @carrots{3%large} and @celery{2 stalks}; cook for 7 minutes until softened.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ source: Mealie
|
|||||||
description: This is basically spaghetti with a sauce made from scratch. The sauce is great and I think this whole recipe is really good especially considering the ingredients are so basic.
|
description: This is basically spaghetti with a sauce made from scratch. The sauce is great and I think this whole recipe is really good especially considering the ingredients are so basic.
|
||||||
|
|
||||||
I added red pepper to the recipe for extra kick.
|
I added red pepper to the recipe for extra kick.
|
||||||
image: https://kitchen.roo.lol/data/images/1a3fbf92-6d4b-4f74-adf3-0b004b5f021a.webp
|
image: https://roux.roo.lol/recipe-images/fettucine-with-spiced-cherry-tomato-sauce.webp
|
||||||
prep_time: 30 minutes
|
prep_time: 30 minutes
|
||||||
cook_time: 1 hour
|
cook_time: 1 hour
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ tags: bulgogi,garnish-and-gather,korean,lettuce wraps
|
|||||||
source: Unknown
|
source: Unknown
|
||||||
description:
|
description:
|
||||||
prep_time: 20 minutes
|
prep_time: 20 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/gaja-s-bulgogi-lettuce-wraps.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Pat @steak bulgogi{8 oz} dry.
|
Pat @steak bulgogi{8 oz} dry.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ tags: crispy rice,garnish-and-gather,gochugaru,korean,salmon
|
|||||||
source: Unknown
|
source: Unknown
|
||||||
description: Salmon with spicy honey sauce served with crispy rice, broccoli, and carrots
|
description: Salmon with spicy honey sauce served with crispy rice, broccoli, and carrots
|
||||||
prep_time: 25 minutes
|
prep_time: 25 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/gochugaru-honey-salmon.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a #pot{}, bring 1 1/2 cups @water{} to a boil for @rice{3/4 cup, long grain}.
|
In a #pot{}, bring 1 1/2 cups @water{} to a boil for @rice{3/4 cup, long grain}.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 6
|
|||||||
category: Thanksgiving
|
category: Thanksgiving
|
||||||
source: https://www.foodnetwork.com/recipes/patrick-and-gina-neely/green-beans-with-lemon-and-garlic-recipe-1938670
|
source: https://www.foodnetwork.com/recipes/patrick-and-gina-neely/green-beans-with-lemon-and-garlic-recipe-1938670
|
||||||
description: The Neelys' healthy Green Beans with Lemon and Garlic recipe from Food Network is the perfect, simple side for any occasion. Quickly blanch and finish on the skillet, these green beans will be on the table in less than 20 minutes.
|
description: The Neelys' healthy Green Beans with Lemon and Garlic recipe from Food Network is the perfect, simple side for any occasion. Quickly blanch and finish on the skillet, these green beans will be on the table in less than 20 minutes.
|
||||||
image: https://kitchen.roo.lol/data/images/704b15f5-0eda-4f7f-8762-1d8f36c2dea6.webp
|
image: https://roux.roo.lol/recipe-images/green-beans-with-lemon-and-garlic.webp
|
||||||
nutrition:
|
nutrition:
|
||||||
calories: 122 kcal
|
calories: 122 kcal
|
||||||
carbohydrate: 11g
|
carbohydrate: 11g
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ source: Unknown
|
|||||||
description:
|
description:
|
||||||
prep_time: 20 minutes
|
prep_time: 20 minutes
|
||||||
cook_time: 20 minutes
|
cook_time: 20 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/indian-spiced-chicken-2.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Bring 1 1/2 cups @water{} & @salt{1/4 tsp} to a boil in a #pot{}.
|
Bring 1 1/2 cups @water{} & @salt{1/4 tsp} to a boil in a #pot{}.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ source: Garnish & Gather — Chef Emily Golub
|
|||||||
description: Cozy kale-and-chestnut stew finished with cream and topped with a bright parsley–walnut pesto. Allergens: dairy, walnuts.
|
description: Cozy kale-and-chestnut stew finished with cream and topped with a bright parsley–walnut pesto. Allergens: dairy, walnuts.
|
||||||
prep_time: 20 minutes
|
prep_time: 20 minutes
|
||||||
cook_time: 35 minutes
|
cook_time: 35 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/kale-and-chestnut-stew.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Remove any large stems from @kale{3 c} and thinly slice into ribbons.
|
Remove any large stems from @kale{3 c} and thinly slice into ribbons.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ servings: 2
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://thekoreanvegan.com/spicy-crunchy-garlic-tofu-kkampoong-tofu/
|
source: https://thekoreanvegan.com/spicy-crunchy-garlic-tofu-kkampoong-tofu/
|
||||||
description:
|
description:
|
||||||
|
image: https://roux.roo.lol/recipe-images/kkanpoong-tofu.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
To a large #bowl{}, add a pinch of @salt{} and 2 tablespoons of @potato starch{} to @tofu{1 16 ounce block, extra firm, pressed and chopped into 1/2 inch bite sized chunks}. Gently toss @tofu{} to make sure they are all evenly coated. Set aside.
|
To a large #bowl{}, add a pinch of @salt{} and 2 tablespoons of @potato starch{} to @tofu{1 16 ounce block, extra firm, pressed and chopped into 1/2 inch bite sized chunks}. Gently toss @tofu{} to make sure they are all evenly coated. Set aside.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ source: Unknown
|
|||||||
description: Crispy shrimp tacos with gochujang sauce and bok choy rice salad
|
description: Crispy shrimp tacos with gochujang sauce and bok choy rice salad
|
||||||
prep_time: 25 minutes
|
prep_time: 25 minutes
|
||||||
cook_time: 25 minutes
|
cook_time: 25 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/korean-shrimp-tacos.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a #pot{}, bring 1 1/2 cups @water{} & @salt{1/4 teaspoon} to a boil.
|
In a #pot{}, bring 1 1/2 cups @water{} & @salt{1/4 teaspoon} to a boil.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ tags: chinese,garnish-and-gather,kung pao,shrimp
|
|||||||
source: Unknown
|
source: Unknown
|
||||||
description:
|
description:
|
||||||
prep_time: 25 minutes
|
prep_time: 25 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/kung-pao-shrimp-with-sichuan-roasted-green-beans.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat #oven{} to 425°.
|
Preheat #oven{} to 425°.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ category: Cookies
|
|||||||
tags: cookies
|
tags: cookies
|
||||||
source: Mealie
|
source: Mealie
|
||||||
description:
|
description:
|
||||||
|
image: https://roux.roo.lol/recipe-images/lactation-cookies.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat #oven{} to 350°.
|
Preheat #oven{} to 350°.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Lactation Cookies
|
|||||||
servings: 12
|
servings: 12
|
||||||
category: Cookies
|
category: Cookies
|
||||||
source: Mealie
|
source: Mealie
|
||||||
|
image: https://roux.roo.lol/recipe-images/lactation-cookies-2.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
@butter{1%cup}
|
@butter{1%cup}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 8
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://www.inspiredtaste.net/40400/lentil-soup/
|
source: https://www.inspiredtaste.net/40400/lentil-soup/
|
||||||
description: This is our favorite lentil soup recipe. I make this soup with lentils, carrots, turmeric, and fresh lemon whenever we are in need of some healthy comfort food. It's savory, satisfying, and utterly delicious. I like quite a bit of fresh lemon in this soup and actually serve soup with slices of lemon. This adds even more lemony flavor and shows that lemon was added to soup.
|
description: This is our favorite lentil soup recipe. I make this soup with lentils, carrots, turmeric, and fresh lemon whenever we are in need of some healthy comfort food. It's savory, satisfying, and utterly delicious. I like quite a bit of fresh lemon in this soup and actually serve soup with slices of lemon. This adds even more lemony flavor and shows that lemon was added to soup.
|
||||||
image: https://kitchen.roo.lol/data/images/47a68b65-c916-4c5d-86f2-ea989ea310a7.webp
|
image: https://roux.roo.lol/recipe-images/lentil-soup-with-lemon-and-turmeric.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Heat @olive oil{2 tablespoons} in a large #pot{} (like a #Dutch oven{}) over medium heat. Add @onions{2 cups, finely chopped (1 medium onion)}, @carrot{1 1/2 cups, chopped (2 large carrots)}, and @salt{1/4 teaspoon} of and cook, stirring occasionally until they are softened and starting to smell sweet, about ~{5 to 7 minutes}. Stir in @garlic{2 teaspoons, minced (3 cloves)}, @ground cumin{1 teaspoon}, @ground coriander{1/2 teaspoon}, @fresh ground black pepper{1/2 teaspoon}, and @ground turmeric{1/2 teaspoon} and cook for ~{30 seconds}.
|
Heat @olive oil{2 tablespoons} in a large #pot{} (like a #Dutch oven{}) over medium heat. Add @onions{2 cups, finely chopped (1 medium onion)}, @carrot{1 1/2 cups, chopped (2 large carrots)}, and @salt{1/4 teaspoon} of and cook, stirring occasionally until they are softened and starting to smell sweet, about ~{5 to 7 minutes}. Stir in @garlic{2 teaspoons, minced (3 cloves)}, @ground cumin{1 teaspoon}, @ground coriander{1/2 teaspoon}, @fresh ground black pepper{1/2 teaspoon}, and @ground turmeric{1/2 teaspoon} and cook for ~{30 seconds}.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Moroccan Spiced Lentil & Chickpea Soup
|
|||||||
servings: 1
|
servings: 1
|
||||||
category: Soup
|
category: Soup
|
||||||
source: Rebel Recipes
|
source: Rebel Recipes
|
||||||
|
image: https://roux.roo.lol/recipe-images/moroccan-spiced-lentil-chickpea-soup.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a large pan, add @olive oil{2%tbsp} and @onion{1%large}(chopped); cook on medium heat for 7-8 minutes until soft and browning.
|
In a large pan, add @olive oil{2%tbsp} and @onion{1%large}(chopped); cook on medium heat for 7-8 minutes until soft and browning.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Moroccan Spiced Lentil & Chickpea Soup
|
|||||||
servings: 1
|
servings: 1
|
||||||
category: Soup
|
category: Soup
|
||||||
source: Rebel Recipes
|
source: Rebel Recipes
|
||||||
|
image: https://roux.roo.lol/recipe-images/moroccan-spiced-lentil-and-chickpea-soup.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a large pan, add @olive oil{2%tbsp} and @onion{1%large}(chopped); cook on medium heat for 7-8 minutes until soft and browning.
|
In a large pan, add @olive oil{2%tbsp} and @onion{1%large}(chopped); cook on medium heat for 7-8 minutes until soft and browning.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ tags:
|
|||||||
- breakfast
|
- breakfast
|
||||||
- rolls
|
- rolls
|
||||||
- orange
|
- orange
|
||||||
|
image: https://roux.roo.lol/recipe-images/orange-rolls.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Make the dough: Heat the #oven to 375°F. Place @light brown sugar{1/3%cup} in a small #bowl and zest the @orange{1} directly over it. Gently rub the zest into the sugar and transfer 2 tablespoons/25 grams of the orange sugar to a large #bowl to use for the dough. Save the rest of the orange sugar for the filling.
|
Make the dough: Heat the #oven to 375°F. Place @light brown sugar{1/3%cup} in a small #bowl and zest the @orange{1} directly over it. Gently rub the zest into the sugar and transfer 2 tablespoons/25 grams of the orange sugar to a large #bowl to use for the dough. Save the rest of the orange sugar for the filling.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Palak Paneer
|
|||||||
servings: 4
|
servings: 4
|
||||||
category: Indian
|
category: Indian
|
||||||
source: Mealie
|
source: Mealie
|
||||||
|
image: https://roux.roo.lol/recipe-images/palak-paneer.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat the #Instant Pot by selecting Sauté and adjust to More for high heat. When the inner cooking pot is hot, add @peanut oil{2%tbsp} and heat until it is shimmering. Add @garlic cloves{5}(minced), @ginger{2%tsp}(minced), and @serrano or jalapeño chile{1}(minced), and sauté for ~{1%minute} or until fragrant.
|
Preheat the #Instant Pot by selecting Sauté and adjust to More for high heat. When the inner cooking pot is hot, add @peanut oil{2%tbsp} and heat until it is shimmering. Add @garlic cloves{5}(minced), @ginger{2%tsp}(minced), and @serrano or jalapeño chile{1}(minced), and sauté for ~{1%minute} or until fragrant.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 10
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://www.theflavorbender.com/make-passion-fruit-curd/#recipe
|
source: https://www.theflavorbender.com/make-passion-fruit-curd/#recipe
|
||||||
description: Makes about 1 1/4 cups of passion fruit curd
|
description: Makes about 1 1/4 cups of passion fruit curd
|
||||||
image: https://kitchen.roo.lol/data/images/803d123d-d98f-4a68-a820-e47c88b7a66a.webp
|
image: https://roux.roo.lol/recipe-images/passion-fruit-curd-recipe.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Indirect Heat Method
|
Indirect Heat Method
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ source: Unknown
|
|||||||
description: Pan-seared trout with pecan-brown butter sauce, served with herbed sweet potatoes, radishes, and wilted greens
|
description: Pan-seared trout with pecan-brown butter sauce, served with herbed sweet potatoes, radishes, and wilted greens
|
||||||
prep_time: 30 minutes
|
prep_time: 30 minutes
|
||||||
cook_time: 30 minutes
|
cook_time: 30 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/pecan-brown-butter-trout.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat #oven{} to 400°F.
|
Preheat #oven{} to 400°F.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 10
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://www.101cookbooks.com/ribollita-recipe/
|
source: https://www.101cookbooks.com/ribollita-recipe/
|
||||||
description: Ribollita, a beautifully thick Tuscan stew made with dark greens, lots of beans, vegetables, olive oil, and thickened with day-old bread. One of my favorites.
|
description: Ribollita, a beautifully thick Tuscan stew made with dark greens, lots of beans, vegetables, olive oil, and thickened with day-old bread. One of my favorites.
|
||||||
image: https://kitchen.roo.lol/data/images/4bc005e2-168b-43bb-a75e-a463eb5bbdc6.webp
|
image: https://roux.roo.lol/recipe-images/ribollita.webp
|
||||||
prep_time: 15 minutes
|
prep_time: 15 minutes
|
||||||
cook_timee:25 minutesinutes
|
cook_timee:25 minutesinutes
|
||||||
totalTime: PT40M
|
totalTime: PT40M
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ servings: 5
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://www.skinnytaste.com/roasted-pumpkin-sage-soup
|
source: https://www.skinnytaste.com/roasted-pumpkin-sage-soup
|
||||||
tags: soup, pumpkin, fall, vegetarian-option, gluten-free
|
tags: soup, pumpkin, fall, vegetarian-option, gluten-free
|
||||||
|
image: https://roux.roo.lol/recipe-images/roasted-pumpkin-sage.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Heat the oven to 400°F.
|
Heat the oven to 400°F.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Saag Paneer
|
|||||||
servings: 1
|
servings: 1
|
||||||
category: Indian
|
category: Indian
|
||||||
source: Mealie
|
source: Mealie
|
||||||
|
image: https://roux.roo.lol/recipe-images/saag-paneer.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat the #Instant Pot by selecting Sauté and adjust to More for high heat. When the inner cooking pot is hot, add @peanut oil{2%tbsp} and heat until it is shimmering. Add @garlic cloves{5}(minced), @ginger{2%tsp}(minced), and @serrano or jalapeno chile{1}(minced), and sauté for ~{1%minute} or until fragrant.
|
Preheat the #Instant Pot by selecting Sauté and adjust to More for high heat. When the inner cooking pot is hot, add @peanut oil{2%tbsp} and heat until it is shimmering. Add @garlic cloves{5}(minced), @ginger{2%tsp}(minced), and @serrano or jalapeno chile{1}(minced), and sauté for ~{1%minute} or until fragrant.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 4
|
|||||||
category: Thanksgiving
|
category: Thanksgiving
|
||||||
source: https://www.marthastewart.com/316639/sauteed-asparagus-with-lemon
|
source: https://www.marthastewart.com/316639/sauteed-asparagus-with-lemon
|
||||||
description:
|
description:
|
||||||
image: https://kitchen.roo.lol/data/images/15498ca3-db5d-461f-a3b7-ce6674fc6bea.webp
|
image: https://roux.roo.lol/recipe-images/sauteed-asparagus-with-lemon.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a large #skillet{}, heat @olive oil{1 tablespoon} over medium. Add @asparagus{2 bunches, 14 ounces each, ends trimmed}, and season with @coarse salt{} and @ground pepper{}. Cook, tossing occasionally, until lightly browned and crisp-tender, ~{10 to 15 minutes}, depending on thickness. Add @lemon zest{4 wide strips} and cook ~{1 minute}. Add @lemon juice{from 1/2 lemon}, toss to coat, and serve warm.
|
In a large #skillet{}, heat @olive oil{1 tablespoon} over medium. Add @asparagus{2 bunches, 14 ounces each, ends trimmed}, and season with @coarse salt{} and @ground pepper{}. Cook, tossing occasionally, until lightly browned and crisp-tender, ~{10 to 15 minutes}, depending on thickness. Add @lemon zest{4 wide strips} and cook ~{1 minute}. Add @lemon juice{from 1/2 lemon}, toss to coat, and serve warm.
|
||||||
@@ -4,7 +4,7 @@ servings: 6
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://acleanbake.com/slow-cooker-chicken-shawarma/
|
source: https://acleanbake.com/slow-cooker-chicken-shawarma/
|
||||||
description: Slow cooker chicken shawarma combines the ease of a slow cooker meal with the complex flavor of a slow-roasted middle eastern chicken feast! This is a quick version of middle eastern favorite: the slow cooker does a lot of the work and it can be prepped ahead of time. Various serving options make this sensational paleo and gluten free dinner recipe adaptable for low carb and Whole30 compliance, too!
|
description: Slow cooker chicken shawarma combines the ease of a slow cooker meal with the complex flavor of a slow-roasted middle eastern chicken feast! This is a quick version of middle eastern favorite: the slow cooker does a lot of the work and it can be prepped ahead of time. Various serving options make this sensational paleo and gluten free dinner recipe adaptable for low carb and Whole30 compliance, too!
|
||||||
image: https://kitchen.roo.lol/data/images/0cea2c65-f549-4f57-97fd-5a1d23941297.webp
|
image: https://roux.roo.lol/recipe-images/slow-cooker-chicken-shawarma.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Place @chicken breasts{2 lbs, boneless, skinless} in #bowl{} of your #slow cooker{}, and add @water{1/2 cup}. Cook on high ~{2 hours} until @chicken{} is cooked through (the juices run clear and @fat{} has rendered out of @chicken{}).
|
Place @chicken breasts{2 lbs, boneless, skinless} in #bowl{} of your #slow cooker{}, and add @water{1/2 cup}. Cook on high ~{2 hours} until @chicken{} is cooked through (the juices run clear and @fat{} has rendered out of @chicken{}).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 10
|
|||||||
category: Thanksgiving
|
category: Thanksgiving
|
||||||
source: https://cooking.nytimes.com/recipes/1020515-southern-macaroni-and-cheese
|
source: https://cooking.nytimes.com/recipes/1020515-southern-macaroni-and-cheese
|
||||||
description: There is macaroni and cheese, and then there is special occasion macaroni and cheese like this one. Unlike most recipes, which start with a roux, this one begins with a milk-and-egg base, which gives the dish an incredibly rich, silky taste. It's adapted from Millie Peartree, the owner of Millie Peartree Fish Fry & Soul Food restaurant in the Bronx, who has been making this dish since she was a little girl. The recipe was passed down in her family for generations, but because of the generous amount of cheese used, the dish was only made for events like Christmas and Thanksgiving. Extra-sharp Cheddar adds tartness and a layer of Colby Jack creates a gooey, molten center. If you can't find a Colby Jack blend, shredded mozzarella or a Mexican-style blend will work in its place.
|
description: There is macaroni and cheese, and then there is special occasion macaroni and cheese like this one. Unlike most recipes, which start with a roux, this one begins with a milk-and-egg base, which gives the dish an incredibly rich, silky taste. It's adapted from Millie Peartree, the owner of Millie Peartree Fish Fry & Soul Food restaurant in the Bronx, who has been making this dish since she was a little girl. The recipe was passed down in her family for generations, but because of the generous amount of cheese used, the dish was only made for events like Christmas and Thanksgiving. Extra-sharp Cheddar adds tartness and a layer of Colby Jack creates a gooey, molten center. If you can't find a Colby Jack blend, shredded mozzarella or a Mexican-style blend will work in its place.
|
||||||
image: https://kitchen.roo.lol/data/images/657048ab-342f-4ca3-84cb-fe9dcb3ae2a4.webp
|
image: https://roux.roo.lol/recipe-images/southern-macaroni-and-cheese.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Heat #oven{} to 350 degrees. Bring a large #pot{} of generously salted water to a boil. Add @elbow macaroni{1%pound} and cook according to package directions until a little under al dente, about ~{4%minutes}. Transfer to a #colander{} and rinse under cold water to stop cooking. Set aside.
|
Heat #oven{} to 350 degrees. Bring a large #pot{} of generously salted water to a boil. Add @elbow macaroni{1%pound} and cook according to package directions until a little under al dente, about ~{4%minutes}. Transfer to a #colander{} and rinse under cold water to stop cooking. Set aside.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ source: Unknown
|
|||||||
description:
|
description:
|
||||||
prep_time: 15 minutes
|
prep_time: 15 minutes
|
||||||
cook_time: 15 minutes
|
cook_time: 15 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/spaghetti-carbonara.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Trim root from @lettuce{1 head} and separate into leaves.
|
Trim root from @lettuce{1 head} and separate into leaves.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Spaghetti Squash w/ Chickpeas & Kale
|
|||||||
servings: 3
|
servings: 3
|
||||||
category: Main
|
category: Main
|
||||||
source: Love and Lemons
|
source: Love and Lemons
|
||||||
|
image: https://roux.roo.lol/recipe-images/spaghetti-squash-w-chickpeas-and-kale.webp
|
||||||
---
|
---
|
||||||
Preheat the oven to 400F.
|
Preheat the oven to 400F.
|
||||||
Prepare your squash following the directions in this post.
|
Prepare your squash following the directions in this post.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Skillet Spanakopita (Greek Spinach Feta Pie)
|
|||||||
servings: 8
|
servings: 8
|
||||||
category: Greek
|
category: Greek
|
||||||
source: From a Chef's Kitchen
|
source: From a Chef's Kitchen
|
||||||
|
image: https://roux.roo.lol/recipe-images/spanakopita.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Preheat the oven to 375 degrees.
|
Preheat the oven to 375 degrees.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ tags:
|
|||||||
- paella
|
- paella
|
||||||
- shrimp
|
- shrimp
|
||||||
- spanish
|
- spanish
|
||||||
|
image: https://roux.roo.lol/recipe-images/spicy-shrimp-paella.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
In a large sauté #pan, cook @chorizo{2%oz} over medium for ~{3-5%minutes}, until cooked through, then remove, keeping drippings in the pan.
|
In a large sauté #pan, cook @chorizo{2%oz} over medium for ~{3-5%minutes}, until cooked through, then remove, keeping drippings in the pan.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 12
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: https://www.bonappetit.com/recipe/strawberry-wild-rice-tartlets
|
source: https://www.bonappetit.com/recipe/strawberry-wild-rice-tartlets
|
||||||
description: This simple six-ingredient dessert is vegan and bursting with texture (crispy wild rice crust) and flavor (rehydrated cherries with fresh strawberries for the filling).
|
description: This simple six-ingredient dessert is vegan and bursting with texture (crispy wild rice crust) and flavor (rehydrated cherries with fresh strawberries for the filling).
|
||||||
image: https://kitchen.roo.lol/data/images/ed78da5c-1aa5-4d0b-90bc-4d317b7f7b89.webp
|
image: https://roux.roo.lol/recipe-images/strawberry-and-wild-rice-tartlets.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Heat #oven{} to 350°. Brush the cups of one standard 12-cup #muffin pan{} or two standard 6-cup #muffin pans{} with @vegetable oil{}. Bring 6 cups lightly salted @water{} to a boil in a medium #saucepan{}. Add @wild rice{1 cup}, cover, and cook until @water{} evaporates and grains split open, about ~{1½ hours}. Spread out @rice{} on a large rimmed #baking sheet{} and let cool. Reserve #pan{}.
|
Heat #oven{} to 350°. Brush the cups of one standard 12-cup #muffin pan{} or two standard 6-cup #muffin pans{} with @vegetable oil{}. Bring 6 cups lightly salted @water{} to a boil in a medium #saucepan{}. Add @wild rice{1 cup}, cover, and cook until @water{} evaporates and grains split open, about ~{1½ hours}. Spread out @rice{} on a large rimmed #baking sheet{} and let cool. Reserve #pan{}.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Thai Veggie Pad See Ew
|
|||||||
servings: 2
|
servings: 2
|
||||||
category: Thai
|
category: Thai
|
||||||
source: Unknown
|
source: Unknown
|
||||||
|
image: https://roux.roo.lol/recipe-images/thai-veggie-pad-see-ew.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Bring water to a boil for rice noodles.
|
Bring water to a boil for rice noodles.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ servings: 1
|
|||||||
category: uncategorized
|
category: uncategorized
|
||||||
source: Unknown
|
source: Unknown
|
||||||
description: Georgie Grasso recipe - GGBO winner 2024 with this recipe featured!
|
description: Georgie Grasso recipe - GGBO winner 2024 with this recipe featured!
|
||||||
image: https://kitchen.roo.lol/data/images/d15cdb2C-71db-42d7-a603-a29389e13bd3.webp
|
image: https://roux.roo.lol/recipe-images/tiramisu.webp
|
||||||
prep_time: 15 minutes
|
prep_time: 15 minutes
|
||||||
cook_time: 30 minutes
|
cook_time: 30 minutes
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ title: Warm Up With a Quick Lemony Chicken Soup
|
|||||||
servings: 6
|
servings: 6
|
||||||
category: Soup
|
category: Soup
|
||||||
source: EatingWell
|
source: EatingWell
|
||||||
|
image: https://roux.roo.lol/recipe-images/warm-up-with-a-quick-lemony-chicken-soup.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Heat @olive oil{1%tbsp} in a Dutch oven over medium-high. Add @carrots{3/4%cup} and @yellow onion{1/2%cup}; cook, stirring often, until vegetables are softened, 3 to 4 minutes. Add @garlic cloves{2}(minced) and @crushed red pepper flakes{3/4%tsp}, and cook, stirring constantly, until fragrant, about 1 minute.
|
Heat @olive oil{1%tbsp} in a Dutch oven over medium-high. Add @carrots{3/4%cup} and @yellow onion{1/2%cup}; cook, stirring often, until vegetables are softened, 3 to 4 minutes. Add @garlic cloves{2}(minced) and @crushed red pepper flakes{3/4%tsp}, and cook, stirring constantly, until fragrant, about 1 minute.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ source: Unknown
|
|||||||
description:
|
description:
|
||||||
prep_time: 25 minutes
|
prep_time: 25 minutes
|
||||||
cook_time: 25 minutes
|
cook_time: 25 minutes
|
||||||
|
image: https://roux.roo.lol/recipe-images/west-african-peanut-chicken.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
Mince @fresno pepper{1}—remove seeds and use only half for less heat.
|
Mince @fresno pepper{1}—remove seeds and use only half for less heat.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Wheat Berry Salad
|
|||||||
servings: 1
|
servings: 1
|
||||||
description: This healthy wheat berry salad is a delicious fall side dish or lunch with dried cranberries, fresh herbs, walnuts, and a tangy vinaigrette.
|
description: This healthy wheat berry salad is a delicious fall side dish or lunch with dried cranberries, fresh herbs, walnuts, and a tangy vinaigrette.
|
||||||
source: https://www.loveandlemons.com/wheat-berry-salad/
|
source: https://www.loveandlemons.com/wheat-berry-salad/
|
||||||
image: https://kitchen.roo.lol/data/images/eb72067c-28c7-4ae3-88ed-1fc0b9aba762.webp
|
image: https://roux.roo.lol/recipe-images/wheat-berry-salad.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
= Wheat berries =
|
= Wheat berries =
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
while IFS='|' read -r file slug; do
|
||||||
|
url=$(grep "^image:" "$file" | sed 's/^image:[[:space:]]*//')
|
||||||
|
echo "[$slug] Downloading from $url"
|
||||||
|
curl -sL -o "recipe-images/$slug.webp" "$url" || echo "[$slug] DOWNLOAD FAILED"
|
||||||
|
done
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
slug="$1"
|
||||||
|
url="$2"
|
||||||
|
|
||||||
|
echo "[$slug] Fetching $url" >&2
|
||||||
|
html=$(curl -sL --max-time 15 "$url" 2>/dev/null) || { echo "[$slug] FETCH FAILED" >&2; echo ""; exit 0; }
|
||||||
|
|
||||||
|
# Try multiple og:image patterns
|
||||||
|
img_url=$(echo "$html" | grep -oP 'property="og:image"\s+content="[^"]*"' | sed 's/.*content="//;s/"//' | head -1)
|
||||||
|
|
||||||
|
if [ -z "$img_url" ]; then
|
||||||
|
img_url=$(echo "$html" | grep -oP 'name="og:image"\s+content="[^"]*"' | sed 's/.*content="//;s/"//' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$img_url" ]; then
|
||||||
|
img_url=$(echo "$html" | grep -oP 'content="[^"]*"[^>]*property="og:image"' | sed 's/.*content="//;s/".*//' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$img_url" ]; then
|
||||||
|
echo "[$slug] Found: $img_url" >&2
|
||||||
|
echo "$img_url"
|
||||||
|
else
|
||||||
|
echo "[$slug] No og:image found" >&2
|
||||||
|
fi
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Final attempt: search DDG text, scrape recipe pages for actual images."""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import 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 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||||
|
}
|
||||||
|
SKIP_DOMAINS = {"pinterest", "facebook", "instagram", "twitter", "youtube", "tiktok"}
|
||||||
|
MIN_IMAGE_SIZE = 10000 # 10KB minimum for a recipe photo
|
||||||
|
|
||||||
|
|
||||||
|
def search_url(query: str) -> str | None:
|
||||||
|
"""Search and return first relevant recipe page URL."""
|
||||||
|
try:
|
||||||
|
results = list(DDGS().text(query, max_results=8))
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Search error: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
url = r.get("href", "")
|
||||||
|
domain = url.split("/")[2] if "//" in url else ""
|
||||||
|
if any(s in domain for s in SKIP_DOMAINS):
|
||||||
|
continue
|
||||||
|
# Prefer recipe sites
|
||||||
|
if any(kw in domain for kw in ["recipe", "cook", "food", "kitchen", "nytimes", "loveandlemons"]):
|
||||||
|
return url
|
||||||
|
|
||||||
|
# Return first non-wikipedia result
|
||||||
|
for r in results:
|
||||||
|
url = r.get("href", "")
|
||||||
|
if "wikipedia.org" not in url:
|
||||||
|
return url
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_recipe_images(url: str) -> list[str]:
|
||||||
|
"""Fetch a page and find actual recipe/food images."""
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, headers=HEADERS, timeout=15, allow_redirects=True)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except requests.RequestException:
|
||||||
|
return []
|
||||||
|
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
images = []
|
||||||
|
|
||||||
|
# 1. Check JSON-LD for recipe image
|
||||||
|
for script in soup.find_all("script", type="application/ld+json"):
|
||||||
|
try:
|
||||||
|
data = json.loads(script.string)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
continue
|
||||||
|
# Handle list or single
|
||||||
|
items = data if isinstance(data, list) else [data]
|
||||||
|
for item in items:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
if item.get("@type") == "Recipe":
|
||||||
|
img = item.get("image")
|
||||||
|
if isinstance(img, str):
|
||||||
|
images.append(img)
|
||||||
|
elif isinstance(img, list):
|
||||||
|
for i in img:
|
||||||
|
if isinstance(i, str):
|
||||||
|
images.append(i)
|
||||||
|
elif isinstance(i, dict):
|
||||||
|
u = i.get("contentUrl") or i.get("url", "")
|
||||||
|
if u:
|
||||||
|
images.append(u)
|
||||||
|
elif isinstance(img, dict):
|
||||||
|
u = img.get("contentUrl") or img.get("url", "")
|
||||||
|
if u:
|
||||||
|
images.append(u)
|
||||||
|
|
||||||
|
# 2. Check og:image
|
||||||
|
for meta in soup.find_all("meta"):
|
||||||
|
content = meta.get("content", "")
|
||||||
|
if (meta.get("property") == "og:image" or meta.get("name") == "og:image" or meta.get("name") == "twitter:image"):
|
||||||
|
if content and "logo" not in content.lower():
|
||||||
|
images.append(content)
|
||||||
|
|
||||||
|
# 3. Check all img tags for large, relevant images
|
||||||
|
for img in soup.find_all("img"):
|
||||||
|
src = img.get("src") or img.get("data-src") or ""
|
||||||
|
if not src.startswith("http"):
|
||||||
|
continue
|
||||||
|
src_lower = src.lower()
|
||||||
|
if any(kw in src_lower for kw in ["logo", "avatar", "icon", "banner", "pixel", "spacer", "thumb"]):
|
||||||
|
continue
|
||||||
|
# Check for recipe-related class/id
|
||||||
|
parent_classes = ""
|
||||||
|
parent = img.parent
|
||||||
|
if parent:
|
||||||
|
parent_classes = " ".join(parent.get("class", []) or []) + " " + (parent.get("id") or "")
|
||||||
|
if any(kw in parent_classes.lower() for kw in ["logo", "nav", "header", "footer", "menu", "icon"]):
|
||||||
|
continue
|
||||||
|
images.append(src)
|
||||||
|
|
||||||
|
# Deduplicate
|
||||||
|
seen = set()
|
||||||
|
unique = []
|
||||||
|
for img in images:
|
||||||
|
if img not in seen:
|
||||||
|
seen.add(img)
|
||||||
|
unique.append(img)
|
||||||
|
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def download_image(url: str, dest: Path) -> bool:
|
||||||
|
"""Download image, validate it's a real image."""
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, headers=HEADERS, timeout=30, stream=True)
|
||||||
|
resp.raise_for_status()
|
||||||
|
ct = resp.headers.get("content-type", "")
|
||||||
|
if not ct.startswith("image/"):
|
||||||
|
return False
|
||||||
|
content = resp.content
|
||||||
|
if len(content) < MIN_IMAGE_SIZE:
|
||||||
|
return False
|
||||||
|
dest.write_bytes(content)
|
||||||
|
return True
|
||||||
|
except requests.RequestException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_title(filepath: Path) -> str:
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
m = re.search(r"^title:\s*(.+)$", content, re.MULTILINE)
|
||||||
|
if m:
|
||||||
|
return m.group(1).strip()
|
||||||
|
return filepath.stem.replace("_", " ").replace("-", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def add_image_field(filepath: Path, slug: str):
|
||||||
|
url = f"https://roux.roo.lol/recipe-images/{slug}.webp"
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
if re.search(r"^image:", content, re.MULTILINE):
|
||||||
|
content = re.sub(r"^image:.*$", f"image: {url}", count=1, flags=re.MULTILINE, string=content)
|
||||||
|
else:
|
||||||
|
content = re.sub(r"^---$", f"image: {url}\n---", count=1, flags=re.MULTILINE, string=content)
|
||||||
|
filepath.write_text(content, encoding="utf-8")
|
||||||
|
print(f" Updated {filepath.name}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def get_source_url(filepath: Path) -> str | None:
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
m = re.search(r"^source:\s*(.+)$", content, re.MULTILINE)
|
||||||
|
if m:
|
||||||
|
val = m.group(1).strip()
|
||||||
|
if val.startswith("http"):
|
||||||
|
return val
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
csv_file = WORKDIR / "docs" / "no-image-has-fm.csv"
|
||||||
|
|
||||||
|
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()))
|
||||||
|
|
||||||
|
success = []
|
||||||
|
failed = []
|
||||||
|
|
||||||
|
for filename, slug in recipes:
|
||||||
|
filepath = WORKDIR / filename
|
||||||
|
if not filepath.exists():
|
||||||
|
failed.append(slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
|
||||||
|
|
||||||
|
# Skip if already has image AND file exists
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
has_field = bool(re.search(r"^image:", content, re.MULTILINE))
|
||||||
|
has_file = dest.exists()
|
||||||
|
if has_field and has_file and dest.stat().st_size > 10000:
|
||||||
|
print(f"[{slug}] Already done, skipping", file=sys.stderr)
|
||||||
|
success.append(slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = get_title(filepath)
|
||||||
|
query = f"{title} recipe"
|
||||||
|
|
||||||
|
print(f"[{slug}] Searching: {query}", file=sys.stderr)
|
||||||
|
page_url = search_url(query)
|
||||||
|
if not page_url:
|
||||||
|
print(f" No search results", file=sys.stderr)
|
||||||
|
failed.append(slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f" Page: {page_url}", file=sys.stderr)
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
img_urls = find_recipe_images(page_url)
|
||||||
|
if not img_urls:
|
||||||
|
print(f" No images found on page", file=sys.stderr)
|
||||||
|
failed.append(slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
downloaded = False
|
||||||
|
for img_url in img_urls[:5]:
|
||||||
|
print(f" Trying: {img_url[:80]}...", file=sys.stderr)
|
||||||
|
if download_image(img_url, dest):
|
||||||
|
print(f" Downloaded ({dest.stat().st_size} bytes)", file=sys.stderr)
|
||||||
|
add_image_field(filepath, slug)
|
||||||
|
downloaded = True
|
||||||
|
break
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
if downloaded:
|
||||||
|
success.append(slug)
|
||||||
|
else:
|
||||||
|
failed.append(slug)
|
||||||
|
|
||||||
|
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: {', '.join(failed)}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find images for remaining 6 recipes."""
|
||||||
|
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"}
|
||||||
|
|
||||||
|
recipes = [
|
||||||
|
("Arugula Pesto Chicken with Pecans & Roasted Butternut.cook", "arugula-pesto-chicken-with-pecans-roasted-butternut", "Arugula Pesto Chicken Pecans Roasted Butternut recipe garnishandgather"),
|
||||||
|
("Bang Bang Tofu.cook", "bang-bang-tofu", "Bang Bang Tofu recipe"),
|
||||||
|
("Chana Chickpea Salad.cook", "chana-chickpea-salad", "Chana Chickpea Salad recipe"),
|
||||||
|
("Gaja's Bulgogi Lettuce Wraps.cook", "gaja-s-bulgogi-lettuce-wraps", "Gaja Bulgogi Lettuce Wraps recipe garnishandgather"),
|
||||||
|
("Pecan-Brown Butter Trout.cook", "pecan-brown-butter-trout", "Pecan Brown Butter Trout recipe"),
|
||||||
|
("West African Peanut Chicken.cook", "west-african-peanut-chicken", "West African Peanut Chicken recipe"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for file, slug, query in recipes:
|
||||||
|
fp = WORKDIR / file
|
||||||
|
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
|
||||||
|
if dest.exists() and dest.stat().st_size > 10000:
|
||||||
|
print(f"[{slug}] Already done")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"[{slug}] Searching: {query}")
|
||||||
|
try:
|
||||||
|
results = list(DDGS().text(query, max_results=5))
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Search error: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for r in results:
|
||||||
|
url = r.get("href", "")
|
||||||
|
if not url or any(s in url for s in ["pinterest","facebook","instagram"]):
|
||||||
|
continue
|
||||||
|
print(f" Fetching: {url}")
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, headers=HEADERS, timeout=15)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Failed: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
img_urls = []
|
||||||
|
|
||||||
|
# og:image
|
||||||
|
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 and "logo" not in c.lower():
|
||||||
|
img_urls.append(c)
|
||||||
|
|
||||||
|
# img tags
|
||||||
|
for img in soup.find_all("img"):
|
||||||
|
src = img.get("src") or img.get("data-src") or ""
|
||||||
|
if src.startswith("http") and "logo" not in src.lower() and "icon" not in src.lower():
|
||||||
|
img_urls.append(src)
|
||||||
|
|
||||||
|
for img_url in img_urls[:5]:
|
||||||
|
try:
|
||||||
|
r2 = requests.get(img_url, headers=HEADERS, timeout=15)
|
||||||
|
r2.raise_for_status()
|
||||||
|
ct = r2.headers.get("content-type", "")
|
||||||
|
if ct.startswith("image/") and len(r2.content) > 10000:
|
||||||
|
dest.write_bytes(r2.content)
|
||||||
|
print(f" Downloaded ({len(r2.content)} bytes): {img_url[:60]}")
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if found:
|
||||||
|
break
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
if found:
|
||||||
|
# Add image field
|
||||||
|
url_val = f"https://roux.roo.lol/recipe-images/{slug}.webp"
|
||||||
|
content = fp.read_text(encoding="utf-8")
|
||||||
|
if re.search(r"^image:", content, re.MULTILINE):
|
||||||
|
content = re.sub(r"^image:.*$", f"image: {url_val}", count=1, flags=re.MULTILINE, string=content)
|
||||||
|
else:
|
||||||
|
content = re.sub(r"^---$", f"image: {url_val}\n---", count=1, flags=re.MULTILINE, string=content)
|
||||||
|
fp.write_text(content, encoding="utf-8")
|
||||||
|
print(f" Updated image field")
|
||||||
|
else:
|
||||||
|
print(f" FAILED")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("\nDone!")
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fix the broken YAML frontmatter: move image field inside YAML, and convert non-WebP images."""
|
||||||
|
|
||||||
|
import re, subprocess, sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
|
||||||
|
|
||||||
|
def fix_frontmatter(content: str) -> str:
|
||||||
|
"""Move image: field inside the YAML block if it's outside."""
|
||||||
|
# Check if image: is before the opening ---
|
||||||
|
lines = content.split('\n')
|
||||||
|
|
||||||
|
# Find the first --- line
|
||||||
|
first_hr = -1
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip() == '---':
|
||||||
|
first_hr = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if first_hr <= 0:
|
||||||
|
return content # No frontmatter or image is at first line
|
||||||
|
|
||||||
|
# Check if the line just before the first --- has image:
|
||||||
|
if first_hr > 0 and lines[first_hr - 1].startswith('image:'):
|
||||||
|
# Extract the image line
|
||||||
|
img_line = lines[first_hr - 1]
|
||||||
|
# Remove it from before ---
|
||||||
|
del lines[first_hr - 1]
|
||||||
|
# Now first_hr is one index less (or same if we deleted the line before)
|
||||||
|
# Find the closing ---
|
||||||
|
closing_hr = -1
|
||||||
|
for i in range(first_hr, len(lines)):
|
||||||
|
if lines[i].strip() == '---':
|
||||||
|
closing_hr = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if closing_hr > first_hr:
|
||||||
|
# Insert image line before the closing ---
|
||||||
|
lines.insert(closing_hr, img_line)
|
||||||
|
else:
|
||||||
|
# Insert after the closing --- (fallback)
|
||||||
|
lines.insert(first_hr + 1, img_line)
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_all_frontmatter():
|
||||||
|
"""Fix frontmatter for all .cook files."""
|
||||||
|
fixed = 0
|
||||||
|
for f in sorted(WORKDIR.glob("*.cook")):
|
||||||
|
content = f.read_text(encoding="utf-8")
|
||||||
|
fixed_content = fix_frontmatter(content)
|
||||||
|
if fixed_content != content:
|
||||||
|
f.write_text(fixed_content, encoding="utf-8")
|
||||||
|
print(f" Fixed: {f.name}")
|
||||||
|
fixed += 1
|
||||||
|
print(f"\nFixed {fixed} files with mis-placed image fields")
|
||||||
|
return fixed
|
||||||
|
|
||||||
|
|
||||||
|
def convert_images():
|
||||||
|
"""Convert non-WebP images to actual WebP format."""
|
||||||
|
converted = 0
|
||||||
|
for img in sorted(WORKDIR.glob("recipe-images/*.webp")):
|
||||||
|
mime = subprocess.run(
|
||||||
|
["file", "-b", "--mime-type", str(img)],
|
||||||
|
capture_output=True, text=True
|
||||||
|
).stdout.strip()
|
||||||
|
|
||||||
|
if mime != "image/webp":
|
||||||
|
print(f" Converting {img.name} (mime: {mime})")
|
||||||
|
# Use temp file to avoid corruption
|
||||||
|
tmp = img.with_suffix(".webp.tmp")
|
||||||
|
result = subprocess.run(
|
||||||
|
["convert", str(img), str(tmp)],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0 and tmp.exists():
|
||||||
|
tmp.replace(img)
|
||||||
|
print(f" Converted successfully")
|
||||||
|
converted += 1
|
||||||
|
else:
|
||||||
|
print(f" Convert failed: {result.stderr}")
|
||||||
|
|
||||||
|
print(f"\nConverted {converted} images to proper WebP")
|
||||||
|
return converted
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=== Fixing YAML frontmatter ===")
|
||||||
|
fix_all_frontmatter()
|
||||||
|
|
||||||
|
print("\n=== Converting image formats ===")
|
||||||
|
convert_images()
|
||||||
|
|
||||||
|
# Final check
|
||||||
|
print("\n=== Final verification ===")
|
||||||
|
bad = 0
|
||||||
|
for f in sorted(WORKDIR.glob("*.cook")):
|
||||||
|
content = f.read_text(encoding="utf-8")
|
||||||
|
# Check if image: is between --- markers
|
||||||
|
if "image:" in content:
|
||||||
|
# Find the opening ---
|
||||||
|
parts = content.split('---', 2)
|
||||||
|
if len(parts) >= 3:
|
||||||
|
if "image:" not in parts[1]:
|
||||||
|
print(f" STILL BROKEN: {f.name}")
|
||||||
|
bad += 1
|
||||||
|
|
||||||
|
if bad == 0:
|
||||||
|
print(" All files have image: inside YAML block ✅")
|
||||||
|
|
||||||
|
webp_count = 0
|
||||||
|
non_webp_count = 0
|
||||||
|
for img in sorted(WORKDIR.glob("recipe-images/*.webp")):
|
||||||
|
mime = subprocess.run(
|
||||||
|
["file", "-b", "--mime-type", str(img)],
|
||||||
|
capture_output=True, text=True
|
||||||
|
).stdout.strip()
|
||||||
|
if mime == "image/webp":
|
||||||
|
webp_count += 1
|
||||||
|
else:
|
||||||
|
non_webp_count += 1
|
||||||
|
if non_webp_count <= 5:
|
||||||
|
print(f" NOT WEBP: {img.name} ({mime})")
|
||||||
|
|
||||||
|
print(f" WebP images: {webp_count}, Non-WebP: {non_webp_count}")
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#!/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()
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Search the web for recipe images, download them, and update .cook files.
|
||||||
|
|
||||||
|
Uses DuckDuckGo search to find recipe pages, then scrapes og:image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from duckduckgo_search import DDGS
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_og_image(url: str) -> str | None:
|
||||||
|
"""Fetch a page and extract og:image URL."""
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, headers=HEADERS, timeout=15, allow_redirects=True)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except requests.RequestException:
|
||||||
|
return None
|
||||||
|
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
|
||||||
|
# Try og:image meta tag (property or name)
|
||||||
|
for meta in soup.find_all("meta"):
|
||||||
|
prop = (meta.get("property") or "").lower()
|
||||||
|
name = (meta.get("name") or "").lower()
|
||||||
|
content = meta.get("content", "")
|
||||||
|
if "og:image" in prop and content:
|
||||||
|
return content
|
||||||
|
if "og:image" in name and content:
|
||||||
|
return content
|
||||||
|
if "twitter:image" in name and content:
|
||||||
|
return content
|
||||||
|
|
||||||
|
# Try schema.org JSON-LD
|
||||||
|
for script in soup.find_all("script", type="application/ld+json"):
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
data = json.loads(script.string)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
img = data.get("image")
|
||||||
|
if isinstance(img, str) and img.startswith("http"):
|
||||||
|
return img
|
||||||
|
if isinstance(img, dict):
|
||||||
|
url = img.get("contentUrl") or img.get("url", "")
|
||||||
|
if url.startswith("http"):
|
||||||
|
return url
|
||||||
|
if isinstance(img, list) and img:
|
||||||
|
first = img[0]
|
||||||
|
if isinstance(first, str):
|
||||||
|
return first
|
||||||
|
if isinstance(first, dict):
|
||||||
|
url = first.get("contentUrl") or first.get("url", "")
|
||||||
|
if url.startswith("http"):
|
||||||
|
return url
|
||||||
|
except (json.JSONDecodeError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def search_and_find_image(query: str) -> str | None:
|
||||||
|
"""Search DuckDuckGo for a recipe and find og:image."""
|
||||||
|
try:
|
||||||
|
results = list(DDGS().text(query, max_results=5))
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Search error: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
url = r.get("href", "")
|
||||||
|
if not url or "pinterest" in url or "facebook" in url or "instagram" in url:
|
||||||
|
continue
|
||||||
|
print(f" Trying: {url}", file=sys.stderr)
|
||||||
|
img = fetch_og_image(url)
|
||||||
|
if img:
|
||||||
|
print(f" Found image: {img}", file=sys.stderr)
|
||||||
|
return img
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
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 re.search(r"^image:", content, re.MULTILINE):
|
||||||
|
content = re.sub(r"^image:.*$", f"image: {url}", count=1, flags=re.MULTILINE, string=content)
|
||||||
|
else:
|
||||||
|
content = re.sub(r"^---$", f"image: {url}\n---", count=1, flags=re.MULTILINE, string=content)
|
||||||
|
|
||||||
|
filepath.write_text(content, encoding="utf-8")
|
||||||
|
print(f" Updated {filepath.name}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def get_source_url(filepath: Path) -> str | None:
|
||||||
|
"""Extract source URL from .cook frontmatter."""
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
match = re.search(r"^source:\s*(.+)$", content, re.MULTILINE)
|
||||||
|
if match:
|
||||||
|
val = match.group(1).strip()
|
||||||
|
if val.startswith("http://") or val.startswith("https://"):
|
||||||
|
return val
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def process_recipe(filename: str, slug: str) -> bool:
|
||||||
|
"""Try to find and download an image for a recipe."""
|
||||||
|
filepath = WORKDIR / filename
|
||||||
|
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
|
||||||
|
|
||||||
|
if dest.exists():
|
||||||
|
print(f"[{slug}] Already exists", file=sys.stderr)
|
||||||
|
add_image_field(filepath, slug)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Strategy 1: Try source URL if available
|
||||||
|
src_url = get_source_url(filepath)
|
||||||
|
if src_url:
|
||||||
|
print(f"[{slug}] Trying source URL: {src_url}", file=sys.stderr)
|
||||||
|
img = fetch_og_image(src_url)
|
||||||
|
if img:
|
||||||
|
if download_image(img, dest):
|
||||||
|
print(f" Downloaded from source", file=sys.stderr)
|
||||||
|
add_image_field(filepath, slug)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Strategy 2: Search by recipe name
|
||||||
|
# Get the title from frontmatter
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
title_match = re.search(r"^title:\s*(.+)$", content, re.MULTILINE)
|
||||||
|
if title_match:
|
||||||
|
query = title_match.group(1).strip() + " recipe"
|
||||||
|
else:
|
||||||
|
query = filename.replace(".cook", "").replace("_", " ").replace("-", " ") + " recipe"
|
||||||
|
|
||||||
|
print(f"[{slug}] Searching: {query}", file=sys.stderr)
|
||||||
|
img = search_and_find_image(query)
|
||||||
|
if img and download_image(img, dest):
|
||||||
|
print(f" Downloaded from search", file=sys.stderr)
|
||||||
|
add_image_field(filepath, slug)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Strategy 3: Try with "og:image" or "recipe image" variations
|
||||||
|
query2 = query + " image"
|
||||||
|
print(f"[{slug}] Searching (alt): {query2}", file=sys.stderr)
|
||||||
|
img = search_and_find_image(query2)
|
||||||
|
if img and download_image(img, dest):
|
||||||
|
print(f" Downloaded from alt search", file=sys.stderr)
|
||||||
|
add_image_field(filepath, slug)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# First process the no-image-has-fm list
|
||||||
|
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()))
|
||||||
|
|
||||||
|
success = []
|
||||||
|
failed = []
|
||||||
|
|
||||||
|
for filename, slug in recipes:
|
||||||
|
filepath = WORKDIR / filename
|
||||||
|
if not filepath.exists():
|
||||||
|
failed.append(slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
|
||||||
|
if dest.exists() and re.search(r"^image:", filepath.read_text(), re.MULTILINE):
|
||||||
|
print(f"[{slug}] Already processed, skipping", file=sys.stderr)
|
||||||
|
success.append(slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ok = process_recipe(filename, slug)
|
||||||
|
if ok:
|
||||||
|
success.append(slug)
|
||||||
|
else:
|
||||||
|
failed.append(slug)
|
||||||
|
|
||||||
|
# Be polite
|
||||||
|
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:
|
||||||
|
with open(WORKDIR / "docs" / "failed-images.csv", "w") as f:
|
||||||
|
for s in failed:
|
||||||
|
f.write(f"{s}\n")
|
||||||
|
print(f"Failed: {', '.join(failed)}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Search DuckDuckGo Images for recipe photos, download, update .cook files."""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from duckduckgo_search import DDGS
|
||||||
|
|
||||||
|
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
|
||||||
|
HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
# Validate it's actually an image
|
||||||
|
content_type = resp.headers.get("content-type", "")
|
||||||
|
if not content_type.startswith("image/"):
|
||||||
|
print(f" Not an image (content-type: {content_type})", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
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."""
|
||||||
|
url = f"https://roux.roo.lol/recipe-images/{slug}.webp"
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
if re.search(r"^image:", content, re.MULTILINE):
|
||||||
|
content = re.sub(r"^image:.*$", f"image: {url}", count=1, flags=re.MULTILINE, string=content)
|
||||||
|
else:
|
||||||
|
content = re.sub(r"^---$", f"image: {url}\n---", count=1, flags=re.MULTILINE, string=content)
|
||||||
|
|
||||||
|
filepath.write_text(content, encoding="utf-8")
|
||||||
|
print(f" Updated {filepath.name}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def search_images(query: str, max_results: int = 5):
|
||||||
|
"""Search DuckDuckGo Images for a recipe and return image URLs."""
|
||||||
|
try:
|
||||||
|
results = list(DDGS().images(query, max_results=max_results))
|
||||||
|
return [r["image"] for r in results if r.get("image")]
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Image search error: {e}", file=sys.stderr)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_title(filepath: Path) -> str:
|
||||||
|
"""Extract title from .cook frontmatter."""
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
match = re.search(r"^title:\s*(.+)$", content, re.MULTILINE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
# Fall back to filename
|
||||||
|
name = filepath.stem
|
||||||
|
return name.replace("_", " ").replace("-", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
csv_file = WORKDIR / "docs" / "failed-images.csv"
|
||||||
|
# Also check which are actually missing
|
||||||
|
existing_csv = WORKDIR / "docs" / "no-image-has-fm.csv"
|
||||||
|
|
||||||
|
recipes = []
|
||||||
|
with open(existing_csv) as f:
|
||||||
|
reader = csv.reader(f, delimiter="|")
|
||||||
|
for row in reader:
|
||||||
|
if len(row) >= 2:
|
||||||
|
recipes.append((row[0].strip(), row[1].strip()))
|
||||||
|
|
||||||
|
# Filter to those without an image file or without image field
|
||||||
|
to_process = []
|
||||||
|
for filename, slug in recipes:
|
||||||
|
filepath = WORKDIR / filename
|
||||||
|
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
|
||||||
|
has_image_file = dest.exists()
|
||||||
|
has_image_field = False
|
||||||
|
if filepath.exists():
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
has_image_field = bool(re.search(r"^image:", content, re.MULTILINE))
|
||||||
|
|
||||||
|
if not has_image_file or not has_image_field:
|
||||||
|
to_process.append((filename, slug))
|
||||||
|
|
||||||
|
print(f"Processing {len(to_process)} remaining recipes...", file=sys.stderr)
|
||||||
|
|
||||||
|
success = []
|
||||||
|
failed = []
|
||||||
|
|
||||||
|
for filename, slug in to_process:
|
||||||
|
filepath = WORKDIR / filename
|
||||||
|
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
|
||||||
|
|
||||||
|
if not filepath.exists():
|
||||||
|
failed.append(slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = get_title(filepath)
|
||||||
|
queries = [
|
||||||
|
f"{title} recipe",
|
||||||
|
title,
|
||||||
|
]
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for query in queries:
|
||||||
|
print(f"[{slug}] Searching images: {query}", file=sys.stderr)
|
||||||
|
urls = search_images(query)
|
||||||
|
for img_url in urls:
|
||||||
|
# Skip tiny icons, logos
|
||||||
|
if any(kw in img_url.lower() for kw in ["logo", "icon", "avatar", "merriam", "wikipedia"]):
|
||||||
|
continue
|
||||||
|
print(f" Trying: {img_url[:80]}...", file=sys.stderr)
|
||||||
|
if download_image(img_url, dest):
|
||||||
|
print(f" Downloaded!", file=sys.stderr)
|
||||||
|
add_image_field(filepath, slug)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
time.sleep(0.2)
|
||||||
|
if found:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if found:
|
||||||
|
success.append(slug)
|
||||||
|
else:
|
||||||
|
failed.append(slug)
|
||||||
|
|
||||||
|
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: {', '.join(failed)}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env 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()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
while IFS='|' read -r file slug; do
|
||||||
|
new_url="https://roux.roo.lol/recipe-images/$slug.webp"
|
||||||
|
sed -i "s|^image:.*|image: $new_url|" "$file"
|
||||||
|
echo "[$slug] Updated image field"
|
||||||
|
done
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ totalTime: 55 minutes
|
|||||||
prepTime: 15 minutes
|
prepTime: 15 minutes
|
||||||
cookTime: 40 minutes
|
cookTime: 40 minutes
|
||||||
tags: arugula, bread crumb, budget, cauliflower, parmesan, sheet-pan, fall, vegetarian
|
tags: arugula, bread crumb, budget, cauliflower, parmesan, sheet-pan, fall, vegetarian
|
||||||
|
image: https://roux.roo.lol/recipe-images/cauliflower-milanese.webp
|
||||||
---
|
---
|
||||||
|
|
||||||
== Ingredients ==
|
== Ingredients ==
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Goal:** Give every `.cook` recipe file an `image:` field in YAML frontmatter pointing to `https://roux.roo.lol/recipe-images/<slug>.webp`, and download each image locally.
|
**Goal:** Give every `.cook` recipe file an `image:` field in YAML frontmatter pointing to `https://roux.roo.lol/recipe-images/<slug>.webp`, and download each image locally.
|
||||||
|
|
||||||
**Architecture:** Three parallel batches — (1) relink the 19 existing images, (2) discover + download + link 48 missing images (recipes with frontmatter), (3) create frontmatter + discover + link 9 more (recipes without frontmatter). Use subagent parallel dispatch for image discovery to keep wall-clock time manageable.
|
**Architecture:** Three parallel batches — (1) relink the 19 existing images, (2) discover + download + link 50 missing images (recipes with frontmatter), (3) create frontmatter + discover + link 9 more (recipes without frontmatter). Use subagent parallel dispatch for image discovery to keep wall-clock time manageable.
|
||||||
|
|
||||||
**Tech Stack:** bash, curl, ImageMagick (convert), subagent researcher for web image search
|
**Tech Stack:** bash, curl, ImageMagick (convert), subagent researcher for web image search
|
||||||
|
|
||||||
@@ -13,50 +13,56 @@
|
|||||||
### Task 0: Setup — slug map, image dir, helper script
|
### Task 0: Setup — slug map, image dir, helper script
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `/home/jbrechtel/recipes/docs/slugs.csv`
|
- Create: `docs/slugs.csv`
|
||||||
- Modify: none yet
|
- Create: `docs/has-image.csv`
|
||||||
|
- Create: `docs/no-image-has-fm.csv`
|
||||||
|
- Create: `docs/no-fm.csv`
|
||||||
|
|
||||||
- [ ] **Step 1: Generate slug mapping**
|
- [ ] **Step 1: Generate slug mapping**
|
||||||
|
|
||||||
Run a one-liner that prints every `.cook` file with its slugified stem:
|
Run a one-liner that prints every `.cook` file with its slugified stem, then deduplicate any collisions:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/jbrechtel/recipes
|
cd /home/jbrechtel/recipes/.worktrees/recipe-images
|
||||||
for f in *.cook; do
|
for f in *.cook; do
|
||||||
stem="${f%.cook}"
|
stem="${f%.cook}"
|
||||||
slug=$(echo "$stem" \
|
slug=$(echo "$stem" \
|
||||||
| tr '[:upper:]' '[:lower:]' \
|
| tr '[:upper:]' '[:lower:]' \
|
||||||
| sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//')
|
| sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//')
|
||||||
echo "$f|$slug"
|
echo "$f|$slug"
|
||||||
done > docs/slugs.csv
|
done | awk -F'|' '
|
||||||
head docs/slugs.csv
|
{
|
||||||
|
slug=$2
|
||||||
|
count[slug]++
|
||||||
|
if (count[slug] > 1) {
|
||||||
|
slug = slug "-" count[slug]
|
||||||
|
}
|
||||||
|
print $1 "|" slug
|
||||||
|
}' > docs/slugs.csv
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: a pipe-delimited CSV like `Alain-Ducase-Bread.cook|alain-ducase-bread`.
|
Total: 78 unique slugs (6 duplicate pairs get `-2` suffix).
|
||||||
|
|
||||||
- [ ] **Step 2: Create image directory**
|
- [ ] **Step 2: Create image directory**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p /home/jbrechtel/recipes/recipe-images
|
mkdir -p recipe-images
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Classify each recipe**
|
- [ ] **Step 3: Classify each recipe**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/jbrechtel/recipes
|
cd /home/jbrechtel/recipes/.worktrees/recipe-images
|
||||||
echo "=== Already has image ==="
|
|
||||||
while IFS='|' read -r f slug; do
|
while IFS='|' read -r f slug; do
|
||||||
if grep -q "^image:" "$f" 2>/dev/null; then echo "$f|$slug"; fi
|
if grep -q "^image:" "$f" 2>/dev/null; then echo "$f|$slug"; fi
|
||||||
done < docs/slugs.csv > docs/has-image.csv
|
done < docs/slugs.csv > docs/has-image.csv
|
||||||
|
|
||||||
echo "=== Has frontmatter, no image ==="
|
|
||||||
while IFS='|' read -r f slug; do
|
while IFS='|' read -r f slug; do
|
||||||
if head -1 "$f" 2>/dev/null | grep -q "^---"; then
|
if head -1 "$f" 2>/dev/null | grep -q "^---"; then
|
||||||
if ! grep -q "^image:" "$f" 2>/dev/null; then echo "$f|$slug"; fi
|
if ! grep -q "^image:" "$f" 2>/dev/null; then echo "$f|$slug"; fi
|
||||||
fi
|
fi
|
||||||
done < docs/slugs.csv > docs/no-image-has-fm.csv
|
done < docs/slugs.csv > docs/no-image-has-fm.csv
|
||||||
|
|
||||||
echo "=== No frontmatter ==="
|
|
||||||
while IFS='|' read -r f slug; do
|
while IFS='|' read -r f slug; do
|
||||||
if ! head -1 "$f" 2>/dev/null | grep -q "^---"; then echo "$f|$slug"; fi
|
if ! head -1 "$f" 2>/dev/null | grep -q "^---"; then echo "$f|$slug"; fi
|
||||||
done < docs/slugs.csv > docs/no-fm.csv
|
done < docs/slugs.csv > docs/no-fm.csv
|
||||||
@@ -64,12 +70,11 @@ done < docs/slugs.csv > docs/no-fm.csv
|
|||||||
wc -l docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv
|
wc -l docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: 19 / 48 / 9 lines respectively.
|
Expected: 19 / 50 / 9 = 78 total.
|
||||||
|
|
||||||
- [ ] **Step 4: Commit setup**
|
- [ ] **Step 4: Commit setup**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/jbrechtel/recipes
|
|
||||||
git add docs/slugs.csv docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv
|
git add docs/slugs.csv docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv
|
||||||
git commit -m "chore: add recipe slug mapping and classification CSVs"
|
git commit -m "chore: add recipe slug mapping and classification CSVs"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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: Gather and Garnish
|
||||||
|
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: 109 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 15 KiB |