From 6ba1ff4505a7888a406e3a8687ed9acb4e2cb24a Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Tue, 9 Feb 2021 17:15:47 +0100 Subject: [PATCH] added chowdown importer --- .idea/dictionaries/vabene1111_PC.xml | 1 + cookbook/forms.py | 3 +- cookbook/integration/chowdown.py | 80 ++++++++++++++++++++++ cookbook/integration/nextcloud_cookbook.py | 1 + cookbook/views/import_export.py | 3 + docs/features/import_export.md | 32 +++++++-- 6 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 cookbook/integration/chowdown.py diff --git a/.idea/dictionaries/vabene1111_PC.xml b/.idea/dictionaries/vabene1111_PC.xml index 76c50769..49ae2f94 100644 --- a/.idea/dictionaries/vabene1111_PC.xml +++ b/.idea/dictionaries/vabene1111_PC.xml @@ -2,6 +2,7 @@ autosync + chowdown csrftoken gunicorn ical diff --git a/cookbook/forms.py b/cookbook/forms.py index 9f602439..e7ee014b 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -136,8 +136,9 @@ class ImportExportBase(forms.Form): PAPRIKA = 'PAPRIKA' NEXTCLOUD = 'NEXTCLOUD' MEALIE = 'MEALIE' + CHOWDOWN = 'CHOWDOWN' - type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, _('Paprika')), (NEXTCLOUD, _('Nextcloud Cookbook')), (MEALIE, _('Mealie')),)) + type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, _('Paprika')), (NEXTCLOUD, _('Nextcloud Cookbook')), (MEALIE, _('Mealie')), (CHOWDOWN, _('Chowdown')),)) class ImportForm(ImportExportBase): diff --git a/cookbook/integration/chowdown.py b/cookbook/integration/chowdown.py new file mode 100644 index 00000000..424c57d1 --- /dev/null +++ b/cookbook/integration/chowdown.py @@ -0,0 +1,80 @@ +import json +import re +from io import BytesIO +from zipfile import ZipFile + +from cookbook.helper.ingredient_parser import parse +from cookbook.integration.integration import Integration +from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword + + +class Chowdown(Integration): + + def import_file_name_filter(self, zip_info_object): + print("testing", zip_info_object.filename) + return re.match(r'^_recipes/([A-Za-z\d\s-])+.md$', zip_info_object.filename) + + def get_recipe_from_file(self, file): + ingredient_mode = False + direction_mode = False + description_mode = False + + ingredients = [] + directions = [] + descriptions = [] + for fl in file.readlines(): + line = fl.decode("utf-8") + if 'title:' in line: + title = line.replace('title:', '').replace('"', '').strip() + if 'image:' in line: + image = line.replace('image:', '').strip() + if 'tags:' in line: + tags = line.replace('tags:', '').strip() + if ingredient_mode: + if len(line) > 2 and 'directions:' not in line: + ingredients.append(line[2:]) + if '---' in line and direction_mode: + direction_mode = False + description_mode = True + if direction_mode: + if len(line) > 2: + directions.append(line[2:]) + if 'ingredients:' in line: + ingredient_mode = True + if 'directions:' in line: + ingredient_mode = False + direction_mode = True + if description_mode and len(line) > 3 and '---' not in line: + descriptions.append(line) + + recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, ) + + for k in tags.split(','): + keyword, created = Keyword.objects.get_or_create(name=k.strip()) + recipe.keywords.add(keyword) + + step = Step.objects.create( + instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions) + ) + + for ingredient in ingredients: + amount, unit, ingredient, note = parse(ingredient) + f, created = Food.objects.get_or_create(name=ingredient) + u, created = Unit.objects.get_or_create(name=unit) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note + )) + recipe.steps.add(step) + + for f in self.files: + if '.zip' in f.name: + import_zip = ZipFile(f.file) + for z in import_zip.filelist: + if re.match(f'^images/{image}$', z.filename): + self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename))) + + return recipe + + +def get_file_from_recipe(self, recipe): + raise NotImplementedError('Method not implemented in storage integration') diff --git a/cookbook/integration/nextcloud_cookbook.py b/cookbook/integration/nextcloud_cookbook.py index 7d5f5793..47580e1a 100644 --- a/cookbook/integration/nextcloud_cookbook.py +++ b/cookbook/integration/nextcloud_cookbook.py @@ -22,6 +22,7 @@ class NextcloudCookbook(Integration): servings=recipe_json['recipeYield']) # TODO parse times (given in PT2H3M ) + # TODO parse keywords ingredients_added = False for s in recipe_json['recipeInstructions']: diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index c7e98da6..cef46694 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext as _ from cookbook.forms import ExportForm, ImportForm, ImportExportBase from cookbook.helper.permission_helper import group_required +from cookbook.integration.chowdown import Chowdown from cookbook.integration.default import Default from cookbook.integration.mealie import Mealie from cookbook.integration.nextcloud_cookbook import NextcloudCookbook @@ -22,6 +23,8 @@ def get_integration(request, export_type): return NextcloudCookbook(request) if export_type == ImportExportBase.MEALIE: return Mealie(request) + if export_type == ImportExportBase.CHOWDOWN: + return Chowdown(request) @group_required('user') diff --git a/docs/features/import_export.md b/docs/features/import_export.md index d80bd91b..11ebedfe 100644 --- a/docs/features/import_export.md +++ b/docs/features/import_export.md @@ -7,12 +7,12 @@ integrations to allow you to both import and export your recipes into whatever f Feel like there is an important integration missing ? Just take a look at the [integration issues](https://github.com/vabene1111/recipes/issues?q=is%3Aissue+is%3Aopen+label%3Aintegration) or open a new one if your favorite one is missing. -!!! warning "WIP" - Please note that this feature is relatively new and many integrations are missing. - Additionally, many recipe applications provide formats that are not structured in an easily machine-readable way - and thus require a lot of work to integrate even tough the module is very versatile. - If you are good at writing parsers feel free to add new integrations for your favorite services. - +!!! info "Export" + I strongly believe in everyone's right to use their data as they please and therefore want to give you + the most possible flexibility with your recipes. + That said for most of the people getting this application running with their recipes is the biggest priority. + Because of this importing as many formats as possible is prioritized over exporting. + Exporter for the different formats will follow over time. ## Default The default integration is the build in (and preferred) way to import and export recipes. @@ -36,7 +36,7 @@ You will get a `Recipes.zip` file. Simply upload the file and choose the Nextclo !!! warning "Folder Structure" Importing only works if the folder structure is correct. If you do not use the standard path or create the - zip file in any other way make sure the strucutre is as follows + zip file in any other way make sure the structure is as follows ``` Recipes.zip/ └── Recipes/ @@ -57,6 +57,24 @@ To migrate your recipes 2. Download the backup by clicking on it and pressing download (this wasn't working for me, so I had to manually pull it from the server) 3. Upload the entire `.zip` file to the importer page and import everything +## Chowdown +Chowdown stores all your recipes in plain text markdown files in a directory called `_recipes`. +Images are saved in a directory called `images`. + +In order to import your Chowdown recipes simply create a `.zip` file from those two folders and import them. +The folder structure should look as follows +``` +Recipes.zip/ + ├── _recipes/ + │ ├── recipe one.md + │ ├── recipe two.md + │ └── ... + └── images/ + ├── image-name.jpg + ├── second-image-name.jpg + └── ... +``` + ## Paprika Paprika can create two types of export. The first is a proprietary `.paprikarecipes` file in some kind of binarized format. The second one is HTML files containing at least a bit of microdata.