mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 04:10:06 -05:00
added mela recipes importer
This commit is contained in:
@@ -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'),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
83
cookbook/integration/melarecipes.py
Normal file
83
cookbook/integration/melarecipes.py
Normal 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')
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
Reference in New Issue
Block a user