summaryrefslogtreecommitdiff
path: root/searx/engines/tineye.py
blob: c35799c6907af48bf9052dfbc4e389ab6546a49a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# SPDX-License-Identifier: AGPL-3.0-or-later
"""This engine implements *Tineye - reverse image search*

Using TinEye, you can search by image or perform what we call a reverse image
search.  You can do that by uploading an image or searching by URL. You can also
simply drag and drop your images to start your search.  TinEye constantly crawls
the web and adds images to its index.  Today, the TinEye index is over 50.2
billion images `[tineye.com] <https://tineye.com/how>`_.

.. hint::

   This SearXNG engine only supports *'searching by URL'* and it does not use
   the official API `[api.tineye.com] <https://api.tineye.com/python/docs/>`_.

"""

from typing import TYPE_CHECKING
from urllib.parse import urlencode
from datetime import datetime
from flask_babel import gettext

if TYPE_CHECKING:
    import logging

    logger = logging.getLogger()

about = {
    "website": 'https://tineye.com',
    "wikidata_id": 'Q2382535',
    "official_api_documentation": 'https://api.tineye.com/python/docs/',
    "use_official_api": False,
    "require_api_key": False,
    "results": 'JSON',
}

engine_type = 'online_url_search'
""":py:obj:`searx.search.processors.online_url_search`"""

categories = ['general']
paging = True
safesearch = False
base_url = 'https://tineye.com'
search_string = '/api/v1/result_json/?page={page}&{query}'

FORMAT_NOT_SUPPORTED = gettext(
    "Could not read that image url. This may be due to an unsupported file"
    " format. TinEye only supports images that are JPEG, PNG, GIF, BMP, TIFF or WebP."
)
"""TinEye error message"""

NO_SIGNATURE_ERROR = gettext(
    "The image is too simple to find matches. TinEye requires a basic level of"
    " visual detail to successfully identify matches."
)
"""TinEye error message"""

DOWNLOAD_ERROR = gettext("The image could not be downloaded.")
"""TinEye error message"""


def request(query, params):
    """Build TinEye HTTP request using ``search_urls`` of a :py:obj:`engine_type`."""

    params['raise_for_httperror'] = False

    if params['search_urls']['data:image']:
        query = params['search_urls']['data:image']
    elif params['search_urls']['http']:
        query = params['search_urls']['http']

    logger.debug("query URL: %s", query)
    query = urlencode({'url': query})

    # see https://github.com/TinEye/pytineye/blob/main/pytineye/api.py
    params['url'] = base_url + search_string.format(query=query, page=params['pageno'])

    params['headers'].update(
        {
            'Connection': 'keep-alive',
            'Accept-Encoding': 'gzip, defalte, br',
            'Host': 'tineye.com',
            'DNT': '1',
            'TE': 'trailers',
        }
    )
    return params


def parse_tineye_match(match_json):
    """Takes parsed JSON from the API server and turns it into a :py:obj:`dict`
    object.

    Attributes `(class Match) <https://github.com/TinEye/pytineye/blob/main/pytineye/api.py>`__

    - `image_url`, link to the result image.
    - `domain`, domain this result was found on.
    - `score`, a number (0 to 100) that indicates how closely the images match.
    - `width`, image width in pixels.
    - `height`, image height in pixels.
    - `size`, image area in pixels.
    - `format`, image format.
    - `filesize`, image size in bytes.
    - `overlay`, overlay URL.
    - `tags`, whether this match belongs to a collection or stock domain.

    - `backlinks`, a list of Backlink objects pointing to the original websites
      and image URLs. List items are instances of :py:obj:`dict`, (`Backlink
      <https://github.com/TinEye/pytineye/blob/main/pytineye/api.py>`__):

      - `url`, the image URL to the image.
      - `backlink`, the original website URL.
      - `crawl_date`, the date the image was crawled.

    """

    # HINT: there exists an alternative backlink dict in the domains list / e.g.::
    #
    #     match_json['domains'][0]['backlinks']

    backlinks = []
    if "backlinks" in match_json:

        for backlink_json in match_json["backlinks"]:
            if not isinstance(backlink_json, dict):
                continue

            crawl_date = backlink_json.get("crawl_date")
            if crawl_date:
                crawl_date = datetime.strptime(crawl_date, '%Y-%m-%d')
            else:
                crawl_date = datetime.min

            backlinks.append(
                {
                    'url': backlink_json.get("url"),
                    'backlink': backlink_json.get("backlink"),
                    'crawl_date': crawl_date,
                    'image_name': backlink_json.get("image_name"),
                }
            )

    return {
        'image_url': match_json.get("image_url"),
        'domain': match_json.get("domain"),
        'score': match_json.get("score"),
        'width': match_json.get("width"),
        'height': match_json.get("height"),
        'size': match_json.get("size"),
        'image_format': match_json.get("format"),
        'filesize': match_json.get("filesize"),
        'overlay': match_json.get("overlay"),
        'tags': match_json.get("tags"),
        'backlinks': backlinks,
    }


def response(resp):
    """Parse HTTP response from TinEye."""

    # handle the 422 client side errors, and the possible 400 status code error
    if resp.status_code in (400, 422):
        json_data = resp.json()
        suggestions = json_data.get('suggestions', {})
        message = f'HTTP Status Code: {resp.status_code}'

        if resp.status_code == 422:
            s_key = suggestions.get('key', '')
            if s_key == "Invalid image URL":
                # test https://docs.searxng.org/_static/searxng-wordmark.svg
                message = FORMAT_NOT_SUPPORTED
            elif s_key == 'NO_SIGNATURE_ERROR':
                # test https://pngimg.com/uploads/dot/dot_PNG4.png
                message = NO_SIGNATURE_ERROR
            elif s_key == 'Download Error':
                # test https://notexists
                message = DOWNLOAD_ERROR
            else:
                logger.warning("Unknown suggestion key encountered: %s", s_key)
        else:  # 400
            description = suggestions.get('description')
            if isinstance(description, list):
                message = ','.join(description)

        # see https://github.com/searxng/searxng/pull/1456#issuecomment-1193105023
        # results.append({'answer': message})
        logger.error(message)
        return []

    # Raise for all other responses
    resp.raise_for_status()

    results = []
    json_data = resp.json()

    for match_json in json_data['matches']:

        tineye_match = parse_tineye_match(match_json)
        if not tineye_match['backlinks']:
            continue

        backlink = tineye_match['backlinks'][0]
        results.append(
            {
                'template': 'images.html',
                'url': backlink['backlink'],
                'thumbnail_src': tineye_match['image_url'],
                'source': backlink['url'],
                'title': backlink['image_name'],
                'img_src': backlink['url'],
                'format': tineye_match['image_format'],
                'widht': tineye_match['width'],
                'height': tineye_match['height'],
                'publishedDate': backlink['crawl_date'],
            }
        )

    # append number of results

    number_of_results = json_data.get('num_matches')
    if number_of_results:
        results.append({'number_of_results': number_of_results})

    return results