WebUI: Fix reloading page after login
[qBittorrent.git] / src / webui / api / searchcontroller.cpp
blobb954da11951ca86cae0fd8d56b9eb91777883576
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2018 Thomas Piccirello <thomas.piccirello@gmail.com>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "searchcontroller.h"
32 #include <limits>
34 #include <QHash>
35 #include <QJsonArray>
36 #include <QJsonObject>
37 #include <QList>
38 #include <QSharedPointer>
40 #include "base/addtorrentmanager.h"
41 #include "base/global.h"
42 #include "base/interfaces/iapplication.h"
43 #include "base/logger.h"
44 #include "base/search/searchdownloadhandler.h"
45 #include "base/search/searchhandler.h"
46 #include "base/utils/datetime.h"
47 #include "base/utils/foreignapps.h"
48 #include "base/utils/random.h"
49 #include "base/utils/string.h"
50 #include "apierror.h"
51 #include "isessionmanager.h"
53 namespace
55 /**
56 * Returns the search categories in JSON format.
58 * The return value is an array of dictionaries.
59 * The dictionary keys are:
60 * - "id"
61 * - "name"
63 QJsonArray getPluginCategories(QStringList categories)
65 QJsonArray categoriesInfo
66 {QJsonObject {
67 {u"id"_s, u"all"_s},
68 {u"name"_s, SearchPluginManager::categoryFullName(u"all"_s)}
69 }};
71 categories.sort(Qt::CaseInsensitive);
72 for (const QString &category : categories)
74 categoriesInfo << QJsonObject
76 {u"id"_s, category},
77 {u"name"_s, SearchPluginManager::categoryFullName(category)}
81 return categoriesInfo;
85 void SearchController::startAction()
87 requireParams({u"pattern"_s, u"category"_s, u"plugins"_s});
89 if (!Utils::ForeignApps::pythonInfo().isValid())
90 throw APIError(APIErrorType::Conflict, tr("Python must be installed to use the Search Engine."));
92 const QString pattern = params()[u"pattern"_s].trimmed();
93 const QString category = params()[u"category"_s].trimmed();
94 const QStringList plugins = params()[u"plugins"_s].split(u'|');
96 QStringList pluginsToUse;
97 if (plugins.size() == 1)
99 const QString pluginsLower = plugins[0].toLower();
100 if (pluginsLower == u"all")
101 pluginsToUse = SearchPluginManager::instance()->allPlugins();
102 else if ((pluginsLower == u"enabled") || (pluginsLower == u"multi"))
103 pluginsToUse = SearchPluginManager::instance()->enabledPlugins();
104 else
105 pluginsToUse << plugins;
107 else
109 pluginsToUse << plugins;
112 if (m_activeSearches.size() >= MAX_CONCURRENT_SEARCHES)
113 throw APIError(APIErrorType::Conflict, tr("Unable to create more than %1 concurrent searches.").arg(MAX_CONCURRENT_SEARCHES));
115 const auto id = generateSearchId();
116 const std::shared_ptr<SearchHandler> searchHandler {SearchPluginManager::instance()->startSearch(pattern, category, pluginsToUse)};
117 QObject::connect(searchHandler.get(), &SearchHandler::searchFinished, this, [id, this]() { m_activeSearches.remove(id); });
118 QObject::connect(searchHandler.get(), &SearchHandler::searchFailed, this, [id, this]() { m_activeSearches.remove(id); });
120 m_searchHandlers.insert(id, searchHandler);
122 m_activeSearches.insert(id);
124 const QJsonObject result = {{u"id"_s, id}};
125 setResult(result);
128 void SearchController::stopAction()
130 requireParams({u"id"_s});
132 const int id = params()[u"id"_s].toInt();
134 const auto iter = m_searchHandlers.find(id);
135 if (iter == m_searchHandlers.end())
136 throw APIError(APIErrorType::NotFound);
138 const std::shared_ptr<SearchHandler> &searchHandler = iter.value();
140 if (searchHandler->isActive())
142 searchHandler->cancelSearch();
143 m_activeSearches.remove(id);
147 void SearchController::statusAction()
149 const int id = params()[u"id"_s].toInt();
151 if ((id != 0) && !m_searchHandlers.contains(id))
152 throw APIError(APIErrorType::NotFound);
154 QJsonArray statusArray;
155 const QList<int> searchIds {(id == 0) ? m_searchHandlers.keys() : QList<int> {id}};
157 for (const int searchId : searchIds)
159 const std::shared_ptr<SearchHandler> &searchHandler = m_searchHandlers[searchId];
160 statusArray << QJsonObject
162 {u"id"_s, searchId},
163 {u"status"_s, searchHandler->isActive() ? u"Running"_s : u"Stopped"_s},
164 {u"total"_s, searchHandler->results().size()}
168 setResult(statusArray);
171 void SearchController::resultsAction()
173 requireParams({u"id"_s});
175 const int id = params()[u"id"_s].toInt();
176 int limit = params()[u"limit"_s].toInt();
177 int offset = params()[u"offset"_s].toInt();
179 const auto iter = m_searchHandlers.find(id);
180 if (iter == m_searchHandlers.end())
181 throw APIError(APIErrorType::NotFound);
183 const std::shared_ptr<SearchHandler> &searchHandler = iter.value();
184 const QList<SearchResult> searchResults = searchHandler->results();
185 const int size = searchResults.size();
187 if (offset > size)
188 throw APIError(APIErrorType::Conflict, tr("Offset is out of range"));
190 // normalize values
191 if (offset < 0)
192 offset = size + offset;
193 if (offset < 0) // check again
194 throw APIError(APIErrorType::Conflict, tr("Offset is out of range"));
195 if (limit <= 0)
196 limit = -1;
198 if ((limit > 0) || (offset > 0))
199 setResult(getResults(searchResults.mid(offset, limit), searchHandler->isActive(), size));
200 else
201 setResult(getResults(searchResults, searchHandler->isActive(), size));
204 void SearchController::deleteAction()
206 requireParams({u"id"_s});
208 const int id = params()[u"id"_s].toInt();
210 const auto iter = m_searchHandlers.find(id);
211 if (iter == m_searchHandlers.end())
212 throw APIError(APIErrorType::NotFound);
214 const std::shared_ptr<SearchHandler> &searchHandler = iter.value();
215 searchHandler->cancelSearch();
216 m_activeSearches.remove(id);
217 m_searchHandlers.erase(iter);
220 void SearchController::downloadTorrentAction()
222 requireParams({u"torrentUrl"_s, u"pluginName"_s});
224 const QString torrentUrl = params()[u"torrentUrl"_s];
225 const QString pluginName = params()[u"pluginName"_s];
227 if (torrentUrl.startsWith(u"magnet:", Qt::CaseInsensitive))
229 app()->addTorrentManager()->addTorrent(torrentUrl);
231 else
233 SearchDownloadHandler *downloadHandler = SearchPluginManager::instance()->downloadTorrent(pluginName, torrentUrl);
234 connect(downloadHandler, &SearchDownloadHandler::downloadFinished
235 , this, [this, downloadHandler](const QString &source)
237 app()->addTorrentManager()->addTorrent(source);
238 downloadHandler->deleteLater();
243 void SearchController::pluginsAction()
245 const QStringList allPlugins = SearchPluginManager::instance()->allPlugins();
246 setResult(getPluginsInfo(allPlugins));
249 void SearchController::installPluginAction()
251 requireParams({u"sources"_s});
253 const QStringList sources = params()[u"sources"_s].split(u'|');
254 for (const QString &source : sources)
255 SearchPluginManager::instance()->installPlugin(source);
258 void SearchController::uninstallPluginAction()
260 requireParams({u"names"_s});
262 const QStringList names = params()[u"names"_s].split(u'|');
263 for (const QString &name : names)
264 SearchPluginManager::instance()->uninstallPlugin(name.trimmed());
267 void SearchController::enablePluginAction()
269 requireParams({u"names"_s, u"enable"_s});
271 const QStringList names = params()[u"names"_s].split(u'|');
272 const bool enable = Utils::String::parseBool(params()[u"enable"_s].trimmed()).value_or(false);
274 for (const QString &name : names)
275 SearchPluginManager::instance()->enablePlugin(name.trimmed(), enable);
278 void SearchController::updatePluginsAction()
280 SearchPluginManager *const pluginManager = SearchPluginManager::instance();
282 connect(pluginManager, &SearchPluginManager::checkForUpdatesFinished, this, &SearchController::checkForUpdatesFinished);
283 connect(pluginManager, &SearchPluginManager::checkForUpdatesFailed, this, &SearchController::checkForUpdatesFailed);
284 pluginManager->checkForUpdates();
287 void SearchController::checkForUpdatesFinished(const QHash<QString, PluginVersion> &updateInfo)
289 if (updateInfo.isEmpty())
291 LogMsg(tr("All plugins are already up to date."), Log::INFO);
292 return;
295 LogMsg(tr("Updating %1 plugins").arg(updateInfo.size()), Log::INFO);
297 SearchPluginManager *const pluginManager = SearchPluginManager::instance();
298 for (const QString &pluginName : asConst(updateInfo.keys()))
300 LogMsg(tr("Updating plugin %1").arg(pluginName), Log::INFO);
301 pluginManager->updatePlugin(pluginName);
305 void SearchController::checkForUpdatesFailed(const QString &reason)
307 LogMsg(tr("Failed to check for plugin updates: %1").arg(reason), Log::INFO);
310 int SearchController::generateSearchId() const
312 while (true)
314 const int id = Utils::Random::rand(1, std::numeric_limits<int>::max());
315 if (!m_searchHandlers.contains(id))
316 return id;
321 * Returns the search results in JSON format.
323 * The return value is an object with a status and an array of dictionaries.
324 * The dictionary keys are:
325 * - "fileName"
326 * - "fileUrl"
327 * - "fileSize"
328 * - "nbSeeders"
329 * - "nbLeechers"
330 * - "engineName"
331 * - "siteUrl"
332 * - "descrLink"
333 * - "pubDate"
335 QJsonObject SearchController::getResults(const QList<SearchResult> &searchResults, const bool isSearchActive, const int totalResults) const
337 QJsonArray searchResultsArray;
338 for (const SearchResult &searchResult : searchResults)
340 searchResultsArray << QJsonObject
342 {u"fileName"_s, searchResult.fileName},
343 {u"fileUrl"_s, searchResult.fileUrl},
344 {u"fileSize"_s, searchResult.fileSize},
345 {u"nbSeeders"_s, searchResult.nbSeeders},
346 {u"nbLeechers"_s, searchResult.nbLeechers},
347 {u"engineName"_s, searchResult.engineName},
348 {u"siteUrl"_s, searchResult.siteUrl},
349 {u"descrLink"_s, searchResult.descrLink},
350 {u"pubDate"_s, Utils::DateTime::toSecsSinceEpoch(searchResult.pubDate)}
354 const QJsonObject result =
356 {u"status"_s, isSearchActive ? u"Running"_s : u"Stopped"_s},
357 {u"results"_s, searchResultsArray},
358 {u"total"_s, totalResults}
361 return result;
365 * Returns the search plugins in JSON format.
367 * The return value is an array of dictionaries.
368 * The dictionary keys are:
369 * - "name"
370 * - "version"
371 * - "fullName"
372 * - "url"
373 * - "supportedCategories"
374 * - "iconPath"
375 * - "enabled"
377 QJsonArray SearchController::getPluginsInfo(const QStringList &plugins) const
379 QJsonArray pluginsArray;
381 for (const QString &plugin : plugins)
383 const PluginInfo *const pluginInfo = SearchPluginManager::instance()->pluginInfo(plugin);
385 pluginsArray << QJsonObject
387 {u"name"_s, pluginInfo->name},
388 {u"version"_s, pluginInfo->version.toString()},
389 {u"fullName"_s, pluginInfo->fullName},
390 {u"url"_s, pluginInfo->url},
391 {u"supportedCategories"_s, getPluginCategories(pluginInfo->supportedCategories)},
392 {u"enabled"_s, pluginInfo->enabled}
396 return pluginsArray;