tineye.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # lint: pylint
  3. """This engine implements *Tineye - reverse image search*
  4. Using TinEye, you can search by image or perform what we call a reverse image
  5. search. You can do that by uploading an image or searching by URL. You can also
  6. simply drag and drop your images to start your search. TinEye constantly crawls
  7. the web and adds images to its index. Today, the TinEye index is over 50.2
  8. billion images `[tineye.com] <https://tineye.com/how>`_.
  9. .. hint::
  10. This SearXNG engine only supports *'searching by URL'* and it does not use
  11. the official API `[api.tineye.com] <https://api.tineye.com/python/docs/>`_.
  12. """
  13. from urllib.parse import urlencode
  14. from datetime import datetime
  15. from flask_babel import gettext
  16. about = {
  17. "website": 'https://tineye.com',
  18. "wikidata_id": 'Q2382535',
  19. "official_api_documentation": 'https://api.tineye.com/python/docs/',
  20. "use_official_api": False,
  21. "require_api_key": False,
  22. "results": 'JSON',
  23. }
  24. engine_type = 'online_url_search'
  25. """:py:obj:`searx.search.processors.online_url_search`"""
  26. categories = ['general']
  27. paging = True
  28. safesearch = False
  29. base_url = 'https://tineye.com'
  30. search_string = '/result_json/?page={page}&{query}'
  31. FORMAT_NOT_SUPPORTED = gettext(
  32. "Could not read that image url. This may be due to an unsupported file"
  33. " format. TinEye only supports images that are JPEG, PNG, GIF, BMP, TIFF or WebP."
  34. )
  35. """TinEye error message"""
  36. NO_SIGNATURE_ERROR = gettext(
  37. "The image is too simple to find matches. TinEye requires a basic level of"
  38. " visual detail to successfully identify matches."
  39. )
  40. """TinEye error message"""
  41. DOWNLOAD_ERROR = gettext("The image could not be downloaded.")
  42. """TinEye error message"""
  43. def request(query, params):
  44. """Build TinEye HTTP request using ``search_urls`` of a :py:obj:`engine_type`."""
  45. params['raise_for_httperror'] = False
  46. if params['search_urls']['data:image']:
  47. query = params['search_urls']['data:image']
  48. elif params['search_urls']['http']:
  49. query = params['search_urls']['http']
  50. logger.debug("query URL: %s", query)
  51. query = urlencode({'url': query})
  52. # see https://github.com/TinEye/pytineye/blob/main/pytineye/api.py
  53. params['url'] = base_url + search_string.format(query=query, page=params['pageno'])
  54. params['headers'].update(
  55. {
  56. 'Connection': 'keep-alive',
  57. 'Accept-Encoding': 'gzip, defalte, br',
  58. 'Host': 'tineye.com',
  59. 'DNT': '1',
  60. 'TE': 'trailers',
  61. }
  62. )
  63. return params
  64. def parse_tineye_match(match_json):
  65. """Takes parsed JSON from the API server and turns it into a :py:obj:`dict`
  66. object.
  67. Attributes `(class Match) <https://github.com/TinEye/pytineye/blob/main/pytineye/api.py>`__
  68. - `image_url`, link to the result image.
  69. - `domain`, domain this result was found on.
  70. - `score`, a number (0 to 100) that indicates how closely the images match.
  71. - `width`, image width in pixels.
  72. - `height`, image height in pixels.
  73. - `size`, image area in pixels.
  74. - `format`, image format.
  75. - `filesize`, image size in bytes.
  76. - `overlay`, overlay URL.
  77. - `tags`, whether this match belongs to a collection or stock domain.
  78. - `backlinks`, a list of Backlink objects pointing to the original websites
  79. and image URLs. List items are instances of :py:obj:`dict`, (`Backlink
  80. <https://github.com/TinEye/pytineye/blob/main/pytineye/api.py>`__):
  81. - `url`, the image URL to the image.
  82. - `backlink`, the original website URL.
  83. - `crawl_date`, the date the image was crawled.
  84. """
  85. # HINT: there exists an alternative backlink dict in the domains list / e.g.::
  86. #
  87. # match_json['domains'][0]['backlinks']
  88. backlinks = []
  89. if "backlinks" in match_json:
  90. for backlink_json in match_json["backlinks"]:
  91. if not isinstance(backlink_json, dict):
  92. continue
  93. crawl_date = backlink_json.get("crawl_date")
  94. if crawl_date:
  95. crawl_date = datetime.fromisoformat(crawl_date[:-3])
  96. else:
  97. crawl_date = datetime.min
  98. backlinks.append(
  99. {
  100. 'url': backlink_json.get("url"),
  101. 'backlink': backlink_json.get("backlink"),
  102. 'crawl_date': crawl_date,
  103. 'image_name': backlink_json.get("image_name"),
  104. }
  105. )
  106. return {
  107. 'image_url': match_json.get("image_url"),
  108. 'domain': match_json.get("domain"),
  109. 'score': match_json.get("score"),
  110. 'width': match_json.get("width"),
  111. 'height': match_json.get("height"),
  112. 'size': match_json.get("size"),
  113. 'image_format': match_json.get("format"),
  114. 'filesize': match_json.get("filesize"),
  115. 'overlay': match_json.get("overlay"),
  116. 'tags': match_json.get("tags"),
  117. 'backlinks': backlinks,
  118. }
  119. def response(resp):
  120. """Parse HTTP response from TinEye."""
  121. results = []
  122. try:
  123. json_data = resp.json()
  124. except Exception as exc: # pylint: disable=broad-except
  125. msg = "can't parse JSON response // %s" % exc
  126. logger.error(msg)
  127. json_data = {'error': msg}
  128. # handle error codes from Tineye
  129. if resp.is_error:
  130. if resp.status_code in (400, 422):
  131. message = 'HTTP status: %s' % resp.status_code
  132. error = json_data.get('error')
  133. s_key = json_data.get('suggestions', {}).get('key', '')
  134. if error and s_key:
  135. message = "%s (%s)" % (error, s_key)
  136. elif error:
  137. message = error
  138. if s_key == "Invalid image URL":
  139. # test https://docs.searxng.org/_static/searxng-wordmark.svg
  140. message = FORMAT_NOT_SUPPORTED
  141. elif s_key == 'NO_SIGNATURE_ERROR':
  142. # test https://pngimg.com/uploads/dot/dot_PNG4.png
  143. message = NO_SIGNATURE_ERROR
  144. elif s_key == 'Download Error':
  145. # test https://notexists
  146. message = DOWNLOAD_ERROR
  147. # see https://github.com/searxng/searxng/pull/1456#issuecomment-1193105023
  148. # results.append({'answer': message})
  149. logger.error(message)
  150. return results
  151. resp.raise_for_status()
  152. # append results from matches
  153. for match_json in json_data['matches']:
  154. tineye_match = parse_tineye_match(match_json)
  155. if not tineye_match['backlinks']:
  156. continue
  157. backlink = tineye_match['backlinks'][0]
  158. results.append(
  159. {
  160. 'template': 'images.html',
  161. 'url': backlink['backlink'],
  162. 'thumbnail_src': tineye_match['image_url'],
  163. 'source': backlink['url'],
  164. 'title': backlink['image_name'],
  165. 'img_src': backlink['url'],
  166. 'format': tineye_match['image_format'],
  167. 'widht': tineye_match['width'],
  168. 'height': tineye_match['height'],
  169. 'publishedDate': backlink['crawl_date'],
  170. }
  171. )
  172. # append number of results
  173. number_of_results = json_data.get('num_matches')
  174. if number_of_results:
  175. results.append({'number_of_results': number_of_results})
  176. return results