search.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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. from PyQt5.QtCore import pyqtSignal, QObject
  22. from searxqt import http as Http
  23. from searxqt.core.searx import SearX, SearxConfigHandler
  24. from searxqt.core.http import ErrorType
  25. from searxqt.core import log
  26. class SearchStatus:
  27. Done = 0
  28. Busy = 1
  29. class SearchBehaviour:
  30. Normal = 0
  31. RandomEvery = 1
  32. class CategoryModel(QObject):
  33. """ stateChanged; emitted when this category gets enabled or disabled.
  34. str: category key
  35. bool: state
  36. """
  37. stateChanged = pyqtSignal(str, bool)
  38. """ changed; emitted when a engine is added or removed from this category.
  39. str: category key
  40. """
  41. changed = pyqtSignal(str)
  42. def __init__(self, key, name, checked=False, parent=None):
  43. QObject.__init__(self, parent=parent)
  44. self.__key = key
  45. self.__name = name
  46. self.__checked = checked
  47. @property
  48. def name(self): return self.__name
  49. @property
  50. def key(self): return self.__key
  51. def isChecked(self):
  52. return self.__checked
  53. def check(self):
  54. self.__checked = True
  55. self.stateChanged.emit(self.key, True)
  56. def uncheck(self):
  57. self.__checked = False
  58. self.stateChanged.emit(self.key, False)
  59. class UserCategoryModel(CategoryModel):
  60. def __init__(self, key, name, checked=False, engines=None, parent=None):
  61. CategoryModel.__init__(self, key, name, checked=checked, parent=parent)
  62. # https://docs.python.org/3/faq/programming.html?highlight=default%20shared%20values%20objects#why-are-default-values-shared-between-objects
  63. self.__engines = engines if engines is not None else []
  64. @property
  65. def engines(self): return self.__engines
  66. def addEngine(self, engineStr):
  67. engineStr = engineStr.lower()
  68. if engineStr in self.__engines:
  69. log.error(f"Attempt to add a engine `{engineStr}` to user " \
  70. f"category `{self.name}` that is already there. " \
  71. "This should not happen.", self)
  72. return
  73. self.__engines.append(engineStr)
  74. self.changed.emit(self.key)
  75. def removeEngine(self, engineStr):
  76. self.__engines.remove(engineStr)
  77. self.changed.emit(self.key)
  78. class CategoriesModel(QObject): # generic
  79. CatModel = CategoryModel
  80. """ stateChanged; emitted when this category gets enabled/disabled.
  81. str: category key
  82. bool: state (enabled/disabled)
  83. """
  84. stateChanged = pyqtSignal(str, bool)
  85. """ changed; emitted when a category has changed (engine added or removed).
  86. str: category key
  87. """
  88. changed = pyqtSignal(str)
  89. """ removed; emitted when a category has been removed.
  90. str: category key
  91. """
  92. removed = pyqtSignal(str)
  93. """ dataChanged; emitted on setData (views must re-generate labels)
  94. """
  95. dataChanged = pyqtSignal()
  96. def __init__(self, parent=None):
  97. QObject.__init__(self, parent=parent)
  98. self._categories = {}
  99. def __contains__(self, key): return bool(key in self._categories)
  100. def __iter__(self): return iter(self._categories)
  101. def __len__(self): return len(self._categories)
  102. def __getitem__(self, key): return self._categories[key]
  103. def clear(self):
  104. for cat in list(self._categories.values()):
  105. self.removeCategory(cat.key)
  106. def data(self):
  107. return [cat.key for cat in self._categories.values()
  108. if cat.isChecked()]
  109. def setData(self, data):
  110. for catKey in data:
  111. if catKey in self:
  112. self[catKey].check()
  113. self.dataChanged.emit()
  114. def addCategory(self, catKey, name, checked=False):
  115. newCat = self.CatModel(catKey, name, checked=checked, parent=self)
  116. newCat.stateChanged.connect(self.stateChanged)
  117. newCat.changed.connect(self.changed)
  118. self._categories.update({catKey: newCat})
  119. return True
  120. def removeCategory(self, key):
  121. self._categories[key].stateChanged.disconnect(self.stateChanged)
  122. self._categories[key].deleteLater()
  123. del self._categories[key]
  124. self.removed.emit(key)
  125. def isChecked(self, key):
  126. return self._categories[key].isChecked()
  127. def checkedCategories(self):
  128. return [key for key in self._categories
  129. if self._categories[key].isChecked()]
  130. def items(self):
  131. return self._categories.items()
  132. def keys(self):
  133. return self._categories.keys()
  134. def values(self):
  135. return self._categories.values()
  136. def copy(self):
  137. return self._categories.copy()
  138. class UserCategoriesModel(CategoriesModel):
  139. CatModel = UserCategoryModel
  140. def __init__(self, parent=None):
  141. CategoriesModel.__init__(self, parent=parent)
  142. def data(self):
  143. data = {}
  144. for catKey, cat in self._categories.items():
  145. data.update({catKey: (cat.name, cat.isChecked(), cat.engines)})
  146. return data
  147. def setData(self, data):
  148. self.clear()
  149. for catKey, catData in data.items():
  150. self.addCategory(
  151. catKey,
  152. catData[0], # name
  153. checked=catData[1],
  154. engines=catData[2]
  155. )
  156. if catData[1]:
  157. self.stateChanged.emit(catKey, True)
  158. self.dataChanged.emit()
  159. def addCategory(self, catKey, name, checked=False, engines=None):
  160. newCat = self.CatModel(
  161. catKey,
  162. name,
  163. checked=checked,
  164. engines=engines,
  165. parent=self
  166. )
  167. newCat.stateChanged.connect(self.stateChanged)
  168. newCat.changed.connect(self.changed)
  169. self._categories.update({catKey: newCat})
  170. return True
  171. class SearchModel(SearX, QObject):
  172. statusChanged = pyqtSignal(int) # SearchStatus
  173. optionsChanged = pyqtSignal()
  174. def __init__(self, httpSettings, parent=None):
  175. SearX.__init__(self, Http.Thread, httpSettings)
  176. QObject.__init__(self, parent=parent)
  177. self._status = SearchStatus.Done
  178. self._randomEveryRequest = False
  179. self._useFallback = True
  180. self._isBusy = False
  181. self._reponse = None
  182. # Options
  183. @property
  184. def useFallback(self):
  185. """
  186. @rtype: bool
  187. """
  188. return self._useFallback
  189. @useFallback.setter
  190. def useFallback(self, state):
  191. """
  192. @type state: bool
  193. """
  194. self._useFallback = state
  195. self.optionsChanged.emit()
  196. @property
  197. def randomEvery(self):
  198. """
  199. @rtype: bool
  200. """
  201. return self._randomEveryRequest
  202. @randomEvery.setter
  203. def randomEvery(self, state):
  204. """
  205. @type state: bool
  206. """
  207. self._randomEveryRequest = state
  208. self.optionsChanged.emit()
  209. @property
  210. def isBusy(self):
  211. return self._isBusy
  212. @property
  213. def response(self):
  214. return self._response
  215. # End options
  216. def status(self): return self._status
  217. def saveSettings(self):
  218. """ Returns current state
  219. """
  220. return {
  221. 'fallback': self.useFallback,
  222. 'randomEvery': self.randomEvery,
  223. 'parseHtml': self.parseHtml,
  224. 'safeSearch': self.safeSearch
  225. }
  226. def loadSettings(self, data):
  227. """ Restore current state
  228. @type data: dict
  229. """
  230. self.useFallback = data.get('fallback', True)
  231. self.randomEvery = data.get('randomEvery', False)
  232. self.parseHtml = data.get('parseHtml', True)
  233. self.safeSearch = data.get('safeSearch', False)
  234. self._response = None
  235. """ SearX re-implementations below
  236. """
  237. def searchFinishedCb(self, response):
  238. self._isBusy = False
  239. self._response = response
  240. self.statusChanged.emit(SearchStatus.Done)
  241. def search(self):
  242. self.statusChanged.emit(SearchStatus.Busy)
  243. SearX.search(self)
  244. self._isBusy = True
  245. class UserInstancesHandler(SearxConfigHandler, QObject):
  246. changed = pyqtSignal()
  247. def __init__(self, httpSettings, parent=None):
  248. SearxConfigHandler.__init__(self, httpSettings)
  249. QObject.__init__(self, parent=parent)
  250. def updateInstanceFinished(self, response):
  251. if response.error != ErrorType.Success:
  252. return
  253. self.changed.emit()
  254. # HandlerProto override
  255. def setData(self, data):
  256. SearxConfigHandler.setData(self, data)
  257. self.changed.emit()
  258. def addInstance(self, url):
  259. if SearxConfigHandler.addInstance(self, url):
  260. self.changed.emit()
  261. return True
  262. return False
  263. def removeMultiInstances(self, urls):
  264. SearxConfigHandler.removeMultiInstances(self, urls)
  265. self.changed.emit()