diff --git a/README.rst b/README.rst index d34c5ec6e..effc2fa17 100644 --- a/README.rst +++ b/README.rst @@ -122,6 +122,7 @@ Scrapers available for: - `https://www.coop.se/ `_ - `https://copykat.com/ `_ - `https://countryliving.com/ `_ +- `https://creativecanning.com/ `_ - `https://cucchiaio.it/ `_ - `https://cuisineaz.com/ `_ - `https://cybercook.com.br/ `_ @@ -253,7 +254,6 @@ Scrapers available for: - `https://rezeptwelt.de/ `_ - `https://rosannapansino.com `_ - `https://sallysbakingaddiction.com `_ -- `https://sallys-blog.de `_ - `https://www.saveur.com/ `_ - `https://seriouseats.com/ `_ - `https://simple-veganista.com/ `_ diff --git a/recipe_scrapers/__init__.py b/recipe_scrapers/__init__.py index bb43ec688..ad4c04a6c 100644 --- a/recipe_scrapers/__init__.py +++ b/recipe_scrapers/__init__.py @@ -169,7 +169,6 @@ from .rezeptwelt import Rezeptwelt from .rosannapansino import RosannaPansino from .sallysbakingaddiction import SallysBakingAddiction -from .sallysblog import SallysBlog from .saveur import Saveur from .seriouseats import SeriousEats from .simpleveganista import SimpleVeganista @@ -384,6 +383,7 @@ PingoDoce.host(): PingoDoce, PopSugar.host(): PopSugar, PracticalSelfReliance.host(): PracticalSelfReliance, + PracticalSelfReliance.host(domain="creativecanning.com"): PracticalSelfReliance, PrimalEdgeHealth.host(): PrimalEdgeHealth, Przepisy.host(): Przepisy, PurelyPope.host(): PurelyPope, @@ -399,7 +399,6 @@ Rezeptwelt.host(): Rezeptwelt, RosannaPansino.host(): RosannaPansino, SallysBakingAddiction.host(): SallysBakingAddiction, - SallysBlog.host(): SallysBlog, Saveur.host(): Saveur, SeriousEats.host(): SeriousEats, SimpleVeganista.host(): SimpleVeganista, diff --git a/recipe_scrapers/_schemaorg.py b/recipe_scrapers/_schemaorg.py index 741b93a26..36dd2f6ca 100644 --- a/recipe_scrapers/_schemaorg.py +++ b/recipe_scrapers/_schemaorg.py @@ -3,6 +3,8 @@ # find a package that parses https://schema.org/Recipe properly (or create one ourselves). +from itertools import chain + import extruct from recipe_scrapers.settings import settings @@ -160,6 +162,10 @@ def ingredients(self): ingredients = ( self.data.get("recipeIngredient") or self.data.get("ingredients") or [] ) + + if ingredients and isinstance(ingredients[0], list): + ingredients = list(chain(*ingredients)) # flatten + return [ normalize_string(ingredient) for ingredient in ingredients if ingredient ] @@ -206,6 +212,9 @@ def _extract_howto_instructions_text(self, schema_item): def instructions(self): instructions = self.data.get("recipeInstructions") or "" + if instructions and isinstance(instructions[0], list): + instructions = list(chain(*instructions)) # flatten + if isinstance(instructions, list): instructions_gist = [] for schema_instruction_item in instructions: @@ -244,4 +253,6 @@ def description(self): description = self.data.get("description") if description is None: raise SchemaOrgException("No description data in SchemaOrg.") + if description and isinstance(description, list): + description = description[0] return normalize_string(description) diff --git a/recipe_scrapers/_utils.py b/recipe_scrapers/_utils.py index 66337444e..4106160fc 100644 --- a/recipe_scrapers/_utils.py +++ b/recipe_scrapers/_utils.py @@ -20,7 +20,7 @@ } TIME_REGEX = re.compile( - r"(\D*(?P[\d.\s/?¼½¾⅓⅔⅕⅖⅗]+)\s*(hours|hrs|hr|h|óra))?(\D*(?P\d+)\s*(minutes|mins|min|m|perc))?", + r"(\D*(?P\d+)\s*(days|D))?(\D*(?P[\d.\s/?¼½¾⅓⅔⅕⅖⅗]+)\s*(hours|hrs|hr|h|óra|:))?(\D*(?P\d+)\s*(minutes|mins|min|m|perc|$))?", re.IGNORECASE, ) @@ -77,7 +77,11 @@ def get_minutes(element, return_zero_on_not_found=False): # noqa: C901: TODO minutes = int(matched.groupdict().get("minutes") or 0) hours_matched = matched.groupdict().get("hours") + days_matched = matched.groupdict().get("days") + # workaround for formats like: 0D4H45M, that are not a valid iso8601 it seems + if days_matched: + minutes += 60 * 60 * float(days_matched.strip()) if hours_matched: hours_matched = hours_matched.strip() if any([symbol in FRACTIONS.keys() for symbol in hours_matched]): diff --git a/recipe_scrapers/comidinhasdochef.py b/recipe_scrapers/comidinhasdochef.py index f05e21d58..3efcb35a9 100644 --- a/recipe_scrapers/comidinhasdochef.py +++ b/recipe_scrapers/comidinhasdochef.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import normalize_string class ComidinhasDoChef(AbstractScraper): @@ -9,36 +8,25 @@ def host(cls): return "comidinhasdochef.com" def author(self): - return self.soup.find("span", {"class": "theauthor"}).get_text(strip=True) + return self.schema.author() def title(self): - return self.soup.find("h1", {"class": "title"}).get_text() + return self.schema.title() def total_time(self): return self.schema.total_time() def yields(self): - yields = self.soup.find("span", {"itemprop": "recipeYield"}) - return yields.get_text() if yields else None + return self.schema.yields() def image(self): return self.schema.image() def ingredients(self): - return [ - normalize_string(ingredient.get_text()) - for ingredient in self.soup.find_all("li", {"itemprop": "recipeIngredient"}) - ] + return self.schema.ingredients() def instructions(self): - instructions = [ - normalize_string(instruction.get_text(strip=True)) - for instruction in self.soup.find_all( - "li", {"itemprop": "recipeInstructions"} - ) - ] - return "\n".join(instructions) + return self.schema.instructions() def ratings(self): - rating = self.soup.find("span", {"itemprop": "ratingValue"}).get_text() - return round(float(rating), 2) + return self.schema.ratings() diff --git a/recipe_scrapers/countryliving.py b/recipe_scrapers/countryliving.py index 46adf2507..159014075 100644 --- a/recipe_scrapers/countryliving.py +++ b/recipe_scrapers/countryliving.py @@ -1,7 +1,6 @@ # mypy: allow-untyped-defs from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class CountryLiving(AbstractScraper): @@ -10,33 +9,16 @@ def host(cls): return "countryliving.com" def title(self): - return self.soup.find("h1", {"class": "content-hed recipe-hed"}).get_text() - - def author(self): - return self.soup.find("span", {"rel": "author"}).get_text() + return self.schema.title() def total_time(self): - return get_minutes( - self.soup.find("span", {"class": "total-time-amount"}).parent - ) + return self.schema.total_time() def yields(self): - yields = self.soup.find( - "div", {"class": "recipe-details-item yields"} - ).get_text() - - return get_yields("{} servings".format(yields)) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.findAll("div", {"class": "ingredient-item"}) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.find("div", {"class": "direction-lists"}).find_all( - "li" - ) - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/cucchiaio.py b/recipe_scrapers/cucchiaio.py index c578efbc8..3615c12b5 100644 --- a/recipe_scrapers/cucchiaio.py +++ b/recipe_scrapers/cucchiaio.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields class Cucchiaio(AbstractScraper): @@ -14,30 +13,11 @@ def author(self): def title(self): return self.schema.title() - def total_time(self): - block = self.soup.find("div", {"class": "scheda-ricetta-new"}) - if block: - return sum(map(get_minutes, block.findAll("tr"))) - return 0 - - def yields(self): - header = self.soup.find("td", string="PORZIONI") - if header: - value = header.find_next("td") - return get_yields(value) - return None - def image(self): - data = self.soup.find("div", {"class": "auto"}).find("img", {"class": "image"}) - if data: - data = data.get("src") - return data + return self.schema.image() def ingredients(self): return self.schema.ingredients() def instructions(self): return self.schema.instructions() - - def ratings(self): - return None diff --git a/recipe_scrapers/cuisineaz.py b/recipe_scrapers/cuisineaz.py index 26a1d0fcb..fe00e3ade 100644 --- a/recipe_scrapers/cuisineaz.py +++ b/recipe_scrapers/cuisineaz.py @@ -7,6 +7,9 @@ class CuisineAZ(AbstractScraper): def host(cls): return "cuisineaz.com" + def author(self): + return self.schema.author() + def title(self): return self.schema.title() diff --git a/recipe_scrapers/delish.py b/recipe_scrapers/delish.py index bbceeb613..f9c231338 100644 --- a/recipe_scrapers/delish.py +++ b/recipe_scrapers/delish.py @@ -1,13 +1,5 @@ # mypy: disallow_untyped_defs=False -# delish.py -# Written by J. Kwon -# Freely released the code to recipe_scraper group -# March 1st, 2020 -# ========================================================== - - from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class Delish(AbstractScraper): @@ -16,38 +8,19 @@ def host(cls): return "delish.com" def title(self): - return normalize_string(self.soup.find("h1").get_text()) + return self.schema.title() - # Return total time to complete dish in minutes (includes prep time) def total_time(self): - total_time_class = self.soup.find("span", {"class": "total-time-amount"}) - return get_minutes(total_time_class) + return self.schema.total_time() def yields(self): - yields_class = self.soup.find("span", {"class": "yields-amount"}) - - return get_yields(yields_class) + return self.schema.yields() def image(self): - try: - # Case when image is at the top of the recipe content div - image = self.soup.find( - "div", {"class": "content-lede-image-wrap aspect-ratio-1x1"} - ).find("img") - return image["data-src"] if image else None - - except Exception: - # If the image is not at the top, it will be found at the - # bottom of the recipe content div - image = self.soup.find("picture") - return image.find("source")["data-srcset"] if image else None + return self.schema.image() def ingredients(self): - ingredients = self.soup.findAll("div", {"class": "ingredient-item"}) - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.find("div", {"class": "direction-lists"}).findAll("li") - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/finedininglovers.py b/recipe_scrapers/finedininglovers.py index fd8607343..092c65ca1 100644 --- a/recipe_scrapers/finedininglovers.py +++ b/recipe_scrapers/finedininglovers.py @@ -1,7 +1,6 @@ # mypy: allow-untyped-defs from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class FineDiningLovers(AbstractScraper): @@ -10,7 +9,7 @@ def host(cls): return "finedininglovers.com" def title(self): - return self.soup.find("h1", {"class": "recipe-full-class"}).get_text() + return self.schema.title() def author(self): container = self.soup.find("div", {"class": "author-name"}) @@ -18,43 +17,16 @@ def author(self): return container.find("a").get_text() def total_time(self): - return get_minutes(self.soup.find("div", {"class": "timing"})) + return self.schema.total_time() def yields(self): - yields = self.soup.find( - "div", {"class": "field--name-field-recipe-serving-num"} - ) - - return get_yields("{} servings".format(yields)) + return self.schema.yields() def ingredients(self): - ingredients_parent = self.soup.find("div", {"class": "ingredients-box"}) - ingredients = ingredients_parent.findAll( - "div", {"class": "paragraph--type--recipe-ingredient"} - ) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions_parent = self.soup.find( - "div", {"class": "field--name-field-recipe-para-steps"} - ) - - if instructions_parent is not None: - instructions = instructions_parent.findAll( - "div", {"class": "paragraph--type--recipe-step"} - ) - else: - instructions_parent = self.soup.find("div", {"class": "ante-body"}) - instructions = instructions_parent.findAll({"li", "p"}) - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() def image(self): - image = self.soup.select_one(".image-zone picture img") - image_url = image["data-src"].split("?")[0] - image_base_url = "https://www.finedininglovers.com" - - return "{}{}".format(image_base_url, image_url) if image else None + return self.schema.image() diff --git a/recipe_scrapers/fredriksfikaallas.py b/recipe_scrapers/fredriksfikaallas.py index 2a1a142be..176aa1a4f 100644 --- a/recipe_scrapers/fredriksfikaallas.py +++ b/recipe_scrapers/fredriksfikaallas.py @@ -9,29 +9,21 @@ class FredriksFikaAllas(AbstractScraper): def host(cls): return "fredriksfika.allas.se" - def author(self): - author = self.soup.find("div", {"class": "c-post_author__name"}).get_text() - return author.replace("Av:", "").strip() - def title(self): - return self.soup.find("div", {"class": "c-post_title"}).get_text() + return self.soup.find("h1").get_text() def category(self): - return ( - self.soup.find("div", {"class": "c-post_author__category"}) - .get_text() - .replace("i ", "") - .strip() - ) + return self.soup.find("div", {"class": "post_category"}).get_text() def image(self): - return self.schema.image() + return self.soup.find("meta", {"property": "og:image", "content": True}).get( + "content" + ) def ingredients(self): ingredients = [] content = self.soup.find("strong", string=re.compile("Ingredienser")) - - contentRows = content.parent.text.split("\n") + contentRows = str(content.parent).split("
") for i in contentRows: if "Ingredienser" not in i: @@ -45,7 +37,7 @@ def instructions(self): instructions = [] content = self.soup.find("strong", string=re.compile("Gör så här")) - contentRows = content.parent.text.split("\n") + contentRows = str(content.parent).split("
") fillData = False for i in contentRows: diff --git a/recipe_scrapers/geniuskitchen.py b/recipe_scrapers/geniuskitchen.py index 7603bc320..de3db0157 100644 --- a/recipe_scrapers/geniuskitchen.py +++ b/recipe_scrapers/geniuskitchen.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class GeniusKitchen(AbstractScraper): @@ -9,39 +8,19 @@ def host(cls): return "geniuskitchen.com" def title(self): - return ( - self.soup.find("title").get_text().replace(" Recipe - Genius Kitchen", "") - ) + return self.schema.title() def total_time(self): - return get_minutes(self.soup.find("td", {"class": "time"})) + return self.schema.total_time() def yields(self): - return get_yields( - self.soup.find("td", {"class": "servings"}).find("span", {"class": "count"}) - ) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.find("ul", {"class": "ingredient-list"}).findAll("li") - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - raw_directions = ( - self.soup.find("div", {"class": "directions-inner container-xs"}) - .find("ol") - .findAll("li") - ) - - directions = [] - - for direction in raw_directions: - if "Submit a Correction" not in direction.get_text(): - directions.append(normalize_string(direction.get_text())) - - return "\n".join(directions) + return self.schema.instructions() def ratings(self): - rating = self.soup.find("span", {"class": "sr-only"}).get_text() - - return round(float(rating), 2) + return self.schema.ratings() diff --git a/recipe_scrapers/gonnawantseconds.py b/recipe_scrapers/gonnawantseconds.py index d5d325a56..5c5af51f9 100644 --- a/recipe_scrapers/gonnawantseconds.py +++ b/recipe_scrapers/gonnawantseconds.py @@ -1,13 +1,5 @@ # mypy: disallow_untyped_defs=False -# gonnawantseconds.py -# Written by G.D. Wallters -# Freely released the code to recipe_scraper group -# 9 February, 2020 -# ======================================================= - - from ._abstract import AbstractScraper -from ._utils import get_minutes, normalize_string class GonnaWantSeconds(AbstractScraper): @@ -16,124 +8,22 @@ def host(cls): return "gonnawantseconds.com" def title(self): - return normalize_string(self.soup.find("h1").get_text()) + return self.schema.title() def total_time(self): - total_time = 0 - try: - tt1 = self.soup.find( - "span", - { - "class": "wprm-recipe-details wprm-recipe-details-hours wprm-recipe-total_time wprm-recipe-total_time-hours" - }, - ).get_text() - except Exception: - tt1 = 0 - tt2 = self.soup.find( - "span", - { - "class": "wprm-recipe-details wprm-recipe-details-minutes wprm-recipe-total_time wprm-recipe-total_time-minutes" - }, - ).get_text() - if tt1: - tt3 = (int(tt1) * 60) + int(tt2) - tt2 = get_minutes(tt3) - if tt3 and (tt2 == 0): - total_time = tt3 - else: - total_time = tt2 - elif tt2: - total_time = tt2 - return total_time + return self.schema.total_time() def yields(self): - recipe_yield = self.soup.find( - "div", - { - "class": "wprm-recipe-servings-container wprm-recipe-block-container wprm-recipe-block-container-inline wprm-block-text-normal" - }, - ).get_text() - if "Servings:" in recipe_yield: - ry = normalize_string(recipe_yield[9:]) - return ry + return self.schema.yields() def image(self): - image = self.soup.find( - "div", {"class": "wprm-recipe-image wprm-block-image-normal"} - ) # , 'src': True}) - img = image.find( - "img", {"class": "attachment-200x200 size-200x200", "src": True} - ) - issrc = img["src"] - return issrc if image else None + return self.schema.image() def ingredients(self): - ingredientsOuter = self.soup.findAll( - "div", {"class": "wprm-recipe-ingredient-group"} - ) - - ingGroup = [] - for ig in ingredientsOuter: - try: - header = ig.find( - "h4", - { - "class": "wprm-recipe-group-name wprm-recipe-ingredient-group-name wprm-block-text-bold" - }, - ).text - except Exception: - header = None - if header is not None: - ingGroup.append(header) - ingredparts = ig.findAll("li") - for i in ingredparts: - x = normalize_string(i.get_text()) - ingGroup.append(x) - return ingGroup - - def _instructions_list(self): - instructions = self.soup.findAll( - "div", {"class": "wprm-recipe-instruction-group"} - ) - data = [] - if len(instructions): - for instruct in instructions: - try: - header = instruct.find( - "h4", - { - "class": "wprm-recipe-group-name wprm-recipe-instruction-group-name wprm-block-text-bold" - }, - ).text - except Exception: - header = None - if header is not None: - data.append(header) - ins = instruct.findAll("div", {"class": "wprm-recipe-instruction-text"}) - - data.append("\n".join([normalize_string(inst.text) for inst in ins])) - return data - return None + return self.schema.ingredients() def instructions(self): - data = self._instructions_list() - return "\n".join(data) if data else None - - def ratings(self): - try: - found = self.soup.find("div", {"class": "wprm-recipe-rating"}) - stars = found.findAll( - "span", - attrs={ - "class": lambda e: e.endswith("wprm-rating-star-full") - if e - else False - }, - ) - except Exception: - stars = [] - return round(float(len(stars)), 2) if stars else None + return self.schema.instructions() def description(self): - d = normalize_string(self.soup.find("span", {"style": "display: block;"}).text) - return d if d else None + return self.schema.description() diff --git a/recipe_scrapers/greatbritishchefs.py b/recipe_scrapers/greatbritishchefs.py index d706a1dd2..b757a7730 100644 --- a/recipe_scrapers/greatbritishchefs.py +++ b/recipe_scrapers/greatbritishchefs.py @@ -1,13 +1,6 @@ # mypy: disallow_untyped_defs=False -# GreatBritishChefs.com scraper -# Written by G.D. Wallters -# Freely released the code to recipe_scraper group -# 6 February, 2020 -# ======================================================= - from ._abstract import AbstractScraper -from ._utils import get_minutes, normalize_string class GreatBritishChefs(AbstractScraper): @@ -16,68 +9,22 @@ def host(cls): return "greatbritishchefs.com" def title(self): - return normalize_string(self.soup.find("h1").get_text()) + return self.schema.title() def total_time(self): - total_time = 0 - tt1 = self.soup.find("span", {"class": "RecipeAttributes__Time"}) - if tt1: - tt = tt1.find("span", {"class": "header-attribute-text"}).get_text() - tt3 = normalize_string(tt) - tt2 = get_minutes(tt3) - if tt3 and (tt2 == 0): - total_time = tt3 - else: - total_time = tt2 - return total_time + return self.schema.total_time() def yields(self): - recipe_yield = self.soup.find("span", {"class": "RecipeAttributes__Serves"}) - if recipe_yield: - recipe_yield = normalize_string( - recipe_yield.find("span", {"class": "header-attribute-text"}).get_text() - ) - return recipe_yield + return self.schema.yields() def image(self): - image = self.soup.find("img", {"id": "head-media"}, "src") - if image: - src = image.get("src", None) - if "http:" in src: - return src - else: - src = "http:" + src - return src if image else None + return self.schema.image() def ingredients(self): - ingredientsOuter = self.soup.findAll( - "ul", {"class": "IngredientsList__ListContainer"} - ) - ingGroup = [] - ingredparts = [] - - for subheader in ingredientsOuter: - ingredparts.extend(subheader.findAll("li")) - - for i in ingredparts: - x = normalize_string(i.get_text()) - if x != "": # Some recipes include an empty li - ingGroup.append(x) - return ingGroup + return self.schema.ingredients() def instructions(self): - instructions = self.soup.find("div", {"class": "Method__List MethodList"}) - - ins = instructions.findAll("div", {"class": "MethodList__StepText"}) - - return "\n".join([normalize_string(inst.text) for inst in ins]) - - def ratings(self): - # This site does not support ratings at this time - return None + return self.schema.instructions() def description(self): - d = normalize_string( - self.soup.find("div", {"class": "RecipeAbstract__Abstract"}).text - ) - return d if d else None + return self.schema.description() diff --git a/recipe_scrapers/hundredandonecookbooks.py b/recipe_scrapers/hundredandonecookbooks.py index 1dd06217b..890642575 100644 --- a/recipe_scrapers/hundredandonecookbooks.py +++ b/recipe_scrapers/hundredandonecookbooks.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class HundredAndOneCookbooks(AbstractScraper): @@ -9,26 +8,16 @@ def host(cls): return "101cookbooks.com" def title(self): - return self.soup.find("h1").get_text() + return self.schema.title() def total_time(self): - return get_minutes( - self.soup.findAll("div", {"class": "wprm-recipe-time"})[-1].get_text() - ) + return self.schema.total_time() def yields(self): - return get_yields( - self.soup.findAll("div", {"class": "wprm-recipe-time"})[0].get_text() - ) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.findAll("li", {"class": "wprm-recipe-ingredient"}) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.findAll("li", {"class": "wprm-recipe-instruction"}) - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/inspiralized.py b/recipe_scrapers/inspiralized.py index 73cfc4738..d1c336ee3 100644 --- a/recipe_scrapers/inspiralized.py +++ b/recipe_scrapers/inspiralized.py @@ -1,7 +1,6 @@ # mypy: allow-untyped-defs from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class Inspiralized(AbstractScraper): @@ -10,26 +9,17 @@ def host(cls): return "inspiralized.com" def title(self): - return self.soup.find("h2").get_text() + return self.schema.title() def author(self): if self.soup.find(string="Ali Maffucci"): return "Ali Maffucci" def total_time(self): - return get_minutes(self.soup.find("span", {"itemprop": "totalTime"})) - - def yields(self): - return get_yields(self.soup.find("span", {"itemprop": "servingSize"})) + return self.schema.total_time() def ingredients(self): - ingredients = self.soup.findAll("li", {"class": "ingredient"}) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.findAll("li", {"class": "instruction"}) - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/kwestiasmaku.py b/recipe_scrapers/kwestiasmaku.py index 082d6d4ea..467692934 100644 --- a/recipe_scrapers/kwestiasmaku.py +++ b/recipe_scrapers/kwestiasmaku.py @@ -9,12 +9,10 @@ def host(cls): return "kwestiasmaku.com" def author(self): - return normalize_string( - self.soup.find("span", {"itemprop": "author"}).get_text() - ) + return self.schema.author() def title(self): - return normalize_string(self.soup.find("div", {"itemprop": "name"}).get_text()) + return self.schema.title() def yields(self): return get_yields( @@ -41,4 +39,4 @@ def instructions(self): return "\n".join([normalize_string(i.get_text()) for i in instructions]) def ratings(self): - return float(self.soup.find("span", {"itemprop": "ratingValue"}).get_text()) + return self.schema.ratings() diff --git a/recipe_scrapers/latelierderoxane.py b/recipe_scrapers/latelierderoxane.py index 419b2ab3c..d4386272c 100644 --- a/recipe_scrapers/latelierderoxane.py +++ b/recipe_scrapers/latelierderoxane.py @@ -9,10 +9,9 @@ def host(cls): return "latelierderoxane.com" def image(self): - image = self.soup.find( - "img", {"class": "attachment-single size-single wp-post-image"} + return self.soup.find("meta", {"property": "og:image", "content": True}).get( + "content" ) - return image["src"] if image else None def title(self): div = self.soup.find("div", {"class": "bloc_titreh1 bloc_blog"}) @@ -38,7 +37,7 @@ def ingredients(self): raw_ingredients = self.soup.find_all("div", {"class": "ingredient"}) formatted_ingredients = [] for ingredient in raw_ingredients: - formatted_ingredients.append(ingredient.get_text()) + formatted_ingredients.append(normalize_string(ingredient.get_text())) return formatted_ingredients def instructions(self): diff --git a/recipe_scrapers/lekkerensimpel.py b/recipe_scrapers/lekkerensimpel.py index fb50442f3..ef2f2b10e 100644 --- a/recipe_scrapers/lekkerensimpel.py +++ b/recipe_scrapers/lekkerensimpel.py @@ -29,18 +29,10 @@ def image(self): return image["content"] if image else None def ingredients(self): - ingredients = self.soup.find("div", {"class": "recipe__necessities"}).find_all( - "li" - ) - return [normalize_string(i.get_text()) for i in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.find("div", {"class": "entry__content"}).find_all("p") - return "\n".join( - [normalize_string(i.get_text()) for i in instructions] - if instructions - else None - ) + return self.schema.instructions() def ratings(self): return self.schema.ratings() @@ -49,8 +41,7 @@ def cuisine(self): return self.schema.cuisine() def description(self): - description = self.soup.find("div", {"class": "entry__content"}).find("p").text - return normalize_string(description) if description else None + return self.schema.description() def language(self): return "nl-NL" diff --git a/recipe_scrapers/matprat.py b/recipe_scrapers/matprat.py index eedc5cbd6..38e41a19f 100644 --- a/recipe_scrapers/matprat.py +++ b/recipe_scrapers/matprat.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class Matprat(AbstractScraper): @@ -9,68 +8,25 @@ def host(cls): return "matprat.no" def title(self): - return self.soup.find("h1").get_text().strip() + return self.schema.title() def total_time(self): - total_time = 0 - tt = self.soup.find("span", {"data-epi-property-name": "RecipeTime"}) - if tt: - tt1 = normalize_string(tt.get_text()) - tt2 = get_minutes(tt1) - if tt1 and (tt2 == 0): - total_time = tt1 - else: - total_time = tt2 - return total_time + return self.schema.total_time() def yields(self): - recipe_yield = self.soup.find("input", {"id": "portionsInput"}) - if recipe_yield: - return str(recipe_yield["value"]) + " serving{}".format( - "s" if int(recipe_yield["value"]) > 1 else "" - ) - else: - return get_yields( - self.soup.find( - "div", {"class": "recipe-adjust-servings__original-serving"} - ).get_text() - ) + return self.schema.yields() def image(self): - image = self.soup.find("div", {"class": "responsive-image"}) - if image: - tag = image.find("img") - src = tag.get("src", None) - return src if image else None + return self.schema.image() def ingredients(self): - details = self.soup.find("div", {"class": "ingredients-list"}) - sections = details.findAll("h3", {"class": "ingredient-section-title"}) - ingredients = details.findAll("ul", {"class": "ingredientsList"}) - - cntr = 0 - ilist = [] - for ingpart in ingredients: - ingreditem = ingpart.findAll("li") - for i in ingreditem: - ilist.append(normalize_string(i.get_text())) - if cntr <= (len(sections) - 1): - if len(sections[cntr].text) > 0: - # txt = f'--- {sections[cntr].text} ---' - txt = "--- {0} ---".format(sections[cntr].text) - ilist.append(txt) - cntr += 1 - return ilist + return self.schema.ingredients() def instructions(self): - instructions = self.soup.find("div", {"class": "rich-text"}) - ins = instructions.findAll("li") - - return "\n".join([normalize_string(inst.text) for inst in ins]) + return self.schema.instructions() def ratings(self): - r = self.soup.find("span", {"data-bind": "text: numberOfVotes"}) - return int(normalize_string(r.get_text())) + return self.schema.ratings() def description(self): - return normalize_string(self.soup.find("div", {"class": "article-intro"}).text) + return self.schema.description() diff --git a/recipe_scrapers/motherthyme.py b/recipe_scrapers/motherthyme.py index 22d0a9518..f09bc2a69 100644 --- a/recipe_scrapers/motherthyme.py +++ b/recipe_scrapers/motherthyme.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class MotherThyme(AbstractScraper): @@ -9,38 +8,19 @@ def host(cls): return "motherthyme.com" def title(self): - return self.soup.find("h2", {"class": "wprm-recipe-name"}).get_text() + return self.schema.title() def total_time(self): - return get_minutes( - self.soup.find("span", {"class": "wprm-recipe-total_time"}).parent - ) + return self.schema.total_time() def yields(self): - yields = self.soup.find("span", {"class": "wprm-recipe-servings"}).get_text() - - return get_yields("{} servings".format(yields)) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.findAll("li", {"class": "wprm-recipe-ingredient"}) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.findAll( - "div", {"class": "wprm-recipe-instruction-text"} - ) - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() def ratings(self): - return round( - float( - self.soup.find( - "span", {"class": "wprm-recipe-rating-average"} - ).get_text() - ), - 2, - ) + return self.schema.ratings() diff --git a/recipe_scrapers/mybakingaddiction.py b/recipe_scrapers/mybakingaddiction.py index 1bbb353f7..69ec097e9 100644 --- a/recipe_scrapers/mybakingaddiction.py +++ b/recipe_scrapers/mybakingaddiction.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class MyBakingAddiction(AbstractScraper): @@ -9,35 +8,19 @@ def host(cls): return "mybakingaddiction.com" def title(self): - return self.soup.find("h1").get_text() + return self.schema.title() def total_time(self): - return get_minutes( - self.soup.find("div", {"class": "mv-create-time-total"}).get_text() - ) + return self.schema.total_time() def yields(self): - return get_yields(self.soup.find("div", {"class": "mv-create-time-yield"})) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.find("div", {"class": "mv-create-ingredients"}).findAll( - "li" - ) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.find( - "div", {"class": "mv-create-instructions"} - ).findAll("li") - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() def ratings(self): - rating = self.soup.find("div", {"class": "mv-create-reviews"}).attrs.get( - "data-mv-create-rating", None - ) - - return round(float(rating), 2) + return self.schema.ratings() diff --git a/recipe_scrapers/mykitchen101en.py b/recipe_scrapers/mykitchen101en.py index 7d5becf79..5a09e453f 100644 --- a/recipe_scrapers/mykitchen101en.py +++ b/recipe_scrapers/mykitchen101en.py @@ -1,10 +1,5 @@ # mypy: disallow_untyped_defs=False -import re - -from bs4 import BeautifulSoup - from ._abstract import AbstractScraper -from ._utils import get_yields, normalize_string class MyKitchen101en(AbstractScraper): @@ -13,35 +8,19 @@ def host(cls): return "mykitchen101en.com" def author(self): - return self.soup.find("a", {"rel": "author"}).get_text() + return self.schema.author() def title(self): - return self.soup.find("h1", {"class": "entry-title"}).get_text() + return self.schema.title() def yields(self): - return get_yields(self.soup.find("p", string=re.compile("Yields: ")).get_text()) + return self.schema.yields() def image(self): return self.schema.image() def ingredients(self): - soup = BeautifulSoup(str(self.soup), features="html.parser") - ingredients = ( - soup.find(name="p", string=re.compile("Ingredients:")) - .find_next("ul") - .find_all("li") - ) - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - soup = BeautifulSoup(str(self.soup), features="html.parser") - instructions = soup.find( - name="p", string=re.compile("Directions:") - ).find_all_next("p") - return "\n".join( - [ - normalize_string(instruction.get_text()) - for instruction in instructions - if instruction.get_text()[:1].isdigit() - ] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/nutritionbynathalie.py b/recipe_scrapers/nutritionbynathalie.py index b2913ae68..4be8e5994 100644 --- a/recipe_scrapers/nutritionbynathalie.py +++ b/recipe_scrapers/nutritionbynathalie.py @@ -2,13 +2,12 @@ import re from ._abstract import AbstractScraper +from ._utils import normalize_string BULLET_CHARACTER_ORD = 8226 class NutritionByNathalie(AbstractScraper): - ingredientMatch = re.compile(r"Ingredients:") - @classmethod def host(cls): return "nutritionbynathalie.com" @@ -16,12 +15,6 @@ def host(cls): def title(self): return self.soup.find("h1").get_text() - def total_time(self): - return 0 - - def yields(self): - return None - def image(self): try: return self.soup.find("img", {"id": re.compile(r"^innercomp_")})["src"] @@ -30,29 +23,22 @@ def image(self): def ingredients(self): ingredients = [] - - elements = self.soup.find_all(string=self.ingredientMatch) - for outerElement in elements: - title = outerElement.find_parent("p") - if not title: - continue - for element in title.next_siblings: - ingredient = element.get_text() - if len(ingredient) == 0 or ord(ingredient[0]) != BULLET_CHARACTER_ORD: - break + element = self.soup.find(string=re.compile(r"Ingredients:")) + parent_div = element.find_parent("div") + paragraphs = parent_div.find_all("p") + for paragraph in paragraphs: + ingredient = paragraph.get_text() + if ord(ingredient[0]) == BULLET_CHARACTER_ORD: ingredients.append(ingredient[2:]) - element = element.nextSibling return ingredients def instructions(self): - title = self.soup.find(string="Directions:").find_parent("p") - - instructions = [] - for child in title.nextSibling.find_all("li"): - instructions.append(child.get_text()) + element = self.soup.find(string=re.compile("Directions:")) + parent_div = element.find_parent("div") + li_items = parent_div.find_all("li") - return "\n".join(instructions) + return "\n".join([normalize_string(li_item.get_text()) for li_item in li_items]) def ratings(self): return None diff --git a/recipe_scrapers/panelinha.py b/recipe_scrapers/panelinha.py index b2570fb0c..2d9dc1d93 100644 --- a/recipe_scrapers/panelinha.py +++ b/recipe_scrapers/panelinha.py @@ -53,6 +53,4 @@ def instructions(self): return "\n".join(instructions) def yields(self): - return normalize_string( - self.soup.find("span", string="Serve").nextSibling.get_text() - ) + return self.schema.yields() diff --git a/recipe_scrapers/practicalselfreliance.py b/recipe_scrapers/practicalselfreliance.py index f16822356..c3de604e8 100644 --- a/recipe_scrapers/practicalselfreliance.py +++ b/recipe_scrapers/practicalselfreliance.py @@ -4,8 +4,8 @@ class PracticalSelfReliance(AbstractScraper): @classmethod - def host(cls): - return "practicalselfreliance.com" + def host(cls, domain="practicalselfreliance.com"): + return domain def title(self): return self.schema.title() diff --git a/recipe_scrapers/realsimple.py b/recipe_scrapers/realsimple.py index de3446421..29ef67ffa 100644 --- a/recipe_scrapers/realsimple.py +++ b/recipe_scrapers/realsimple.py @@ -1,6 +1,8 @@ # mypy: disallow_untyped_defs=False +import re + from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string +from ._utils import get_yields class RealSimple(AbstractScraper): @@ -8,31 +10,22 @@ class RealSimple(AbstractScraper): def host(cls): return "realsimple.com" + def author(self): + return self.schema.author() + def title(self): - return self.soup.find("h1").get_text(strip=True) + return self.schema.title() def total_time(self): - return get_minutes(self.soup.findAll("div", {"class": "recipe-meta-item"})[1]) + return self.schema.total_time() def yields(self): return get_yields( - self.soup.findAll("div", {"class": "recipe-meta-item"})[2] - .find("div", {"class": "recipe-meta-item-body"}) - .get_text() + self.soup.find("div", string=re.compile(r"Yield:")).parent.get_text() ) def ingredients(self): - ingredients = self.soup.find("div", {"class": "ingredients"}).findAll("li") - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.findAll("div", {"class": "step"}) - - return "\n".join( - [ - normalize_string(instruction.find("p").get_text()) - for instruction in instructions - if instruction.find("p") is not None - ] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/reishunger.py b/recipe_scrapers/reishunger.py index c44c0c4ce..63cfa437e 100644 --- a/recipe_scrapers/reishunger.py +++ b/recipe_scrapers/reishunger.py @@ -68,8 +68,4 @@ def instructions(self): return "\n".join(results) def ratings(self): - block = self.soup.find("div", {"class": "nrating"}) - if block: - cnt = len(block.findAll("span", {"class": "fa-star"})) - return cnt - return block + return self.schema.ratings() diff --git a/recipe_scrapers/sallysblog.py b/recipe_scrapers/sallysblog.py deleted file mode 100644 index 9debd593a..000000000 --- a/recipe_scrapers/sallysblog.py +++ /dev/null @@ -1,43 +0,0 @@ -# mypy: disallow_untyped_defs=False -from ._abstract import AbstractScraper -from ._utils import get_minutes, normalize_string - - -class SallysBlog(AbstractScraper): - @classmethod - def host(cls): - return "sallys-blog.de" - - def title(self): - return normalize_string( - self.soup.find("h1", {"class": "blog--detail-headline"}).get_text() - ) - - def total_time(self): - return get_minutes(self.soup.find("span", {"id": "zubereitungszeit"})) - - def yields(self): - amount = self.soup.find("input", {"class": "float-left"}).get("value") - unit = self.soup.find("span", {"id": "is_singular"}).get_text() - - return f"{amount} {unit}" - - def ingredients(self): - ingredients = self.soup.findAll("li", {"class": "quantity"}) - - return [normalize_string(i.get_text()) for i in ingredients] - - def instructions(self): - instructionBlock = self.soup.find( - "div", {"class": "blog--detail-description block"} - ) - instructions = instructionBlock.findAll( - "div", {"class": ["content_type_2", "content_type_3", "content_type_4"]} - ) - - return "\n".join( - [ - normalize_string(instruction.find("p").get_text()) - for instruction in instructions - ] - ) diff --git a/recipe_scrapers/saveur.py b/recipe_scrapers/saveur.py index b80a83822..22f0cd3ae 100644 --- a/recipe_scrapers/saveur.py +++ b/recipe_scrapers/saveur.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class Saveur(AbstractScraper): @@ -15,28 +14,13 @@ def title(self): return self.soup.find("h1").get_text() def total_time(self): - prep_time = self.soup.find("meta", {"property": "prepTime"}) - cook_time = self.soup.find("meta", {"property": "cookTime"}) - return sum( - [ - get_minutes(prep_time.get("content")) if prep_time else 0, - get_minutes(cook_time.get("content")) if cook_time else 0, - ] - ) + return self.schema.total_time() def yields(self): - return get_yields( - self.soup.find("span", {"property": "recipeYield"}).get_text() - ) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.findAll("li", {"property": "recipeIngredient"}) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.findAll("li", {"property": "recipeInstructions"}) - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/seriouseats.py b/recipe_scrapers/seriouseats.py index fdd719557..944f15fa5 100644 --- a/recipe_scrapers/seriouseats.py +++ b/recipe_scrapers/seriouseats.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_yields class SeriousEats(AbstractScraper): @@ -18,11 +17,7 @@ def total_time(self): return self.schema.total_time() def yields(self): - recipe_servings = self.soup.find("div", {"class": "recipe-serving"}) - recipe_yield = self.soup.find("div", {"class": "recipe-yield"}) - return get_yields( - (recipe_servings or recipe_yield).find("span", {"class": "meta-text__data"}) - ) + return self.schema.yields() def ingredients(self): return self.schema.ingredients() diff --git a/recipe_scrapers/simplyquinoa.py b/recipe_scrapers/simplyquinoa.py index 6abc4a2ee..1aac25ad5 100644 --- a/recipe_scrapers/simplyquinoa.py +++ b/recipe_scrapers/simplyquinoa.py @@ -1,6 +1,6 @@ # mypy: disallow_untyped_defs=False + from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class SimplyQuinoa(AbstractScraper): @@ -9,32 +9,19 @@ def host(cls): return "simplyquinoa.com" def title(self): - return self.soup.find("h2", {"class": "wprm-recipe-name"}).get_text() + return self.schema.title() def total_time(self): - return get_minutes( - self.soup.find("span", {"class": "wprm-recipe-total_time"}).parent - ) + return self.schema.total_time() def yields(self): - yields = self.soup.find("span", {"class": "wprm-recipe-servings"}).get_text() - - return get_yields("{} servings".format(yields)) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.findAll("li", {"class": "wprm-recipe-ingredient"}) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.findAll( - "div", {"class": "wprm-recipe-instruction-text"} - ) - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() def ratings(self): - data = self.soup.find("span", {"class": "wprm-recipe-rating-average"}) - return round(float(data.get_text()), 2) if data else None + return self.schema.ratings() diff --git a/recipe_scrapers/southernliving.py b/recipe_scrapers/southernliving.py index 769aed8c3..a460dc5f8 100644 --- a/recipe_scrapers/southernliving.py +++ b/recipe_scrapers/southernliving.py @@ -1,13 +1,6 @@ # mypy: disallow_untyped_defs=False -# southernliving.com scraper -# Written by G.D. Wallters -# Freely released the code to recipe_scraper group -# 9 February, 2020 -# ======================================================= - from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class SouthernLiving(AbstractScraper): @@ -19,10 +12,10 @@ def title(self): return self.schema.title() def total_time(self): - return get_minutes(self.schema.total_time()) + return self.schema.total_time() def yields(self): - return get_yields(self.schema.yields()) + return self.schema.yields() def image(self): return self.schema.image() @@ -31,21 +24,7 @@ def ingredients(self): return self.schema.ingredients() def instructions(self): - instructions = self.soup.find("ul", {"class": "instructions-section"}).findAll( - "li", {"class": "instructions-section-item"} - ) - return "\n".join( - [ - normalize_string( - instruction.find("div", {"class": "paragraph"}).get_text() - ) - for instruction in instructions - ] - ) + return self.schema.instructions() def description(self): - des = self.soup.find( - "div", - attrs={"class": lambda e: e.startswith("recipe-summary") if e else False}, - ) - return normalize_string(des.get_text()) + return self.schema.description() diff --git a/recipe_scrapers/tastesoflizzyt.py b/recipe_scrapers/tastesoflizzyt.py index 8ca17e40f..f7858376e 100644 --- a/recipe_scrapers/tastesoflizzyt.py +++ b/recipe_scrapers/tastesoflizzyt.py @@ -1,6 +1,6 @@ # mypy: disallow_untyped_defs=False + from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class TastesOfLizzyT(AbstractScraper): @@ -9,28 +9,16 @@ def host(cls): return "tastesoflizzyt.com" def title(self): - return self.soup.find("h2", {"class": "wprm-recipe-name"}).get_text() + return self.schema.title() def total_time(self): - return get_minutes( - self.soup.find("div", {"class": "wprm-recipe-total-time-container"}) - ) + return self.schema.total_time() def yields(self): - return get_yields(self.soup.find("span", {"class": "wprm-recipe-servings"})) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.find( - "ul", {"class": "wprm-recipe-ingredients"} - ).findAll("li") - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.find( - "ul", {"class": "wprm-recipe-instructions"} - ).findAll("li") - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/tasty.py b/recipe_scrapers/tasty.py index 3f609d59c..36cfc028e 100644 --- a/recipe_scrapers/tasty.py +++ b/recipe_scrapers/tasty.py @@ -10,9 +10,6 @@ def host(cls): def title(self): return self.schema.title() - def total_time(self): - return self.schema.total_time() - def yields(self): return self.schema.yields() diff --git a/recipe_scrapers/thespruceeats.py b/recipe_scrapers/thespruceeats.py index 69f8f13d0..26d887dc9 100644 --- a/recipe_scrapers/thespruceeats.py +++ b/recipe_scrapers/thespruceeats.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, normalize_string class TheSpruceEats(AbstractScraper): @@ -9,52 +8,19 @@ def host(cls): return "thespruceeats.com" def title(self): - return self.soup.find("h1", {"class": "heading__title"}).get_text() + return self.schema.title() def total_time(self): - return get_minutes( - self.soup.find("span", string="Total: ").find_next_sibling( - "span", {"class": "meta-text__data"} - ) - ) + return self.schema.total_time() def yields(self): - return ( - self.soup.find("span", string="Servings: ") - .find_next_sibling("span", {"class": "meta-text__data"}) - .get_text() - ) + return self.schema.yields() def image(self): - image = self.soup.find("img", {"class": "primary-image"}) - return image["src"] if image else None + return self.schema.image() def ingredients(self): - """ - It uses two class to get the ingredient list items since sometimes 'ingredient' class is - present for simple recipes but if the recipe contains 2 or more sub-recipe / nested recipe - than 'structured-ingredients__list-item' class is present. - In any case only one class is present. - """ - ingredients = self.soup.find( - "section", - {"class": "section--ingredients"}, - ).find_all("li", {"class": ["structured-ingredients__list-item", "ingredient"]}) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - nested_instructions = self.soup.find( - "section", {"class": "section--instructions"} - ).find_all("ol") - - instructions = [] - for each_instruction in nested_instructions: - instructions = instructions + (each_instruction.find_all("li")) - - return "\n".join( - [ - normalize_string(instruction.find("p").get_text()) - for instruction in instructions - ] - ) + return self.schema.instructions() diff --git a/recipe_scrapers/twopeasandtheirpod.py b/recipe_scrapers/twopeasandtheirpod.py index d1cb5a324..14331bb29 100644 --- a/recipe_scrapers/twopeasandtheirpod.py +++ b/recipe_scrapers/twopeasandtheirpod.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, get_yields, normalize_string class TwoPeasAndTheirPod(AbstractScraper): @@ -9,34 +8,19 @@ def host(cls): return "twopeasandtheirpod.com" def title(self): - return self.soup.find("h2", {"class": "wprm-recipe-name"}).get_text() + return self.schema.title() def total_time(self): - minutes = self.soup.select_one(".wprm-recipe-total_time").get_text() - unit = self.soup.select_one(".wprm-recipe-total_time-unit").get_text() - - return get_minutes("{} {}".format(minutes, unit)) + return self.schema.total_time() def yields(self): - return get_yields( - self.soup.select_one( - "div.wprm-recipe-details-container dl:nth-of-type(5) dd" - ).get_text() - ) + return self.schema.yields() def ingredients(self): - ingredients = self.soup.findAll("li", {"class": "wprm-recipe-ingredient"}) - - return [normalize_string(ingredient.get_text()) for ingredient in ingredients] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.select(".wprm-recipe-instruction-text") - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() def image(self): - image = self.soup.find("div", {"class": "wprm-recipe-image"}).find("img") - - return image["src"] if image else None + return self.schema.image() diff --git a/recipe_scrapers/whatsgabycooking.py b/recipe_scrapers/whatsgabycooking.py index d9efafb21..203899c50 100644 --- a/recipe_scrapers/whatsgabycooking.py +++ b/recipe_scrapers/whatsgabycooking.py @@ -1,6 +1,5 @@ # mypy: disallow_untyped_defs=False from ._abstract import AbstractScraper -from ._utils import get_minutes, normalize_string class WhatsGabyCooking(AbstractScraper): @@ -9,26 +8,16 @@ def host(cls): return "whatsgabycooking.com" def title(self): - return self.soup.find("h1", {"class": "entry-title"}).get_text() + return self.schema.title() def total_time(self): - return get_minutes(self.soup.find("p", {"class": "header-recipe-time"})) + return self.schema.total_time() def yields(self): return self.schema.yields() def ingredients(self): - ingredients = self.soup.findAll("li", {"class": "wprm-recipe-ingredient"}) - - return [ - normalize_string(ingredient.get_text()) - for ingredient in ingredients - if len(ingredient) > 0 - ] + return self.schema.ingredients() def instructions(self): - instructions = self.soup.findAll("li", {"class": "wprm-recipe-instruction"}) - - return "\n".join( - [normalize_string(instruction.get_text()) for instruction in instructions] - ) + return self.schema.instructions() diff --git a/tests/test_750g.py b/tests/test_750g.py index 9e7ebce75..396f0a176 100644 --- a/tests/test_750g.py +++ b/tests/test_750g.py @@ -28,7 +28,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "500g de carottes", "3 jus d'orange", diff --git a/tests/test_abril.py b/tests/test_abril.py index d9cef61cf..d1b789826 100644 --- a/tests/test_abril.py +++ b/tests/test_abril.py @@ -34,7 +34,7 @@ def test_yields(self): self.assertEqual("4 servings", self.harvester_class.yields()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "500 gramas de alcatra cortada em tirinhas", "1/4 xícara (chá) de manteiga", diff --git a/tests/test_allrecipes.py b/tests/test_allrecipes.py index f62597584..92241a27a 100644 --- a/tests/test_allrecipes.py +++ b/tests/test_allrecipes.py @@ -46,7 +46,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "¼ cup olive oil", "1 tablespoon minced garlic", @@ -112,7 +112,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "2 tablespoons white sugar", "1 tablespoon brown sugar", diff --git a/tests/test_amazingribs.py b/tests/test_amazingribs.py index b9c1ac533..01040191b 100644 --- a/tests/test_amazingribs.py +++ b/tests/test_amazingribs.py @@ -28,7 +28,7 @@ def test_yields(self): self.assertEqual("2 servings", self.harvester_class.yields()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "2 teaspoons whole black peppercorns", "2 teaspoons fresh ground black pepper", diff --git a/tests/test_ambitiouskitchen.py b/tests/test_ambitiouskitchen.py index 9a5069072..1b3bc5dc1 100644 --- a/tests/test_ambitiouskitchen.py +++ b/tests/test_ambitiouskitchen.py @@ -38,7 +38,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "For the noodles:", "10 lasagna noodles", diff --git a/tests/test_archanaskitchen.py b/tests/test_archanaskitchen.py index bf22fc64b..2bd266a6d 100644 --- a/tests/test_archanaskitchen.py +++ b/tests/test_archanaskitchen.py @@ -34,7 +34,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "4 tablespoons Extra Virgin Olive Oil", "4 tablespoons Lemon juice", diff --git a/tests/test_arla.py b/tests/test_arla.py index 6db6d1cd2..e69ec8072 100644 --- a/tests/test_arla.py +++ b/tests/test_arla.py @@ -28,7 +28,7 @@ def test_yields(self): def test_image(self): self.assertEqual( - "https://cdn-rdb.arla.com/Files/arla-se/235235459/f96b874a-5b9c-4936-b3e2-5071e76136c0.jpg?mode=crop&w=1200&h=630&scale=both&format=jpg&quality=80&ak=f525e733&hm=35af1404", + "https://cdn-rdb.arla.com/Files/arla-se/235235459/f96b874a-5b9c-4936-b3e2-5071e76136c0.jpg?mode=crop&w=1300&h=525&ak=f525e733&hm=1de43e21", self.harvester_class.image(), ) @@ -41,7 +41,7 @@ def test_ingredients(self): "8 skivor surdegsbröd", "50 g Arla® Svenskt Smör, smält", "2 saltgurkor", - "½ gul paprika", + "½ grön paprika", "1 schalottenlök", "1 dl majonnäs", "1 msk chilisås", @@ -54,14 +54,7 @@ def test_ingredients(self): def test_instructions(self): self.assertEqual( - "Dressing:\nBörja med dressingen. Finhacka paprika och lök. Blanda med övr" - "iga ingredienser och ställ åt sidan.\nSkär köttet i tunna skivor. Låt sur" - "kålen rinna av ordentligt och blanda med osten.\nPensla bröden med smör p" - "å båda sidor. Lägg surkål och ost på hälften av bröden. Lägg på köttet. K" - "licka dressingen över köttet och lägg på resterande bröd.\nGrilla på båda" - " sidor i en het grillpanna eller smörgåsgrill.\nDela grillspett på mitten" - " och trä genom mackorna. Skär saltgurkorna på längden och fäst på grillsp" - "etten.", + "Dressing:\nBörja med dressingen. Finhacka paprika och lök. Blanda med övriga ingredienser och ställ åt sidan.\nSista instruktionen\nSkär köttet i tunna skivor. Låt surkålen rinna av ordentligt och blanda med osten.\nPensla bröden med smör på båda sidor. Lägg surkål och ost på hälften av bröden. Lägg på köttet. Klicka dressingen över köttet och lägg på resterande bröd.\nGrilla på båda sidor i en het grillpanna eller smörgåsgrill.\nDela grillspett på mitten och trä genom mackorna. Skär saltgurkorna på längden och fäst på grillspetten.", self.harvester_class.instructions(), ) diff --git a/tests/test_atelierdeschefs.py b/tests/test_atelierdeschefs.py index da70e3cae..c52befb28 100644 --- a/tests/test_atelierdeschefs.py +++ b/tests/test_atelierdeschefs.py @@ -28,7 +28,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "Lait 1/2 \u00e9cr\u00e9m\u00e9 : 25 cl", "Beurre doux : 120 g", diff --git a/tests/test_averiecooks.py b/tests/test_averiecooks.py index 4363eab57..ec2f4391a 100644 --- a/tests/test_averiecooks.py +++ b/tests/test_averiecooks.py @@ -17,7 +17,7 @@ def test_canonical_url(self): def test_title(self): self.assertEqual( - self.harvester_class.title(), "Balsamic Watermelon and Cucumber Salad" + self.harvester_class.title(), "Balsamic Watermelon Cucumber Salad" ) def test_author(self): @@ -33,7 +33,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "500 millilters balsamic vinegar", "1 cup granulated sugar, divided", @@ -48,8 +48,6 @@ def test_ingredients(self): def test_instructions(self): return self.assertEqual( - """To a high-sided medium/large kettle (use one bigger than you think you need), add the vinegar, 1/2 cup sugar, and heat over medium to medium-high until mixture boils and can sustain a fast rolling boil. Boil for about 15 to 20 minutes, or until reduced by about 80% and has thickened and is syrupy; stir intermittently and keep an eye on it so it doesn't bubble over. -When the sauce looks like it's about halfway done, taste the sauce, and if it's too vinegary and bitter for you, add part of or all of the remaining sugar. I personally use almost 1 cup. Sauce will thicken up more as it cools. Alternatively, you can use store bought balsamic glaze. -To a medium bowl, add all the remaining ingredients, stir to combine, and drizzle as much of the balsamic reduction as desired. You will have lots of balsamic reduction leftover, but it will keep for weeks in a sealed container in the fridge. As long as you're going to the trouble to make it, you may as well have extra for future recipes, because it's great drizzled over chicken, salmon, etc.""", + "To a high-sided medium/large kettle (use one bigger than you think you need), add the vinegar, 1/2 cup sugar, and heat over medium to medium-high until mixture boils and can sustain a fast rolling boil.\nBoil for about 15 to 20 minutes, or until reduced by about 80% and has thickened and is syrupy; stir intermittently and keep an eye on it so it doesn't bubble over.\nWhen the sauce looks like it's about halfway done, taste the sauce, and if it's too vinegary and bitter for you, add part of or all of the remaining sugar. I personally use almost 1 cup. Sauce will thicken up more as it cools.\nTo a medium bowl, add all the remaining ingredients, stir to combine, and drizzle as much of the balsamic reduction as desired.", self.harvester_class.instructions(), ) diff --git a/tests/test_bakingmischeif.py b/tests/test_bakingmischeif.py index bd99633fb..d5cdeade6 100644 --- a/tests/test_bakingmischeif.py +++ b/tests/test_bakingmischeif.py @@ -28,7 +28,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "1 boneless 2-3 pound chuck roast ( trimmed and cut into fist-size chunks)", "Salt and pepper", diff --git a/tests/test_bakingsense.py b/tests/test_bakingsense.py index d98f3d4a5..c1ce3cb67 100644 --- a/tests/test_bakingsense.py +++ b/tests/test_bakingsense.py @@ -27,35 +27,36 @@ def test_total_time(self): self.assertEqual(60, self.harvester_class.total_time()) def test_yields(self): - self.assertEqual("12 servings", self.harvester_class.yields()) + self.assertEqual("16 servings", self.harvester_class.yields()) def test_image(self): self.assertEqual( - "https://www.baking-sense.com/wp-content/uploads/2020/05/sourdough-bundt-9a-480x480.jpg", + "https://www.baking-sense.com/wp-content/uploads/2020/05/sourdough-bundt-9a.jpg", self.harvester_class.image(), ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ - "2 large eggs plus 2 yolks, room temperature", + "2 large eggs (room temperature)", + "2 large yolks (room temperature)", "1 tablespoon vanilla extract", - "1 cup sourdough discard (100% hydration), room temperature", - "2 cups (9 oz, 252g) cake flour", - "1 1/3 cups (11 oz, 308g) granulated sugar", + "8 oz sourdough discard (1 cup, room temperature)", + "9 oz cake flour (2 cups)", + "11 oz granulated sugar (1 1/3 cups)", "2 teaspoons baking powder", "1/2 teaspoon table salt", - '1 1/2 sticks (6 oz, 168g) unsalted butter, room temperature, cut into 1" chunks', - "1/2 cup (4oz, 120ml) buttermilk , room temperature", - "2 cups (8 oz, 224g) confectioner's sugar", + '6 oz unsalted butter (room temperature, cut into 1" chunks)', + "4 oz buttermilk (1/2 cup, room temperature)", + "8 oz confectioner's sugar (2 cups)", "1 teaspoon vanilla", - "1/4 cup (2 oz, 60ml) buttermilk", + "2 oz buttermilk (1/4 cup)", ], self.harvester_class.ingredients(), ) def test_instructions(self): return self.assertEqual( - "Preheat the oven to 350°F. Generously butter and flour a 12 cup Bundt pan.\nWhisk together the eggs, yolks, vanilla and the discard, set aside.\nSift the flour, sugar, baking powder and salt into a mixer bowl. Mix on low speed to combine the dry ingredients. With the mixer running, toss the chunks of butter into the flour mixture.\nAdd the buttermilk and increase the speed to medium. Mix on medium high for 2 minutes to aerate the batter. Scrape the bowl and beater.\nAdd the egg mixture in 3 batches, scraping the bowl between each addition. Pour the batter into the prepared pan.\nBake until the cake springs back when lightly pressed or a toothpick inserted into the center comes out clean, about 40 minutes.\nCool for 10 minutes in the pan. Invert the cake onto a cooling rack set over a clean sheet pan. Cool until slightly warm before glazing.\nCombine the sugar, vanilla and buttermilk in a small bowl and whisk until smooth.\nPour the glaze over the still slightly warm cake. You can scoop up the glaze from the sheet pan and use it to fill in any gaps in the glaze or leave it with the drips.\nCool completely and allow the glaze to set. Transfer to a serving plate.", + "Preheat the oven to 350°F. Generously butter and flour a 12 cup Bundt pan.\nMake the batter\nWhisk together the eggs, yolks, vanilla and the discard, set aside.\nSift the flour, sugar, baking powder and salt into a mixer bowl. Mix on low speed to combine the dry ingredients. With the mixer running, toss the chunks of butter into the flour mixture.\nAdd the buttermilk and increase the speed to medium. Mix on medium high for 2 minutes to aerate the batter. Scrape the bowl and beater.\nAdd the egg mixture in 3 batches, scraping the bowl between each addition. Pour the batter into the prepared pan.\nBake until the cake springs back when lightly pressed or a toothpick inserted into the center comes out clean, about 40 minutes.\nCool for 10 minutes in the pan. Invert the cake onto a cooling rack set over a clean sheet pan. Cool until slightly warm before glazing.\nMake the Glaze\nCombine the sugar, vanilla and buttermilk in a small bowl and whisk until smooth.\nPour the glaze over the still slightly warm cake. You can scoop up the glaze from the sheet pan and use it to fill in any gaps in the glaze or leave it with the drips.\nCool completely and allow the glaze to set. Transfer to a serving plate.", self.harvester_class.instructions(), ) diff --git a/tests/test_bbc.py b/tests/test_bbc.py index a0da25794..ade86ec50 100644 --- a/tests/test_bbc.py +++ b/tests/test_bbc.py @@ -39,7 +39,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "100g/3½oz butter", "250g/8¾oz digestive biscuits, crushed", diff --git a/tests/test_bbcgoodfood.py b/tests/test_bbcgoodfood.py index 1e10e2ce6..0e39e7311 100644 --- a/tests/test_bbcgoodfood.py +++ b/tests/test_bbcgoodfood.py @@ -34,7 +34,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "250g self-raising flour", "25g cocoa powder", diff --git a/tests/test_bigoven.py b/tests/test_bigoven.py index 8107a21fd..a72d0ac54 100644 --- a/tests/test_bigoven.py +++ b/tests/test_bigoven.py @@ -34,7 +34,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "1 packet active dry yeast ; (or 2 ¼ teaspoons)", "2 cups warm water ; divided", diff --git a/tests/test_blueapron.py b/tests/test_blueapron.py index ec766c7af..c0d211bbd 100644 --- a/tests/test_blueapron.py +++ b/tests/test_blueapron.py @@ -37,7 +37,7 @@ def test_total_time(self): self.assertEqual(30, self.harvester_class.total_time()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "2 Pasture-Raised Eggs", "1 15.5 -Ounce Can Chickpeas", diff --git a/tests/test_bonappetit.py b/tests/test_bonappetit.py index 1108b1a91..c05a3f650 100644 --- a/tests/test_bonappetit.py +++ b/tests/test_bonappetit.py @@ -16,9 +16,7 @@ def test_canonical_url(self): ) def test_title(self): - self.assertEqual( - self.harvester_class.title(), "Pork Chops with Celery and Almond Salad" - ) + self.assertEqual(self.harvester_class.title(), "Pork Chops with Celery Salad") def test_author(self): self.assertEqual(self.harvester_class.author(), "Adam Rapoport") @@ -31,12 +29,12 @@ def test_yields(self): def test_image(self): self.assertEqual( - "https://assets.bonappetit.com/photos/59e4d7dc3279981dd6c79847/16:9/w_1000,c_limit/pork-chops-with-celery-and-almond-salad.jpg", + "https://assets.bonappetit.com/photos/59e4d7dc3279981dd6c79847/5:7/w_1936,h_2710,c_limit/pork-chops-with-celery-and-almond-salad.jpg", self.harvester_class.image(), ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "¼ cup dried unsweetened cranberries", "3 tablespoons unseasoned rice vinegar", diff --git a/tests/test_bowlofdelicious.py b/tests/test_bowlofdelicious.py index 4a98c71b0..9165ec539 100644 --- a/tests/test_bowlofdelicious.py +++ b/tests/test_bowlofdelicious.py @@ -33,9 +33,9 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ - '2 ahi tuna (yellowfin tuna) steaks ((about 4 oz. each, at least 1.5" thick))', + '2 ahi tuna (yellowfin tuna) steaks ((about 4 oz. each, 1" thick - see notes for thinner or thicker))', "2 tablespoons soy sauce", "1 tablespoon toasted sesame oil (see notes)", "1 tablespoon honey (see notes)", @@ -50,6 +50,6 @@ def test_ingredients(self): def test_instructions(self): return self.assertEqual( - "Pat the ahi tuna steaks dry with a paper towel. Place on a plate or inside a plastic bag.\nMix the soy sauce (2 tablespoons), toasted sesame oil (1 tablespoon), honey (1 tablespoon) kosher salt (1/2 teaspoon- OMIT if marinating for more than a couple hours, see notes), pepper (1/4 teaspoon), and cayenne pepper (1/4 teaspoon) until honey is fully dissolved. Pour over the ahi tuna steaks and turn over to coat completely. Optional: allow to marinate for at least 10 minutes, or up to overnight in the refrigerator. Also optional: Reserve a spoonful or two of the marinade before coating the fish for drizzling on top after you've cooked it.\nHeat a medium skillet (preferably non-stick or a well-seasoned cast iron skillet) on medium-high to high until very hot ( or medium medium-high for nonstick). I recommend giving cast iron 3-5 minutes to get hot and nonstick about 1 minute, depending on how thick it is.\nAdd the canola oil (1 tablespoon) to the hot pan. Sear the tuna for 2 minutes on each side for medium rare (1.5 minutes on each side for rare; 3 on each side for medium). (Note: different burners get hotter depending on your stove. Use your best judgement whether you use medium, medium-high, or high heat, as the marinade may burn if too high heat is used)\nRemove to a cutting board and allow to rest for at least 3 minutes. Slice into 1/2 inch slices and serve garnished with green onions, toasted sesame seeds, and a squeeze of fresh lime juice, if desired.", + "Pat the ahi tuna steaks dry with a paper towel. Place on a plate or inside a plastic bag.\nMix the soy sauce (2 tablespoons), toasted sesame oil (1 tablespoon), honey (1 tablespoon) kosher salt (1/2 teaspoon- OMIT if marinating for more than a couple hours, see notes), pepper (1/4 teaspoon), and cayenne pepper (1/4 teaspoon) until honey is fully dissolved. Pour over the ahi tuna steaks and turn over to coat completely. Optional: allow to marinate for at least 10 minutes, or up to overnight in the refrigerator. Also optional: Reserve a spoonful or two of the marinade before coating the fish for drizzling on top after you've cooked it.\nHeat a medium skillet (preferably non-stick or a well-seasoned cast iron skillet) on medium-high to high until very hot ( or medium medium-high for nonstick). I recommend giving cast iron 3-5 minutes to get hot and nonstick about 1 minute, depending on how thick it is.\nAdd the canola oil (1 tablespoon) to the hot pan. Sear the tuna for 1 - 1½ minutes on each side for medium rare ( 2 -2½ minutes for medium-well to well, 30 seconds for very rare. See notes - this will vary based on thickness of the tuna steaks). (Note: different burners get hotter depending on your stove. Use your best judgement whether you use medium, medium-high, or high heat, as the marinade may burn if too high heat is used)\nRemove to a cutting board. Slice into 1/2 inch slices and serve garnished with green onions, toasted sesame seeds, and a squeeze of fresh lime juice, if desired.", self.harvester_class.instructions(), ) diff --git a/tests/test_budgetbytes.py b/tests/test_budgetbytes.py index b854e39d4..7dd8849de 100644 --- a/tests/test_budgetbytes.py +++ b/tests/test_budgetbytes.py @@ -30,7 +30,7 @@ def test_yields(self): self.assertEqual("4 items", self.harvester_class.yields()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "2 Tbsp olive oil ($0.24)", "2 cloves garlic ($0.16)", diff --git a/tests/test_cdkitchen.py b/tests/test_cdkitchen.py index 194676a68..ad7b817ba 100644 --- a/tests/test_cdkitchen.py +++ b/tests/test_cdkitchen.py @@ -28,7 +28,7 @@ def test_yields(self): self.assertEqual("4 servings", self.harvester_class.yields()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "2 veal shoulder arm or blade steaks, cut 1 inch thick", "2 baking potatoes, cut lengthwise into 8 wedges", diff --git a/tests/test_chefkoch.py b/tests/test_chefkoch.py index 6919d4a4f..0d5661870 100644 --- a/tests/test_chefkoch.py +++ b/tests/test_chefkoch.py @@ -24,7 +24,7 @@ def test_author(self): def test_description(self): self.assertEqual( self.harvester_class.description(), - "Hackbraten supersaftig - saftiger Hackbraten mit viel Soße. Über 1110 Bewertungen und für lecker befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", + "Hackbraten supersaftig - saftiger Hackbraten mit viel Soße. Über 1290 Bewertungen und für lecker befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps!", ) def test_yields(self): @@ -32,12 +32,12 @@ def test_yields(self): def test_image(self): self.assertEqual( - "https://img.chefkoch-cdn.de/rezepte/1170311223132029/bilder/1158321/crop-960x540/hackbraten-supersaftig.jpg", + "https://img.chefkoch-cdn.de/rezepte/1170311223132029/bilder/1435439/crop-960x540/hackbraten-supersaftig.jpg", self.harvester_class.image(), ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "1 ½ Semmel(n) , altbacken", "2 Gewürzgurke(n)", @@ -45,13 +45,13 @@ def test_ingredients(self): "1 kl. Bund Petersilie", "2 EL Zitronensaft", "50 g Butter", - "600 g Hackfleisch , gemischt", + "600 g Hackfleisch, gemischt", "2 kleine Ei(er)", "125 ml Fleischbrühe", "125 ml Sahne", "1 EL Crème fraîche", - "1 TL Paprikapulver , edelsüß", - "Salz und Pfeffer , schwarzer", + "1 TL Paprikapulver, edelsüß", + "Salz und Pfeffer, schwarzer", "Cayennepfeffer", "Fett für die Form", ], @@ -60,6 +60,6 @@ def test_ingredients(self): def test_instructions(self): return self.assertEqual( - "Die Semmeln in Scheiben schneiden und mit Wasser \u00fcbergie\u00dfen, quellen lassen. Gut ausdr\u00fccken. Die Gew\u00fcrzgurken in sehr feine W\u00fcrfel schneiden. Zwiebeln ebenfalls in feine W\u00fcrfel schneiden.\n\n1 EL Butter erhitzen und die Zwiebeln glasig anschwitzen. Petersilie dazugeben. Zwiebel-Petersilienmischung in eine Sch\u00fcssel geben, Semmeln, Gew\u00fcrzgurken, Hackfleisch, Eier und Zitronensaft zuf\u00fcgen. Alles mit Salz, Cayennepfeffer und schwarzem Pfeffer w\u00fcrzen und kr\u00e4ftig durchkneten. \n\nDie restliche Butter schmelzen, eine Form fetten. Den Fleischteig zu einem Laib formen und in die Form legen. Auf der unteren Schiene 30 Minuten (Umluft 180\u00b0C) backen, dabei immer mit der fl\u00fcssigen Butter bestreichen. \n\nDie Fleischbr\u00fche erhitzen und mit der Sahne, der Cr\u00e8me fra\u00eeche und dem Paprikapulver verr\u00fchren. (Wer sehr viel So\u00dfe mag, kann die So\u00dfenmenge einfach verdoppeln). Die So\u00dfe \u00fcber den Hackbraten gie\u00dfen und weitere 10 - 15 Minuten garen. \n\nDazu passen hervorragend Salzkartoffeln.", + "Die Semmeln in Scheiben schneiden und mit Wasser übergießen, quellen lassen. Gut ausdrücken. Die Gewürzgurken in sehr feine Würfel schneiden. Zwiebeln ebenfalls in feine Würfel schneiden.\n\n1 EL Butter erhitzen und die Zwiebeln glasig anschwitzen. Petersilie dazugeben. \n\nZwiebel-Petersilienmischung in eine Schüssel geben. Semmeln, Gewürzgurken, Hackfleisch, Eier und Zitronensaft zufügen. Alles mit Salz, Cayennepfeffer und schwarzem Pfeffer würzen und kräftig durchkneten. \n\nDie restliche Butter schmelzen, eine Form fetten. Den Fleischteig zu einem Laib formen und in die Form legen. Auf der unteren Schiene 30 Minuten (Umluft 180 °C) backen, dabei immer mit der flüssigen Butter bestreichen. \n\nDie Fleischbrühe erhitzen und mit der Sahne, der Crème fraîche und dem Paprikapulver verrühren. (wer sehr viel Soße mag, kann die Soßenmenge einfach verdoppeln). Die Soße über den Hackbraten gießen und weitere 10 - 15 Minuten garen. \n\nDazu passen hervorragend Salzkartoffeln.", self.harvester_class.instructions(), ) diff --git a/tests/test_closetcooking.py b/tests/test_closetcooking.py index d4d6e4029..8435f1678 100644 --- a/tests/test_closetcooking.py +++ b/tests/test_closetcooking.py @@ -27,7 +27,7 @@ def test_yields(self): self.assertEqual("5 servings", self.harvester_class.yields()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "1 tablespoon oil", "1 pound chicken, boneless and skinless, diced", diff --git a/tests/test_comidinhasdochef.py b/tests/test_comidinhasdochef.py index 391889bbe..85da5122a 100644 --- a/tests/test_comidinhasdochef.py +++ b/tests/test_comidinhasdochef.py @@ -11,54 +11,51 @@ def test_host(self): def test_author(self): self.assertEqual("Pedro Cavalcanti", self.harvester_class.author()) + def test_canonical_url(self): + self.assertEqual( + "https://comidinhasdochef.com/coxa-de-frango-na-fritadeira-eletrica/", + self.harvester_class.canonical_url(), + ) + def test_title(self): - self.assertEqual("Pernil de Cordeiro com Vinho", self.harvester_class.title()) + self.assertEqual( + "Coxa de Frango na Fritadeira Elétrica", self.harvester_class.title() + ) def test_total_time(self): - self.assertEqual(105, self.harvester_class.total_time()) + self.assertEqual(30, self.harvester_class.total_time()) def test_yields(self): - self.assertEqual("6 porções", self.harvester_class.yields()) + self.assertEqual("5 servings", self.harvester_class.yields()) def test_image(self): self.assertEqual( - "https://comidinhasdochef.com/wp-content/uploads/2021/08/Pernil-de-Cordeiro-com-Vinho.jpg", + "https://comidinhasdochef.com/wp-content/uploads/2022/01/Coxa-de-Frango-na-Fritadeira-Elétrica00.jpg", self.harvester_class.image(), ) def test_ingredients(self): - expected_ingredients = [ - "1 pernil de cordeiro", - "600 ml de vinho branco", - "4 batatas descascadas e fatiadas", - "3 cebolas picadas", - "30 g de folhas de hortelã", - "30 g de tomilho fresco", - "2 cabeças de alho cortadas ao meio", - "30 g de alecrim", - "1 colher (sopa) de suco de limão", - "2 colheres (sopa) de sal", - "1 colher (sopa) de pimenta do reino", - "½ colher (sopa) de raspas de limão siciliano", - ] - self.assertEqual(expected_ingredients, self.harvester_class.ingredients()) + self.assertEqual( + [ + "500 g de coxas de frango", + "1 colher (sopa) de mostarda", + "1 colher (sopa) de azeite", + "2 colheres (sopa) de shoyo", + "1 unidade de limões espremidos", + "1 colher (sopa) de salsinha desidratada", + "1 dente de alho", + "0 e 1/2 colher (sopa) de colorau", + "pimenta do reino a gosto", + "sal a gosto", + ], + self.harvester_class.ingredients(), + ) def test_instructions(self): - expected_instructions = ( - "Limpe bem o pernil e faça furos com uma faca para que o tempero penetre bem;\n" - "Em seguida acomode o perfil em um saco próprio para alimentos e coloque o suco de limão siciliano por cima do pernil;\n" - "Adicione as raspas de limão, a cebola picada, as cabeças de alho, a pimenta do reino, o sal, as hortelãs, o alecrim, o tomilho e o vinho branco;\n" - "Amarre o saco e vá misturando bem;\n" - "Deixe o pernil para marinar de um dia para o outro;\n" - "Dica : Na metade desse tempo vire o pernil para que fique bem temperado;\n" - "No dia seguinte com o pernil já bem temperado pegue uma forma, forre com papel alumínio e acomode as batatas cortadas em rodelas por toda a forma;\n" - "Coloque o pernil em cima das batatas e coloque as cabeças de alho também na forma;\n" - "Use um pouco do tempero para regar o pernil, fazendo com que ele fique ainda mais suculento;\n" - "Em seguida cubra a forma com papel alumínio e leve para assar em forno pré aquecido a 220º C por cerca de 01 hora e meia;\n" - "Após esse tempo retire o papel alumínio e leve novamente para assar até dourar bem o pernil;\n" - "Em seguida retire do forno e prontinho, já pode servir." + self.assertEqual( + "Como preparar Coxa de Frango na Fritadeira Elétrica\nPasso 1\nEm uma tigela coloque as coxas de frango e adicione todos os ingredientes ( a mostarda, o azeite, o shoyu, o suco de limão, a salsa, o alho picado, o colorau, a pimenta do reino e o sal);\nPasso 2\nCom as mãos misture bem, massageando as coxas de frango para que o tempero pegue bem;\nPasso 3\nEm seguida tampe a tigela e reserve por 25-30 minutos;\nPasso 4\nPré aqueça sua fritadeira elétrica a 200º C por 5 minutos;\nPasso 5\nColoque as coxas de frango e deixe fritar por 10 minutos;\nPasso 6\nAbra a fritadeira, vire o frango e deixe por mais 10 minutos, mantendo sempre a temperatura de 200º C;\nPasso 7\nRetire as coxas de frango da fritadeira e sirva em seguida.", + self.harvester_class.instructions(), ) - self.assertEqual(expected_instructions, self.harvester_class.instructions()) def test_ratings(self): - self.assertEqual(5.0, self.harvester_class.ratings()) + self.assertEqual(4.9, self.harvester_class.ratings()) diff --git a/tests/test_cookeatshare.py b/tests/test_cookeatshare.py index 916e81b01..0aa51f74b 100644 --- a/tests/test_cookeatshare.py +++ b/tests/test_cookeatshare.py @@ -25,7 +25,7 @@ def test_total_time(self): self.assertEqual(None, self.harvester_class.total_time()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ '4 med. potatoes, pared & cut lengthwise, in 1/4" slices', "1 lg. carrot, pared & sliced", diff --git a/tests/test_cookieandkate.py b/tests/test_cookieandkate.py index ec3735c4a..a8cf30b6d 100644 --- a/tests/test_cookieandkate.py +++ b/tests/test_cookieandkate.py @@ -30,7 +30,7 @@ def test_yields(self): self.assertEqual("8 servings", self.harvester_class.yields()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "8 SimplyNature Organic Cage Free Eggs", "½ cup milk of choice", diff --git a/tests/test_cookpad.py b/tests/test_cookpad.py index fce4c4336..3c1c53485 100644 --- a/tests/test_cookpad.py +++ b/tests/test_cookpad.py @@ -15,7 +15,7 @@ def test_canonical_url(self): ) def test_title(self): - self.assertEqual(self.harvester_class.title(), "30分で簡単本格バターチキンカレー") + self.assertEqual(self.harvester_class.title(), "30分で簡単♡本格バターチキンカレー♡") def test_author(self): self.assertEqual(self.harvester_class.author(), "reoririna") @@ -30,7 +30,7 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "♥鶏モモ肉 500g前後", "♥玉ねぎ 2個", diff --git a/tests/test_cookstr.py b/tests/test_cookstr.py index 6637df285..ef53c0c9d 100644 --- a/tests/test_cookstr.py +++ b/tests/test_cookstr.py @@ -25,7 +25,7 @@ def test_total_yields_raises_exception(self): self.assertRaises(Exception, self.harvester_class.yields) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "1 recipe Chocolate Cake Mix", "1/2 cup coffee or water", diff --git a/tests/test_copykat.py b/tests/test_copykat.py index c285c3fa2..ebfa09bab 100644 --- a/tests/test_copykat.py +++ b/tests/test_copykat.py @@ -16,7 +16,9 @@ def test_canonical_url(self): ) def test_title(self): - self.assertEqual(self.harvester_class.title(), "Make Tender Beef Tips in Gravy") + self.assertEqual( + self.harvester_class.title(), "Make Tender Beef Tips and Gravy" + ) def test_author(self): self.assertEqual(self.harvester_class.author(), "Stephanie Manley") @@ -29,12 +31,12 @@ def test_yields(self): def test_image(self): self.assertEqual( - "https://copykat.com/wp-content/uploads/2016/05/Beef-Tips-in-the-Instant-Pot-2-1.jpg", + "https://copykat.com/wp-content/uploads/2020/11/Beef-Tips-and-Gravy-Pin2.jpg", self.harvester_class.image(), ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "1 cup flour", "1 teaspoon salt", @@ -44,14 +46,14 @@ def test_ingredients(self): "1 tablespoon vegetable oil", "2 teaspoons gravy master", "1 cup onions (chopped)", - "16 ounces noodles", + "16 ounces noodles (or rice for serving)", ], self.harvester_class.ingredients(), ) def test_instructions(self): return self.assertEqual( - "In a small bowl add flour, salt, and pepper. Stir the salt and pepper into the flour. Cut and trim roast into small bite-sized pieces. Dredge beef pieces in seasoned flour shake off excess flour.\nSet the Instant Pot to saute, add oil. When the oil has heated drop in several pieces of the beef. Cook seasoned beef on all sides until lightly browned. Cook beef in small batches. When all of the beef is cooked add it back to the Instant Pot.\nAdd 1 cup of onion, two teaspoons of Gravy Master, and beef stock. Place lid on high and cook for 15 minutes on high pressure. Release pot after cooking with either a quick release or a natural release.\nSlow cooker directions\nPlease use the same ingredients as listed below. For this recipe season your flour as mentioned in the recipe, then brown the meat in a large skillet in small batches with some vegetable oil. Add the beef broth you will simmer for 4 to 6 hours on low. If the liquid hasn't thickened up to your desire, you can thicken it up by mixing 1 tablespoon of butter and one tablespoon of flour that has been mixed together. Stir this in to the beef broth, and it will thicken up the liquid in the slow cooker.", + "In a small bowl add flour, salt, and pepper. Stir the salt and pepper into the flour. Cut and trim roast into small bite-sized pieces. Dredge beef pieces in seasoned flour shake off excess flour.\nInstant Pot Directions\nSet the Instant Pot to saute, add oil. When the oil has heated drop in several pieces of the beef. Cook seasoned beef on all sides until lightly browned. Cook beef in small batches. When all of the beef is cooked add it back to the Instant Pot.\nAdd 1 cup of onion, two teaspoons of Gravy Master, and beef stock. Place lid on high and cook for 15 minutes on high pressure. Release pot after cooking with either a quick release or a natural release.\nSlow Cooker Directions\nBrown the beef in a large skillet in small batches with some vegetable oil. Add the browned beef, beef broth, onion, and Gravy Master to the slow cooker. Cook for 4 to 6 hours on low.\nIf the liquid hasn't thickened up to your desired consistency, you can thicken it up by mixing 1 tablespoon of butter and one tablespoon of flour together. Stir this into the liquid and it will thicken up the gravy in the slow cooker.\nServing\nPrepare noodles or rice according to package instructions.\nServe beef tips and gravy over noodles or rice.", self.harvester_class.instructions(), ) @@ -60,6 +62,6 @@ def test_ratings(self): def test_description(self): self.assertEqual( - "You can make amazingly tender beef tips in gravy.", + "You can make amazingly tender beef tips and gravy in an Instant Pot or slow cooker.", self.harvester_class.description(), ) diff --git a/tests/test_countryliving.py b/tests/test_countryliving.py index 32b5b7c97..13ee697ce 100644 --- a/tests/test_countryliving.py +++ b/tests/test_countryliving.py @@ -20,9 +20,6 @@ def test_title(self): self.harvester_class.title(), "Roasted Mushroom and Bacon Dutch Baby" ) - def test_author(self): - self.assertEqual(self.harvester_class.author(), "Erika Dugan") - def test_total_time(self): self.assertEqual(70, self.harvester_class.total_time()) @@ -30,7 +27,7 @@ def test_yields(self): self.assertEqual("4 servings", self.harvester_class.yields()) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ "1 lb. mixed mushrooms (such as cremini, beech, or shiitake), roughly chopped 4 slices bacon, sliced", "3 large eggs", diff --git a/tests/test_cucchiaio.py b/tests/test_cucchiaio.py index c20223e57..e210958e7 100644 --- a/tests/test_cucchiaio.py +++ b/tests/test_cucchiaio.py @@ -9,6 +9,12 @@ class TestCucchiaioScraper(ScraperTest): def test_host(self): self.assertEqual("cucchiaio.it", self.harvester_class.host()) + def test_canonical_url(self): + self.assertEqual( + "https://www.cucchiaio.it/ricetta/pesce-spada-al-miele-millefiori-pomodorini-e-patatine-novelle/", + self.harvester_class.canonical_url(), + ) + def test_author(self): self.assertEqual("Il Cucchiaio d'Argento", self.harvester_class.author()) @@ -18,15 +24,9 @@ def test_title(self): self.harvester_class.title(), ) - def test_total_time(self): - self.assertEqual(60, self.harvester_class.total_time()) - - def test_yields(self): - self.assertEqual("4 items", self.harvester_class.yields()) - def test_image(self): self.assertEqual( - "https://statics.cucchiaio.it/content/cucchiaio/it/ricette/2017/10/pesce-spada-al-miele-millefiori-pomodorini-e-patatine-novelle/jcr:content/header-par/image-single.img10.jpg/1610381008015.jpg", + "https://www.cucchiaio.it/content/cucchiaio/it/ricette/2017/10/pesce-spada-al-miele-millefiori-pomodorini-e-patatine-novelle/jcr:content/header-par/image-single.img10.jpg/1610381008015.jpg", self.harvester_class.image(), ) @@ -52,9 +52,9 @@ def test_ingredients(self): def test_instructions(self): self.assertEqual( - "1. Iniziate la preparazione del pesce spada al miele millefiori, pomodorini e patatine novelle mettendo a marinare le fette di pesce. Poggiatele su un piatto, aggiungete alcune foglie di mirto, l'aglio a fettine, un giro d'olio e lasciatele marinare per dieci minuti. In un tegame scaldate tre cucchiai d'olio, adagiatevi le fette di pesce spada e cuocetele per circa 2 minuti per lato o comunque fino a quando si forma una crosticina. Trasferitele su una teglia e passatele per circa 5 minuti in forno a 200°. Sfornatele e fatele riposare al caldo per una decina di minuti.\n2. Intanto tagliate a cubetti i pomodorini, cospargeteli con un po' di sale, un pizzico di pepe, prezzemolo ed erba cipollina tritati finemente. Preparate la salsina al miele: in una casseruola scaldate l'aceto facendolo ridurre di un quarto, aggiungete il miele millefiori e assaggiate per controllarne il sapore, se troppo agro aggiungete altro miele. Lasciate raffreddare.\n3. Infine, incorporate il tutto ai pomodori, mescolate bene, aggiungete olio, pinoli, regolate di sale e fate riposare. Sbollentate le patatine in acqua salata, asciugatele e insaporitele in una padella con un filo d’olio.\n4. Disponete le fette di pesce spada sul piatto da portata: completate con le patatine e cospargete su tutto la salsina al miele.", + "Iniziate la preparazione del pesce spada al miele millefiori, pomodorini e patatine novelle mettendo a marinare le fette di pesce. Poggiatele su un piatto, aggiungete alcune foglie di mirto, l'aglio a fettine, un giro d'olio e lasciatele marinare per dieci minuti. In un tegame scaldate tre cucchiai d'olio, adagiatevi le fette di pesce spada e cuocetele per circa 2 minuti per lato o comunque fino a quando si forma una crosticina. Trasferitele su una teglia e passatele per circa 5 minuti in forno a 200°. Sfornatele e fatele riposare al caldo per una decina di minuti.\nIntanto tagliate a cubetti i pomodorini, cospargeteli con un po' di sale, un pizzico di pepe, prezzemolo ed erba cipollina tritati finemente. Preparate la salsina al miele: in una casseruola scaldate l'aceto facendolo ridurre di un quarto, aggiungete il miele millefiori e assaggiate per controllarne il sapore, se troppo agro aggiungete altro miele. Lasciate raffreddare.\nInfine, incorporate il tutto ai pomodori, mescolate bene, aggiungete olio, pinoli, regolate di sale e fate riposare. Sbollentate le patatine in acqua salata, asciugatele e insaporitele in una padella con un filo d’olio.\nDisponete le fette di pesce spada sul piatto da portata: completate con le patatine e cospargete su tutto la salsina al miele.", self.harvester_class.instructions(), ) def test_ratings(self): - self.assertEqual(None, self.harvester_class.ratings()) + self.assertEqual(4.0, self.harvester_class.ratings()) diff --git a/tests/test_cuisineaz.py b/tests/test_cuisineaz.py index 231256237..bf280509d 100644 --- a/tests/test_cuisineaz.py +++ b/tests/test_cuisineaz.py @@ -19,30 +19,31 @@ def test_title(self): self.assertEqual(self.harvester_class.title(), "Filet de saumon au four") def test_author(self): - self.assertEqual(self.harvester_class.author(), "CuisineAZ.com") + self.assertEqual(self.harvester_class.author(), "Cuisine AZ") def test_yields(self): self.assertEqual("4 servings", self.harvester_class.yields()) def test_image(self): self.assertEqual( - "https://img.cuisineaz.com/610x610/2014-02-24/i78436-filet-de-saumon-au-four.jpeg", + "https://img.cuisineaz.com/660x660/2014/02/24/i78436-filet-de-saumon-au-four.jpeg", self.harvester_class.image(), ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ - "600 g de filet de saumon", - "30 ml de huile d'olive", - "4 g de origan séché", - "sel, poivre du moulin", + "600 g Filet de saumon", + "30 ml Huile d'olive", + "4 g Origan, séché", + "1 pincée(s) sel", + "1 pincée(s) Poivre", ], self.harvester_class.ingredients(), ) def test_instructions(self): return self.assertEqual( - "Préchauffez le four à th.7 (210°C). Coupez le filet en pavés de saumon de même taille, correspondant au nombre de parts désirées, et déposez-les sur une plaque huilée ou dans un plat à four. Arrosez-les d'huile d'olive à leur tour. Salez et poivrez à votre convenance et parsemez d'origan.\nDisposez le plat ou la plaque au centre du four. Comptez environ 10 min pour un filet de 2 à 2,5 cm d'épaisseur. Le temps nécessaire à la bonne cuisson du saumon dépend non seulement de l'épaisseur des pavés mais aussi de la température réelle de votre four, c'est pourquoi il est important de vérifier régulièrement la cuisson du saumon à l'aide d'une fourchette.\nLorsque vos pavés de saumon sont cuits, servez-les immédiatement, accompagnés d'une bonne salade bien assaisonnée, et éventuellement d'une belle timbale de riz basmati.", + "Etape 1\nPréchauffez le four th.7 (210°C). Coupez le filet en pavés de saumon de même taille, correspondant au nombre de parts désirées, et déposez-les sur une plaque huilée ou dans un plat à four. Arrosez-les d'huile d'olive. Salez et poivrez à votre convenance et parsemez d'origan.\nEtape 2\nDisposez le plat ou la plaque au centre du four. Comptez environ 10 min pour un filet de 2 à 2,5 cm d'épaisseur. Le temps nécessaire à la bonne cuisson du saumon dépend non seulement de l'épaisseur des pavés mais aussi de la température réelle de votre four, c'est pourquoi il est important de vérifier régulièrement la cuisson du saumon à l'aide d'une fourchette.\nEtape 3\nLorsque vos pavés de saumon sont cuits, servez-les immédiatement, accompagnés d'une bonne salade bien assaisonnée, et éventuellement d'une belle timbale de riz basmati.", self.harvester_class.instructions(), ) diff --git a/tests/test_cybercook.py b/tests/test_cybercook.py index e059c43e2..ebd30cde2 100644 --- a/tests/test_cybercook.py +++ b/tests/test_cybercook.py @@ -34,24 +34,24 @@ def test_image(self): ) def test_ingredients(self): - self.assertCountEqual( + self.assertEqual( [ - "200 gr de molho de tomate", - "1 unidade de cebola picada", - "2 dentes de alho", - "1 lata de creme de leite sem soro", - "600 gr de peito de frango sem osso", - "2 colheres (sopa) de óleo de soja", - "sal a gosto", - "pimenta-do-reino branca a gosto", - "100 gr de champignon em conserva", + "Molho de tomate 200 gramas", + "Cebola 1 unidade", + "Alho 2 dentes", + "Creme de Leite 1 lata", + "Peito de Frango 600 gramas", + "Óleo de soja 2 colheres (sopa)", + "Sal a gosto", + "Pimenta-do-Reino Branca a gosto", + "Champignon em conserva 100 gramas", ], self.harvester_class.ingredients(), ) def test_instructions(self): return self.assertEqual( - "Primeiro corte o frango em cubinhos.\nEm uma panela média, coloque o óleo, a cebola e espere dourar.\nDepois coloque o frango o tablete de caldo de galinha e o sal a gosto, aqueça até o ponto de fritura.\nMexa bem e tampe meia panela para que crie água, espere.\nSumir a água e começar a fritura.\nQuando o frango já tiver dourado, acrescente o molho de tomate.\nDepois coloque a lata de creme de leite e mexa até espalhar, com a mesma lata encha de água.\nMexa mais uma vez até misturar e deixe levantar fervura.\nAcrescente o oregano e pronto.\nO strogonoff está pronto para ser servido.", + "Primeiro corte o frango em cubinhos.\nEm uma panela média, coloque o óleo, o alho e a cebola e espere dourar.\nDepois coloque o frango e o sal e a pimenta a gosto, aqueça até o ponto de fritura.\nMexa bem e tampe meia panela para que crie água, espere.\nSumir a água e começar a fritura.\nQuando o frango já tiver dourado, acrescente o molho de tomate e o champignon.\nDepois coloque a lata de creme de leite e mexa até espalhar, com a mesma lata encha de água.\nMexa mais uma vez até misturar e deixe levantar fervura.\nO strogonoff está pronto para ser servido.", self.harvester_class.instructions(), ) diff --git a/tests/test_data/arla.testhtml b/tests/test_data/arla.testhtml index abc9f70c8..9c0975832 100644 --- a/tests/test_data/arla.testhtml +++ b/tests/test_data/arla.testhtml @@ -1,1337 +1,1356 @@ - - - - - - - - - - - - - -Reuben sandwich - Recept | Arla - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - -
- -
-
- - - -
-
-
- - - - - Reuben sandwich - -
-
-
- - -
- -

Reuben sandwich

-
-
-
- - - - 40 min - Huvudrätt - Förrätt - Mellanmål -
-
-
-
-
-
-

En reuben sandwich är en amerikansk macka, vanligtvis med pastrami - men vi använder svensk oxbringa, saltgurka och så kallad rysk dressing.

- - -
-
- - Provlagat av Arla Mat - - Provlagat av Arla Mat -
-
- -
-
-
- -
-
-
-
-
-
-
-
- - - - - Klart! - -
-
-
-
-
-
-

Näringsvärden

-
Energi:
-

738 kcal

-
- - - - - - - - - - - - - - - - - - - -
Protein:28 g
Kolhydrater:49 g
Fett:48 g
-
-
- - -
- -
-
-
-
-

Ingredienser

-
-
-
-
-
-
-
-
- - - -
-
- -
-
-
- - - - - - - -
- - - - - - + + + + + + + + + +Reuben sandwich - Recept | Arla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+
+ + + +
+
+
+ + + + + Reuben sandwich + +
+
+
+ + +
+ +

Reuben sandwich

+
+
+
+ + + + 40 min + Huvudrätt + Förrätt + Mellanmål +
+
+
+
+
+
+ + +
+ +
+
En reuben sandwich är en amerikansk macka, vanligtvis med pastrami - men vi använder svensk oxbringa, saltgurka och så kallad rysk dressing.
+ + +
+
+ + Provlagat av Arla Mat + + Provlagat av Arla Mat +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+ + + + + Klart! + +
+
+
+
+
+
+
+

Näringsvärden

+
Energi:
+

738 kcal

+
+ + + + + + + + + + + + + + + + + + + +
Protein:28 g
Kolhydrater:49 g
Fett:48 g
+
+
+ + +
+ +
+
+
+
+

Ingredienser

+
+
+
+
+
+ + + + Matspar + + +
+
+
+
+ + + +
+
+ +
+
+
+ + + + + + + +
+ + + + + + + + + - - - - - - - - +})(); + + + + + + + + + diff --git a/tests/test_data/averiecooks.testhtml b/tests/test_data/averiecooks.testhtml index 0a121ae43..9e8b1adf4 100644 --- a/tests/test_data/averiecooks.testhtml +++ b/tests/test_data/averiecooks.testhtml @@ -1,23 +1,1319 @@ - + + Balsamic Watermelon and Cucumber Salad - Averie Cooks

Balsamic Watermelon and Cucumber Salad

This post may contain affiliate links.

Balsamic Watermelon and Cucumber Salad – An EASY, healthy, and light salad with watermelon, cucumber, arugula, goat cheese, candied nuts, and drizzled with a homemade balsamic glaze!! A PERFECT summer salad for those days when it’s too hot to cook!!

Balsamic Watermelon and Cucumber Salad - An EASY, healthy, and light salad with watermelon, cucumber, arugula, goat cheese, candied nuts, and drizzled with a homemade balsamic glaze!! A PERFECT summer salad for those days when it's too hot to cook!!

 

 

 

 

Balsamic Watermelon Cucumber Goat Cheese Salad 

This salad is a spinoff of a salad I love to eat at one of my favorite Puerto Vallarta beach bars. It’s refreshing, light, and the combination of all the flavors is just perfection.

There’s juicy sweet watermelon, cucumber, strong and peppery arugula, tanginess from goat cheese, crunchy candied nuts, and a homemade balsamic glaze. If you’ve never thought that watermelon can be delicious in a more savory application, think again because this salad will just knock your socks off.

It’s so fresh and perfect for hot summer days.

As written, this is an easy, healthy vegetarian and gluten-free recipe but you can make it vegan by skipping the goat cheese or using a vegan cheese.

Balsamic Watermelon and Cucumber Salad - An EASY, healthy, and light salad with watermelon, cucumber, arugula, goat cheese, candied nuts, and drizzled with a homemade balsamic glaze!! A PERFECT summer salad for those days when it's too hot to cook!!

Ingredients In Watermelon Cucumber Salad With Balsamic Glaze

You only need a handful of ingredients to make this salad. You’ll include:

  • Balsamic vinegar
  • Sugar
  • Watermelon 
  • Cucumber
  • Arugula
  • Goat cheese
  • Candies walnuts, pecans, or macadamia nuts

Balsamic Watermelon and Cucumber Salad - An EASY, healthy, and light salad with watermelon, cucumber, arugula, goat cheese, candied nuts, and drizzled with a homemade balsamic glaze!! A PERFECT summer salad for those days when it's too hot to cook!!

How To Make Homemade Balsamic Reduction

Making a balsamic glaze, or reduction, is as easy as boiling together balsamic vinegar and granulated sugar for about 15 to 20 minutes, or until the volume has reduced by about 80% and the sauce is thick and syrupy.

I used a 500 milliliter (about 17 ounces) bottle of balsamic vinegar and added close to one cup sugar.

You can start with 1/2 to 3/4 cup sugar and taste your sauce when it looks like it’s about halfway done, but for me that’s not quite sweet enough and it’s too intensely vinegary, so I went with closer to 1 cup for this batch.

Use a bigger kettle than what you think you need because the sauce will bubble up vigorously and ample room in your kettle is advantageous to avoid it from bubbling over.

You can also use store bought balsamic glaze. Trader Joe’s is one I tend to keep on hand for times like this and there are many other brands out there these days if you don’t have a Trader Joe’s in your area.

Balsamic Watermelon and Cucumber Salad – An EASY, healthy, and light salad with watermelon, cucumber, arugula, goat cheese, candied nuts, and drizzled with a homemade balsamic glaze!! A PERFECT summer salad for those days when it’s too hot to cook!!

Balsamic Watermelon and Cucumber Salad - An EASY, healthy, and light salad with watermelon, cucumber, arugula, goat cheese, candied nuts, and drizzled with a homemade balsamic glaze!! A PERFECT summer salad for those days when it's too hot to cook!!
Yield: 2

Balsamic Watermelon and Cucumber Salad

Prep Time 5 minutes
Cook Time 15 minutes
Total Time 20 minutes

Balsamic Watermelon and Cucumber Salad - An EASY, healthy, and light salad with watermelon, cucumber, arugula, goat cheese, candied nuts, and drizzled with a homemade balsamic glaze!! A PERFECT summer salad for those days when it's too hot to cook!!

Ingredients

  • 500 millilters balsamic vinegar
  • 1 cup granulated sugar, divided
  • 3 cups watermelon, seeded and cubed (I recommend seedless, firm watermelon)
  • 1 large cucumber or English cucumber, peeled and cubed
  • 1 cup argula (1 heaping handful)
  • 1/3 cup goat cheese, crumbled or as desired
  • 1/3 cup candied nuts, or as desired

Instructions

  1. To a high-sided medium/large kettle (use one bigger than you think you need), add the vinegar, 1/2 cup sugar, and heat over medium to medium-high until mixture boils and can sustain a fast rolling boil. Boil for about 15 to 20 minutes, or until reduced by about 80% and has thickened and is syrupy; stir intermittently and keep an eye on it so it doesn't bubble over.
  2. When the sauce looks like it's about halfway done, taste the sauce, and if it's too vinegary and bitter for you, add part of or all of the remaining sugar. I personally use almost 1 cup. Sauce will thicken up more as it cools. Alternatively, you can use store bought balsamic glaze.
  3. To a medium bowl, add all the remaining ingredients, stir to combine, and drizzle as much of the balsamic reduction as desired. You will have lots of balsamic reduction leftover, but it will keep for weeks in a sealed container in the fridge. As long as you're going to the trouble to make it, you may as well have extra for future recipes, because it's great drizzled over chicken, salmon, etc.

Nutrition Information:

Yield:

2

Serving Size:

1

Amount Per Serving: Calories: 4245Total Fat: 20gSaturated Fat: 8gTrans Fat: 0gUnsaturated Fat: 12gCholesterol: 17mgSodium: 1160mgCarbohydrates: 812gFiber: 4gSugar: 717gProtein: 34g

The nutrition information is taking into account ALL of the balsamic reduction and you won't need anywhere near all of it for this salad, probably just a few tablespoons, but it is skewing the data very extremely artificially high.

Related Recipes 

Grilled Toast with Strawberry Balsamic Mint Salsa – Impress your friends and family with these fun, FAST and EASY grilled toasts!! The salsa has so much FLAVOR from the strawberries, mint, lime, juice, and balsamic vinegar!!

Grilled Toast with Strawberry Balsamic Mint Salsa - Impress your friends and family with these fun, FAST and EASY grilled toasts!! The salsa has so much FLAVOR from the strawberries, mint, lime, juice, and balsamic vinegar!!

Grilled Chicken With Watermelon Mango Mint Salsa – The chicken is tender, juicy, and the EASY salsa made with watermelon, mango, and mint adds so much fresh FLAVOR!! Healthy, FAST, EASY, zero cleanup, perfect for backyard barbecues or easy weeknight dinners!!

Grilled Chicken With Watermelon Mango Mint Salsa - The chicken is tender, juicy, and the EASY salsa made with watermelon, mango, and mint adds so much fresh FLAVOR!! Healthy, FAST, EASY, zero cleanup, perfect for backyard barbecues or easy weeknight dinners!!

Blueberry Corn Salsa – Berries, corn, jalapeno, cilantro, and more in this easy and healthy salsa that’s ready in 5 minutes! So good you can eat it on it’s own like a salad! The sweet fruit balances the heat and it’s a guaranteed hit!

Blueberry Corn Salsa - Berries, corn, jalapeno, cilantro, and more in this EASY and healthy salsa that's ready in 5 minutes!! So good you can eat it on it's own like a salad! The sweet fruit balances the heat and it's a guaranteed HIT!!

We are a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com.

Leave a Reply

Please note: I have only made the recipe as written, and cannot give advice or predict what will happen if you change something. If you have a question regarding changing, altering, or making substitutions to the recipe, please check out the FAQ page for more info.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

    5 Comments on “Balsamic Watermelon and Cucumber Salad”

  1. Hi Averie. Today we had this Balsamic Watermelon and Cucumber Salad for dinner with grilled chicken. It knocked our socks off!!! Thank you. Textures, color, flavors — Wow! Hubby could not get enough. Each mouthful had its own surprise: first the pop of pepper from the arugula, second the tang of the cheese, then the sweet crunchiness of the nuts. My only problem was with the balsamic dressing itself. I cooked it as directed and after 15 minutes, it had the texture of candy. As it cooled it hardened, so there was no way I could drizzle it. I ended up just mixing balsamic vinegar and sugar until I liked the taste, and then drizzling that over the salad. No cooking at all. I’m not sure what I did wrong, but it turned out well.

    Rating: 5
    • Thanks for the 5 star review and glad that you loved the salad! I agree it’s so many great textures and flavors in every bite!

      As for the balsamic reduction, if it was that thick and had the texture of candy, you way overboiled it. You must have been boiling it at a pretty fast boil, or maybe your stove runs hot, or who knows, but you could safely stop and check after 7-8 mins next time if at 15 it was that dense. Now you have a feel for what you’re looking for and remember it thickens up as it cools, too.

  2. I have been wanting to make some kind of watermelon cucumber salad and this sounds deliciously different with goat cheese and nuts!!

  3. Hi Averie, Please revisit the calories posted for this recipe. Seems to be incorrect.

    Thank you,
    JoAnna

+ +
+
+ + +
+ + \ No newline at end of file diff --git a/tests/test_data/bakingsense.testhtml b/tests/test_data/bakingsense.testhtml index f4620ac6a..21fdec248 100644 --- a/tests/test_data/bakingsense.testhtml +++ b/tests/test_data/bakingsense.testhtml @@ -4,17 +4,16 @@ - + - + - + @@ -22,84 +21,365 @@ - - - + + + Sourdough Bundt Cake with Buttermilk Glaze - Baking Sense® + - - + + - - - + - - - - + - - + + - - - + + + @@ -129,8 +409,8 @@ var ck_data = {"ajaxurl":"https:\/\/www.baking-sense.com\/wp-admin\/admin-ajax.p - - Skip to Content + + Skip to Content
@@ -140,7 +420,7 @@ var ck_data = {"ajaxurl":"https:\/\/www.baking-sense.com\/wp-admin\/admin-ajax.p Search Magnifying Glass - +
@@ -162,15 +442,15 @@ var ck_data = {"ajaxurl":"https:\/\/www.baking-sense.com\/wp-admin\/admin-ajax.p
-
-
- - + + +
+
+
+ +
+ +
+

Sourdough Bundt Cake with Buttermilk Glaze

+ -

Sourdough Bundt Cake with Buttermilk Glaze

-
-
+ - -
-
-
-
-
-
-
+
+

Sourdough Bundt Cake with Buttermilk Glaze is a perfect snack cake. The tangy-sweet buttermilk glaze forms an ultra-thin coating. A little sourdough discard transforms an ordinary cake into an extraordinary treat.

-
+
-

If you don’t have one, you can learn How to Make a Sourdough Starter. Then I can show you how to Feed and Maintain Sourdough Starter or How to Keep a Small Sourdough Starter.

+

If you don’t already have one, I can show you how to make a sourdough starter and how to feed a sourdough starter.

-

How to use sourdough discard to make a great Bundt cake:

+

How to use sourdough discard to make a great Bundt cake:

@@ -249,19 +527,19 @@ var ck_data = {"ajaxurl":"https:\/\/www.baking-sense.com\/wp-admin\/admin-ajax.p -

Scroll through the process photos to see how to make a Bundt Cake enhanced with sourdough discard:

+

Scroll through the process photos to see how to make a Bundt Cake enhanced with sourdough discard:

-
A photo showing sourdough discard being added to eggs and vanilla
Add the discard to the eggs and vanilla and whisk the mixture until combined.
+
A photo showing sourdough discard being added to eggs and vanilla
Add the discard to the eggs and vanilla and whisk the mixture until combined.
-
three side by side photos showing sourdough bundt cake before and after baking and then cooling on a rack
Pour the batter into a generously buttered and floured bundt pan. Cool in the pan for 10 minutes then turn out onto a rack over a clean sheet pan.
+
three side by side photos showing sourdough bundt cake before and after baking and then cooling on a rack
Pour the batter into a generously buttered and floured bundt pan. Cool in the pan for 10 minutes then turn out onto a rack over a clean sheet pan.
-
a closeup shot showing glaze being poured onto a slightly warm bundt cake
Pour the glaze over the cake while it is still slightly warm. The glaze will melt a little and adhere to the cake before it sets.
+
a closeup shot showing glaze being poured onto a slightly warm bundt cake
Pour the glaze over the cake while it is still slightly warm. The glaze will melt a little and adhere to the cake before it sets.
@@ -273,7 +551,7 @@ var ck_data = {"ajaxurl":"https:\/\/www.baking-sense.com\/wp-admin\/admin-ajax.p -
+
@@ -281,428 +559,365 @@ var ck_data = {"ajaxurl":"https:\/\/www.baking-sense.com\/wp-admin\/admin-ajax.p -

If you love this recipe as much as I do, I’d really appreciate a 5-star review.

- - - -
- -
- -
+

If you love this recipe as much as I do, I’d really appreciate a 5-star review.

-
- a slice of sourdough bundt cake on a plate with a few strawberries
-

Sourdough Bundt Cake with Buttermilk Glaze

- -
- -
- Yield: - 12-16 -
- -
- Prep Time: - 20 minutes -
-
- Cook Time: - 40 minutes -
-
- Total Time: - 1 hour -
+
+
a slice of sourdough bundt cake on a plate with a few strawberries
+
+ Print Recipe + +
5 from 3 reviews
-
-

Sourdough Bundt Cake with Buttermilk Glaze is a perfect snack cake. The tangy-sweet buttermilk glaze forms an ultra-thin coating over the melt-in-your-mouth cake.

-
-
- -
- -
-
- -
-

Ingredients

- -

Sourdough Cake

-
    -
  • - 2 large eggs plus 2 yolks, room temperature
  • -
  • - 1 tablespoon vanilla extract
  • -
  • - 1 cup sourdough discard (100% hydration), room temperature
  • -
  • - 2 cups (9 oz, 252g) cake flour
  • -
  • - 1 1/3 cups (11 oz, 308g) granulated sugar
  • -
  • - 2 teaspoons baking powder
  • -
  • - 1/2 teaspoon table salt
  • -
  • - 1 1/2 sticks (6 oz, 168g) unsalted butter, room temperature, cut into 1" chunks
  • -
  • - 1/2 cup (4oz, 120ml) buttermilk , room temperature
  • -
-

Buttermilk Glaze

-
    -
  • - 2 cups (8 oz, 224g) confectioner's sugar
  • -
  • - 1 teaspoon vanilla
  • -
  • - 1/4 cup (2 oz, 60ml) buttermilk
  • -
-
-
-

Instructions

-
  1. Preheat the oven to 350°F. Generously butter and flour a 12 cup Bundt pan.

Make the batter

  1. Whisk together the eggs, yolks, vanilla and the discard, set aside.
  2. Sift the flour, sugar, baking powder and salt into a mixer bowl. Mix on low speed to combine the dry ingredients. With the mixer running, toss the chunks of butter into the flour mixture.
  3. Add the buttermilk and increase the speed to medium. Mix on medium high for 2 minutes to aerate the batter. Scrape the bowl and beater.
  4. Add the egg mixture in 3 batches, scraping the bowl between each addition. Pour the batter into the prepared pan.
  5. Bake until the cake springs back when lightly pressed or a toothpick inserted into the center comes out clean, about 40 minutes.
  6. Cool for 10 minutes in the pan. Invert the cake onto a cooling rack set over a clean sheet pan. Cool until slightly warm before glazing.

Make the Glaze

  1. Combine the sugar, vanilla and buttermilk in a small bowl and whisk until smooth.
  2. Pour the glaze over the still slightly warm cake. You can scoop up the glaze from the sheet pan and use it to fill in any gaps in the glaze or leave it with the drips.
  3. Cool completely and allow the glaze to set. Transfer to a serving plate.
- -
-
- - - -
-
-

Did you make this recipe?

-

Please leave a comment on the blog or share a photo on Instagram

-
-
- -
- -
- -
+
+
+
+ +
+
+ +
+ Recipe Rating +




+
- - - - - -
-
-
-
-

-

-
- -
- - -
-
-
-
Previous
-
Brown Butter Flourless Almond Cake
-
-
- - - -
- a chocolate shortcakes biscuit topped with raspberries and blackberries and whipped cream
-
-
Next
-
Chocolate Shortcakes with Chocolate Chips
-
-
- -
-
+

+
- -
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + - +if(is_iframe){iframe_count+=1}}});if(image_count>0||iframe_count>0||rocketlazy_count>0){lazyLoadInstance.update()}});var b=document.getElementsByTagName("body")[0];var config={childList:!0,subtree:!0};observer.observe(b,config)}},!1) + + \ No newline at end of file diff --git a/tests/test_data/chefkoch.testhtml b/tests/test_data/chefkoch.testhtml index 93069201e..0ef77668d 100644 --- a/tests/test_data/chefkoch.testhtml +++ b/tests/test_data/chefkoch.testhtml @@ -1,43 +1,168 @@ - - - - - - - - + + + - + + + Hackbraten supersaftig von Delphinella | Chefkoch - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - + + + + + + + - + - + - - + + - - + })">
- - - + - +
- - - + + + - - +key" class="i-amphtml-layout-container" i-amphtml-layout="container"> + + - + - +