searx.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. ########################################################################
  2. # Searx-Qt - Lightweight desktop application for Searx.
  3. # Copyright (C) 2020-2022 CYBERDEViL
  4. #
  5. # This file is part of Searx-Qt.
  6. #
  7. # Searx-Qt is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Searx-Qt is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. import time
  22. import urllib.parse
  23. from copy import deepcopy
  24. from searxqt.core.requests import JsonResult, ErrorType
  25. from searxqt.core import jsonVerify
  26. from searxqt.core.handler import HandlerProto, NetworkTypes
  27. from searxqt.translations import _
  28. # Values used for the expected json structure verification.
  29. v_str = jsonVerify.Value(str)
  30. v_int = jsonVerify.Value(int)
  31. v_float = jsonVerify.Value(float)
  32. v_bool = jsonVerify.Value(bool)
  33. v_intFloat = jsonVerify.MultiValue((int, float))
  34. v_noneStr = jsonVerify.MultiValue((jsonVerify.NoneType, str))
  35. v_noneStrInt = jsonVerify.MultiValue((jsonVerify.NoneType, str, int))
  36. v_intStr = jsonVerify.MultiValue((int, str))
  37. v_intFloat = jsonVerify.MultiValue((int, float))
  38. v_intFloatNone = jsonVerify.MultiValue((int, float, jsonVerify.NoneType))
  39. v_strFloat = jsonVerify.MultiValue((str, float))
  40. v_ignore = jsonVerify.IgnoreValue()
  41. class SearchResult(JsonResult):
  42. ExpectedStructure = {
  43. "query": v_str,
  44. "number_of_results": v_intFloat,
  45. "results": [{
  46. "url": v_str,
  47. "title": v_str,
  48. "engine": v_str,
  49. "parsed_url": [
  50. v_str,
  51. v_str,
  52. v_str,
  53. v_str,
  54. v_str,
  55. v_str
  56. ],
  57. "engines": [v_str],
  58. "positions": [v_int],
  59. "content": v_str,
  60. "score": v_float,
  61. "category": v_str,
  62. "pretty_url": v_str,
  63. "is_onion": False,
  64. "publishedDate": v_str,
  65. "pubdate": v_str,
  66. "thumbnail_src": v_str,
  67. "template": v_str,
  68. "author": v_noneStr,
  69. "source": v_str,
  70. "img_format": v_str,
  71. "img_src": v_noneStr,
  72. # Files
  73. "seed": v_intStr,
  74. "leech": v_intStr,
  75. "magnetlink": v_str,
  76. "torrentfile": v_str,
  77. "filesize": v_intFloatNone,
  78. "infohash": v_str,
  79. "files": v_noneStrInt,
  80. "link": v_str,
  81. # Videos
  82. "thumbnail": v_str,
  83. "embedded": v_str,
  84. "length": v_str,
  85. # IT
  86. "code_language": v_str,
  87. "codelines": [[v_int, v_str]],
  88. "repository": v_str,
  89. # Location
  90. "longitude": v_strFloat,
  91. "latitude": v_strFloat,
  92. "boundingbox": [v_strFloat],
  93. # Ignore; to many types/structures. TODO
  94. "geojson": v_ignore,
  95. # Ignore; to many types/structures. TODO
  96. "address": v_ignore,
  97. # Ignore; to many types/structures. TODO
  98. "osm": v_ignore,
  99. }],
  100. "answers": [v_str],
  101. "corrections": [v_str],
  102. "infoboxes": [{
  103. "infobox": v_str,
  104. "id": v_str,
  105. "content": v_str, # TODO some instances return empty list..
  106. "img_src": v_noneStr,
  107. "attributes": [
  108. {
  109. "label": v_str,
  110. "value": v_str,
  111. "entity": v_str
  112. }
  113. ],
  114. "urls": [
  115. {
  116. "title": v_str,
  117. "url": v_str,
  118. "official": v_bool,
  119. "entity": v_str
  120. }
  121. ],
  122. "relatedTopics": v_ignore, #[v_strDict],
  123. "engine": v_str,
  124. "engines": [v_str]
  125. }],
  126. "suggestions": [v_str],
  127. "unresponsive_engines": [[v_str, v_str]]
  128. }
  129. def __init__(self, url, response, err="", errType=ErrorType.Success):
  130. JsonResult.__init__(self, url, response, err=err, errType=errType)
  131. def verifyFurther(self):
  132. JsonResult.verifyFurther(self)
  133. # One of the following keys have to be not empty.
  134. validKeys = [
  135. 'results',
  136. 'answers',
  137. 'corrections',
  138. 'infoboxes',
  139. 'suggestions'
  140. ]
  141. if self._errType == ErrorType.Success:
  142. data = self.json()
  143. valid = False
  144. for key in validKeys:
  145. if len(data.get(key, [])):
  146. valid = True
  147. break
  148. if not valid:
  149. self._errType = ErrorType.NoResults
  150. self._err = "NoResults: got: `{0}`".format(self.json())
  151. class SearxConfigResult(JsonResult):
  152. ExpectedStructure = {
  153. "autocomplete": v_str,
  154. "brand": {
  155. "CONTACT_URL": v_str,
  156. "DOCS_URL": v_str,
  157. "GIT_URL": v_str,
  158. "GIT_BRANCH": v_str
  159. },
  160. "categories": [v_str],
  161. "default_doi_resolver": v_str,
  162. "default_locale": v_str,
  163. "default_theme": v_str,
  164. "doi_resolvers": [v_str],
  165. "engines": [
  166. {
  167. "categories": [v_str],
  168. "enabled": v_bool,
  169. "language_support": v_bool,
  170. "name": v_str,
  171. "paging": v_bool,
  172. "safesearch": v_bool,
  173. "shortcut": v_str,
  174. "supported_languages": [v_str],
  175. "time_range_support": v_bool,
  176. "timeout": v_intFloat
  177. }
  178. ],
  179. "instance_name": v_str,
  180. "locales": {
  181. "": v_str
  182. },
  183. "plugins": [
  184. {
  185. "enabled": v_bool,
  186. "name": v_str
  187. }
  188. ],
  189. "safe_search": v_int,
  190. "version": v_str
  191. }
  192. def __init__(self, url, response, err="", errType=ErrorType.Success):
  193. JsonResult.__init__(self, url, response, err=err, errType=errType)
  194. class Categories:
  195. types = {
  196. 'general': (_('General'), 'category_general'),
  197. 'files': (_('Files'), 'category_files'),
  198. 'images': (_('Images'), 'category_images'),
  199. 'videos': (_('Videos'), 'category_videos'),
  200. 'it': (_('IT'), 'category_it'),
  201. 'map': (_('Location'), 'category_map'),
  202. 'music': (_('Music'), 'category_music'),
  203. 'news': (_('News'), 'category_news'),
  204. 'science': (_('Science'), 'category_science'),
  205. 'social media': (_('Social'), 'category_social media'),
  206. 'onions': (_('Onions'), 'category_onions'),
  207. 'shopping': (_('Shopping'), 'category_shopping')
  208. }
  209. def __init__(self):
  210. self._options = {}
  211. self.__makeOptions()
  212. def __makeOptions(self):
  213. self._options.clear()
  214. for key, t in self.types.items():
  215. self._options.update({key: False})
  216. def reset(self):
  217. self.__makeOptions()
  218. def get(self, key):
  219. return self._options[key]
  220. def set(self, key, state):
  221. """
  222. @param key: One of the keys in Categories.types
  223. @type key: str
  224. @param state: Enabled / disabled state
  225. @type state: bool
  226. """
  227. self._options[key] = state
  228. def dict(self):
  229. newDict = {}
  230. for key, state in self._options.items():
  231. if state:
  232. newDict.update({self.types[key][1]: 'on'})
  233. return newDict
  234. def enabledKeys(self):
  235. """ Returns a list with enabled engine strings (key from
  236. Categories.types)
  237. """
  238. return [key for key, state in self._options.items() if state]
  239. class Engines(list):
  240. def __init__(self):
  241. list.__init__(self)
  242. def dict(self):
  243. if not self:
  244. return {}
  245. return {
  246. 'engines': ",".join(self)
  247. }
  248. class SearX:
  249. Periods = {
  250. '': _('Anytime'),
  251. 'day': _('Last day'),
  252. 'week': _('Last week'),
  253. 'month': _('Last month'),
  254. 'year': _('Last year')
  255. }
  256. # https://github.com/asciimoo/searx/blob/master/searx/languages.py
  257. Languages = {
  258. '': _('No language'),
  259. 'all': _('Default language'),
  260. 'af-NA': 'Afrikaans - af-NA',
  261. 'ca-AD': 'Català - ca-AD',
  262. 'da-DK': 'Dansk - da-DK',
  263. 'de': 'Deutsch - de',
  264. 'de-AT': 'Deutsch (Österreich) - de-AT',
  265. 'de-CH': 'Deutsch (Schweiz) - de-CH',
  266. 'de-DE': 'Deutsch (Deutschland) - de-DE',
  267. 'et-EE': 'Eesti - et-EE',
  268. 'en': 'English - en',
  269. 'en-AU': 'English (Australia) - en-AU',
  270. 'en-CA': 'English (Canada) - en-CA',
  271. 'en-GB': 'English (United Kingdom) - en-GB',
  272. 'en-IE': 'English (Ireland) - en-IE',
  273. 'en-IN': 'English (India) - en-IN',
  274. 'en-NZ': 'English (New Zealand) - en-NZ',
  275. 'en-PH': 'English (Philippines) - en-PH',
  276. 'en-SG': 'English (Singapore) - en-SG',
  277. 'en-US': 'English (United States) - en-US',
  278. 'es': 'Español - es',
  279. 'es-AR': 'Español (Argentina) - es-AR',
  280. 'es-CL': 'Español (Chile) - es-CL',
  281. 'es-ES': 'Español (España) - es-ES',
  282. 'es-MX': 'Español (México) - es-MX',
  283. 'fr': 'Français - fr',
  284. 'fr-BE': 'Français (Belgique) - fr-BE',
  285. 'fr-CA': 'Français (Canada) - fr-CA',
  286. 'fr-CH': 'Français (Suisse) - fr-CH',
  287. 'fr-FR': 'Français (France) - fr-FR',
  288. 'hr-HR': 'Hrvatski - hr-HR',
  289. 'id-ID': 'Indonesia - id-ID',
  290. 'it-IT': 'Italiano - it-IT',
  291. 'sw-KE': 'Kiswahili - sw-KE',
  292. 'lv-LV': 'Latviešu - lv-LV',
  293. 'lt-LT': 'Lietuvių - lt-LT',
  294. 'hu-HU': 'Magyar - hu-HU',
  295. 'ms-MY': 'Melayu - ms-MY',
  296. 'nl': 'Nederlands - nl',
  297. 'nl-BE': 'Nederlands (België) - nl-BE',
  298. 'nl-NL': 'Nederlands (Nederland) - nl-NL',
  299. 'nb-NO': 'Norsk Bokmål - nb-NO',
  300. 'pl-PL': 'Polski - pl-PL',
  301. 'pt': 'Português - pt',
  302. 'pt-BR': 'Português (Brasil) - pt-BR',
  303. 'pt-PT': 'Português (Portugal) - pt-PT',
  304. 'ro-RO': 'Română - ro-RO',
  305. 'sk-SK': 'Slovenčina - sk-SK',
  306. 'sl-SI': 'Slovenščina - sl-SI',
  307. 'sr-RS': 'Srpski - sr-RS',
  308. 'fi-FI': 'Suomi - fi-FI',
  309. 'sv-SE': 'Svenska - sv-SE',
  310. 'vi-VN': 'Tiếng Việt - vi-VN',
  311. 'tr-TR': 'Türkçe - tr-TR',
  312. 'is-IS': 'Íslenska - is-IS',
  313. 'cs-CZ': 'Čeština - cs-CZ',
  314. 'el-GR': 'Ελληνικά - el-GR',
  315. 'be-BY': 'Беларуская - be-BY',
  316. 'bg-BG': 'Български - bg-BG',
  317. 'ru-RU': 'Русский - ru-RU',
  318. 'uk-UA': 'Українська - uk-UA',
  319. 'hy-AM': 'Հայերեն - hy-AM',
  320. 'he-IL': 'עברית - he-IL',
  321. 'ar-SA': 'العربية - ar-SA',
  322. 'fa-IR': 'فارسی - fa-IR',
  323. 'th-TH': 'ไทย - th-TH',
  324. 'zh': '中文 - zh',
  325. 'zh-CN': '中文 (中国) - zh-CN',
  326. 'zh-TW': '中文 (台灣) - zh-TW',
  327. 'ja-JP': '日本語 - ja-JP',
  328. 'ko-KR': '한국어 - ko-KR'
  329. }
  330. def __init__(self, requestHandler):
  331. self._requestHandler = requestHandler
  332. self._url = ""
  333. self._categories = Categories()
  334. self._engines = Engines()
  335. self._lang = ''
  336. self._pageno = '' # int formatted as string
  337. self._timeRange = '' # '', 'day', 'week', 'month' or 'year'
  338. self._kwargs = {
  339. 'data': {
  340. 'q': '',
  341. 'format': 'json'
  342. },
  343. }
  344. @property
  345. def categories(self): return self._categories
  346. @property
  347. def engines(self): return self._engines
  348. @property
  349. def url(self):
  350. """
  351. @return: Instance url
  352. @rtype: str
  353. """
  354. return self._url
  355. @url.setter
  356. def url(self, url):
  357. """
  358. @param url: Instance url
  359. @type url: str
  360. """
  361. self._url = url
  362. @property
  363. def query(self):
  364. """
  365. @return: Search query
  366. @rtype: str
  367. """
  368. return self._kwargs['data']['q']
  369. @query.setter
  370. def query(self, q):
  371. """
  372. @param q: Search query
  373. @type q: str
  374. """
  375. self._kwargs['data']['q'] = q
  376. @property
  377. def lang(self):
  378. """
  379. @return: Language code
  380. @rtype: str
  381. """
  382. return self._lang
  383. @lang.setter
  384. def lang(self, lang):
  385. """
  386. @param lang: Language code
  387. @type lang: str
  388. """
  389. self._lang = lang
  390. @property
  391. def pageno(self):
  392. """
  393. @return: Page number
  394. @rtype: int
  395. """
  396. return int(self._pageno)
  397. @pageno.setter
  398. def pageno(self, i):
  399. """
  400. @param i: Page number
  401. @type i: int
  402. """
  403. self._pageno = str(i)
  404. @property
  405. def timeRange(self):
  406. """
  407. @return: Search time range ('', 'day', 'week', 'month' or 'year')
  408. @rtype: str
  409. """
  410. return self._timeRange
  411. @timeRange.setter
  412. def timeRange(self, value):
  413. """
  414. @param value: Key from SearX.Periods
  415. @type value: str
  416. """
  417. self._timeRange = value
  418. @property
  419. def requestKwargs(self):
  420. """ Returns current data that will be send with the POST
  421. request used for the search operation. The search query,
  422. language, page-number and enabled categories/engines.
  423. @rtype: dict
  424. """
  425. kwargs = deepcopy(self._kwargs['data'])
  426. # After testing found that searx will honor only engines when
  427. # both engines and categories are set.
  428. if self.engines:
  429. kwargs.update(self.engines.dict())
  430. elif self.categories:
  431. kwargs.update(self.categories.dict())
  432. if self.lang:
  433. kwargs.update({'lang': self.lang})
  434. if self.pageno:
  435. kwargs.update({'pageno': self.pageno})
  436. if self.timeRange:
  437. kwargs.update({'timeRange': self.timeRange})
  438. return kwargs
  439. def reset(self):
  440. self.url = ""
  441. self.timeRange = ""
  442. self.lang = ""
  443. self.pageno = 1
  444. self.categories.reset()
  445. self.engines.clear()
  446. def search(self):
  447. """ Preform search operation with current set values.
  448. @returns: The result of this search.
  449. @rtype: SearchResult
  450. """
  451. return self._requestHandler.get(
  452. self.url,
  453. data=self.requestKwargs,
  454. ResultType=SearchResult
  455. )
  456. class SearxConfigHandler(HandlerProto):
  457. def __init__(self, requestsHandler):
  458. HandlerProto.__init__(self, requestsHandler)
  459. def updateInstance(self, url):
  460. newUrl = urllib.parse.urljoin(url, "/config")
  461. result = self.requestsHandler.get(newUrl, ResultType=SearxConfigResult)
  462. if result:
  463. instance = self.instances[url]
  464. j = result.json()
  465. """ Update instance version
  466. """
  467. instance.update({
  468. "version": j.get("version", "")
  469. })
  470. """ Update instance network_type to use our own network type
  471. definitions as class NetworkTypes (core/handler.py)
  472. """
  473. instance.update({"network_type": NetworkTypes.netTypeFromUrl(url)})
  474. """ Update Engines
  475. What we get:
  476. "engines": [
  477. categories (list, str)
  478. enabled (bool)
  479. language_support (bool)
  480. name (str)
  481. paging (bool)
  482. safesearch (bool)
  483. shortcut (str)
  484. supported_languages (list, str)
  485. time_range_support (bool)
  486. timeout (float)
  487. ]
  488. What instanceModel wants
  489. "engines" : {
  490. "not evil": {
  491. "error_rate": 15,
  492. "errors": [
  493. 0
  494. ]
  495. }
  496. }
  497. What enginesModel wants
  498. "engines": {
  499. "1337x": {
  500. "categories": [
  501. "videos"
  502. ],
  503. "language_support": true,
  504. "paging": true,
  505. "safesearch": false,
  506. "shortcut": "1337x",
  507. "time_range_support": false
  508. },
  509. """
  510. newInstanceEngines = {}
  511. newEnginesEngines = {}
  512. for engine in j.get('engines', []):
  513. name = engine.get('name', "")
  514. if not name:
  515. continue
  516. newInstanceEngines.update({
  517. name: {}
  518. })
  519. if name not in self.engines:
  520. newEnginesEngines.update({
  521. name: {
  522. "categories": list(engine.get('categories', [])),
  523. "language_support": engine.get(
  524. 'language_support',
  525. False
  526. ),
  527. "paging": engine.get('paging', False),
  528. "safesearch": engine.get('safesearch', False),
  529. "shortcut": engine.get('shortcut', ""),
  530. "time_range_support": engine.get(
  531. 'time_range_support',
  532. False
  533. )
  534. }
  535. })
  536. instance.update({
  537. "engines": dict(newInstanceEngines)
  538. })
  539. self.engines.update(newEnginesEngines)
  540. """ Update instance lastUpdated
  541. """
  542. instance.update({
  543. "lastUpdated": time.time()
  544. })
  545. return True
  546. return False
  547. def addInstance(self, url):
  548. if url not in self.instances:
  549. self._instances[url] = {}
  550. return True
  551. return False
  552. def removeInstance(self, url):
  553. """
  554. @param url: url of the instance to remove.
  555. @type url: str
  556. """
  557. del self._instances[url]
  558. def removeMultiInstances(self, urls):
  559. """ Remove instance(s) by url without emitting changed for every
  560. instance that got removed.
  561. @param urls: list with urls of instances to remove.
  562. @type urls: list
  563. """
  564. for url in urls:
  565. del self._instances[url]