qBittorrent
searchcontroller.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  *
19  * In addition, as a special exception, the copyright holders give permission to
20  * link this program with the OpenSSL project's "OpenSSL" library (or with
21  * modified versions of it that use the same license as the "OpenSSL" library),
22  * and distribute the linked executables. You must obey the GNU General Public
23  * License in all respects for all of the code used other than "OpenSSL". If you
24  * modify file(s), you may extend this exception to your version of the file(s),
25  * but you are not obligated to do so. If you do not wish to do so, delete this
26  * exception statement from your version.
27  */
28 
29 #include "searchcontroller.h"
30 
31 #include <limits>
32 
33 #include <QHash>
34 #include <QJsonArray>
35 #include <QJsonObject>
36 #include <QList>
37 #include <QSharedPointer>
38 
39 #include "base/global.h"
40 #include "base/logger.h"
42 #include "base/utils/foreignapps.h"
43 #include "base/utils/random.h"
44 #include "base/utils/string.h"
45 #include "apierror.h"
46 #include "isessionmanager.h"
47 
48 using SearchHandlerPtr = QSharedPointer<SearchHandler>;
49 using SearchHandlerDict = QMap<int, SearchHandlerPtr>;
50 
51 namespace
52 {
53  const QLatin1String ACTIVE_SEARCHES("activeSearches");
54  const QLatin1String SEARCH_HANDLERS("searchHandlers");
55 
56  void removeActiveSearch(ISession *session, const int id)
57  {
58  auto activeSearches = session->getData<QSet<int>>(ACTIVE_SEARCHES);
59  if (activeSearches.remove(id))
60  session->setData(ACTIVE_SEARCHES, QVariant::fromValue(activeSearches));
61  }
62 
71  QJsonArray getPluginCategories(QStringList categories)
72  {
73  QJsonArray categoriesInfo
74  {QJsonObject {
75  {QLatin1String("id"), "all"},
76  {QLatin1String("name"), SearchPluginManager::categoryFullName("all")}
77  }};
78 
79  categories.sort(Qt::CaseInsensitive);
80  for (const QString &category : categories)
81  {
82  categoriesInfo << QJsonObject
83  {
84  {QLatin1String("id"), category},
85  {QLatin1String("name"), SearchPluginManager::categoryFullName(category)}
86  };
87  }
88 
89  return categoriesInfo;
90  }
91 }
92 
94 {
95  requireParams({"pattern", "category", "plugins"});
96 
97  if (!Utils::ForeignApps::pythonInfo().isValid())
98  throw APIError(APIErrorType::Conflict, tr("Python must be installed to use the Search Engine."));
99 
100  const QString pattern = params()["pattern"].trimmed();
101  const QString category = params()["category"].trimmed();
102  const QStringList plugins = params()["plugins"].split('|');
103 
104  QStringList pluginsToUse;
105  if (plugins.size() == 1)
106  {
107  const QString pluginsLower = plugins[0].toLower();
108  if (pluginsLower == "all")
109  pluginsToUse = SearchPluginManager::instance()->allPlugins();
110  else if ((pluginsLower == "enabled") || (pluginsLower == "multi"))
111  pluginsToUse = SearchPluginManager::instance()->enabledPlugins();
112  else
113  pluginsToUse << plugins;
114  }
115  else
116  {
117  pluginsToUse << plugins;
118  }
119 
120  ISession *const session = sessionManager()->session();
121  auto activeSearches = session->getData<QSet<int>>(ACTIVE_SEARCHES);
122  if (activeSearches.size() >= MAX_CONCURRENT_SEARCHES)
123  throw APIError(APIErrorType::Conflict, tr("Unable to create more than %1 concurrent searches.").arg(MAX_CONCURRENT_SEARCHES));
124 
125  const auto id = generateSearchId();
126  const SearchHandlerPtr searchHandler {SearchPluginManager::instance()->startSearch(pattern, category, pluginsToUse)};
127  QObject::connect(searchHandler.data(), &SearchHandler::searchFinished, this, [session, id, this]() { searchFinished(session, id); });
128  QObject::connect(searchHandler.data(), &SearchHandler::searchFailed, this, [session, id, this]() { searchFailed(session, id); });
129 
130  auto searchHandlers = session->getData<SearchHandlerDict>(SEARCH_HANDLERS);
131  searchHandlers.insert(id, searchHandler);
132  session->setData(SEARCH_HANDLERS, QVariant::fromValue(searchHandlers));
133 
134  activeSearches.insert(id);
135  session->setData(ACTIVE_SEARCHES, QVariant::fromValue(activeSearches));
136 
137  const QJsonObject result = {{"id", id}};
138  setResult(result);
139 }
140 
142 {
143  requireParams({"id"});
144 
145  const int id = params()["id"].toInt();
146  ISession *const session = sessionManager()->session();
147 
148  const auto searchHandlers = session->getData<SearchHandlerDict>(SEARCH_HANDLERS);
149  if (!searchHandlers.contains(id))
151 
152  const SearchHandlerPtr searchHandler = searchHandlers[id];
153 
154  if (searchHandler->isActive())
155  {
156  searchHandler->cancelSearch();
157  removeActiveSearch(session, id);
158  }
159 }
160 
162 {
163  const int id = params()["id"].toInt();
164 
165  const auto searchHandlers = sessionManager()->session()->getData<SearchHandlerDict>(SEARCH_HANDLERS);
166  if ((id != 0) && !searchHandlers.contains(id))
168 
169  QJsonArray statusArray;
170  const QList<int> searchIds {(id == 0) ? searchHandlers.keys() : QList<int> {id}};
171 
172  for (const int searchId : searchIds)
173  {
174  const SearchHandlerPtr searchHandler = searchHandlers[searchId];
175  statusArray << QJsonObject
176  {
177  {"id", searchId},
178  {"status", searchHandler->isActive() ? "Running" : "Stopped"},
179  {"total", searchHandler->results().size()}
180  };
181  }
182 
183  setResult(statusArray);
184 }
185 
187 {
188  requireParams({"id"});
189 
190  const int id = params()["id"].toInt();
191  int limit = params()["limit"].toInt();
192  int offset = params()["offset"].toInt();
193 
194  const auto searchHandlers = sessionManager()->session()->getData<SearchHandlerDict>(SEARCH_HANDLERS);
195  if (!searchHandlers.contains(id))
197 
198  const SearchHandlerPtr searchHandler = searchHandlers[id];
199  const QList<SearchResult> searchResults = searchHandler->results();
200  const int size = searchResults.size();
201 
202  if (offset > size)
203  throw APIError(APIErrorType::Conflict, tr("Offset is out of range"));
204 
205  // normalize values
206  if (offset < 0)
207  offset = size + offset;
208  if (offset < 0) // check again
209  throw APIError(APIErrorType::Conflict, tr("Offset is out of range"));
210  if (limit <= 0)
211  limit = -1;
212 
213  if ((limit > 0) || (offset > 0))
214  setResult(getResults(searchResults.mid(offset, limit), searchHandler->isActive(), size));
215  else
216  setResult(getResults(searchResults, searchHandler->isActive(), size));
217 }
218 
220 {
221  requireParams({"id"});
222 
223  const int id = params()["id"].toInt();
224  ISession *const session = sessionManager()->session();
225 
226  auto searchHandlers = session->getData<SearchHandlerDict>(SEARCH_HANDLERS);
227  if (!searchHandlers.contains(id))
229 
230  const SearchHandlerPtr searchHandler = searchHandlers[id];
231  searchHandler->cancelSearch();
232  searchHandlers.remove(id);
233  session->setData(SEARCH_HANDLERS, QVariant::fromValue(searchHandlers));
234 
235  removeActiveSearch(session, id);
236 }
237 
239 {
240  const QStringList allPlugins = SearchPluginManager::instance()->allPlugins();
241  setResult(getPluginsInfo(allPlugins));
242 }
243 
245 {
246  requireParams({"sources"});
247 
248  const QStringList sources = params()["sources"].split('|');
249  for (const QString &source : sources)
251 }
252 
254 {
255  requireParams({"names"});
256 
257  const QStringList names = params()["names"].split('|');
258  for (const QString &name : names)
260 }
261 
263 {
264  requireParams({"names", "enable"});
265 
266  const QStringList names = params()["names"].split('|');
267  const bool enable = Utils::String::parseBool(params()["enable"].trimmed()).value_or(false);
268 
269  for (const QString &name : names)
270  SearchPluginManager::instance()->enablePlugin(name.trimmed(), enable);
271 }
272 
274 {
275  SearchPluginManager *const pluginManager = SearchPluginManager::instance();
276 
279  pluginManager->checkForUpdates();
280 }
281 
282 void SearchController::checkForUpdatesFinished(const QHash<QString, PluginVersion> &updateInfo)
283 {
284  if (updateInfo.isEmpty())
285  {
286  LogMsg(tr("All plugins are already up to date."), Log::INFO);
287  return;
288  }
289 
290  LogMsg(tr("Updating %1 plugins").arg(updateInfo.size()), Log::INFO);
291 
292  SearchPluginManager *const pluginManager = SearchPluginManager::instance();
293  for (const QString &pluginName : asConst(updateInfo.keys()))
294  {
295  LogMsg(tr("Updating plugin %1").arg(pluginName), Log::INFO);
296  pluginManager->updatePlugin(pluginName);
297  }
298 }
299 
300 void SearchController::checkForUpdatesFailed(const QString &reason)
301 {
302  LogMsg(tr("Failed to check for plugin updates: %1").arg(reason), Log::INFO);
303 }
304 
305 void SearchController::searchFinished(ISession *session, const int id)
306 {
307  removeActiveSearch(session, id);
308 }
309 
310 void SearchController::searchFailed(ISession *session, const int id)
311 {
312  removeActiveSearch(session, id);
313 }
314 
316 {
317  const auto searchHandlers = sessionManager()->session()->getData<SearchHandlerDict>(SEARCH_HANDLERS);
318 
319  while (true)
320  {
321  const int id = Utils::Random::rand(1, std::numeric_limits<int>::max());
322  if (!searchHandlers.contains(id))
323  return id;
324  }
325 }
326 
340 QJsonObject SearchController::getResults(const QList<SearchResult> &searchResults, const bool isSearchActive, const int totalResults) const
341 {
342  QJsonArray searchResultsArray;
343  for (const SearchResult &searchResult : searchResults)
344  {
345  searchResultsArray << QJsonObject
346  {
347  {"fileName", searchResult.fileName},
348  {"fileUrl", searchResult.fileUrl},
349  {"fileSize", searchResult.fileSize},
350  {"nbSeeders", searchResult.nbSeeders},
351  {"nbLeechers", searchResult.nbLeechers},
352  {"siteUrl", searchResult.siteUrl},
353  {"descrLink", searchResult.descrLink}
354  };
355  }
356 
357  const QJsonObject result =
358  {
359  {"status", isSearchActive ? "Running" : "Stopped"},
360  {"results", searchResultsArray},
361  {"total", totalResults}
362  };
363 
364  return result;
365 }
366 
380 QJsonArray SearchController::getPluginsInfo(const QStringList &plugins) const
381 {
382  QJsonArray pluginsArray;
383 
384  for (const QString &plugin : plugins)
385  {
386  const PluginInfo *const pluginInfo = SearchPluginManager::instance()->pluginInfo(plugin);
387 
388  pluginsArray << QJsonObject
389  {
390  {"name", pluginInfo->name},
391  {"version", QString(pluginInfo->version)},
392  {"fullName", pluginInfo->fullName},
393  {"url", pluginInfo->url},
394  {"supportedCategories", getPluginCategories(pluginInfo->supportedCategories)},
395  {"enabled", pluginInfo->enabled}
396  };
397  }
398 
399  return pluginsArray;
400 }
void requireParams(const QVector< QString > &requiredParams) const
const StringMap & params() const
ISessionManager * sessionManager() const
void setResult(const QString &result)
void searchFinished(ISession *session, int id)
QJsonArray getPluginsInfo(const QStringList &plugins) const
void searchFailed(ISession *session, int id)
int generateSearchId() const
void checkForUpdatesFailed(const QString &reason)
QJsonObject getResults(const QList< SearchResult > &searchResults, bool isSearchActive, int totalResults) const
void checkForUpdatesFinished(const QHash< QString, PluginVersion > &updateInfo)
const int MAX_CONCURRENT_SEARCHES
void searchFinished(bool cancelled=false)
void searchFailed()
void updatePlugin(const QString &name)
PluginInfo * pluginInfo(const QString &name) const
void enablePlugin(const QString &name, bool enabled=true)
QStringList enabledPlugins() const
SearchHandler * startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins)
static QString categoryFullName(const QString &categoryName)
QStringList allPlugins() const
void installPlugin(const QString &source)
void checkForUpdatesFinished(const QHash< QString, PluginVersion > &updateInfo)
void checkForUpdatesFailed(const QString &reason)
static SearchPluginManager * instance()
bool uninstallPlugin(const QString &name)
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
@ INFO
Definition: logger.h:46
PythonInfo pythonInfo()
uint32_t rand(uint32_t min=0, uint32_t max=std::numeric_limits< uint32_t >::max())
Definition: random.cpp:132
std::optional< bool > parseBool(const QString &string)
Definition: string.cpp:72
const QLatin1String ACTIVE_SEARCHES("activeSearches")
const QLatin1String SEARCH_HANDLERS("searchHandlers")
void removeActiveSearch(ISession *session, const int id)
QJsonArray getPluginCategories(QStringList categories)
QSharedPointer< SearchHandler > SearchHandlerPtr
QMap< int, SearchHandlerPtr > SearchHandlerDict
virtual QVariant getData(const QString &id) const =0
virtual void setData(const QString &id, const QVariant &data)=0
virtual ISession * session()=0
QStringList supportedCategories
PluginVersion version