From 6e91f3477941a61188186bade0b87b1be3763297 Mon Sep 17 00:00:00 2001 From: its_me_gb Date: Mon, 26 Apr 2021 16:55:08 +0100 Subject: [PATCH 1/4] add RecipeKeeper as an import/export method --- cookbook/forms.py | 3 +- cookbook/integration/recipekeeper.py | 59 ++++++++++++++++++++++++++++ cookbook/views/import_export.py | 3 ++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 cookbook/integration/recipekeeper.py diff --git a/cookbook/forms.py b/cookbook/forms.py index 39a230e4..56c584cc 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -112,6 +112,7 @@ class ImportExportBase(forms.Form): SAFRON = 'SAFRON' CHEFTAP = 'CHEFTAP' PEPPERPLATE = 'PEPPERPLATE' + RECIPEKEEPER = 'RECIPEKEEPER' RECIPESAGE = 'RECIPESAGE' DOMESTICA = 'DOMESTICA' MEALMASTER = 'MEALMASTER' @@ -120,7 +121,7 @@ class ImportExportBase(forms.Form): type = forms.ChoiceField(choices=( (DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'), - (PEPPERPLATE, 'Pepperplate'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), + (PEPPERPLATE, 'Pepperplate'), (RECIPEKEEPER, 'Recipe Keeper'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), )) diff --git a/cookbook/integration/recipekeeper.py b/cookbook/integration/recipekeeper.py new file mode 100644 index 00000000..dc375d4c --- /dev/null +++ b/cookbook/integration/recipekeeper.py @@ -0,0 +1,59 @@ +import re + +from django.utils.translation import gettext as _ + +from cookbook.helper.ingredient_parser import parse, get_food, get_unit +from cookbook.integration.integration import Integration +from cookbook.models import Recipe, Step, Food, Unit, Ingredient + + +class RecipeKeeper(Integration): + + def import_file_name_filter(self, zip_info_object): + return re.match(r'^recipes.html$', zip_info_object.filename) + + def get_recipe_from_file(self, file): + source_url = '' + + ingredient_mode = 0 + + ingredients = [] + directions = [] + for i, fl in enumerate(file.readlines(), start=0): + line = fl.decode("utf-8") + if i == 0: + title = line.strip() + else: + if line.startswith('https:') or line.startswith('http:'): + source_url = line.strip() + else: + if ingredient_mode == 1 and len(line.strip()) == 0: + ingredient_mode = 2 + if re.match(r'^([0-9])[^.](.)*$', line) and ingredient_mode < 2: + ingredient_mode = 1 + ingredients.append(line.strip()) + else: + directions.append(line.strip()) + + recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, ) + + step = Step.objects.create(instruction='\n'.join(directions)) + + if source_url != '': + step.instruction += '\n' + source_url + step.save() + + for ingredient in ingredients: + if len(ingredient.strip()) > 0: + amount, unit, ingredient, note = parse(ingredient) + f = get_food(ingredient, self.request.space) + u = get_unit(unit, self.request.space) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note + )) + recipe.steps.add(step) + + return recipe + + def get_file_from_recipe(self, recipe): + raise NotImplementedError('Method not implemented in storage integration') diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 12763067..36717441 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -19,6 +19,7 @@ from cookbook.integration.mealie import Mealie from cookbook.integration.mealmaster import MealMaster from cookbook.integration.nextcloud_cookbook import NextcloudCookbook from cookbook.integration.paprika import Paprika +from cookbook.integration.recipekeeper import RecipeKeeper from cookbook.integration.recipesage import RecipeSage from cookbook.integration.rezkonv import RezKonv from cookbook.integration.safron import Safron @@ -44,6 +45,8 @@ def get_integration(request, export_type): return Pepperplate(request, export_type) if export_type == ImportExportBase.DOMESTICA: return Domestica(request, export_type) + if export_type == ImportExportBase.RECIPEKEEPER: + return RecipeKeeper(request, export_type) if export_type == ImportExportBase.RECIPESAGE: return RecipeSage(request, export_type) if export_type == ImportExportBase.REZKONV: From eba3bfa82898e09999dedc4b70377290d523eac7 Mon Sep 17 00:00:00 2001 From: its_me_gb Date: Tue, 27 Apr 2021 15:32:07 +0100 Subject: [PATCH 2/4] Basic import functionality working --- cookbook/integration/integration.py | 13 ++++- cookbook/integration/recipekeeper.py | 74 +++++++++++++++------------- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 33ed72cf..ba55c58d 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -105,7 +105,18 @@ class Integration: try: self.files = files for f in files: - if '.zip' in f['name'] or '.paprikarecipes' in f['name']: + if 'RecipeKeeper' in f['name']: + import_zip = ZipFile(f['file']) + for z in import_zip.filelist: + if self.import_file_name_filter(z): + data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8')) + for d in data_list: + recipe = self.get_recipe_from_file(d) + recipe.keywords.add(self.keyword) + il.msg += f'{recipe.pk} - {recipe.name} \n' + self.handle_duplicates(recipe, import_duplicates) + import_zip.close() + elif '.zip' in f['name'] or '.paprikarecipes' in f['name']: import_zip = ZipFile(f['file']) for z in import_zip.filelist: if self.import_file_name_filter(z): diff --git a/cookbook/integration/recipekeeper.py b/cookbook/integration/recipekeeper.py index dc375d4c..91e3a46c 100644 --- a/cookbook/integration/recipekeeper.py +++ b/cookbook/integration/recipekeeper.py @@ -1,10 +1,11 @@ import re +from bs4 import BeautifulSoup from django.utils.translation import gettext as _ from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Food, Unit, Ingredient +from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword class RecipeKeeper(Integration): @@ -12,46 +13,51 @@ class RecipeKeeper(Integration): def import_file_name_filter(self, zip_info_object): return re.match(r'^recipes.html$', zip_info_object.filename) + def split_recipe_file(self, file): + recipe_html = BeautifulSoup(file, 'html.parser') + return recipe_html.find_all('div',class_='recipe-details') + def get_recipe_from_file(self, file): - source_url = '' + # 'file' comes is as a beautifulsoup object + recipe = Recipe.objects.create(name=file.find("h2",{"itemprop":"name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, ) - ingredient_mode = 0 + # add 'Courses' and 'Categories' as keywords + for course in file.find_all("span", {"itemprop": "recipeCourse"}): + keyword, created = Keyword.objects.get_or_create(name=course.text, space=self.request.space) + recipe.keywords.add(keyword) - ingredients = [] - directions = [] - for i, fl in enumerate(file.readlines(), start=0): - line = fl.decode("utf-8") - if i == 0: - title = line.strip() - else: - if line.startswith('https:') or line.startswith('http:'): - source_url = line.strip() - else: - if ingredient_mode == 1 and len(line.strip()) == 0: - ingredient_mode = 2 - if re.match(r'^([0-9])[^.](.)*$', line) and ingredient_mode < 2: - ingredient_mode = 1 - ingredients.append(line.strip()) - else: - directions.append(line.strip()) + for category in file.find_all("meta", {"itemprop":"recipeCategory"}): + keyword, created = Keyword.objects.get_or_create(name=category.get("content"), space=self.request.space) + recipe.keywords.add(keyword) - recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, ) + # TODO: import prep and cook times + # Recipe Keeper uses ISO 8601 format for its duration periods. - step = Step.objects.create(instruction='\n'.join(directions)) + ingredients_added = False + for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"): + + if s.text == "": + continue - if source_url != '': - step.instruction += '\n' + source_url - step.save() + step = Step.objects.create( + instruction=s.text + ) + if not ingredients_added: + ingredients_added = True + for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"): + if ingredient.text == "": + continue + amount, unit, ingredient, note = parse(ingredient.text.strip()) + f = get_food(ingredient, self.request.space) + u = get_unit(unit, self.request.space) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note + )) + recipe.steps.add(step) - for ingredient in ingredients: - if len(ingredient.strip()) > 0: - amount, unit, ingredient, note = parse(ingredient) - f = get_food(ingredient, self.request.space) - u = get_unit(unit, self.request.space) - step.ingredients.add(Ingredient.objects.create( - food=f, unit=u, amount=amount, note=note - )) - recipe.steps.add(step) + # if source_url != '': + # step.instruction += '\n' + source_url + # step.save() return recipe From d00fa10b9fa7ab05c7fa805e7316929a485432cb Mon Sep 17 00:00:00 2001 From: its_me_gb Date: Wed, 28 Apr 2021 13:16:55 +0100 Subject: [PATCH 3/4] Import the recipe image from the zip file. --- cookbook/integration/recipekeeper.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cookbook/integration/recipekeeper.py b/cookbook/integration/recipekeeper.py index 91e3a46c..dea102f1 100644 --- a/cookbook/integration/recipekeeper.py +++ b/cookbook/integration/recipekeeper.py @@ -1,5 +1,7 @@ import re from bs4 import BeautifulSoup +from io import BytesIO +from zipfile import ZipFile from django.utils.translation import gettext as _ @@ -15,18 +17,18 @@ class RecipeKeeper(Integration): def split_recipe_file(self, file): recipe_html = BeautifulSoup(file, 'html.parser') - return recipe_html.find_all('div',class_='recipe-details') + return recipe_html.find_all('div', class_='recipe-details') def get_recipe_from_file(self, file): # 'file' comes is as a beautifulsoup object - recipe = Recipe.objects.create(name=file.find("h2",{"itemprop":"name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, ) + recipe = Recipe.objects.create(name=file.find("h2", {"itemprop": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, ) # add 'Courses' and 'Categories' as keywords for course in file.find_all("span", {"itemprop": "recipeCourse"}): keyword, created = Keyword.objects.get_or_create(name=course.text, space=self.request.space) recipe.keywords.add(keyword) - for category in file.find_all("meta", {"itemprop":"recipeCategory"}): + for category in file.find_all("meta", {"itemprop": "recipeCategory"}): keyword, created = Keyword.objects.get_or_create(name=category.get("content"), space=self.request.space) recipe.keywords.add(keyword) @@ -35,7 +37,7 @@ class RecipeKeeper(Integration): ingredients_added = False for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"): - + if s.text == "": continue @@ -55,6 +57,16 @@ class RecipeKeeper(Integration): )) recipe.steps.add(step) + # import the Primary recipe image that is stored in the Zip + try: + for f in self.files: + if '.zip' in f['name']: + import_zip = ZipFile(f['file']) + self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src")))) + except Exception as e: + pass + + # TODO: Import the source url # if source_url != '': # step.instruction += '\n' + source_url # step.save() From 0ec29636b330823052a5098b07e958fc4d97bbd1 Mon Sep 17 00:00:00 2001 From: its_me_gb Date: Fri, 30 Apr 2021 09:14:31 +0100 Subject: [PATCH 4/4] Add original source url to the first recipe step --- cookbook/integration/recipekeeper.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cookbook/integration/recipekeeper.py b/cookbook/integration/recipekeeper.py index dea102f1..dc207916 100644 --- a/cookbook/integration/recipekeeper.py +++ b/cookbook/integration/recipekeeper.py @@ -35,6 +35,7 @@ class RecipeKeeper(Integration): # TODO: import prep and cook times # Recipe Keeper uses ISO 8601 format for its duration periods. + source_url_added = False ingredients_added = False for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"): @@ -44,6 +45,14 @@ class RecipeKeeper(Integration): step = Step.objects.create( instruction=s.text ) + + if not source_url_added: + # If there is a source URL, add it to the first step field. + if file.find("span", {"itemprop": "recipeSource"}).text != '': + step.instruction += "\n\nImported from: " + file.find("span", {"itemprop": "recipeSource"}).text + step.save() + source_url_added = True + if not ingredients_added: ingredients_added = True for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"): @@ -66,11 +75,6 @@ class RecipeKeeper(Integration): except Exception as e: pass - # TODO: Import the source url - # if source_url != '': - # step.instruction += '\n' + source_url - # step.save() - return recipe def get_file_from_recipe(self, recipe):