diff --git a/cookbook/templates/url_import.html b/cookbook/templates/url_import.html index f7e588482..7e20178b3 100644 --- a/cookbook/templates/url_import.html +++ b/cookbook/templates/url_import.html @@ -1,10 +1,177 @@ - - - - - $Title$ - - -$END$ - - \ No newline at end of file +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans 'URL Import' %}{% endblock %} + +{% block extra_head %} + + {% include 'include/vue_base.html' %} + + + + +{% endblock %} + +{% block content %} + +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+ + [[recipe_data]] +
+ + + +
+
+
+
+
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/cookbook/urls.py b/cookbook/urls.py index e127befe3..3bb392e0a 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -53,12 +53,14 @@ urlpatterns = [ path('data/batch/import', data.batch_import, name='data_batch_import'), path('data/sync/wait', data.sync_wait, name='data_sync_wait'), path('data/statistics', data.statistics, name='data_stats'), + path('data/import/url', data.import_url, name='data_import_url'), path('api/get_external_file_link//', api.get_external_file_link, name='api_get_external_file_link'), path('api/get_recipe_file//', api.get_recipe_file, name='api_get_recipe_file'), path('api/sync_all/', api.sync_all, name='api_sync'), path('api/log_cooking//', api.log_cooking, name='api_log_cooking'), path('api/plan-ical//', api.get_plan_ical, name='api_get_plan_ical'), + path('api/recipe-from-url//', api.recipe_from_url, name='api_recipe_from_url'), path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), path('dal/ingredient/', dal.IngredientsAutocomplete.as_view(), name='dal_ingredient'), diff --git a/cookbook/views/api.py b/cookbook/views/api.py index b22cc6da7..c481a3ce5 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -2,12 +2,14 @@ import io import json import re +import requests from annoying.decorators import ajax_request from annoying.functions import get_object_or_None +from bs4 import BeautifulSoup from django.contrib import messages from django.contrib.auth.models import User from django.db.models import Q -from django.http import HttpResponse, FileResponse +from django.http import HttpResponse, FileResponse, JsonResponse from django.shortcuts import redirect from django.utils.translation import gettext as _ from icalendar import Calendar, Event @@ -16,7 +18,7 @@ from rest_framework.exceptions import APIException from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListModelMixin from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin -from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook +from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Keyword from cookbook.provider.dropbox import Dropbox from cookbook.provider.nextcloud import Nextcloud from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer @@ -242,3 +244,72 @@ def get_plan_ical(request, html_week): response["Content-Disposition"] = f'attachment; filename=meal_plan_{html_week}.ics' return response + + +@group_required('user') +def recipe_from_url(request, url): + headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'} + response = requests.get(url, headers=headers) + + if response.status_code == 403: + return JsonResponse({'error': _('The requested page refused to provide any information (Status Code 403).')}) + + soup = BeautifulSoup(response.text, "html.parser") + for ld in soup.find_all('script', type='application/ld+json'): + ld_json = json.loads(ld.string) + + # recipes type might be wrapped in @graph type + if '@graph' in ld_json: + for x in ld_json['@graph']: + if '@type' in x and x['@type'] == 'Recipe': + ld_json = x + + if '@type' in ld_json and ld_json['@type'] == 'Recipe': + + if 'recipeIngredient' in ld_json: + ingredients = [] + + for x in ld_json['recipeIngredient']: + ingredient_split = x.split() + if len(ingredient_split) > 2: + ingredients.append({'amount': ingredient_split[0], 'unit': ingredient_split[1], 'ingredient': " ".join(ingredient_split[2:])}) + if len(ingredient_split) == 2: + ingredients.append({'amount': ingredient_split[0], 'unit': '', 'ingredient': " ".join(ingredient_split[1:])}) + if len(ingredient_split) == 1: + ingredients.append({'amount': 0, 'unit': '', 'ingredient': " ".join(ingredient_split)}) + + ld_json['recipeIngredient'] = ingredients + + if 'keywords' in ld_json: + keywords = [] + if type(ld_json['keywords']) == str: + ld_json['keywords'] = ld_json['keywords'].split(',') + + for kw in ld_json['keywords']: + if k := Keyword.objects.filter(name=kw).first(): + keywords.append({'id': str(k.id), 'text': str(k).strip()}) + else: + keywords.append({'id': "null", 'text': kw.strip()}) + + ld_json['keywords'] = keywords + + if 'recipeInstructions' in ld_json: + instructions = '' + if type(ld_json['recipeInstructions']) == list: + for i in ld_json['recipeInstructions']: + if type(i) == str: + instructions += i + else: + instructions += i['text'] + '\n\n' + ld_json['recipeInstructions'] = instructions + + if 'image' in ld_json: + if (type(ld_json['image'])) == list: + if type(ld_json['image'][0]) == str: + ld_json['image'] = ld_json['image'][0] + elif 'url' in ld_json['image'][0]: + ld_json['image'] = ld_json['image'][0]['url'] + + return JsonResponse(ld_json) + + return JsonResponse({'error': _('The requested site does not provide any recognized data format to import the recipe from.')}) diff --git a/cookbook/views/data.py b/cookbook/views/data.py index d59fb26f4..0e2e7db4c 100644 --- a/cookbook/views/data.py +++ b/cookbook/views/data.py @@ -88,6 +88,11 @@ def batch_edit(request): return render(request, 'batch/edit.html', {'form': form}) +@group_required('user') +def import_url(request): + return render(request, 'url_import.html', {}) + + class Object(object): pass diff --git a/requirements.txt b/requirements.txt index 10dc51027..68635e67e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,4 +22,5 @@ webdavclient3==3.14.4 whitenoise==5.1.0 icalendar==4.0.6 pyyaml==5.3.1 -uritemplate==3.0.1 \ No newline at end of file +uritemplate==3.0.1 +beautifulsoup4==4.9.1 \ No newline at end of file