search.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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.core.searx import SearX, SearxConfigHandler
  23. from searxqt.core import log
  24. from searxqt.thread import Thread, ThreadManagerProto
  25. from searxqt.translations import _
  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, requestHandler, parent=None):
  175. SearX.__init__(self, requestHandler)
  176. QObject.__init__(self, parent=parent)
  177. self._status = SearchStatus.Done
  178. self._randomEveryRequest = False
  179. self._useFallback = True
  180. # Options
  181. @property
  182. def useFallback(self):
  183. """
  184. @rtype: bool
  185. """
  186. return self._useFallback
  187. @useFallback.setter
  188. def useFallback(self, state):
  189. """
  190. @type state: bool
  191. """
  192. self._useFallback = state
  193. self.optionsChanged.emit()
  194. @property
  195. def randomEvery(self):
  196. """
  197. @rtype: bool
  198. """
  199. return self._randomEveryRequest
  200. @randomEvery.setter
  201. def randomEvery(self, state):
  202. """
  203. @type state: bool
  204. """
  205. self._randomEveryRequest = state
  206. self.optionsChanged.emit()
  207. # End options
  208. def status(self): return self._status
  209. def saveSettings(self):
  210. """ Returns current state
  211. """
  212. return {
  213. 'fallback': self.useFallback,
  214. 'randomEvery': self.randomEvery
  215. }
  216. def loadSettings(self, data):
  217. """ Restore current state
  218. @type data: dict
  219. """
  220. self.useFallback = data.get('fallback', True)
  221. self.randomEvery = data.get('randomEvery', False)
  222. """ SearX re-implementations below
  223. """
  224. def search(self, requestKwargs={}):
  225. self.statusChanged.emit(SearchStatus.Busy)
  226. result = SearX.search(self)
  227. self.statusChanged.emit(SearchStatus.Done)
  228. return result
  229. class UserInstancesHandler(SearxConfigHandler, ThreadManagerProto):
  230. """
  231. """
  232. changed = pyqtSignal()
  233. def __init__(self, requestsHandler, parent=None):
  234. """
  235. @param requestsHandler:
  236. @type requestsHandler: core.requests.RequestsHandler
  237. """
  238. SearxConfigHandler.__init__(self, requestsHandler)
  239. ThreadManagerProto.__init__(self, parent=parent)
  240. self._currentThreadUrl = ""
  241. # ThreadManagerProto override
  242. def currentJobStr(self):
  243. if self.hasActiveJobs():
  244. url = self._currentThreadUrl
  245. queueCount = self.queueCount()
  246. return _(f"<b>Updating data:</b> {url} ({queueCount} left)")
  247. return ""
  248. # HandlerProto override
  249. def setData(self, data):
  250. self._threadQueue.clear()
  251. SearxConfigHandler.setData(self, data)
  252. self.changed.emit()
  253. def addInstance(self, url):
  254. if SearxConfigHandler.addInstance(self, url):
  255. self.changed.emit()
  256. return True
  257. return False
  258. def removeMultiInstances(self, urls):
  259. SearxConfigHandler.removeMultiInstances(self, urls)
  260. self.changed.emit()
  261. def updateInstance(self, url):
  262. if self._thread:
  263. if url not in self._threadQueue:
  264. self._threadQueue.append(url)
  265. else:
  266. self._thread = Thread(
  267. SearxConfigHandler.updateInstance,
  268. args=[self, url],
  269. parent=self
  270. )
  271. self._currentThreadUrl = url
  272. self._thread.finished.connect(
  273. self.__updateInstanceThreadFinished
  274. )
  275. self.threadStarted.emit()
  276. self._thread.start()
  277. def __clearUpdateThread(self):
  278. self._thread.finished.disconnect(
  279. self.__updateInstanceThreadFinished
  280. )
  281. # Wait before deleting because the `finished` signal is emited
  282. # from the thread itself, so this method could be called before the
  283. # thread is actually finished and result in a crash.
  284. self._thread.wait()
  285. self._thread.deleteLater()
  286. self._thread = None
  287. def __updateInstanceThreadFinished(self):
  288. result = self._thread.result()
  289. self.__clearUpdateThread()
  290. if result:
  291. self.changed.emit()
  292. self._currentThreadUrl = ""
  293. self.threadFinished.emit()
  294. if self._threadQueue:
  295. url = self._threadQueue.pop(0)
  296. self.updateInstance(url)