Repository: searxng/searxng

Issue:
Yummly engine
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->

**Working URL to the engine**
<!-- Please check if the engine is responding correctly before submitting it. -->
https://www.yummly.com/

**Why do you want to add this engine?**
<!-- What's special about this engine? Is it open source or libre? -->
I think that it would be cool to have a category for recipes like duckduckgo has.  That way people can find recipes easily.  Not open source or libre that I know of.

**Features of this engine**
<!-- Features of this engine: Doesn't track its users, fast, easy to integrate, ... -->
Finds recipes for people.
**How can SearXNG fetch the information from this engine?**
<!-- List API URL, example code (using the correct markdown) and more
that could be useful for the developers in order to implement this engine.
If you don't know what to write, let this part blank.>
It works with the ddg bangs(I know searx has this built in).
https://developer.yummly.com/documentation.html  talks and has examples about there api.
**Applicable category of this engine**
<!-- Where should this new engine fit in SearXNG? Current categories in SearXNG:
general, files, images, it, map, music, news, science, social media and videos.
You can add multiple categories at the same time. -->
Create a new category called recipes.

**Additional context**
<!-- Add any other context about this engine here. -->
Example of what I think it should look similar to https://duckduckgo.com/?q=shrimp+scampi&ia=recipes&iax=recipes



Discussion:
The interesting URL seems to be `https://duckduckgo.com/js/spice/recipes/{{ search }}`
@dalf Here is a list of spices that could be used https://github.com/Snazzah/duck-duck-scrape/tree/master/src/spices.
And their docs: https://duck-duck-scrape.js.org/modules.html#currency


I wonder if ```dictionaryDefinition```, ```dictionaryPronunciation``` and/or ```dictionaryHyphenation``` could be implemented as autocomplete backends, something [similiar to this demo.](https://github.com/allendema/searx/commit/25bf3a81b54811fdd8f1469e668b9dd31e7e906c)

DDG have a list of zeroclickanswers too but sure how that all works.
https://github.com/duckduckgo/zeroclickinfo-goodies/tree/master/t

Resolving patch:
diff --git a/searx/engines/lemmy.py b/searx/engines/lemmy.py
--- a/searx/engines/lemmy.py
+++ b/searx/engines/lemmy.py
@@ -42,10 +42,9 @@
 from datetime import datetime
 from urllib.parse import urlencode
 
-from markdown_it import MarkdownIt
 from flask_babel import gettext
 
-from searx.utils import html_to_text
+from searx.utils import markdown_to_text
 
 about = {
     "website": 'https://lemmy.ml/',
@@ -78,11 +77,6 @@ def request(query, params):
     return params
 
 
-def _format_content(content):
-    html = MarkdownIt("commonmark", {"typographer": True}).enable(["replacements", "smartquotes"]).render(content)
-    return html_to_text(html)
-
-
 def _get_communities(json):
     results = []
 
@@ -97,7 +91,7 @@ def _get_communities(json):
             {
                 'url': result['community']['actor_id'],
                 'title': result['community']['title'],
-                'content': _format_content(result['community'].get('description', '')),
+                'content': markdown_to_text(result['community'].get('description', '')),
                 'img_src': result['community'].get('icon', result['community'].get('banner')),
                 'publishedDate': datetime.strptime(counts['published'][:19], '%Y-%m-%dT%H:%M:%S'),
                 'metadata': metadata,
@@ -114,7 +108,7 @@ def _get_users(json):
             {
                 'url': result['person']['actor_id'],
                 'title': result['person']['name'],
-                'content': _format_content(result['person'].get('bio', '')),
+                'content': markdown_to_text(result['person'].get('bio', '')),
             }
         )
 
@@ -140,7 +134,7 @@ def _get_posts(json):
 
         content = result['post'].get('body', '').strip()
         if content:
-            content = _format_content(content)
+            content = markdown_to_text(content)
 
         results.append(
             {
@@ -164,7 +158,7 @@ def _get_comments(json):
 
         content = result['comment'].get('content', '').strip()
         if content:
-            content = _format_content(content)
+            content = markdown_to_text(content)
 
         metadata = (
             f"&#x25B2; {result['counts']['upvotes']} &#x25BC; {result['counts']['downvotes']}"
@@ -176,7 +170,7 @@ def _get_comments(json):
             {
                 'url': result['comment']['ap_id'],
                 'title': result['post']['name'],
-                'content': _format_content(result['comment']['content']),
+                'content': markdown_to_text(result['comment']['content']),
                 'publishedDate': datetime.strptime(result['comment']['published'][:19], '%Y-%m-%dT%H:%M:%S'),
                 'metadata': metadata,
             }
diff --git a/searx/engines/yummly.py b/searx/engines/yummly.py
new file mode 100644
--- /dev/null
+++ b/searx/engines/yummly.py
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# lint: pylint
+"""Yummly
+"""
+
+from urllib.parse import urlencode
+
+from flask_babel import gettext
+from searx.utils import markdown_to_text
+
+about = {
+    "website": 'https://yummly.com',
+    "wikidata_id": 'Q8061140',
+    # not used since it requires an api key
+    "official_api_documentation": 'https://developer.yummly.com/documentation.html',
+    "use_official_api": False,
+    "require_api_key": False,
+    "results": 'JSON',
+}
+paging = True
+categories = []
+
+api_url = "https://mapi.yummly.com"
+number_of_results = 10
+show_pro_recipes = False
+base_url = "https://www.yummly.com"
+
+
+def request(query, params):
+    args = {
+        'q': query,
+        'start': (params['pageno'] - 1) * number_of_results,
+        'maxResult': number_of_results,
+    }
+
+    params['url'] = f"{api_url}/mapi/v23/content/search?{urlencode(args)}&allowedContent=single_recipe"
+
+    return params
+
+
+def response(resp):
+    results = []
+
+    json = resp.json()
+
+    for result in json['feed']:
+        # don't show pro recipes since they can't be accessed without an account
+        if not show_pro_recipes and result['proRecipe']:
+            continue
+
+        content = result['seo']['web']['meta-tags']['description']
+        description = result['content']['description']
+        if description is not None:
+            content = markdown_to_text(description['text'])
+
+        img_src = None
+        if result['display']['images']:
+            img_src = result['display']['images'][0]
+        elif result['content']['details']['images']:
+            img_src = result['content']['details']['images'][0]['resizableImageUrl']
+
+        url = result['display']['source']['sourceRecipeUrl']
+        if 'www.yummly.com/private' in url:
+            url = base_url + '/' + result['tracking-id']
+
+        results.append(
+            {
+                'url': url,
+                'title': result['display']['displayName'],
+                'content': content,
+                'img_src': img_src,
+                'metadata': f"{gettext('Language')}: {result['locale'].split('-')[0]}",
+            }
+        )
+
+    for suggestion in json['relatedPhrases']['keywords']:
+        results.append({'suggestion': suggestion})
+
+    return results
diff --git a/searx/utils.py b/searx/utils.py
--- a/searx/utils.py
+++ b/searx/utils.py
@@ -15,6 +15,7 @@
 from random import choice
 from html.parser import HTMLParser
 from urllib.parse import urljoin, urlparse
+from markdown_it import MarkdownIt
 
 from lxml import html
 from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError, _ElementStringResult, _ElementUnicodeResult
@@ -158,6 +159,29 @@ def html_to_text(html_str: str) -> str:
     return s.get_text()
 
 
+def markdown_to_text(markdown_str: str) -> str:
+    """Extract text from a Markdown string
+
+    Args:
+        * markdown_str (str): string Markdown
+
+    Returns:
+        * str: extracted text
+
+    Examples:
+        >>> markdown_to_text('[example](https://example.com)')
+        'example'
+
+        >>> markdown_to_text('## Headline')
+        'Headline'
+    """
+
+    html_str = (
+        MarkdownIt("commonmark", {"typographer": True}).enable(["replacements", "smartquotes"]).render(markdown_str)
+    )
+    return html_to_text(html_str)
+
+
 def extract_text(xpath_results, allow_none: bool = False) -> Optional[str]:
     """Extract text from a lxml result
 


Test patch: