search.py 11 KB

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