added mela recipes importer

This commit is contained in:
vabene1111
2022-04-04 21:09:47 +02:00
parent a2954554b5
commit 85aad42529
9 changed files with 118 additions and 44 deletions

View File

@@ -153,6 +153,7 @@ class ImportExportBase(forms.Form):
RECIPESAGE = 'RECIPESAGE' RECIPESAGE = 'RECIPESAGE'
DOMESTICA = 'DOMESTICA' DOMESTICA = 'DOMESTICA'
MEALMASTER = 'MEALMASTER' MEALMASTER = 'MEALMASTER'
MELARECIPES = 'MELARECIPES'
REZKONV = 'REZKONV' REZKONV = 'REZKONV'
OPENEATS = 'OPENEATS' OPENEATS = 'OPENEATS'
PLANTOEAT = 'PLANTOEAT' PLANTOEAT = 'PLANTOEAT'
@@ -165,7 +166,7 @@ class ImportExportBase(forms.Form):
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'),
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'),
)) ))

View File

@@ -208,6 +208,9 @@ class IngredientParser:
note = '' note = ''
unit_note = '' unit_note = ''
if len(x) == 0:
raise ValueError('string to parse cannot be empty')
# if the string contains parenthesis early on remove it and place it at the end # if the string contains parenthesis early on remove it and place it at the end
# because its likely some kind of note # because its likely some kind of note
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x): if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
@@ -271,4 +274,9 @@ class IngredientParser:
if unit_note not in note: if unit_note not in note:
note += ' ' + unit_note note += ' ' + unit_note
return amount, self.apply_unit_automation(unit.strip()), self.apply_food_automation(ingredient.strip()), note.strip() try:
unit = self.apply_unit_automation(unit.strip())
except Exception:
pass
return amount, unit, self.apply_food_automation(ingredient.strip()), note.strip()

View File

@@ -255,40 +255,22 @@ def parse_servings(servings):
return servings return servings
def parse_cooktime(cooktime): def parse_time(recipe_time):
if type(cooktime) not in [int, float]: if type(recipe_time) not in [int, float]:
try: try:
cooktime = float(re.search(r'\d+', cooktime).group()) recipe_time = float(re.search(r'\d+', recipe_time).group())
except (ValueError, AttributeError): except (ValueError, AttributeError):
try: try:
cooktime = round(iso_parse_duration(cooktime).seconds / 60) recipe_time = round(iso_parse_duration(recipe_time).seconds / 60)
except ISO8601Error: except ISO8601Error:
try: try:
if (type(cooktime) == list and len(cooktime) > 0): if (type(recipe_time) == list and len(recipe_time) > 0):
cooktime = cooktime[0] recipe_time = recipe_time[0]
cooktime = round(parse_duration(cooktime).seconds / 60) recipe_time = round(parse_duration(recipe_time).seconds / 60)
except AttributeError: except AttributeError:
cooktime = 0 recipe_time = 0
return cooktime return recipe_time
def parse_preptime(preptime):
if type(preptime) not in [int, float]:
try:
preptime = float(re.search(r'\d+', preptime).group())
except ValueError:
try:
preptime = round(iso_parse_duration(preptime).seconds / 60)
except ISO8601Error:
try:
if (type(preptime) == list and len(preptime) > 0):
preptime = preptime[0]
preptime = round(parse_duration(preptime).seconds / 60)
except AttributeError:
preptime = 0
return preptime
def parse_keywords(keyword_json, space): def parse_keywords(keyword_json, space):

View File

@@ -172,7 +172,7 @@ class Integration:
traceback.print_exc() traceback.print_exc()
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n') self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
import_zip.close() import_zip.close()
elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name']: elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name'] or '.melarecipe' in f['name']:
data_list = self.split_recipe_file(f['file']) data_list = self.split_recipe_file(f['file'])
il.total_recipes += len(data_list) il.total_recipes += len(data_list)
for d in data_list: for d in data_list:

View File

@@ -0,0 +1,83 @@
import base64
import json
from io import BytesIO
from gettext import gettext as _
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_time
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step
class MelaRecipes(Integration):
def split_recipe_file(self, file):
return [json.loads(file.getvalue().decode("utf-8"))]
def get_files_from_recipes(self, recipes, el, cookie):
raise NotImplementedError('Method not implemented in storage integration')
def get_recipe_from_file(self, file):
recipe_json = file
recipe = Recipe.objects.create(
name=recipe_json['title'].strip(),
created_by=self.request.user, internal=True, space=self.request.space)
if 'yield' in recipe_json:
recipe.servings = parse_servings(recipe_json['yield'])
if 'cookTime' in recipe_json:
recipe.waiting_time = parse_time(recipe_json['cookTime'])
if 'prepTime' in recipe_json:
recipe.working_time = parse_time(recipe_json['prepTime'])
if 'favorite' in recipe_json and recipe_json['favorite']:
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=_('Favorite'))[0])
if 'categories' in recipe_json:
try:
for x in recipe_json['categories']:
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=x)[0])
except Exception:
pass
instruction = ''
if 'text' in recipe_json:
instruction += f'*{recipe_json["text"].strip()}* \n'
if 'instructions' in recipe_json:
instruction += recipe_json["instructions"].strip() + ' \n'
if 'notes' in recipe_json:
instruction += recipe_json["notes"].strip() + ' \n'
if 'link' in recipe_json:
recipe.source_url = recipe_json['link']
step = Step.objects.create(
instruction=instruction, space=self.request.space,
)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_json['ingredients'].split('\n'):
if ingredient.strip() != '':
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)
if recipe_json.get("images", None):
try:
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['images'][0])), filetype='.jpeg')
except Exception:
pass
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')

View File

@@ -1,29 +1,17 @@
import json
import uuid
from datetime import datetime from datetime import datetime
from io import BytesIO
import requests
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseRedirect
from django.core.files import File
from django.db.transaction import atomic
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.utils.translation import ngettext from django.utils.translation import ngettext
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
from PIL import UnidentifiedImageError
from requests.exceptions import MissingSchema
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from cookbook.forms import BatchEditForm, SyncForm from cookbook.forms import BatchEditForm, SyncForm
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import group_required, has_group_permission from cookbook.helper.permission_helper import group_required, has_group_permission
from cookbook.helper.recipe_url_import import parse_cooktime from cookbook.models import (Comment, Food, Keyword, Recipe, RecipeImport, Sync,
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe, RecipeImport, Step, Sync,
Unit, UserPreference, BookmarkletImport) Unit, UserPreference, BookmarkletImport)
from cookbook.tables import SyncTable from cookbook.tables import SyncTable
from recipes import settings from recipes import settings

View File

@@ -20,6 +20,7 @@ from cookbook.integration.default import Default
from cookbook.integration.domestica import Domestica from cookbook.integration.domestica import Domestica
from cookbook.integration.mealie import Mealie from cookbook.integration.mealie import Mealie
from cookbook.integration.mealmaster import MealMaster from cookbook.integration.mealmaster import MealMaster
from cookbook.integration.melarecipes import MelaRecipes
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
from cookbook.integration.openeats import OpenEats from cookbook.integration.openeats import OpenEats
from cookbook.integration.paprika import Paprika from cookbook.integration.paprika import Paprika
@@ -74,6 +75,8 @@ def get_integration(request, export_type):
return CopyMeThat(request, export_type) return CopyMeThat(request, export_type)
if export_type == ImportExportBase.PDF: if export_type == ImportExportBase.PDF:
return PDFexport(request, export_type) return PDFexport(request, export_type)
if export_type == ImportExportBase.MELARECIPES:
return MelaRecipes(request, export_type)
@group_required('user') @group_required('user')

View File

@@ -38,6 +38,7 @@ Overview of the capabilities of the different integrations.
| Plantoeat | ✔️ | ❌ | ✔ | | Plantoeat | ✔️ | ❌ | ✔ |
| CookBookApp | ✔️ | ⌚ | ✔️ | | CookBookApp | ✔️ | ⌚ | ✔️ |
| CopyMeThat | ✔️ | ❌ | ✔️ | | CopyMeThat | ✔️ | ❌ | ✔️ |
| CopyMeThat | ✔️ | ⌚ | ✔️ |
| PDF (experimental) | ⌚️ | ✔️ | ✔️ | | PDF (experimental) | ⌚️ | ✔️ | ✔️ |
✔️ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented ✔️ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented
@@ -225,6 +226,13 @@ CookBookApp can export .zip files containing .html files. Upload the entire ZIP
CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes. CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes.
## Melarecipes
Melarecipes provides multiple export formats but only the `MelaRecipes` format can export the complete collection.
Perform this export and open the `.melarecipes` file using your favorite archive opening program (e.g 7zip).
Repeat this if the file contains another `.melarecipes` file until you get a list of one or many `.melarecipe` files.
Upload all `.melarecipe` files you want to import to tandoor and start the import.
## PDF ## PDF
The PDF Exporter is an experimental feature that uses the puppeteer browser renderer to render each recipe and export it to PDF. The PDF Exporter is an experimental feature that uses the puppeteer browser renderer to render each recipe and export it to PDF.

View File

@@ -9,6 +9,7 @@ export const INTEGRATIONS = [
{id: 'DOMESTICA', name: "Domestica", import: true, export: false}, {id: 'DOMESTICA', name: "Domestica", import: true, export: false},
{id: 'MEALIE', name: "Mealie", import: true, export: false}, {id: 'MEALIE', name: "Mealie", import: true, export: false},
{id: 'MEALMASTER', name: "Mealmaster", import: true, export: false}, {id: 'MEALMASTER', name: "Mealmaster", import: true, export: false},
{id: 'MELARECIPES', name: "Melarecipes", import: true, export: false},
{id: 'NEXTCLOUD', name: "Nextcloud Cookbook", import: true, export: false}, {id: 'NEXTCLOUD', name: "Nextcloud Cookbook", import: true, export: false},
{id: 'OPENEATS', name: "Openeats", import: true, export: false}, {id: 'OPENEATS', name: "Openeats", import: true, export: false},
{id: 'PAPRIKA', name: "Paprika", import: true, export: false}, {id: 'PAPRIKA', name: "Paprika", import: true, export: false},