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"
36 #include <QJsonObject>
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"
51 #include "isessionmanager.h"
56 * Returns the search categories in JSON format.
58 * The return value is an array of dictionaries.
59 * The dictionary keys are:
63 QJsonArray
getPluginCategories(QStringList categories
)
65 QJsonArray categoriesInfo
68 {u
"name"_s
, SearchPluginManager::categoryFullName(u
"all"_s
)}
71 categories
.sort(Qt::CaseInsensitive
);
72 for (const QString
&category
: categories
)
74 categoriesInfo
<< QJsonObject
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();
105 pluginsToUse
<< plugins
;
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
}};
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
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();
188 throw APIError(APIErrorType::Conflict
, tr("Offset is out of range"));
192 offset
= size
+ offset
;
193 if (offset
< 0) // check again
194 throw APIError(APIErrorType::Conflict
, tr("Offset is out of range"));
198 if ((limit
> 0) || (offset
> 0))
199 setResult(getResults(searchResults
.mid(offset
, limit
), searchHandler
->isActive(), size
));
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
);
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
);
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
314 const int id
= Utils::Random::rand(1, std::numeric_limits
<int>::max());
315 if (!m_searchHandlers
.contains(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:
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
}
365 * Returns the search plugins in JSON format.
367 * The return value is an array of dictionaries.
368 * The dictionary keys are:
373 * - "supportedCategories"
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
}