From f3d370870d7f9016eecbedda51af6a3fd01a8d8c Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Mon, 29 Jan 2018 17:05:29 +0300 Subject: [PATCH 1/2] Improve Search handling Legacy SearchEngine class really has three roles: 1. Manage search plugins, 2. Handle the search job, and 3. Handle the download of the torrent file using the search plugin. Now it is splitted into 3 classes: SearchManager, SearchHandler and SearchDownloadHandler. Search GUI is also improved. --- src/base/CMakeLists.txt | 8 +- src/base/base.pri | 8 +- src/base/search/searchdownloadhandler.cpp | 66 ++++ src/base/search/searchdownloadhandler.h | 53 +++ src/base/search/searchhandler.cpp | 199 +++++++++++ src/base/search/searchhandler.h | 90 +++++ .../searchpluginmanager.cpp} | 282 ++++----------- .../searchpluginmanager.h} | 69 +--- src/gui/search/pluginselectdlg.cpp | 16 +- src/gui/search/pluginselectdlg.h | 6 +- src/gui/search/searchtab.cpp | 255 ++++++++----- src/gui/search/searchtab.h | 70 ++-- src/gui/search/searchwidget.cpp | 337 +++++++----------- src/gui/search/searchwidget.h | 41 +-- 14 files changed, 855 insertions(+), 645 deletions(-) create mode 100644 src/base/search/searchdownloadhandler.cpp create mode 100644 src/base/search/searchdownloadhandler.h create mode 100644 src/base/search/searchhandler.cpp create mode 100644 src/base/search/searchhandler.h rename src/base/{searchengine.cpp => search/searchpluginmanager.cpp} (65%) rename src/base/{searchengine.h => search/searchpluginmanager.h} (73%) diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index a81155322..74d02ba10 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -44,6 +44,9 @@ rss/rss_feed.h rss/rss_folder.h rss/rss_item.h rss/rss_session.h +search/searchdownloadhandler.h +search/searchhandler.h +search/searchpluginmanager.h utils/fs.h utils/gzip.h utils/misc.h @@ -61,7 +64,6 @@ logger.h preferences.h profile.h scanfoldersmodel.h -searchengine.h settingsstorage.h torrentfileguard.h torrentfilter.h @@ -109,6 +111,9 @@ rss/rss_feed.cpp rss/rss_folder.cpp rss/rss_item.cpp rss/rss_session.cpp +search/searchdownloadhandler.cpp +search/searchhandler.cpp +search/searchpluginmanager.cpp utils/fs.cpp utils/gzip.cpp utils/misc.cpp @@ -123,7 +128,6 @@ logger.cpp preferences.cpp profile.cpp scanfoldersmodel.cpp -searchengine.cpp settingsstorage.cpp torrentfileguard.cpp torrentfilter.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 0afca4c40..c89da2dbd 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -52,7 +52,9 @@ HEADERS += \ $$PWD/rss/rss_item.h \ $$PWD/rss/rss_session.h \ $$PWD/scanfoldersmodel.h \ - $$PWD/searchengine.h \ + $$PWD/search/searchhandler.h \ + $$PWD/search/searchdownloadhandler.h \ + $$PWD/search/searchpluginmanager.h \ $$PWD/settingsstorage.h \ $$PWD/settingvalue.h \ $$PWD/torrentfileguard.h \ @@ -115,7 +117,9 @@ SOURCES += \ $$PWD/rss/rss_item.cpp \ $$PWD/rss/rss_session.cpp \ $$PWD/scanfoldersmodel.cpp \ - $$PWD/searchengine.cpp \ + $$PWD/search/searchdownloadhandler.cpp \ + $$PWD/search/searchhandler.cpp \ + $$PWD/search/searchpluginmanager.cpp \ $$PWD/settingsstorage.cpp \ $$PWD/torrentfileguard.cpp \ $$PWD/torrentfilter.cpp \ diff --git a/src/base/search/searchdownloadhandler.cpp b/src/base/search/searchdownloadhandler.cpp new file mode 100644 index 000000000..e3d853e5b --- /dev/null +++ b/src/base/search/searchdownloadhandler.cpp @@ -0,0 +1,66 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "searchdownloadhandler.h" + +#include + +#include "../utils/fs.h" +#include "../utils/misc.h" +#include "searchpluginmanager.h" + +SearchDownloadHandler::SearchDownloadHandler(const QString &siteUrl, const QString &url, SearchPluginManager *manager) + : QObject {manager} + , m_manager {manager} + , m_downloadProcess {new QProcess {this}} +{ + m_downloadProcess->setEnvironment(QProcess::systemEnvironment()); + connect(m_downloadProcess, static_cast(&QProcess::finished) + , this, &SearchDownloadHandler::downloadProcessFinished); + const QStringList params { + Utils::Fs::toNativePath(m_manager->engineLocation() + "/nova2dl.py"), + siteUrl, + url + }; + // Launch search + m_downloadProcess->start(Utils::Misc::pythonExecutable(), params, QIODevice::ReadOnly); +} + +void SearchDownloadHandler::downloadProcessFinished(int exitcode) +{ + QString path; + + if ((exitcode == 0) && (m_downloadProcess->exitStatus() == QProcess::NormalExit)) { + const QString line = QString::fromUtf8(m_downloadProcess->readAllStandardOutput()).trimmed(); + const QVector parts = line.splitRef(' '); + if (parts.size() == 2) + path = parts[0].toString(); + } + + emit downloadFinished(path); +} diff --git a/src/base/search/searchdownloadhandler.h b/src/base/search/searchdownloadhandler.h new file mode 100644 index 000000000..229768100 --- /dev/null +++ b/src/base/search/searchdownloadhandler.h @@ -0,0 +1,53 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +class QProcess; +class SearchPluginManager; + +class SearchDownloadHandler : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(SearchDownloadHandler) + + friend class SearchPluginManager; + + SearchDownloadHandler(const QString &siteUrl, const QString &url, SearchPluginManager *manager); + +signals: + void downloadFinished(const QString &path); + +private: + void downloadProcessFinished(int exitcode); + + SearchPluginManager *m_manager; + QProcess *m_downloadProcess; +}; diff --git a/src/base/search/searchhandler.cpp b/src/base/search/searchhandler.cpp new file mode 100644 index 000000000..0cff6df6b --- /dev/null +++ b/src/base/search/searchhandler.cpp @@ -0,0 +1,199 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015, 2018 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "searchhandler.h" + +#include +#include + +#include "../utils/fs.h" +#include "../utils/misc.h" +#include "searchpluginmanager.h" + +namespace +{ + enum SearchResultColumn + { + PL_DL_LINK, + PL_NAME, + PL_SIZE, + PL_SEEDS, + PL_LEECHS, + PL_ENGINE_URL, + PL_DESC_LINK, + NB_PLUGIN_COLUMNS + }; +} + +SearchHandler::SearchHandler(const QString &pattern, const QString &category, const QStringList &usedPlugins, SearchPluginManager *manager) + : QObject {manager} + , m_pattern {pattern} + , m_category {category} + , m_usedPlugins {usedPlugins} + , m_manager {manager} + , m_searchProcess {new QProcess {this}} + , m_searchTimeout {new QTimer {this}} +{ + // Load environment variables (proxy) + m_searchProcess->setEnvironment(QProcess::systemEnvironment()); + + const QStringList params { + Utils::Fs::toNativePath(m_manager->engineLocation() + "/nova2.py"), + m_usedPlugins.join(","), + m_category + }; + + // Launch search + m_searchProcess->setProgram(Utils::Misc::pythonExecutable()); + m_searchProcess->setArguments(params + m_pattern.split(" ")); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + connect(m_searchProcess, &QProcess::errorOccurred, this, &SearchHandler::processFailed); +#else + connect(m_searchProcess, static_cast(&QProcess::error) + , this, &SearchHandler::processFailed); +#endif + connect(m_searchProcess, &QProcess::readyReadStandardOutput, this, &SearchHandler::readSearchOutput); + connect(m_searchProcess, static_cast(&QProcess::finished) + , this, &SearchHandler::processFinished); + + m_searchTimeout->setSingleShot(true); + connect(m_searchTimeout, &QTimer::timeout, this, &SearchHandler::cancelSearch); + m_searchTimeout->start(180000); // 3 min + + // deferred start allows clients to handle starting-related signals + QTimer::singleShot(0, this, [this]() { m_searchProcess->start(QIODevice::ReadOnly); }); +} + +bool SearchHandler::isActive() const +{ + return (m_searchProcess->state() != QProcess::NotRunning); +} + +void SearchHandler::cancelSearch() +{ + if ((m_searchProcess->state() == QProcess::NotRunning) || m_searchCancelled) + return; + +#ifdef Q_OS_WIN + m_searchProcess->kill(); +#else + m_searchProcess->terminate(); +#endif + m_searchCancelled = true; + m_searchTimeout->stop(); +} + +// Slot called when QProcess is Finished +// QProcess can be finished for 3 reasons: +// Error | Stopped by user | Finished normally +void SearchHandler::processFinished(int exitcode) +{ + m_searchTimeout->stop(); + + if (m_searchCancelled) + emit searchFinished(true); + else if ((m_searchProcess->exitStatus() == QProcess::NormalExit) && (exitcode == 0)) + emit searchFinished(false); + else + emit searchFailed(); +} + +// search QProcess return output as soon as it gets new +// stuff to read. We split it into lines and parse each +// line to SearchResult calling parseSearchResult(). +void SearchHandler::readSearchOutput() +{ + QByteArray output = m_searchProcess->readAllStandardOutput(); + output.replace("\r", ""); + QList lines = output.split('\n'); + if (!m_searchResultLineTruncated.isEmpty()) + lines.prepend(m_searchResultLineTruncated + lines.takeFirst()); + m_searchResultLineTruncated = lines.takeLast().trimmed(); + + QList searchResultList; + foreach (const QByteArray &line, lines) { + SearchResult searchResult; + if (parseSearchResult(QString::fromUtf8(line), searchResult)) + searchResultList << searchResult; + } + + if (!searchResultList.isEmpty()) { + m_results.append(searchResultList); + emit newSearchResults(searchResultList); + } +} + +void SearchHandler::processFailed() +{ + if (!m_searchCancelled) + emit searchFailed(); +} + +// Parse one line of search results list +// Line is in the following form: +// file url | file name | file size | nb seeds | nb leechers | Search engine url +bool SearchHandler::parseSearchResult(const QString &line, SearchResult &searchResult) +{ + const QStringList parts = line.split("|"); + const int nbFields = parts.size(); + if (nbFields < (NB_PLUGIN_COLUMNS - 1)) return false; // -1 because desc_link is optional + + searchResult = SearchResult(); + searchResult.fileUrl = parts.at(PL_DL_LINK).trimmed(); // download URL + searchResult.fileName = parts.at(PL_NAME).trimmed(); // Name + searchResult.fileSize = parts.at(PL_SIZE).trimmed().toLongLong(); // Size + bool ok = false; + searchResult.nbSeeders = parts.at(PL_SEEDS).trimmed().toLongLong(&ok); // Seeders + if (!ok || (searchResult.nbSeeders < 0)) + searchResult.nbSeeders = -1; + searchResult.nbLeechers = parts.at(PL_LEECHS).trimmed().toLongLong(&ok); // Leechers + if (!ok || (searchResult.nbLeechers < 0)) + searchResult.nbLeechers = -1; + searchResult.siteUrl = parts.at(PL_ENGINE_URL).trimmed(); // Search site URL + if (nbFields == NB_PLUGIN_COLUMNS) + searchResult.descrLink = parts.at(PL_DESC_LINK).trimmed(); // Description Link + + return true; +} + +SearchPluginManager *SearchHandler::manager() const +{ + return m_manager; +} + +QList SearchHandler::results() const +{ + return m_results; +} + +QString SearchHandler::pattern() const +{ + return m_pattern; +} diff --git a/src/base/search/searchhandler.h b/src/base/search/searchhandler.h new file mode 100644 index 000000000..fae52d05c --- /dev/null +++ b/src/base/search/searchhandler.h @@ -0,0 +1,90 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015, 2018 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include +#include + +class QProcess; +class QTimer; + +struct SearchResult +{ + QString fileName; + QString fileUrl; + qlonglong fileSize; + qlonglong nbSeeders; + qlonglong nbLeechers; + QString siteUrl; + QString descrLink; +}; + +class SearchPluginManager; + +class SearchHandler : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(SearchHandler) + + friend class SearchPluginManager; + + SearchHandler(const QString &pattern, const QString &category + , const QStringList &usedPlugins, SearchPluginManager *manager); + +public: + bool isActive() const; + QString pattern() const; + SearchPluginManager *manager() const; + QList results() const; + + void cancelSearch(); + +signals: + void searchFinished(bool cancelled = false); + void searchFailed(); + void newSearchResults(const QList &results); + +private: + void readSearchOutput(); + void processFailed(); + void processFinished(int exitcode); + bool parseSearchResult(const QString &line, SearchResult &searchResult); + + const QString m_pattern; + const QString m_category; + const QStringList m_usedPlugins; + SearchPluginManager *m_manager; + QProcess *m_searchProcess; + QTimer *m_searchTimeout; + QByteArray m_searchResultLineTruncated; + bool m_searchCancelled = false; + QList m_results; +}; diff --git a/src/base/searchengine.cpp b/src/base/search/searchpluginmanager.cpp similarity index 65% rename from src/base/searchengine.cpp rename to src/base/search/searchpluginmanager.cpp index 23ef24363..f02870869 100644 --- a/src/base/searchengine.cpp +++ b/src/base/search/searchpluginmanager.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015, 2018 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -27,33 +27,25 @@ * exception statement from your version. */ +#include "searchpluginmanager.h" + +#include +#include #include -#include #include -#include +#include +#include #include -#include -#include "base/utils/fs.h" -#include "base/utils/misc.h" #include "base/logger.h" -#include "base/preferences.h" -#include "base/profile.h" #include "base/net/downloadmanager.h" #include "base/net/downloadhandler.h" -#include "searchengine.h" - -enum SearchResultColumn -{ - PL_DL_LINK, - PL_NAME, - PL_SIZE, - PL_SEEDS, - PL_LEECHS, - PL_ENGINE_URL, - PL_DESC_LINK, - NB_PLUGIN_COLUMNS -}; +#include "base/preferences.h" +#include "base/profile.h" +#include "base/utils/fs.h" +#include "base/utils/misc.h" +#include "searchdownloadhandler.h" +#include "searchhandler.h" static inline void removePythonScriptIfExists(const QString &scriptPath) { @@ -61,39 +53,36 @@ static inline void removePythonScriptIfExists(const QString &scriptPath) Utils::Fs::forceRemove(scriptPath + "c"); } -const QHash SearchEngine::m_categoryNames = SearchEngine::initializeCategoryNames(); +const QHash SearchPluginManager::m_categoryNames { + {"all", QT_TRANSLATE_NOOP("SearchEngine", "All categories")}, + {"movies", QT_TRANSLATE_NOOP("SearchEngine", "Movies")}, + {"tv", QT_TRANSLATE_NOOP("SearchEngine", "TV shows")}, + {"music", QT_TRANSLATE_NOOP("SearchEngine", "Music")}, + {"games", QT_TRANSLATE_NOOP("SearchEngine", "Games")}, + {"anime", QT_TRANSLATE_NOOP("SearchEngine", "Anime")}, + {"software", QT_TRANSLATE_NOOP("SearchEngine", "Software")}, + {"pictures", QT_TRANSLATE_NOOP("SearchEngine", "Pictures")}, + {"books", QT_TRANSLATE_NOOP("SearchEngine", "Books")} +}; -SearchEngine::SearchEngine() +SearchPluginManager::SearchPluginManager() : m_updateUrl(QString("http://searchplugins.qbittorrent.org/%1/engines/").arg(Utils::Misc::pythonVersion() >= 3 ? "nova3" : "nova")) - , m_searchStopped(false) { updateNova(); - - m_searchProcess = new QProcess(this); - m_searchProcess->setEnvironment(QProcess::systemEnvironment()); - connect(m_searchProcess, &QProcess::started, this, &SearchEngine::searchStarted); - connect(m_searchProcess, &QProcess::readyReadStandardOutput, this, &SearchEngine::readSearchOutput); - connect(m_searchProcess, static_cast(&QProcess::finished), this, &SearchEngine::processFinished); - - m_searchTimeout = new QTimer(this); - m_searchTimeout->setSingleShot(true); - connect(m_searchTimeout, &QTimer::timeout, this, &SearchEngine::onTimeout); - update(); } -SearchEngine::~SearchEngine() +SearchPluginManager::~SearchPluginManager() { qDeleteAll(m_plugins); - cancelSearch(); } -QStringList SearchEngine::allPlugins() const +QStringList SearchPluginManager::allPlugins() const { return m_plugins.keys(); } -QStringList SearchEngine::enabledPlugins() const +QStringList SearchPluginManager::enabledPlugins() const { QStringList plugins; foreach (const PluginInfo *plugin, m_plugins.values()) { @@ -104,7 +93,7 @@ QStringList SearchEngine::enabledPlugins() const return plugins; } -QStringList SearchEngine::supportedCategories() const +QStringList SearchPluginManager::supportedCategories() const { QStringList result; foreach (const PluginInfo *plugin, m_plugins.values()) { @@ -119,17 +108,12 @@ QStringList SearchEngine::supportedCategories() const return result; } -PluginInfo *SearchEngine::pluginInfo(const QString &name) const -{ - return m_plugins.value(name, 0); -} - -bool SearchEngine::isActive() const +PluginInfo *SearchPluginManager::pluginInfo(const QString &name) const { - return (m_searchProcess->state() != QProcess::NotRunning); + return m_plugins.value(name); } -void SearchEngine::enablePlugin(const QString &name, bool enabled) +void SearchPluginManager::enablePlugin(const QString &name, bool enabled) { PluginInfo *plugin = m_plugins.value(name, 0); if (plugin) { @@ -148,13 +132,13 @@ void SearchEngine::enablePlugin(const QString &name, bool enabled) } // Updates shipped plugin -void SearchEngine::updatePlugin(const QString &name) +void SearchPluginManager::updatePlugin(const QString &name) { installPlugin(QString("%1%2.py").arg(m_updateUrl).arg(name)); } // Install or update plugin from file or url -void SearchEngine::installPlugin(const QString &source) +void SearchPluginManager::installPlugin(const QString &source) { qDebug("Asked to install plugin at %s", qUtf8Printable(source)); @@ -162,8 +146,8 @@ void SearchEngine::installPlugin(const QString &source) using namespace Net; DownloadHandler *handler = DownloadManager::instance()->downloadUrl(source, true); connect(handler, static_cast(&DownloadHandler::downloadFinished) - , this, &SearchEngine::pluginDownloaded); - connect(handler, &DownloadHandler::downloadFailed, this, &SearchEngine::pluginDownloadFailed); + , this, &SearchPluginManager::pluginDownloaded); + connect(handler, &DownloadHandler::downloadFailed, this, &SearchPluginManager::pluginDownloadFailed); } else { QString path = source; @@ -180,7 +164,7 @@ void SearchEngine::installPlugin(const QString &source) } } -void SearchEngine::installPlugin_impl(const QString &name, const QString &path) +void SearchPluginManager::installPlugin_impl(const QString &name, const QString &path) { PluginVersion newVersion = getPluginVersion(path); qDebug() << "Version to be installed:" << newVersion; @@ -229,7 +213,7 @@ void SearchEngine::installPlugin_impl(const QString &name, const QString &path) } } -bool SearchEngine::uninstallPlugin(const QString &name) +bool SearchPluginManager::uninstallPlugin(const QString &name) { // remove it from hard drive QDir pluginsFolder(pluginsLocation()); @@ -246,7 +230,7 @@ bool SearchEngine::uninstallPlugin(const QString &name) return true; } -void SearchEngine::updateIconPath(PluginInfo * const plugin) +void SearchPluginManager::updateIconPath(PluginInfo * const plugin) { if (!plugin) return; QString iconPath = QString("%1/%2.png").arg(pluginsLocation()).arg(plugin->name); @@ -260,86 +244,45 @@ void SearchEngine::updateIconPath(PluginInfo * const plugin) } } -void SearchEngine::checkForUpdates() +void SearchPluginManager::checkForUpdates() { // Download version file from update server using namespace Net; DownloadHandler *handler = DownloadManager::instance()->downloadUrl(m_updateUrl + "versions.txt"); connect(handler, static_cast(&DownloadHandler::downloadFinished) - , this, &SearchEngine::versionInfoDownloaded); - connect(handler, &DownloadHandler::downloadFailed, this, &SearchEngine::versionInfoDownloadFailed); -} - -void SearchEngine::cancelSearch() -{ - if (m_searchProcess->state() != QProcess::NotRunning) { -#ifdef Q_OS_WIN - m_searchProcess->kill(); -#else - m_searchProcess->terminate(); -#endif - m_searchStopped = true; - m_searchTimeout->stop(); - - m_searchProcess->waitForFinished(1000); - } + , this, &SearchPluginManager::versionInfoDownloaded); + connect(handler, &DownloadHandler::downloadFailed, this, &SearchPluginManager::versionInfoDownloadFailed); } -void SearchEngine::downloadTorrent(const QString &siteUrl, const QString &url) +SearchDownloadHandler *SearchPluginManager::downloadTorrent(const QString &siteUrl, const QString &url) { - QProcess *downloadProcess = new QProcess(this); - downloadProcess->setEnvironment(QProcess::systemEnvironment()); - connect(downloadProcess, static_cast(&QProcess::finished), this, &SearchEngine::torrentFileDownloadFinished); - m_downloaders << downloadProcess; - QStringList params { - Utils::Fs::toNativePath(engineLocation() + "/nova2dl.py"), - siteUrl, - url - }; - // Launch search - downloadProcess->start(Utils::Misc::pythonExecutable(), params, QIODevice::ReadOnly); + return new SearchDownloadHandler {siteUrl, url, this}; } -void SearchEngine::startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins) +SearchHandler *SearchPluginManager::startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins) { - // Search process already running or // No search pattern entered - if ((m_searchProcess->state() != QProcess::NotRunning) || pattern.isEmpty()) { - emit searchFailed(); - return; - } - - // Reload environment variables (proxy) - m_searchProcess->setEnvironment(QProcess::systemEnvironment()); + Q_ASSERT(!pattern.isEmpty()); - QStringList params; - m_searchStopped = false; - params << Utils::Fs::toNativePath(engineLocation() + "/nova2.py"); - params << usedPlugins.join(","); - params << category; - params << pattern.split(" "); - - // Launch search - m_searchProcess->start(Utils::Misc::pythonExecutable(), params, QIODevice::ReadOnly); - m_searchTimeout->start(180000); // 3min + return new SearchHandler {pattern, category, usedPlugins, this}; } -QString SearchEngine::categoryFullName(const QString &categoryName) +QString SearchPluginManager::categoryFullName(const QString &categoryName) { return tr(m_categoryNames.value(categoryName).toUtf8().constData()); } -QString SearchEngine::pluginFullName(const QString &pluginName) +QString SearchPluginManager::pluginFullName(const QString &pluginName) { return pluginInfo(pluginName) ? pluginInfo(pluginName)->fullName : QString(); } -QString SearchEngine::pluginsLocation() +QString SearchPluginManager::pluginsLocation() { return QString("%1/engines").arg(engineLocation()); } -QString SearchEngine::engineLocation() +QString SearchPluginManager::engineLocation() { QString folder = "nova"; if (Utils::Misc::pythonVersion() >= 3) @@ -351,32 +294,19 @@ QString SearchEngine::engineLocation() return location; } -// Slot called when QProcess is Finished -// QProcess can be finished for 3 reasons : -// Error | Stopped by user | Finished normally -void SearchEngine::processFinished(int exitcode) -{ - m_searchTimeout->stop(); - - if (exitcode == 0) - emit searchFinished(m_searchStopped); - else - emit searchFailed(); -} - -void SearchEngine::versionInfoDownloaded(const QString &url, const QByteArray &data) +void SearchPluginManager::versionInfoDownloaded(const QString &url, const QByteArray &data) { Q_UNUSED(url) parseVersionInfo(data); } -void SearchEngine::versionInfoDownloadFailed(const QString &url, const QString &reason) +void SearchPluginManager::versionInfoDownloadFailed(const QString &url, const QString &reason) { Q_UNUSED(url) emit checkForUpdatesFailed(tr("Update server is temporarily unavailable. %1").arg(reason)); } -void SearchEngine::pluginDownloaded(const QString &url, QString filePath) +void SearchPluginManager::pluginDownloaded(const QString &url, QString filePath) { filePath = Utils::Fs::fromNativePath(filePath); @@ -386,7 +316,7 @@ void SearchEngine::pluginDownloaded(const QString &url, QString filePath) Utils::Fs::forceRemove(filePath); } -void SearchEngine::pluginDownloadFailed(const QString &url, const QString &reason) +void SearchPluginManager::pluginDownloadFailed(const QString &url, const QString &reason) { QString pluginName = url.split('/').last(); pluginName.replace(".py", "", Qt::CaseInsensitive); @@ -396,23 +326,8 @@ void SearchEngine::pluginDownloadFailed(const QString &url, const QString &reaso emit pluginInstallationFailed(pluginName, tr("Failed to download the plugin file. %1").arg(reason)); } -void SearchEngine::torrentFileDownloadFinished(int exitcode) -{ - QProcess *downloadProcess = static_cast(sender()); - if (exitcode == 0) { - QString line = QString::fromUtf8(downloadProcess->readAllStandardOutput()).trimmed(); - QStringList parts = line.split(' '); - if (parts.size() == 2) - emit torrentFileDownloaded(parts[0]); - } - - qDebug() << "Deleting downloadProcess"; - m_downloaders.removeOne(downloadProcess); - downloadProcess->deleteLater(); -} - // Update nova.py search plugin if necessary -void SearchEngine::updateNova() +void SearchPluginManager::updateNova() { qDebug("Updating nova"); @@ -477,35 +392,7 @@ void SearchEngine::updateNova() Utils::Fs::removeDirRecursive(destDir.absoluteFilePath("__pycache__")); } -void SearchEngine::onTimeout() -{ - cancelSearch(); -} - -// search QProcess return output as soon as it gets new -// stuff to read. We split it into lines and parse each -// line to SearchResult calling parseSearchResult(). -void SearchEngine::readSearchOutput() -{ - QByteArray output = m_searchProcess->readAllStandardOutput(); - output.replace("\r", ""); - QList lines = output.split('\n'); - if (!m_searchResultLineTruncated.isEmpty()) - lines.prepend(m_searchResultLineTruncated + lines.takeFirst()); - m_searchResultLineTruncated = lines.takeLast().trimmed(); - - QList searchResultList; - foreach (const QByteArray &line, lines) { - SearchResult searchResult; - if (parseSearchResult(QString::fromUtf8(line), searchResult)) - searchResultList << searchResult; - } - - if (!searchResultList.isEmpty()) - emit newSearchResults(searchResultList); -} - -void SearchEngine::update() +void SearchPluginManager::update() { QProcess nova; nova.setEnvironment(QProcess::systemEnvironment()); @@ -565,34 +452,7 @@ void SearchEngine::update() } } -// Parse one line of search results list -// Line is in the following form: -// file url | file name | file size | nb seeds | nb leechers | Search engine url -bool SearchEngine::parseSearchResult(const QString &line, SearchResult &searchResult) -{ - const QStringList parts = line.split("|"); - const int nbFields = parts.size(); - if (nbFields < (NB_PLUGIN_COLUMNS - 1)) return false; // -1 because desc_link is optional - - searchResult = SearchResult(); - searchResult.fileUrl = parts.at(PL_DL_LINK).trimmed(); // download URL - searchResult.fileName = parts.at(PL_NAME).trimmed(); // Name - searchResult.fileSize = parts.at(PL_SIZE).trimmed().toLongLong(); // Size - bool ok = false; - searchResult.nbSeeders = parts.at(PL_SEEDS).trimmed().toLongLong(&ok); // Seeders - if (!ok || (searchResult.nbSeeders < 0)) - searchResult.nbSeeders = -1; - searchResult.nbLeechers = parts.at(PL_LEECHS).trimmed().toLongLong(&ok); // Leechers - if (!ok || (searchResult.nbLeechers < 0)) - searchResult.nbLeechers = -1; - searchResult.siteUrl = parts.at(PL_ENGINE_URL).trimmed(); // Search site URL - if (nbFields == NB_PLUGIN_COLUMNS) - searchResult.descrLink = parts.at(PL_DESC_LINK).trimmed(); // Description Link - - return true; -} - -void SearchEngine::parseVersionInfo(const QByteArray &info) +void SearchPluginManager::parseVersionInfo(const QByteArray &info) { qDebug("Checking if update is needed"); @@ -627,7 +487,7 @@ void SearchEngine::parseVersionInfo(const QByteArray &info) emit checkForUpdatesFinished(updateInfo); } -bool SearchEngine::isUpdateNeeded(QString pluginName, PluginVersion newVersion) const +bool SearchPluginManager::isUpdateNeeded(QString pluginName, PluginVersion newVersion) const { PluginInfo *plugin = pluginInfo(pluginName); if (!plugin) return true; @@ -637,29 +497,12 @@ bool SearchEngine::isUpdateNeeded(QString pluginName, PluginVersion newVersion) return (newVersion > oldVersion); } -QString SearchEngine::pluginPath(const QString &name) +QString SearchPluginManager::pluginPath(const QString &name) { return QString("%1/%2.py").arg(pluginsLocation()).arg(name); } -QHash SearchEngine::initializeCategoryNames() -{ - QHash result; - - result["all"] = QT_TRANSLATE_NOOP("SearchEngine", "All categories"); - result["movies"] = QT_TRANSLATE_NOOP("SearchEngine", "Movies"); - result["tv"] = QT_TRANSLATE_NOOP("SearchEngine", "TV shows"); - result["music"] = QT_TRANSLATE_NOOP("SearchEngine", "Music"); - result["games"] = QT_TRANSLATE_NOOP("SearchEngine", "Games"); - result["anime"] = QT_TRANSLATE_NOOP("SearchEngine", "Anime"); - result["software"] = QT_TRANSLATE_NOOP("SearchEngine", "Software"); - result["pictures"] = QT_TRANSLATE_NOOP("SearchEngine", "Pictures"); - result["books"] = QT_TRANSLATE_NOOP("SearchEngine", "Books"); - - return result; -} - -PluginVersion SearchEngine::getPluginVersion(QString filePath) +PluginVersion SearchPluginManager::getPluginVersion(QString filePath) { QFile plugin(filePath); if (!plugin.exists()) { @@ -682,8 +525,9 @@ PluginVersion SearchEngine::getPluginVersion(QString filePath) LogMsg(tr("Search plugin '%1' contains invalid version string ('%2')") .arg(Utils::Fs::fileName(filePath)).arg(QString::fromUtf8(line)), Log::MsgType::WARNING); } - else + else { qDebug() << "plugin" << filePath << "version: " << version; + } break; } } diff --git a/src/base/searchengine.h b/src/base/search/searchpluginmanager.h similarity index 73% rename from src/base/searchengine.h rename to src/base/search/searchpluginmanager.h index 191b5f1f5..2e815aa6e 100644 --- a/src/base/searchengine.h +++ b/src/base/search/searchpluginmanager.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015, 2018 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -27,19 +27,14 @@ * exception statement from your version. */ -#ifndef SEARCHENGINE_H -#define SEARCHENGINE_H +#pragma once #include -#include #include #include #include "base/utils/version.h" -class QProcess; -class QTimer; - using PluginVersion = Utils::Version; Q_DECLARE_METATYPE(PluginVersion) @@ -54,32 +49,23 @@ struct PluginInfo bool enabled; }; -struct SearchResult -{ - QString fileName; - QString fileUrl; - qlonglong fileSize; - qlonglong nbSeeders; - qlonglong nbLeechers; - QString siteUrl; - QString descrLink; -}; +class SearchDownloadHandler; +class SearchHandler; -class SearchEngine: public QObject +class SearchPluginManager : public QObject { Q_OBJECT + Q_DISABLE_COPY(SearchPluginManager) public: - SearchEngine(); - ~SearchEngine(); + SearchPluginManager(); + ~SearchPluginManager() override; QStringList allPlugins() const; QStringList enabledPlugins() const; QStringList supportedCategories() const; PluginInfo *pluginInfo(const QString &name) const; - bool isActive() const; - void enablePlugin(const QString &name, bool enabled = true); void updatePlugin(const QString &name); void installPlugin(const QString &source); @@ -87,22 +73,16 @@ public: static void updateIconPath(PluginInfo * const plugin); void checkForUpdates(); - void startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins); - void cancelSearch(); - - void downloadTorrent(const QString &siteUrl, const QString &url); + SearchHandler *startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins); + SearchDownloadHandler *downloadTorrent(const QString &siteUrl, const QString &url); static PluginVersion getPluginVersion(QString filePath); static QString categoryFullName(const QString &categoryName); QString pluginFullName(const QString &pluginName); static QString pluginsLocation(); + static QString engineLocation(); signals: - void searchStarted(); - void searchFinished(bool cancelled); - void searchFailed(); - void newSearchResults(const QList &results); - void pluginEnabled(const QString &name, bool enabled); void pluginInstalled(const QString &name); void pluginInstallationFailed(const QString &name, const QString &reason); @@ -113,40 +93,23 @@ signals: void checkForUpdatesFinished(const QHash &updateInfo); void checkForUpdatesFailed(const QString &reason); - void torrentFileDownloaded(const QString &path); - -private slots: - void onTimeout(); - void readSearchOutput(); - void processFinished(int exitcode); - void versionInfoDownloaded(const QString &url, const QByteArray &data); - void versionInfoDownloadFailed(const QString &url, const QString &reason); - void pluginDownloaded(const QString &url, QString filePath); - void pluginDownloadFailed(const QString &url, const QString &reason); - void torrentFileDownloadFinished(int exitcode); - private: void update(); void updateNova(); - bool parseSearchResult(const QString &line, SearchResult &searchResult); void parseVersionInfo(const QByteArray &info); void installPlugin_impl(const QString &name, const QString &path); bool isUpdateNeeded(QString pluginName, PluginVersion newVersion) const; - static QString engineLocation(); + void versionInfoDownloaded(const QString &url, const QByteArray &data); + void versionInfoDownloadFailed(const QString &url, const QString &reason); + void pluginDownloaded(const QString &url, QString filePath); + void pluginDownloadFailed(const QString &url, const QString &reason); + static QString pluginPath(const QString &name); - static QHash initializeCategoryNames(); static const QHash m_categoryNames; const QString m_updateUrl; QHash m_plugins; - QProcess *m_searchProcess; - bool m_searchStopped; - QTimer *m_searchTimeout; - QByteArray m_searchResultLineTruncated; - QList m_downloaders; }; - -#endif // SEARCHENGINE_H diff --git a/src/gui/search/pluginselectdlg.cpp b/src/gui/search/pluginselectdlg.cpp index dad2e0897..46b2d943b 100644 --- a/src/gui/search/pluginselectdlg.cpp +++ b/src/gui/search/pluginselectdlg.cpp @@ -61,7 +61,7 @@ enum PluginColumns PLUGIN_ID }; -PluginSelectDlg::PluginSelectDlg(SearchEngine *pluginManager, QWidget *parent) +PluginSelectDlg::PluginSelectDlg(SearchPluginManager *pluginManager, QWidget *parent) : QDialog(parent) , m_ui(new Ui::PluginSelectDlg()) , m_pluginManager(pluginManager) @@ -90,12 +90,12 @@ PluginSelectDlg::PluginSelectDlg(SearchEngine *pluginManager, QWidget *parent) loadSupportedSearchPlugins(); - connect(m_pluginManager, &SearchEngine::pluginInstalled, this, &PluginSelectDlg::pluginInstalled); - connect(m_pluginManager, &SearchEngine::pluginInstallationFailed, this, &PluginSelectDlg::pluginInstallationFailed); - connect(m_pluginManager, &SearchEngine::pluginUpdated, this, &PluginSelectDlg::pluginUpdated); - connect(m_pluginManager, &SearchEngine::pluginUpdateFailed, this, &PluginSelectDlg::pluginUpdateFailed); - connect(m_pluginManager, &SearchEngine::checkForUpdatesFinished, this, &PluginSelectDlg::checkForUpdatesFinished); - connect(m_pluginManager, &SearchEngine::checkForUpdatesFailed, this, &PluginSelectDlg::checkForUpdatesFailed); + connect(m_pluginManager, &SearchPluginManager::pluginInstalled, this, &PluginSelectDlg::pluginInstalled); + connect(m_pluginManager, &SearchPluginManager::pluginInstallationFailed, this, &PluginSelectDlg::pluginInstallationFailed); + connect(m_pluginManager, &SearchPluginManager::pluginUpdated, this, &PluginSelectDlg::pluginUpdated); + connect(m_pluginManager, &SearchPluginManager::pluginUpdateFailed, this, &PluginSelectDlg::pluginUpdateFailed); + connect(m_pluginManager, &SearchPluginManager::checkForUpdatesFinished, this, &PluginSelectDlg::checkForUpdatesFinished); + connect(m_pluginManager, &SearchPluginManager::checkForUpdatesFailed, this, &PluginSelectDlg::checkForUpdatesFailed); Utils::Gui::resize(this); show(); @@ -387,7 +387,7 @@ void PluginSelectDlg::iconDownloaded(const QString &url, QString filePath) if (!plugin) continue; QString iconPath = QString("%1/%2.%3") - .arg(SearchEngine::pluginsLocation()) + .arg(SearchPluginManager::pluginsLocation()) .arg(id) .arg(url.endsWith(".ico", Qt::CaseInsensitive) ? "ico" : "png"); if (QFile::copy(filePath, iconPath)) { diff --git a/src/gui/search/pluginselectdlg.h b/src/gui/search/pluginselectdlg.h index c07c3dd4e..c67c19cf0 100644 --- a/src/gui/search/pluginselectdlg.h +++ b/src/gui/search/pluginselectdlg.h @@ -34,7 +34,7 @@ #include -#include "base/searchengine.h" +#include "base/search/searchpluginmanager.h" class QDropEvent; class QStringList; @@ -50,7 +50,7 @@ class PluginSelectDlg: public QDialog Q_OBJECT public: - explicit PluginSelectDlg(SearchEngine *pluginManager, QWidget *parent = 0); + explicit PluginSelectDlg(SearchPluginManager *pluginManager, QWidget *parent = 0); ~PluginSelectDlg(); QList findItemsWithUrl(QString url); @@ -89,7 +89,7 @@ private: void finishPluginUpdate(); Ui::PluginSelectDlg *m_ui; - SearchEngine *m_pluginManager; + SearchPluginManager *m_pluginManager; QStringList m_updatedPlugins; int m_asyncOps; int m_pendingUpdates; diff --git a/src/gui/search/searchtab.cpp b/src/gui/search/searchtab.cpp index 182f53848..2c0fb044e 100644 --- a/src/gui/search/searchtab.cpp +++ b/src/gui/search/searchtab.cpp @@ -1,6 +1,7 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006 Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,37 +25,41 @@ * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. - * - * Contact : chris@qbittorrent.org */ +#include "searchtab.h" + #include +#include +#include #include -#include -#include -#include #include -#include #include +#include #include -#include +#include +#include #include +#include +#include -#include "base/utils/misc.h" +#include "base/bittorrent/session.h" #include "base/preferences.h" +#include "base/search/searchdownloadhandler.h" +#include "base/search/searchhandler.h" +#include "base/search/searchpluginmanager.h" #include "base/settingvalue.h" +#include "base/utils/misc.h" +#include "addnewtorrentdialog.h" #include "guiiconprovider.h" -#include "searchsortmodel.h" #include "searchlistdelegate.h" -#include "searchwidget.h" -#include "searchtab.h" +#include "searchsortmodel.h" #include "ui_searchtab.h" -SearchTab::SearchTab(SearchWidget *parent) +SearchTab::SearchTab(SearchHandler *searchHandler, QWidget *parent) : QWidget(parent) - , m_ui(new Ui::SearchTab()) - , m_parent(parent) - , m_status(Status::NoResults) + , m_ui(new Ui::SearchTab) + , m_searchHandler(searchHandler) { m_ui->setupUi(this); @@ -84,6 +89,7 @@ SearchTab::SearchTab(SearchWidget *parent) m_proxyModel = new SearchSortModel(this); m_proxyModel->setDynamicSortFilter(true); m_proxyModel->setSourceModel(m_searchListModel); + m_proxyModel->setNameFilter(searchHandler->pattern()); m_ui->resultsBrowser->setModel(m_proxyModel); m_searchDelegate = new SearchListDelegate(this); @@ -113,30 +119,41 @@ SearchTab::SearchTab(SearchWidget *parent) if ((m_ui->resultsBrowser->columnWidth(i) <= 0) && !m_ui->resultsBrowser->isColumnHidden(i)) m_ui->resultsBrowser->resizeColumnToContents(i); - // Connect signals to slots (search part) - connect(m_ui->resultsBrowser, SIGNAL(doubleClicked(const QModelIndex&)), this, SLOT(downloadItem(const QModelIndex&))); - header()->setContextMenuPolicy(Qt::CustomContextMenu); - connect(header(), SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(displayToggleColumnsMenu(const QPoint &))); - connect(header(), SIGNAL(sectionResized(int, int, int)), this, SLOT(saveSettings())); - connect(header(), SIGNAL(sectionMoved(int, int, int)), this, SLOT(saveSettings())); - connect(header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), this, SLOT(saveSettings())); + connect(header(), &QWidget::customContextMenuRequested, this, &SearchTab::displayToggleColumnsMenu); + connect(header(), &QHeaderView::sectionResized, this, &SearchTab::saveSettings); + connect(header(), &QHeaderView::sectionMoved, this, &SearchTab::saveSettings); + connect(header(), &QHeaderView::sortIndicatorChanged, this, &SearchTab::saveSettings); fillFilterComboBoxes(); updateFilter(); - connect(m_ui->filterMode, SIGNAL(currentIndexChanged(int)), this, SLOT(updateFilter())); - connect(m_ui->minSeeds, SIGNAL(editingFinished()), this, SLOT(updateFilter())); - connect(m_ui->minSeeds, SIGNAL(valueChanged(int)), this, SLOT(updateFilter())); - connect(m_ui->maxSeeds, SIGNAL(editingFinished()), this, SLOT(updateFilter())); - connect(m_ui->maxSeeds, SIGNAL(valueChanged(int)), this, SLOT(updateFilter())); - connect(m_ui->minSize, SIGNAL(editingFinished()), this, SLOT(updateFilter())); - connect(m_ui->minSize, SIGNAL(valueChanged(double)), this, SLOT(updateFilter())); - connect(m_ui->maxSize, SIGNAL(editingFinished()), this, SLOT(updateFilter())); - connect(m_ui->maxSize, SIGNAL(valueChanged(double)), this, SLOT(updateFilter())); - connect(m_ui->minSizeUnit, SIGNAL(currentIndexChanged(int)), this, SLOT(updateFilter())); - connect(m_ui->maxSizeUnit, SIGNAL(currentIndexChanged(int)), this, SLOT(updateFilter())); + connect(m_ui->filterMode, static_cast(&QComboBox::currentIndexChanged) + , this, &SearchTab::updateFilter); + connect(m_ui->minSeeds, &QAbstractSpinBox::editingFinished, this, &SearchTab::updateFilter); + connect(m_ui->minSeeds, static_cast(&QSpinBox::valueChanged) + , this, &SearchTab::updateFilter); + connect(m_ui->maxSeeds, &QAbstractSpinBox::editingFinished, this, &SearchTab::updateFilter); + connect(m_ui->maxSeeds, static_cast(&QSpinBox::valueChanged) + , this, &SearchTab::updateFilter); + connect(m_ui->minSize, &QAbstractSpinBox::editingFinished, this, &SearchTab::updateFilter); + connect(m_ui->minSize, static_cast(&QDoubleSpinBox::valueChanged) + , this, &SearchTab::updateFilter); + connect(m_ui->maxSize, &QAbstractSpinBox::editingFinished, this, &SearchTab::updateFilter); + connect(m_ui->maxSize, static_cast(&QDoubleSpinBox::valueChanged) + , this, &SearchTab::updateFilter); + connect(m_ui->minSizeUnit, static_cast(&QComboBox::currentIndexChanged) + , this, &SearchTab::updateFilter); + connect(m_ui->maxSizeUnit, static_cast(&QComboBox::currentIndexChanged) + , this, &SearchTab::updateFilter); + + connect(m_ui->resultsBrowser, &QAbstractItemView::doubleClicked, this, &SearchTab::onItemDoubleClicked); + + connect(searchHandler, &SearchHandler::newSearchResults, this, &SearchTab::appendSearchResults); + connect(searchHandler, &SearchHandler::searchFinished, this, &SearchTab::searchFinished); + connect(searchHandler, &SearchHandler::searchFailed, this, &SearchTab::searchFailed); + connect(this, &QObject::destroyed, searchHandler, &QObject::deleteLater); } SearchTab::~SearchTab() @@ -145,80 +162,138 @@ SearchTab::~SearchTab() delete m_ui; } -void SearchTab::downloadItem(const QModelIndex &index) +void SearchTab::onItemDoubleClicked(const QModelIndex &index) { - QString torrentUrl = m_proxyModel->data(m_proxyModel->index(index.row(), SearchSortModel::DL_LINK)).toString(); - QString siteUrl = m_proxyModel->data(m_proxyModel->index(index.row(), SearchSortModel::ENGINE_URL)).toString(); setRowColor(index.row(), QApplication::palette().color(QPalette::LinkVisited)); - m_parent->downloadTorrent(siteUrl, torrentUrl); + downloadTorrent(index); } -QHeaderView* SearchTab::header() const +QHeaderView *SearchTab::header() const { return m_ui->resultsBrowser->header(); } -QTreeView* SearchTab::getCurrentTreeView() const +// Set the color of a row in data model +void SearchTab::setRowColor(int row, const QColor &color) { - return m_ui->resultsBrowser; + m_proxyModel->setDynamicSortFilter(false); + for (int i = 0; i < m_proxyModel->columnCount(); ++i) + m_proxyModel->setData(m_proxyModel->index(row, i), color, Qt::ForegroundRole); + + m_proxyModel->setDynamicSortFilter(true); } -SearchSortModel* SearchTab::getCurrentSearchListProxy() const +SearchTab::Status SearchTab::status() const { - return m_proxyModel; + return m_status; } -QStandardItemModel* SearchTab::getCurrentSearchListModel() const +int SearchTab::visibleResultsCount() const { - return m_searchListModel; + return m_proxyModel->rowCount(); } -// Set the color of a row in data model -void SearchTab::setRowColor(int row, const QColor &color) +void SearchTab::cancelSearch() { - m_proxyModel->setDynamicSortFilter(false); - for (int i = 0; i < m_proxyModel->columnCount(); ++i) - m_proxyModel->setData(m_proxyModel->index(row, i), color, Qt::ForegroundRole); + m_searchHandler->cancelSearch(); +} - m_proxyModel->setDynamicSortFilter(true); +void SearchTab::downloadTorrents() +{ + const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()}; + for (const QModelIndex &rowIndex : rows) + downloadTorrent(rowIndex); } -SearchTab::Status SearchTab::status() const +void SearchTab::openTorrentPages() { - return m_status; + const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()}; + for (const QModelIndex &rowIndex : rows) { + const QString descrLink = m_proxyModel->data( + m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK)).toString(); + if (!descrLink.isEmpty()) + QDesktopServices::openUrl(QUrl::fromEncoded(descrLink.toUtf8())); + } +} + +void SearchTab::copyTorrentURLs() +{ + const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()}; + QStringList urls; + for (const QModelIndex &rowIndex : rows) { + const QString descrLink = m_proxyModel->data( + m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK)).toString(); + if (!descrLink.isEmpty()) + urls << descrLink; + } + + if (!urls.empty()) { + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(urls.join("\n")); + } } void SearchTab::setStatus(Status value) { + if (m_status == value) return; + m_status = value; setStatusTip(statusText(value)); - const int thisTabIndex = m_parent->searchTabs()->indexOf(this); - m_parent->searchTabs()->setTabToolTip(thisTabIndex, statusTip()); - m_parent->searchTabs()->setTabIcon(thisTabIndex, GuiIconProvider::instance()->getIcon(statusIconName(value))); + emit statusChanged(); +} + +void SearchTab::downloadTorrent(const QModelIndex &rowIndex) +{ + const QString torrentUrl = m_proxyModel->data( + m_proxyModel->index(rowIndex.row(), SearchSortModel::DL_LINK)).toString(); + const QString siteUrl = m_proxyModel->data( + m_proxyModel->index(rowIndex.row(), SearchSortModel::ENGINE_URL)).toString(); + + if (torrentUrl.startsWith("bc://bt/", Qt::CaseInsensitive) || torrentUrl.startsWith("magnet:", Qt::CaseInsensitive)) { + addTorrentToSession(torrentUrl); + } + else { + SearchDownloadHandler *downloadHandler = m_searchHandler->manager()->downloadTorrent(siteUrl, torrentUrl); + connect(downloadHandler, &SearchDownloadHandler::downloadFinished, this, &SearchTab::addTorrentToSession); + connect(downloadHandler, &SearchDownloadHandler::downloadFinished, downloadHandler, &SearchDownloadHandler::deleteLater); + } +} + +void SearchTab::addTorrentToSession(const QString &source) +{ + if (source.isEmpty()) return; + + if (AddNewTorrentDialog::isEnabled()) + AddNewTorrentDialog::show(source, this); + else + BitTorrent::Session::instance()->addTorrent(source); } void SearchTab::updateResultsCount() { - const int totalResults = getCurrentSearchListModel() ? getCurrentSearchListModel()->rowCount(QModelIndex()) : 0; - const int filteredResults = getCurrentSearchListProxy() ? getCurrentSearchListProxy()->rowCount(QModelIndex()) : totalResults; + const int totalResults = m_searchListModel->rowCount(); + const int filteredResults = m_proxyModel->rowCount(); m_ui->resultsLbl->setText(tr("Results (showing %1 out of %2):", "i.e: Search results") .arg(filteredResults).arg(totalResults)); + + m_noSearchResults = (totalResults == 0); + emit resultsCountUpdated(); } void SearchTab::updateFilter() { using Utils::Misc::SizeUnit; - SearchSortModel* filterModel = getCurrentSearchListProxy(); - filterModel->enableNameFilter(filteringMode() == NameFilteringMode::OnlyNames); + + m_proxyModel->enableNameFilter(filteringMode() == NameFilteringMode::OnlyNames); // we update size and seeds filter parameters in the model even if they are disabled - filterModel->setSeedsFilter(m_ui->minSeeds->value(), m_ui->maxSeeds->value()); - filterModel->setSizeFilter( + m_proxyModel->setSeedsFilter(m_ui->minSeeds->value(), m_ui->maxSeeds->value()); + m_proxyModel->setSizeFilter( sizeInBytes(m_ui->minSize->value(), static_cast(m_ui->minSizeUnit->currentIndex())), sizeInBytes(m_ui->maxSize->value(), static_cast(m_ui->maxSizeUnit->currentIndex()))); nameFilteringModeSetting() = filteringMode(); - filterModel->invalidate(); + m_proxyModel->invalidate(); updateResultsCount(); } @@ -273,24 +348,6 @@ QString SearchTab::statusText(SearchTab::Status st) } } -QString SearchTab::statusIconName(SearchTab::Status st) -{ - switch (st) { - case Status::Ongoing: - return QLatin1String("task-ongoing"); - case Status::Finished: - return QLatin1String("task-complete"); - case Status::Aborted: - return QLatin1String("task-reject"); - case Status::Error: - return QLatin1String("task-attention"); - case Status::NoResults: - return QLatin1String("task-attention"); - default: - return QString(); - } -} - SearchTab::NameFilteringMode SearchTab::filteringMode() const { return static_cast(m_ui->filterMode->itemData(m_ui->filterMode->currentIndex()).toInt()); @@ -341,6 +398,40 @@ void SearchTab::displayToggleColumnsMenu(const QPoint&) } } +void SearchTab::searchFinished(bool cancelled) +{ + if (cancelled) + setStatus(Status::Aborted); + else if (m_noSearchResults) + setStatus(Status::NoResults); + else + setStatus(Status::Finished); +} + +void SearchTab::searchFailed() +{ + setStatus(Status::Error); +} + +void SearchTab::appendSearchResults(const QList &results) +{ + for (const SearchResult &result : results) { + // Add item to search result list + int row = m_searchListModel->rowCount(); + m_searchListModel->insertRow(row); + + m_searchListModel->setData(m_searchListModel->index(row, SearchSortModel::NAME), result.fileName); // Name + m_searchListModel->setData(m_searchListModel->index(row, SearchSortModel::DL_LINK), result.fileUrl); // download URL + m_searchListModel->setData(m_searchListModel->index(row, SearchSortModel::SIZE), result.fileSize); // Size + m_searchListModel->setData(m_searchListModel->index(row, SearchSortModel::SEEDS), result.nbSeeders); // Seeders + m_searchListModel->setData(m_searchListModel->index(row, SearchSortModel::LEECHES), result.nbLeechers); // Leechers + m_searchListModel->setData(m_searchListModel->index(row, SearchSortModel::ENGINE_URL), result.siteUrl); // Search site URL + m_searchListModel->setData(m_searchListModel->index(row, SearchSortModel::DESC_LINK), result.descrLink); // Description Link + } + + updateResultsCount(); +} + CachedSettingValue &SearchTab::nameFilteringModeSetting() { static CachedSettingValue setting("Search/FilteringMode", NameFilteringMode::OnlyNames); diff --git a/src/gui/search/searchtab.h b/src/gui/search/searchtab.h index 4e03308e0..465909a5c 100644 --- a/src/gui/search/searchtab.h +++ b/src/gui/search/searchtab.h @@ -1,6 +1,7 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006 Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,12 +25,9 @@ * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. - * - * Contact : chris@qbittorrent.org */ -#ifndef SEARCHTAB_H -#define SEARCHTAB_H +#pragma once #include @@ -38,46 +36,36 @@ class QLabel; class QModelIndex; -class QTreeView; class QHeaderView; +class QStandardItem; class QStandardItemModel; class QVBoxLayout; template class CachedSettingValue; +class SearchHandler; class SearchSortModel; class SearchListDelegate; -class SearchWidget; +struct SearchResult; namespace Ui { class SearchTab; } -class SearchTab: public QWidget +class SearchTab : public QWidget { Q_OBJECT + Q_DISABLE_COPY(SearchTab) public: - enum class NameFilteringMode { Everywhere, OnlyNames }; - Q_ENUM(NameFilteringMode) - explicit SearchTab(SearchWidget *parent); - ~SearchTab(); - - QStandardItemModel* getCurrentSearchListModel() const; - SearchSortModel* getCurrentSearchListProxy() const; - QTreeView* getCurrentTreeView() const; - QHeaderView* header() const; - - void setRowColor(int row, const QColor &color); - enum class Status { Ongoing, @@ -87,36 +75,50 @@ public: NoResults }; - void setStatus(Status value); + explicit SearchTab(SearchHandler *searchHandler, QWidget *parent = nullptr); + ~SearchTab() override; + Status status() const; + int visibleResultsCount() const; - void updateResultsCount(); + void cancelSearch(); -public slots: - void downloadItem(const QModelIndex &index); + void downloadTorrents(); + void openTorrentPages(); + void copyTorrentURLs(); -private slots: +signals: + void resultsCountUpdated(); + void statusChanged(); + +private: void loadSettings(); void saveSettings() const; void updateFilter(); void displayToggleColumnsMenu(const QPoint&); - -private: + void onItemDoubleClicked(const QModelIndex &index); + void searchFinished(bool cancelled); + void searchFailed(); + void appendSearchResults(const QList &results); + void updateResultsCount(); + void setStatus(Status value); + void downloadTorrent(const QModelIndex &rowIndex); + void addTorrentToSession(const QString &source); void fillFilterComboBoxes(); NameFilteringMode filteringMode() const; - static QString statusText(Status st); - static QString statusIconName(Status st); + QHeaderView *header() const; + void setRowColor(int row, const QColor &color); + static QString statusText(Status st); static CachedSettingValue& nameFilteringModeSetting(); Ui::SearchTab *m_ui; + SearchHandler *m_searchHandler; QStandardItemModel *m_searchListModel; SearchSortModel *m_proxyModel; SearchListDelegate *m_searchDelegate; - SearchWidget *m_parent; - Status m_status; + Status m_status = Status::Ongoing; + bool m_noSearchResults = true; }; Q_DECLARE_METATYPE(SearchTab::NameFilteringMode) - -#endif // SEARCHTAB_H diff --git a/src/gui/search/searchwidget.cpp b/src/gui/search/searchwidget.cpp index 6d8d80593..cae9f7c17 100644 --- a/src/gui/search/searchwidget.cpp +++ b/src/gui/search/searchwidget.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015, 2018 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -25,59 +25,76 @@ * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. - * - * Contact : chris@qbittorrent.org */ #include "searchwidget.h" -#include -#include -#include -#include +#include +#ifdef Q_OS_WIN +#include +#endif + +#include #include +#include +#include #include -#include +#include #include -#include -#include -#include -#include -#include #include -#include +#include +#include +#include +#include #include +#include #include -#include -#ifdef Q_OS_WIN -#include -#endif - #include "base/bittorrent/session.h" +#include "base/preferences.h" +#include "base/search/searchpluginmanager.h" +#include "base/search/searchhandler.h" #include "base/utils/fs.h" #include "base/utils/misc.h" -#include "base/preferences.h" -#include "base/searchengine.h" -#include "searchlistdelegate.h" -#include "mainwindow.h" #include "addnewtorrentdialog.h" #include "guiiconprovider.h" +#include "mainwindow.h" #include "pluginselectdlg.h" +#include "searchlistdelegate.h" #include "searchsortmodel.h" #include "searchtab.h" - #include "ui_searchwidget.h" #define SEARCHHISTORY_MAXSIZE 50 #define URL_COLUMN 5 +namespace +{ + QString statusIconName(SearchTab::Status st) + { + switch (st) { + case SearchTab::Status::Ongoing: + return QLatin1String("task-ongoing"); + case SearchTab::Status::Finished: + return QLatin1String("task-complete"); + case SearchTab::Status::Aborted: + return QLatin1String("task-reject"); + case SearchTab::Status::Error: + return QLatin1String("task-attention"); + case SearchTab::Status::NoResults: + return QLatin1String("task-attention"); + default: + return QString(); + } + } +} + SearchWidget::SearchWidget(MainWindow *mainWindow) : QWidget(mainWindow) , m_ui(new Ui::SearchWidget()) + , m_tabStatusChangedMapper(new QSignalMapper(this)) , m_mainWindow(mainWindow) , m_isNewQueryString(false) - , m_noSearchResults(true) { m_ui->setupUi(this); @@ -113,42 +130,41 @@ SearchWidget::SearchWidget(MainWindow *mainWindow) m_ui->tabWidget->setIconSize(iconSize); #endif connect(m_ui->tabWidget, &QTabWidget::tabCloseRequested, this, &SearchWidget::closeTab); + connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, &SearchWidget::tabChanged); - m_searchEngine = new SearchEngine; - connect(m_searchEngine, &SearchEngine::searchStarted, this, &SearchWidget::searchStarted); - connect(m_searchEngine, &SearchEngine::newSearchResults, this, &SearchWidget::appendSearchResults); - connect(m_searchEngine, &SearchEngine::searchFinished, this, &SearchWidget::searchFinished); - connect(m_searchEngine, &SearchEngine::searchFailed, this, &SearchWidget::searchFailed); - connect(m_searchEngine, &SearchEngine::torrentFileDownloaded, this, &SearchWidget::addTorrentToSession); + connect(m_tabStatusChangedMapper, static_cast(&QSignalMapper::mapped) + , this, &SearchWidget::tabStatusChanged); + m_searchManager = new SearchPluginManager; const auto onPluginChanged = [this]() { fillCatCombobox(); fillPluginComboBox(); selectActivePage(); }; - connect(m_searchEngine, &SearchEngine::pluginInstalled, this, onPluginChanged); - connect(m_searchEngine, &SearchEngine::pluginUninstalled, this, onPluginChanged); - connect(m_searchEngine, &SearchEngine::pluginUpdated, this, onPluginChanged); - connect(m_searchEngine, &SearchEngine::pluginEnabled, this, onPluginChanged); + connect(m_searchManager, &SearchPluginManager::pluginInstalled, this, onPluginChanged); + connect(m_searchManager, &SearchPluginManager::pluginUninstalled, this, onPluginChanged); + connect(m_searchManager, &SearchPluginManager::pluginUpdated, this, onPluginChanged); + connect(m_searchManager, &SearchPluginManager::pluginEnabled, this, onPluginChanged); // Fill in category combobox onPluginChanged(); connect(m_ui->m_searchPattern, &LineEdit::returnPressed, m_ui->searchButton, &QPushButton::click); connect(m_ui->m_searchPattern, &LineEdit::textEdited, this, &SearchWidget::searchTextEdited); - connect(m_ui->selectPlugin, static_cast(&QComboBox::currentIndexChanged), this, &SearchWidget::selectMultipleBox); + connect(m_ui->selectPlugin, static_cast(&QComboBox::currentIndexChanged) + , this, &SearchWidget::selectMultipleBox); } void SearchWidget::fillCatCombobox() { m_ui->comboCategory->clear(); - m_ui->comboCategory->addItem(SearchEngine::categoryFullName("all"), QVariant("all")); + m_ui->comboCategory->addItem(SearchPluginManager::categoryFullName("all"), QVariant("all")); using QStrPair = QPair; QList tmpList; - foreach (const QString &cat, m_searchEngine->supportedCategories()) - tmpList << qMakePair(SearchEngine::categoryFullName(cat), cat); + foreach (const QString &cat, m_searchManager->supportedCategories()) + tmpList << qMakePair(SearchPluginManager::categoryFullName(cat), cat); std::sort(tmpList.begin(), tmpList.end(), [](const QStrPair &l, const QStrPair &r) { return (QString::localeAwareCompare(l.first, r.first) < 0); }); foreach (const QStrPair &p, tmpList) { @@ -169,8 +185,8 @@ void SearchWidget::fillPluginComboBox() using QStrPair = QPair; QList tmpList; - foreach (const QString &name, m_searchEngine->enabledPlugins()) - tmpList << qMakePair(m_searchEngine->pluginFullName(name), name); + foreach (const QString &name, m_searchManager->enabledPlugins()) + tmpList << qMakePair(m_searchManager->pluginFullName(name), name); std::sort(tmpList.begin(), tmpList.end(), [](const QStrPair &l, const QStrPair &r) { return (l.first < r.first); } ); foreach (const QStrPair &p, tmpList) @@ -192,7 +208,7 @@ QString SearchWidget::selectedPlugin() const void SearchWidget::selectActivePage() { - if (m_searchEngine->allPlugins().isEmpty()) { + if (m_searchManager->allPlugins().isEmpty()) { m_ui->stackedPages->setCurrentWidget(m_ui->emptyPage); m_ui->m_searchPattern->setEnabled(false); m_ui->comboCategory->setEnabled(false); @@ -211,36 +227,30 @@ void SearchWidget::selectActivePage() SearchWidget::~SearchWidget() { qDebug("Search destruction"); - delete m_searchEngine; + delete m_searchManager; delete m_ui; } -void SearchWidget::downloadTorrent(const QString &siteUrl, const QString &url) +void SearchWidget::updateButtons() { - if (url.startsWith("bc://bt/", Qt::CaseInsensitive) || url.startsWith("magnet:", Qt::CaseInsensitive)) - addTorrentToSession(url); - else - m_searchEngine->downloadTorrent(siteUrl, url); + if (m_currentSearchTab && (m_currentSearchTab->visibleResultsCount() > 0)) { + m_ui->downloadButton->setEnabled(true); + m_ui->goToDescBtn->setEnabled(true); + m_ui->copyURLBtn->setEnabled(true); + } + else { + m_ui->downloadButton->setEnabled(false); + m_ui->goToDescBtn->setEnabled(false); + m_ui->copyURLBtn->setEnabled(false); + } } -void SearchWidget::tab_changed(int t) +void SearchWidget::tabChanged(int index) { - //when we switch from a tab that is not empty to another that is empty the download button - //doesn't have to be available - if (t > -1) { - //-1 = no more tab - m_currentSearchTab = m_allTabs.at(m_ui->tabWidget->currentIndex()); - if (m_currentSearchTab->getCurrentSearchListModel()->rowCount()) { - m_ui->downloadButton->setEnabled(true); - m_ui->goToDescBtn->setEnabled(true); - m_ui->copyURLBtn->setEnabled(true); - } - else { - m_ui->downloadButton->setEnabled(false); - m_ui->goToDescBtn->setEnabled(false); - m_ui->copyURLBtn->setEnabled(false); - } - } + // when we switch from a tab that is not empty to another that is empty + // the download button doesn't have to be available + m_currentSearchTab = (index < 0 ? nullptr : m_allTabs.at(m_ui->tabWidget->currentIndex())); + updateButtons(); } void SearchWidget::selectMultipleBox(int index) @@ -250,17 +260,9 @@ void SearchWidget::selectMultipleBox(int index) on_pluginsButton_clicked(); } -void SearchWidget::addTorrentToSession(const QString &source) -{ - if (AddNewTorrentDialog::isEnabled()) - AddNewTorrentDialog::show(source, this); - else - BitTorrent::Session::instance()->addTorrent(source); -} - void SearchWidget::on_pluginsButton_clicked() { - new PluginSelectDlg(m_searchEngine, this); + new PluginSelectDlg(m_searchManager, this); } void SearchWidget::searchTextEdited(QString) @@ -275,11 +277,6 @@ void SearchWidget::giveFocusToSearchInput() m_ui->m_searchPattern->setFocus(); } -QTabWidget *SearchWidget::searchTabs() const -{ - return m_ui->tabWidget; -} - // Function called when we click on search button void SearchWidget::on_searchButton_clicked() { @@ -288,9 +285,8 @@ void SearchWidget::on_searchButton_clicked() return; } - if (m_searchEngine->isActive()) { - m_searchEngine->cancelSearch(); - + if (m_activeSearchTab) { + m_activeSearchTab->cancelSearch(); if (!m_isNewQueryString) { m_ui->searchButton->setText(tr("Search")); return; @@ -306,173 +302,84 @@ void SearchWidget::on_searchButton_clicked() return; } - // Tab Addition - m_currentSearchTab = new SearchTab(this); - m_activeSearchTab = m_currentSearchTab; - m_allTabs.append(m_currentSearchTab); - QString tabName = pattern; - tabName.replace(QRegExp("&{1}"), "&&"); - m_ui->tabWidget->addTab(m_currentSearchTab, tabName); - m_ui->tabWidget->setCurrentWidget(m_currentSearchTab); - m_currentSearchTab->getCurrentSearchListProxy()->setNameFilter(pattern); - QStringList plugins; - if (selectedPlugin() == "all") plugins = m_searchEngine->allPlugins(); - else if (selectedPlugin() == "enabled") plugins = m_searchEngine->enabledPlugins(); - else if (selectedPlugin() == "multi") plugins = m_searchEngine->enabledPlugins(); + if (selectedPlugin() == "all") plugins = m_searchManager->allPlugins(); + else if (selectedPlugin() == "enabled") plugins = m_searchManager->enabledPlugins(); + else if (selectedPlugin() == "multi") plugins = m_searchManager->enabledPlugins(); else plugins << selectedPlugin(); qDebug("Search with category: %s", qUtf8Printable(selectedCategory())); - // Update SearchEngine widgets - m_noSearchResults = true; - - // Changing the text of the current label - m_activeSearchTab->updateResultsCount(); - // Launch search - m_searchEngine->startSearch(pattern, selectedCategory(), plugins); -} + auto *searchHandler = m_searchManager->startSearch(pattern, selectedCategory(), plugins); -void SearchWidget::searchStarted() -{ - // Update SearchEngine widgets - m_activeSearchTab->setStatus(SearchTab::Status::Ongoing); - m_ui->searchButton->setText(tr("Stop")); -} - -// Slot called when search is Finished -// Search can be finished for 3 reasons : -// Error | Stopped by user | Finished normally -void SearchWidget::searchFinished(bool cancelled) -{ - if (m_mainWindow->isNotificationsEnabled() && (m_mainWindow->currentTabWidget() != this)) - m_mainWindow->showNotificationBaloon(tr("Search Engine"), tr("Search has finished")); + // Tab Addition + auto *newTab = new SearchTab(searchHandler, this); + m_allTabs.append(newTab); - if (m_activeSearchTab.isNull()) return; // The active tab was closed + QString tabName = pattern; + tabName.replace(QRegExp("&{1}"), "&&"); + m_ui->tabWidget->addTab(newTab, tabName); + m_ui->tabWidget->setCurrentWidget(newTab); - if (cancelled) - m_activeSearchTab->setStatus(SearchTab::Status::Aborted); - else if (m_noSearchResults) - m_activeSearchTab->setStatus(SearchTab::Status::NoResults); - else - m_activeSearchTab->setStatus(SearchTab::Status::Finished); + connect(newTab, &SearchTab::resultsCountUpdated, this, &SearchWidget::resultsCountUpdated); + connect(newTab, &SearchTab::statusChanged + , m_tabStatusChangedMapper, static_cast(&QSignalMapper::map)); + m_tabStatusChangedMapper->setMapping(newTab, newTab); - m_activeSearchTab = 0; - m_ui->searchButton->setText(tr("Search")); + m_ui->searchButton->setText(tr("Stop")); + m_activeSearchTab = newTab; + tabStatusChanged(newTab); } -void SearchWidget::searchFailed() +void SearchWidget::resultsCountUpdated() { - if (m_mainWindow->isNotificationsEnabled() && (m_mainWindow->currentTabWidget() != this)) - m_mainWindow->showNotificationBaloon(tr("Search Engine"), tr("Search has failed")); - - if (m_activeSearchTab.isNull()) return; // The active tab was closed - -#ifdef Q_OS_WIN - m_activeSearchTab->setStatus(SearchTab::Status::Aborted); -#else - m_activeSearchTab->setStatus(SearchTab::Status::Error); -#endif + updateButtons(); } -void SearchWidget::appendSearchResults(const QList &results) +void SearchWidget::tabStatusChanged(QWidget *tab) { - if (m_activeSearchTab.isNull()) { - m_searchEngine->cancelSearch(); - return; - } - - Q_ASSERT(m_activeSearchTab); - - QStandardItemModel *curModel = m_activeSearchTab->getCurrentSearchListModel(); - Q_ASSERT(curModel); - - foreach (const SearchResult &result, results) { - // Add item to search result list - int row = curModel->rowCount(); - curModel->insertRow(row); + const int tabIndex = m_ui->tabWidget->indexOf(tab); + m_ui->tabWidget->setTabToolTip(tabIndex, tab->statusTip()); + m_ui->tabWidget->setTabIcon(tabIndex, GuiIconProvider::instance()->getIcon( + statusIconName(static_cast(tab)->status()))); + + if ((tab == m_activeSearchTab) && (m_activeSearchTab->status() != SearchTab::Status::Ongoing)) { + Q_ASSERT(m_activeSearchTab->status() != SearchTab::Status::Ongoing); + + if (m_mainWindow->isNotificationsEnabled() && (m_mainWindow->currentTabWidget() != this)) { + if (m_activeSearchTab->status() == SearchTab::Status::Error) + m_mainWindow->showNotificationBaloon(tr("Search Engine"), tr("Search has failed")); + else + m_mainWindow->showNotificationBaloon(tr("Search Engine"), tr("Search has finished")); + } - curModel->setData(curModel->index(row, SearchSortModel::DL_LINK), result.fileUrl); // download URL - curModel->setData(curModel->index(row, SearchSortModel::NAME), result.fileName); // Name - curModel->setData(curModel->index(row, SearchSortModel::SIZE), result.fileSize); // Size - curModel->setData(curModel->index(row, SearchSortModel::SEEDS), result.nbSeeders); // Seeders - curModel->setData(curModel->index(row, SearchSortModel::LEECHES), result.nbLeechers); // Leechers - curModel->setData(curModel->index(row, SearchSortModel::ENGINE_URL), result.siteUrl); // Search site URL - curModel->setData(curModel->index(row, SearchSortModel::DESC_LINK), result.descrLink); // Description Link + m_activeSearchTab = nullptr; + m_ui->searchButton->setText(tr("Search")); } - - m_noSearchResults = false; - m_activeSearchTab->updateResultsCount(); - - // Enable clear & download buttons - m_ui->downloadButton->setEnabled(true); - m_ui->goToDescBtn->setEnabled(true); - m_ui->copyURLBtn->setEnabled(true); } void SearchWidget::closeTab(int index) { - // Search is run for active tab so if user decided to close it, then stop search - if (!m_activeSearchTab.isNull() && index == m_ui->tabWidget->indexOf(m_activeSearchTab)) { - qDebug("Closed active search Tab"); - if (m_searchEngine->isActive()) - m_searchEngine->cancelSearch(); - m_activeSearchTab = 0; - } + SearchTab *tab = m_allTabs.takeAt(index); + if (tab == m_activeSearchTab) + m_ui->searchButton->setText(tr("Search")); - delete m_allTabs.takeAt(index); - - if (!m_allTabs.size()) { - m_ui->downloadButton->setEnabled(false); - m_ui->goToDescBtn->setEnabled(false); - m_ui->copyURLBtn->setEnabled(false); - } + delete tab; } // Download selected items in search results list void SearchWidget::on_downloadButton_clicked() { - //QModelIndexList selectedIndexes = currentSearchTab->getCurrentTreeView()->selectionModel()->selectedIndexes(); - QModelIndexList selectedIndexes = - m_allTabs.at(m_ui->tabWidget->currentIndex())->getCurrentTreeView()->selectionModel()->selectedIndexes(); - foreach (const QModelIndex &index, selectedIndexes) { - if (index.column() == SearchSortModel::NAME) - m_allTabs.at(m_ui->tabWidget->currentIndex())->downloadItem(index); - } + m_allTabs.at(m_ui->tabWidget->currentIndex())->downloadTorrents(); } void SearchWidget::on_goToDescBtn_clicked() { - QModelIndexList selectedIndexes = - m_allTabs.at(m_ui->tabWidget->currentIndex())->getCurrentTreeView()->selectionModel()->selectedIndexes(); - foreach (const QModelIndex &index, selectedIndexes) { - if (index.column() == SearchSortModel::NAME) { - QSortFilterProxyModel *model = m_allTabs.at(m_ui->tabWidget->currentIndex())->getCurrentSearchListProxy(); - const QString descUrl = model->data(model->index(index.row(), SearchSortModel::DESC_LINK)).toString(); - if (!descUrl.isEmpty()) - QDesktopServices::openUrl(QUrl::fromEncoded(descUrl.toUtf8())); - } - } + m_allTabs.at(m_ui->tabWidget->currentIndex())->openTorrentPages(); } void SearchWidget::on_copyURLBtn_clicked() { - QStringList urls; - QModelIndexList selectedIndexes = - m_allTabs.at(m_ui->tabWidget->currentIndex())->getCurrentTreeView()->selectionModel()->selectedIndexes(); - - foreach (const QModelIndex &index, selectedIndexes) { - if (index.column() == SearchSortModel::NAME) { - QSortFilterProxyModel *model = m_allTabs.at(m_ui->tabWidget->currentIndex())->getCurrentSearchListProxy(); - const QString descUrl = model->data(model->index(index.row(), SearchSortModel::DESC_LINK)).toString(); - if (!descUrl.isEmpty()) - urls << descUrl.toUtf8(); - } - } - - if (!urls.empty()) { - QClipboard *clipboard = QApplication::clipboard(); - clipboard->setText(urls.join("\n")); - } + m_allTabs.at(m_ui->tabWidget->currentIndex())->copyTorrentURLs(); } diff --git a/src/gui/search/searchwidget.h b/src/gui/search/searchwidget.h index 1f582ae70..b4537fdae 100644 --- a/src/gui/search/searchwidget.h +++ b/src/gui/search/searchwidget.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015, 2018 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -25,22 +25,19 @@ * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. - * - * Contact : chris@qbittorrent.org */ -#ifndef SEARCHWIDGET_H -#define SEARCHWIDGET_H +#pragma once #include #include #include +class QSignalMapper; class QTabWidget; class MainWindow; -class SearchEngine; -struct SearchResult; +class SearchPluginManager; class SearchTab; namespace Ui @@ -48,56 +45,46 @@ namespace Ui class SearchWidget; } -class SearchWidget: public QWidget +class SearchWidget : public QWidget { Q_OBJECT Q_DISABLE_COPY(SearchWidget) public: explicit SearchWidget(MainWindow *mainWindow); - ~SearchWidget(); + ~SearchWidget() override; - void downloadTorrent(const QString &siteUrl, const QString &url); void giveFocusToSearchInput(); - QTabWidget* searchTabs() const; - private slots: - // Search slots - void tab_changed(int); //to prevent the use of the download button when the tab is empty void on_searchButton_clicked(); void on_downloadButton_clicked(); void on_goToDescBtn_clicked(); void on_copyURLBtn_clicked(); void on_pluginsButton_clicked(); +private: + void tabChanged(int index); void closeTab(int index); - void appendSearchResults(const QList &results); - void searchStarted(); - void searchFinished(bool cancelled); - void searchFailed(); + void resultsCountUpdated(); + void tabStatusChanged(QWidget *tab); void selectMultipleBox(int index); - void addTorrentToSession(const QString &source); - void fillCatCombobox(); void fillPluginComboBox(); void selectActivePage(); void searchTextEdited(QString); + void updateButtons(); -private: QString selectedCategory() const; QString selectedPlugin() const; Ui::SearchWidget *m_ui; - SearchEngine *m_searchEngine; + SearchPluginManager *m_searchManager; + QSignalMapper *m_tabStatusChangedMapper; QPointer m_currentSearchTab; // Selected tab QPointer m_activeSearchTab; // Tab with running search - QList> m_allTabs; // To store all tabs + QList m_allTabs; // To store all tabs MainWindow *m_mainWindow; bool m_isNewQueryString; - bool m_noSearchResults; - QByteArray m_searchResultLineTruncated; }; - -#endif // SEARCHWIDGET_H From 279bce2014079c39209829be3e1690b621ff91a1 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Mon, 5 Feb 2018 11:43:29 +0300 Subject: [PATCH 2/2] Make SearchManager singleton --- src/base/search/searchpluginmanager.cpp | 20 +++++++++++++--- src/base/search/searchpluginmanager.h | 3 +++ src/gui/search/searchwidget.cpp | 31 +++++++++++++------------ src/gui/search/searchwidget.h | 2 -- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/base/search/searchpluginmanager.cpp b/src/base/search/searchpluginmanager.cpp index f02870869..9849a2164 100644 --- a/src/base/search/searchpluginmanager.cpp +++ b/src/base/search/searchpluginmanager.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include "base/logger.h" @@ -47,12 +48,17 @@ #include "searchdownloadhandler.h" #include "searchhandler.h" -static inline void removePythonScriptIfExists(const QString &scriptPath) +namespace { - Utils::Fs::forceRemove(scriptPath); - Utils::Fs::forceRemove(scriptPath + "c"); + inline void removePythonScriptIfExists(const QString &scriptPath) + { + Utils::Fs::forceRemove(scriptPath); + Utils::Fs::forceRemove(scriptPath + "c"); + } } +QPointer SearchPluginManager::m_instance = nullptr; + const QHash SearchPluginManager::m_categoryNames { {"all", QT_TRANSLATE_NOOP("SearchEngine", "All categories")}, {"movies", QT_TRANSLATE_NOOP("SearchEngine", "Movies")}, @@ -68,6 +74,9 @@ const QHash SearchPluginManager::m_categoryNames { SearchPluginManager::SearchPluginManager() : m_updateUrl(QString("http://searchplugins.qbittorrent.org/%1/engines/").arg(Utils::Misc::pythonVersion() >= 3 ? "nova3" : "nova")) { + Q_ASSERT(!m_instance); // only one instance is allowed + m_instance = this; + updateNova(); update(); } @@ -77,6 +86,11 @@ SearchPluginManager::~SearchPluginManager() qDeleteAll(m_plugins); } +SearchPluginManager *SearchPluginManager::instance() +{ + return m_instance; +} + QStringList SearchPluginManager::allPlugins() const { return m_plugins.keys(); diff --git a/src/base/search/searchpluginmanager.h b/src/base/search/searchpluginmanager.h index 2e815aa6e..e3723c540 100644 --- a/src/base/search/searchpluginmanager.h +++ b/src/base/search/searchpluginmanager.h @@ -61,6 +61,8 @@ public: SearchPluginManager(); ~SearchPluginManager() override; + static SearchPluginManager *instance(); + QStringList allPlugins() const; QStringList enabledPlugins() const; QStringList supportedCategories() const; @@ -107,6 +109,7 @@ private: static QString pluginPath(const QString &name); + static QPointer m_instance; static const QHash m_categoryNames; const QString m_updateUrl; diff --git a/src/gui/search/searchwidget.cpp b/src/gui/search/searchwidget.cpp index cae9f7c17..26995f171 100644 --- a/src/gui/search/searchwidget.cpp +++ b/src/gui/search/searchwidget.cpp @@ -135,17 +135,18 @@ SearchWidget::SearchWidget(MainWindow *mainWindow) connect(m_tabStatusChangedMapper, static_cast(&QSignalMapper::mapped) , this, &SearchWidget::tabStatusChanged); - m_searchManager = new SearchPluginManager; + // NOTE: Although SearchManager is Application-wide component now, we still create it the legacy way. + auto *searchManager = new SearchPluginManager; const auto onPluginChanged = [this]() { fillCatCombobox(); fillPluginComboBox(); selectActivePage(); }; - connect(m_searchManager, &SearchPluginManager::pluginInstalled, this, onPluginChanged); - connect(m_searchManager, &SearchPluginManager::pluginUninstalled, this, onPluginChanged); - connect(m_searchManager, &SearchPluginManager::pluginUpdated, this, onPluginChanged); - connect(m_searchManager, &SearchPluginManager::pluginEnabled, this, onPluginChanged); + connect(searchManager, &SearchPluginManager::pluginInstalled, this, onPluginChanged); + connect(searchManager, &SearchPluginManager::pluginUninstalled, this, onPluginChanged); + connect(searchManager, &SearchPluginManager::pluginUpdated, this, onPluginChanged); + connect(searchManager, &SearchPluginManager::pluginEnabled, this, onPluginChanged); // Fill in category combobox onPluginChanged(); @@ -163,7 +164,7 @@ void SearchWidget::fillCatCombobox() using QStrPair = QPair; QList tmpList; - foreach (const QString &cat, m_searchManager->supportedCategories()) + foreach (const QString &cat, SearchPluginManager::instance()->supportedCategories()) tmpList << qMakePair(SearchPluginManager::categoryFullName(cat), cat); std::sort(tmpList.begin(), tmpList.end(), [](const QStrPair &l, const QStrPair &r) { return (QString::localeAwareCompare(l.first, r.first) < 0); }); @@ -185,8 +186,8 @@ void SearchWidget::fillPluginComboBox() using QStrPair = QPair; QList tmpList; - foreach (const QString &name, m_searchManager->enabledPlugins()) - tmpList << qMakePair(m_searchManager->pluginFullName(name), name); + foreach (const QString &name, SearchPluginManager::instance()->enabledPlugins()) + tmpList << qMakePair(SearchPluginManager::instance()->pluginFullName(name), name); std::sort(tmpList.begin(), tmpList.end(), [](const QStrPair &l, const QStrPair &r) { return (l.first < r.first); } ); foreach (const QStrPair &p, tmpList) @@ -208,7 +209,7 @@ QString SearchWidget::selectedPlugin() const void SearchWidget::selectActivePage() { - if (m_searchManager->allPlugins().isEmpty()) { + if (SearchPluginManager::instance()->allPlugins().isEmpty()) { m_ui->stackedPages->setCurrentWidget(m_ui->emptyPage); m_ui->m_searchPattern->setEnabled(false); m_ui->comboCategory->setEnabled(false); @@ -227,7 +228,7 @@ void SearchWidget::selectActivePage() SearchWidget::~SearchWidget() { qDebug("Search destruction"); - delete m_searchManager; + delete SearchPluginManager::instance(); delete m_ui; } @@ -262,7 +263,7 @@ void SearchWidget::selectMultipleBox(int index) void SearchWidget::on_pluginsButton_clicked() { - new PluginSelectDlg(m_searchManager, this); + new PluginSelectDlg(SearchPluginManager::instance(), this); } void SearchWidget::searchTextEdited(QString) @@ -303,15 +304,15 @@ void SearchWidget::on_searchButton_clicked() } QStringList plugins; - if (selectedPlugin() == "all") plugins = m_searchManager->allPlugins(); - else if (selectedPlugin() == "enabled") plugins = m_searchManager->enabledPlugins(); - else if (selectedPlugin() == "multi") plugins = m_searchManager->enabledPlugins(); + if (selectedPlugin() == "all") plugins = SearchPluginManager::instance()->allPlugins(); + else if (selectedPlugin() == "enabled") plugins = SearchPluginManager::instance()->enabledPlugins(); + else if (selectedPlugin() == "multi") plugins = SearchPluginManager::instance()->enabledPlugins(); else plugins << selectedPlugin(); qDebug("Search with category: %s", qUtf8Printable(selectedCategory())); // Launch search - auto *searchHandler = m_searchManager->startSearch(pattern, selectedCategory(), plugins); + auto *searchHandler = SearchPluginManager::instance()->startSearch(pattern, selectedCategory(), plugins); // Tab Addition auto *newTab = new SearchTab(searchHandler, this); diff --git a/src/gui/search/searchwidget.h b/src/gui/search/searchwidget.h index b4537fdae..319d9093d 100644 --- a/src/gui/search/searchwidget.h +++ b/src/gui/search/searchwidget.h @@ -37,7 +37,6 @@ class QSignalMapper; class QTabWidget; class MainWindow; -class SearchPluginManager; class SearchTab; namespace Ui @@ -80,7 +79,6 @@ private: QString selectedPlugin() const; Ui::SearchWidget *m_ui; - SearchPluginManager *m_searchManager; QSignalMapper *m_tabStatusChangedMapper; QPointer m_currentSearchTab; // Selected tab QPointer m_activeSearchTab; // Tab with running search