diff --git a/Changelog b/Changelog index e221b4b2c..b3fed745d 100644 --- a/Changelog +++ b/Changelog @@ -5,6 +5,7 @@ - FEATURE: Bittorrent FAST extension support - FEATURE: Added RSS support - FEATURE: Support files prioritizing in a torrent + - FEATURE: Brand new search engine plugins system - FEATURE: Finished torrents are now moved to another tab for seeding - FEATURE: Display more infos about the torrent in its properties - FEATURE: Allow the user to edit torrents' trackers diff --git a/TODO b/TODO index 6b9bf394a..544e9c2b1 100644 --- a/TODO +++ b/TODO @@ -38,7 +38,6 @@ - Allow to limit the number of downloading torrents simultaneously (other are paused until a download finishes) - Add "Mark all as read" feature for RSS - Allow to customize lists refreshing interval (in options) -- Use search engines as plugins (split them, load them dynamically) to allow the user to add some // in v1.0.0 (partial) - WIP - Check storage st creation + hasher in torrent creation @@ -80,6 +79,7 @@ beta5->beta6 changelog: - FEATURE: A lot of code optimization (CPU & memory usage) - FEATURE: Added support for .ico format (useful for RSS favicons) - FEATURE: Replaced Meganova search engine by TorrentReactor +- FEATURE: Brand new search engine plugins system - I18N: Updated Greek, Dutch and Romanian translation - I18N: Removed no longer maintained Traditional chinese translation - BUGFIX: Made torrent deletion from hard-drive safer diff --git a/src/engineSelect.ui b/src/engineSelect.ui new file mode 100644 index 000000000..efbd36668 --- /dev/null +++ b/src/engineSelect.ui @@ -0,0 +1,117 @@ + + engineSelect + + + + 0 + 0 + 527 + 254 + + + + Search plugins + + + + + + + 75 + true + true + + + + Installed search engines: + + + + + + + Qt::CustomContextMenu + + + QAbstractItemView::ExtendedSelection + + + true + + + false + + + + Name + + + + + Url + + + + + Enabled + + + + + + + + + true + + + + You can get new search engine plugins here: <a href="http:plugins.qbittorrent.org">http://plugins.qbittorrent.org</a> + + + + + + + + + Install a new one + + + + + + + Check for updates + + + + + + + Close + + + + + + + + + Enable + + + + + Disable + + + + + Uninstall + + + + + + diff --git a/src/engineSelectDlg.cpp b/src/engineSelectDlg.cpp new file mode 100644 index 000000000..b4e4b62e8 --- /dev/null +++ b/src/engineSelectDlg.cpp @@ -0,0 +1,443 @@ +/* + * Bittorrent Client using Qt4 and libtorrent. + * 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. + * + * Contact : chris@qbittorrent.org + */ + +#include "engineSelectDlg.h" +#include "downloadThread.h" +#include "misc.h" +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_MAGICK + #include + using namespace Magick; +#endif + +#define ENGINE_NAME 0 +#define ENGINE_URL 1 +#define ENGINE_STATE 2 + +engineSelectDlg::engineSelectDlg(QWidget *parent) : QDialog(parent) { + setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + pluginsTree->header()->resizeSection(0, 170); + pluginsTree->header()->resizeSection(1, 220); + actionEnable->setIcon(QIcon(QString::fromUtf8(":/Icons/button_ok.png"))); + actionDisable->setIcon(QIcon(QString::fromUtf8(":/Icons/button_cancel.png"))); + actionUninstall->setIcon(QIcon(QString::fromUtf8(":/Icons/skin/remove.png"))); + connect(actionEnable, SIGNAL(triggered()), this, SLOT(enableSelection())); + connect(actionDisable, SIGNAL(triggered()), this, SLOT(disableSelection())); + connect(pluginsTree, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(displayContextMenu(const QPoint&))); + downloader = new downloadThread(this); + connect(downloader, SIGNAL(downloadFinished(QString, QString)), this, SLOT(processDownloadedFile(QString, QString))); + connect(downloader, SIGNAL(downloadFailure(QString, QString)), this, SLOT(handleDownloadFailure(QString, QString))); + loadSettings(); + loadSupportedSearchEngines(); + connect(pluginsTree, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(toggleEngineState(QTreeWidgetItem*, int))); + show(); +} + +engineSelectDlg::~engineSelectDlg() { + qDebug("Destroying engineSelectDlg"); + saveSettings(); + emit enginesChanged(); + qDebug("Before deleting downloader"); + delete downloader; + qDebug("Engine plugins dialog destroyed"); +} + +void engineSelectDlg::loadSettings() { + QSettings settings(QString::fromUtf8("qBittorrent"), QString::fromUtf8("qBittorrent")); + known_engines = settings.value(QString::fromUtf8("SearchEngines/knownEngines"), QStringList()).toStringList(); + known_enginesEnabled = settings.value(QString::fromUtf8("SearchEngines/knownEnginesEnabled"), QList()).toList(); +} + +void engineSelectDlg::saveSettings() { + QSettings settings(QString::fromUtf8("qBittorrent"), QString::fromUtf8("qBittorrent")); + settings.setValue(QString::fromUtf8("SearchEngines/knownEngines"), installed_engines); + settings.setValue(QString::fromUtf8("SearchEngines/knownEnginesEnabled"), enginesEnabled); +} + +void engineSelectDlg::on_updateButton_clicked() { + // Download version file from primary server + downloader->downloadUrl("http://www.dchris.eu/search_engine/versions.txt"); +} + +void engineSelectDlg::toggleEngineState(QTreeWidgetItem *item, int) { + int index = pluginsTree->indexOfTopLevelItem(item); + Q_ASSERT(index != -1); + bool new_val = !enginesEnabled.at(index).toBool(); + enginesEnabled.replace(index, QVariant(new_val)); + QString enabledTxt; + if(new_val){ + enabledTxt = tr("True"); + setRowColor(index, "green"); + }else{ + enabledTxt = tr("False"); + setRowColor(index, "red"); + } + item->setText(ENGINE_STATE, enabledTxt); +} + +void engineSelectDlg::displayContextMenu(const QPoint& pos) { + QMenu myContextMenu(this); + QModelIndex index; + // Enable/disable pause/start action given the DL state + QList items = pluginsTree->selectedItems(); + bool has_enable = false, has_disable = false; + QTreeWidgetItem *item; + foreach(item, items) { + int index = pluginsTree->indexOfTopLevelItem(item); + Q_ASSERT(index != -1); + if(enginesEnabled.at(index).toBool() and !has_disable) { + myContextMenu.addAction(actionDisable); + has_disable = true; + } + if(!enginesEnabled.at(index).toBool() and !has_enable) { + myContextMenu.addAction(actionEnable); + has_enable = true; + } + if(has_enable && has_disable) break; + } + myContextMenu.addSeparator(); + myContextMenu.addAction(actionUninstall); + myContextMenu.exec(mapToGlobal(pos)+QPoint(12, 58)); +} + +void engineSelectDlg::on_closeButton_clicked() { + close(); +} + +void engineSelectDlg::on_actionUninstall_triggered() { + QList items = pluginsTree->selectedItems(); + QTreeWidgetItem *item; + bool change = false; + bool error = false; + foreach(item, items) { + int index = pluginsTree->indexOfTopLevelItem(item); + Q_ASSERT(index != -1); + QString name = installed_engines.at(index); + if(QFile::exists(":/search_engine/engines/"+name+".py")) { + error = true; + // Disable it instead + enginesEnabled.replace(index, QVariant(false)); + item->setText(ENGINE_STATE, tr("False")); + setRowColor(index, "red"); + continue; + }else { + // Proceed with uninstall + // remove it from hard drive + QFile::remove(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator()+name+".py"); + if(QFile::exists(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator()+name+".png")) { + QFile::remove(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator()+name+".png"); + } + // Remove it from lists + installed_engines.removeAt(index); + enginesEnabled.removeAt(index); + pluginsTree->takeTopLevelItem(index); + change = true; + } + } + if(change) + saveSettings(); + if(error) + QMessageBox::warning(0, tr("Uninstall warning"), tr("Some plugins could not be uninstalled because they are included in qBittorrent.\n Only the ones you added yourself can be uninstalled.\nHowever, those plugins were disabled.")); + else + QMessageBox::information(0, tr("Uninstall success"), tr("All selected plugins were uninstalled successfuly")); +} + +void engineSelectDlg::enableSelection() { + QList items = pluginsTree->selectedItems(); + QTreeWidgetItem *item; + foreach(item, items) { + int index = pluginsTree->indexOfTopLevelItem(item); + Q_ASSERT(index != -1); + enginesEnabled.replace(index, QVariant(true)); + item->setText(ENGINE_STATE, tr("True")); + setRowColor(index, "green"); + } +} + +void engineSelectDlg::disableSelection() { + QList items = pluginsTree->selectedItems(); + QTreeWidgetItem *item; + foreach(item, items) { + int index = pluginsTree->indexOfTopLevelItem(item); + Q_ASSERT(index != -1); + enginesEnabled.replace(index, QVariant(false)); + item->setText(ENGINE_STATE, tr("False")); + setRowColor(index, "red"); + } +} + +// Set the color of a row in data model +void engineSelectDlg::setRowColor(int row, QString color){ + QTreeWidgetItem *item = pluginsTree->topLevelItem(row); + for(int i=0; icolumnCount(); ++i){ + item->setData(i, Qt::ForegroundRole, QVariant(QColor(color))); + } +} + +void engineSelectDlg::loadSupportedSearchEngines() { + // Some clean up first + pluginsTree->clear(); + installed_engines.clear(); + enginesEnabled.clear(); + QStringList params; + // Ask nova core for the supported search engines + QProcess nova; + params << "--supported_engines"; + nova.start(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"nova2.py", params, QIODevice::ReadOnly); + nova.waitForStarted(); + nova.waitForFinished(); + QByteArray result = nova.readAll(); + result = result.replace("\n", ""); + qDebug("read: %s", result.data()); + QByteArray e; + foreach(e, result.split(',')) { + QString en = QString(e); + installed_engines << en; + int index = known_engines.indexOf(en); + if(index == -1) + enginesEnabled << true; + else + enginesEnabled << known_enginesEnabled.at(index).toBool(); + } + params.clear(); + params << "--supported_engines_infos"; + nova.start(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"nova2.py", params, QIODevice::ReadOnly); + nova.waitForStarted(); + nova.waitForFinished(); + result = nova.readAll(); + result = result.replace("\n", ""); + qDebug("read: %s", result.data()); + unsigned int i = 0; + foreach(e, result.split(',')) { + QString nameUrlCouple(e); + QStringList line = nameUrlCouple.split('|'); + if(line.size() != 2) continue; + // Download favicon + QString enabledTxt; + if(enginesEnabled.at(i).toBool()){ + enabledTxt = tr("True"); + }else{ + enabledTxt = tr("False"); + } + line << enabledTxt; + QTreeWidgetItem *item = new QTreeWidgetItem(pluginsTree, line); + QString iconPath = misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator()+installed_engines.at(i)+".png"; + if(QFile::exists(iconPath)) { + // Good, we already have the icon + item->setData(ENGINE_NAME, Qt::DecorationRole, QVariant(QIcon(iconPath))); + } else { + // Icon is missing, we must download it + downloader->downloadUrl(line.at(1)+"/favicon.ico"); + } + if(enginesEnabled.at(i).toBool()) + setRowColor(i, "green"); + else + setRowColor(i, "red"); + ++i; + } +} + +QList engineSelectDlg::findItemsWithUrl(QString url){ + QList res; + for(int i=0; itopLevelItemCount(); ++i) { + QTreeWidgetItem *item = pluginsTree->topLevelItem(i); + if(url.startsWith(item->text(ENGINE_URL))) + res << item; + } + return res; +} + +bool engineSelectDlg::isUpdateNeeded(QString plugin_name, float new_version) { + float old_version = misc::getPluginVersion(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator()+plugin_name+".py"); + return (new_version > old_version); +} + +void engineSelectDlg::on_installButton_clicked() { + QStringList pathsList = QFileDialog::getOpenFileNames(0, + tr("Select search plugins"), QDir::homePath(), + tr("qBittorrent search plugins")+QString::fromUtf8(" (*.py)")); + QString path; + foreach(path, pathsList) { + if(!path.endsWith(".py")) continue; + float new_version = misc::getPluginVersion(path); + QString plugin_name = path.split(QDir::separator()).last(); + plugin_name.replace(".py", ""); + if(!isUpdateNeeded(plugin_name, new_version)) { + QMessageBox::information(this, tr("Search plugin install")+" -- "+tr("qBittorrent"), tr("A more recent version of %1 search engine plugin is already installed.", "%1 is the name of the search engine").arg(plugin_name.toUtf8().data())); + continue; + } + // Process with install + QString dest_path = misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator()+plugin_name+".py"; + bool update = false; + if(QFile::exists(dest_path)) { + QFile::remove(dest_path); + update = true; + } + // Copy the plugin + QFile::copy(path, dest_path); + // Refresh plugin list + loadSupportedSearchEngines(); + // TODO: do some more checking to be sure it was installed successfuly? + if(update) { + QMessageBox::information(this, tr("Search plugin install")+" -- "+tr("qBittorrent"), tr("%1 search engine plugin was successfuly updated.", "%1 is the name of the search engine").arg(plugin_name.toUtf8().data())); + continue; + } else { + QMessageBox::information(this, tr("Search plugin install")+" -- "+tr("qBittorrent"), tr("%1 search engine plugin was successfuly installed.", "%1 is the name of the search engine").arg(plugin_name.toUtf8().data())); + continue; + } + } +} + +bool engineSelectDlg::parseVersionsFile(QString versions_file, QString updateServer) { + qDebug("Checking if update is needed"); + bool file_correct = false; + QFile versions(versions_file); + if(!versions.open(QIODevice::ReadOnly | QIODevice::Text)){ + qDebug("* Error: Could not read versions.txt file"); + return false; + } + while(!versions.atEnd()) { + QByteArray line = versions.readLine(); + line.replace("\n", ""); + line = line.trimmed(); + if(line.isEmpty()) continue; + if(line.startsWith("#")) continue; + QList list = line.split(' '); + if(list.size() != 2) continue; + QString plugin_name = QString(list.first()); + if(!plugin_name.endsWith(":")) continue; + plugin_name.chop(1); // remove trailing ':' + bool ok; + float version = list.last().toFloat(&ok); + qDebug("read line %s: %.2f", plugin_name.toUtf8().data(), version); + if(!ok) continue; + file_correct = true; + if(isUpdateNeeded(plugin_name, version)) { + qDebug("Plugin: %s is outdated", plugin_name.toUtf8().data()); + // Downloading update + downloader->downloadUrl(updateServer+plugin_name+".zip"); // Actually this is really a .py + downloader->downloadUrl(updateServer+plugin_name+".png"); + } + } + // Close file + versions.close(); + // Clean up tmp file + QFile::remove(versions_file); + return file_correct; +} + +void engineSelectDlg::processDownloadedFile(QString url, QString filePath) { + if(url.endsWith("favicon.ico")){ + // Icon downloaded + QImage fileIcon; +#ifdef HAVE_MAGICK + try{ + QFile::copy(filePath, filePath+".ico"); + Image image(QDir::cleanPath(filePath+".ico").toUtf8().data()); + // Convert to PNG since we can't read ICO format + image.magick("PNG"); + // Resize to 16x16px + image.sample(Geometry(16, 16)); + image.write(filePath.toUtf8().data()); + QFile::remove(filePath+".ico"); + }catch(Magick::Exception &error_){ + qDebug("favicon conversion to PNG failure: %s", error_.what()); + } +#endif + if(fileIcon.load(filePath)) { + QList items = findItemsWithUrl(url); + QTreeWidgetItem *item; + foreach(item, items){ + int index = pluginsTree->indexOfTopLevelItem(item); + Q_ASSERT(index != -1); + QString iconPath = misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator()+installed_engines.at(index)+".png"; + QFile::copy(filePath, iconPath); + item->setData(ENGINE_NAME, Qt::DecorationRole, QVariant(QIcon(iconPath))); + } + } + // Delete tmp file + QFile::remove(filePath); + return; + } + if(url == "http://www.dchris.eu/search_engine/versions.txt") { + if(!parseVersionsFile(filePath, "http://www.dchris.eu/search_engine/")) { + downloader->downloadUrl("http://hydr0g3n.free.fr/search_engine/versions.txt"); + return; + } + } + if(url == "http://hydr0g3n.free.fr/search_engine/versions.txt") { + if(!parseVersionsFile(filePath, "http://hydr0g3n.free.fr/search_engine/")) { + QMessageBox::information(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), tr("Sorry, update server is temporarily unavailable.")); + return; + } + } + if(url.endsWith(".zip")) { + // a plugin update has been downloaded + QString plugin_name = url.split('/').last(); + plugin_name.replace(".zip", ""); + QString dest_path = misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator()+plugin_name+".py"; + bool new_plugin = false; + if(QFile::exists(dest_path)) { + // Delete the old plugin + QFile::remove(dest_path); + } else { + // This is a new plugin + new_plugin = true; + } + // Copy the new plugin + QFile::copy(filePath, dest_path); + if(new_plugin) { + // if it is new, refresh the list of plugins + loadSupportedSearchEngines(); + } + QMessageBox::information(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), tr("%1 search plugin was successfuly updated.", "%1 is the name of the search engine").arg(plugin_name.toUtf8().data())); + } +} + +void engineSelectDlg::handleDownloadFailure(QString url, QString reason) { + if(url.endsWith("favicon.ico")){ + qDebug("Could not download favicon: %s, reason: %s", url.toUtf8().data(), reason.toUtf8().data()); + return; + } + if(url == "http://www.dchris.eu/search_engine/versions.txt") { + // Primary update server failed, try secondary + qDebug("Primary update server failed, try secondary"); + downloader->downloadUrl("http://hydr0g3n.free.fr/search_engine/versions.txt"); + return; + } + if(url == "http://hydr0g3n.free.fr/search_engine/versions.txt") { + QMessageBox::warning(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), tr("Sorry, update server is temporarily unavailable.")); + return; + } + if(url.endsWith(".zip")) { + // a plugin update download has been failed + QString plugin_name = url.split('/').last(); + plugin_name.replace(".zip", ""); + QMessageBox::warning(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), tr("Sorry, %1 search plugin update failed.", "%1 is the name of the search engine").arg(plugin_name.toUtf8().data())); + } +} diff --git a/src/engineSelectDlg.h b/src/engineSelectDlg.h new file mode 100644 index 000000000..1ce8f5da8 --- /dev/null +++ b/src/engineSelectDlg.h @@ -0,0 +1,69 @@ +/* + * Bittorrent Client using Qt4 and libtorrent. + * 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. + * + * Contact : chris@qbittorrent.org + */ + +#ifndef ENGINE_SELECT_DLG_H +#define ENGINE_SELECT_DLG_H + +#include "ui_engineSelect.h" + +class downloadThread; + +class engineSelectDlg : public QDialog, public Ui::engineSelect{ + Q_OBJECT + + private: + // Search related + QStringList installed_engines; + QVariantList enginesEnabled; + QStringList known_engines; + QVariantList known_enginesEnabled; + downloadThread *downloader; + + public: + engineSelectDlg(QWidget *parent); + ~engineSelectDlg(); + QList findItemsWithUrl(QString url); + + protected: + bool parseVersionsFile(QString versions_file, QString updateServer); + bool isUpdateNeeded(QString plugin_name, float new_version); + + signals: + void enginesChanged(); + + protected slots: + void loadSettings(); + void saveSettings(); + void on_closeButton_clicked(); + void loadSupportedSearchEngines(); + void toggleEngineState(QTreeWidgetItem*, int); + void setRowColor(int row, QString color); + void processDownloadedFile(QString url, QString filePath); + void handleDownloadFailure(QString url, QString reason); + void displayContextMenu(const QPoint& pos); + void enableSelection(); + void disableSelection(); + void on_actionUninstall_triggered(); + void on_updateButton_clicked(); + void on_installButton_clicked(); +}; + +#endif diff --git a/src/misc.h b/src/misc.h index dac2a0fb7..27704e9eb 100644 --- a/src/misc.h +++ b/src/misc.h @@ -328,6 +328,28 @@ class misc : public QObject{ list.insert(i, value); } + static float getPluginVersion(QString filePath) { + QFile plugin(filePath); + if(!plugin.exists()){ + return 0.0; + } + if(!plugin.open(QIODevice::ReadOnly | QIODevice::Text)){ + return 0.0; + } + float version = 0.0; + while (!plugin.atEnd()){ + QByteArray line = plugin.readLine(); + if(line.startsWith("#VERSION: ")){ + line = line.split(' ').last(); + line.replace("\n", ""); + version = line.toFloat(); + qDebug("plugin version: %.2f", version); + break; + } + } + return version; + } + // Take a number of seconds and return an user-friendly // time duration like "1d 2h 10m". static QString userFriendlyDuration(qlonglong seconds) { diff --git a/src/search.qrc b/src/search.qrc index 7599c0b99..060d078d3 100644 --- a/src/search.qrc +++ b/src/search.qrc @@ -1,5 +1,16 @@ - search_engine/nova.py + search_engine/nova2.py + search_engine/novaprinter.py + search_engine/engines/isohunt.py + search_engine/engines/btjunkie.py + search_engine/engines/torrentreactor.py + search_engine/engines/mininova.py + search_engine/engines/piratebay.py + search_engine/engines/torrentreactor.png + search_engine/engines/mininova.png + search_engine/engines/piratebay.png + search_engine/engines/btjunkie.png + search_engine/engines/isohunt.png \ No newline at end of file diff --git a/src/search.ui b/src/search.ui index c83dbc0df..292e9d706 100644 --- a/src/search.ui +++ b/src/search.ui @@ -13,256 +13,200 @@ Search - - 9 - - - 6 - - - 0 - 6 + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16777215 + 35 + + + + + Sans Serif + 9 + 75 + false + true + false + false + + + + Search Pattern: + + + + + + + + 16777215 + 22 + + + + + + + + + 16777215 + 29 + + + + Search + + + + + + + false + + + + 16777215 + 29 + + + + Stop + + + + + + + Search engines... + + + + + + + + + + + + 16777215 + 35 + + + + + Sans Serif + 9 + 75 + false + true + false + false + + + + Status: + + + - + - 131 - 132 + 400 + 0 - 125 - 132 + 16777215 + 35 - - Search Engines - - - - 9 - - - 6 - - - - - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - + + + Sans Serif + 9 + 50 + true + false + false + false + + + + Stopped + - - - 0 + + + Qt::Horizontal - - 6 + + + 188 + 21 + - - - - 0 - - - 6 - - - - - - 16777215 - 35 - - - - - Sans Serif - 9 - 75 - false - true - false - false - - - - Search Pattern: - - - - - - - - 16777215 - 22 - - - - - - - - - 16777215 - 29 - - - - Search - - - - - - - false - - - - 16777215 - 29 - - - - Stop - - - - - - - - - 0 - - - 6 - - - - - - 16777215 - 35 - - - - - Sans Serif - 9 - 75 - false - true - false - false - - - - Status: - - - - - - - - 400 - 0 - - - - - 16777215 - 35 - - - - - Sans Serif - 9 - 50 - true - false - false - false - - - - Stopped - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - + - - 0 - 6 + + 0 + + + 0 + + + 0 + + + 0 + - - 0 - 6 + + 0 + + + 0 + + + 0 + + + 0 + @@ -329,12 +273,21 @@ - - 0 - 6 + + 0 + + + 0 + + + 0 + + + 0 + @@ -368,13 +321,6 @@ - - - - Update search plugin - - - diff --git a/src/searchEngine.cpp b/src/searchEngine.cpp index 189dc38af..14c35acda 100644 --- a/src/searchEngine.cpp +++ b/src/searchEngine.cpp @@ -45,8 +45,6 @@ SearchEngine::SearchEngine(bittorrent *BTSession, QSystemTrayIcon *myTrayIcon, bool systrayIntegration) : QWidget(), BTSession(BTSession), myTrayIcon(myTrayIcon), systrayIntegration(systrayIntegration){ setupUi(this); downloader = new downloadThread(this); - connect(downloader, SIGNAL(downloadFinished(QString, QString)), this, SLOT(novaUpdateDownloaded(QString, QString))); - connect(downloader, SIGNAL(downloadFailure(QString, QString)), this, SLOT(handleNovaDownloadFailure(QString, QString))); // Set Search results list model SearchListModel = new QStandardItemModel(0,5); SearchListModel->setHeaderData(SEARCH_NAME, Qt::Horizontal, tr("Name", "i.e: file name")); @@ -81,21 +79,8 @@ SearchEngine::SearchEngine(bittorrent *BTSession, QSystemTrayIcon *myTrayIcon, b connect(searchProcess, SIGNAL(started()), this, SLOT(searchStarted())); connect(searchProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(readSearchOutput())); connect(searchProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(searchFinished(int,QProcess::ExitStatus))); - // Set search engines names - mininova->setText("Mininova"); - piratebay->setText("ThePirateBay"); -// reactor->setText("TorrentReactor"); - isohunt->setText("Isohunt"); -// btjunkie->setText("BTJunkie"); - reactor->setText("TorrentReactor"); - // Check last checked search engines - loadCheckedSearchEngines(); - connect(mininova, SIGNAL(stateChanged(int)), this, SLOT(saveCheckedSearchEngines(int))); - connect(piratebay, SIGNAL(stateChanged(int)), this, SLOT(saveCheckedSearchEngines(int))); -// connect(reactor, SIGNAL(stateChanged(int)), this, SLOT(saveCheckedSearchEngines(int))); - connect(isohunt, SIGNAL(stateChanged(int)), this, SLOT(saveCheckedSearchEngines(int))); -// connect(btjunkie, SIGNAL(stateChanged(int)), this, SLOT(saveCheckedSearchEngines(int))); - connect(reactor, SIGNAL(stateChanged(int)), this, SLOT(saveCheckedSearchEngines(int))); + // Check last enabled search engines + loadEngineSettings(); // Update nova.py search plugin if necessary updateNova(); } @@ -183,18 +168,6 @@ void SearchEngine::sortSearchListString(int index, Qt::SortOrder sortOrder){ SearchListModel->removeRows(0, nbRows_old); } -// Save last checked search engines to a file -void SearchEngine::saveCheckedSearchEngines(int) const{ - QSettings settings("qBittorrent", "qBittorrent"); - settings.beginGroup("SearchEngines"); - settings.setValue("mininova", mininova->isChecked()); - settings.setValue("piratebay", piratebay->isChecked()); - settings.setValue("isohunt", isohunt->isChecked()); - settings.setValue("reactor", reactor->isChecked()); - settings.endGroup(); - qDebug("Saved checked search engines"); -} - // Save columns width in a file to remember them // (download list) void SearchEngine::saveColWidthSearchList() const{ @@ -208,6 +181,11 @@ void SearchEngine::saveColWidthSearchList() const{ qDebug("Search list columns width saved"); } +void SearchEngine::on_enginesButton_clicked() { + engineSelectDlg *dlg = new engineSelectDlg(this); + connect(dlg, SIGNAL(enginesChanged()), this, SLOT(loadEngineSettings())); +} + // Load columns width in a file that were saved previously // (search list) bool SearchEngine::loadColWidthSearchList(){ @@ -226,19 +204,6 @@ bool SearchEngine::loadColWidthSearchList(){ return true; } -// load last checked search engines from a file -void SearchEngine::loadCheckedSearchEngines(){ - qDebug("Loading checked search engines"); - QSettings settings("qBittorrent", "qBittorrent"); - settings.beginGroup("SearchEngines"); - mininova->setChecked(settings.value("mininova", true).toBool()); - piratebay->setChecked(settings.value("piratebay", false).toBool()); - isohunt->setChecked(settings.value("isohunt", false).toBool()); - reactor->setChecked(settings.value("reactor", false).toBool()); - settings.endGroup(); - qDebug("Loaded checked search engines"); -} - // get the last searchs from a QSettings to a QStringList void SearchEngine::startSearchHistory(){ QSettings settings("qBittorrent", "qBittorrent"); @@ -247,6 +212,24 @@ void SearchEngine::startSearchHistory(){ settings.endGroup(); } +void SearchEngine::loadEngineSettings() { + qDebug("Loading engine settings"); + enabled_engines.clear(); + QSettings settings(QString::fromUtf8("qBittorrent"), QString::fromUtf8("qBittorrent")); + QStringList known_engines = settings.value(QString::fromUtf8("SearchEngines/knownEngines"), QStringList()).toStringList(); + QVariantList known_enginesEnabled = settings.value(QString::fromUtf8("SearchEngines/knownEnginesEnabled"), QList()).toList(); + QString engine; + unsigned int i = 0; + foreach(engine, known_engines) { + if(known_enginesEnabled.at(i).toBool()) + enabled_engines << engine; + ++i; + } + if(enabled_engines.empty()) + enabled_engines << "all"; + qDebug("Engine settings loaded"); +} + // Save the history list into the QSettings for the next session void SearchEngine::saveSearchHistory() { @@ -282,33 +265,12 @@ void SearchEngine::on_search_button_clicked(){ // Getting checked search engines - if(!mininova->isChecked() && ! piratebay->isChecked() && !reactor->isChecked() && !isohunt->isChecked()/* && !btjunkie->isChecked()*/ /*&& !meganova->isChecked()*/){ - QMessageBox::critical(0, tr("No search engine selected"), tr("You must select at least one search engine.")); - return; - } + Q_ASSERT(!enabled_engines.empty()); QStringList params; QStringList engineNames; search_stopped = false; - // Get checked search engines - if(mininova->isChecked()){ - engineNames << "mininova"; - } - if(piratebay->isChecked()){ - engineNames << "piratebay"; - } -// if(reactor->isChecked()){ -// engineNames << "reactor"; -// } - if(isohunt->isChecked()){ - engineNames << "isohunt"; - } -// if(btjunkie->isChecked()){ -// engineNames << "btjunkie"; -// } - if(reactor->isChecked()){ - engineNames << "reactor"; - } - params << engineNames.join(","); + + params << enabled_engines.join(","); params << pattern.split(" "); // Update SearchEngine widgets no_search_results = true; @@ -316,7 +278,7 @@ void SearchEngine::on_search_button_clicked(){ search_result_line_truncated.clear(); results_lbl->setText(tr("Results")+" (0):"); // Launch search - searchProcess->start(misc::qBittorrentPath()+"nova.py", params, QIODevice::ReadOnly); + searchProcess->start(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"nova2.py", params, QIODevice::ReadOnly); } void SearchEngine::searchStarted(){ @@ -360,137 +322,105 @@ void SearchEngine::readSearchOutput(){ results_lbl->setText(tr("Results")+QString::fromUtf8(" (")+misc::toQString(nb_search_results)+QString::fromUtf8("):")); } -// Returns version of nova.py search engine -float SearchEngine::getNovaVersion(QString novaPath) const{ - QFile dest_nova(novaPath); - if(!dest_nova.exists()){ - return 0.0; - } - if(!dest_nova.open(QIODevice::ReadOnly | QIODevice::Text)){ - return 0.0; - } - float version = 0.0; - while (!dest_nova.atEnd()){ - QByteArray line = dest_nova.readLine(); - if(line.startsWith("# Version: ")){ - line = line.split(' ').last(); - line.chop(1); // removes '\n' - version = line.toFloat(); - qDebug("Search plugin version: %.2f", version); - break; - } - } - return version; -} - -// Returns changelog of nova.py search engine -QByteArray SearchEngine::getNovaChangelog(QString novaPath, float my_version) const{ - QFile dest_nova(novaPath); - if(!dest_nova.exists()){ - return QByteArray("None"); - } - if(!dest_nova.open(QIODevice::ReadOnly | QIODevice::Text)){ - return QByteArray("None"); - } - QByteArray changelog; - bool in_changelog = false; - while (!dest_nova.atEnd()){ - QByteArray line = dest_nova.readLine(); - line = line.trimmed(); - if(line.startsWith("# Changelog:")){ - in_changelog = true; - }else{ - if(line.isEmpty()){ - in_changelog = false; - } - if(line.startsWith("# End Changelog")) break; - QString end_version = "# Version: "; - char tmp[5]; - snprintf(tmp, 5, "%.2f", my_version); - end_version+=QString::fromUtf8(tmp); - if(line.startsWith((const char*)end_version.toUtf8())) break; - if(in_changelog){ - line.remove(0,1); - line += "\n"; - changelog.append(line); - } - } - } - return changelog; -} - // Update nova.py search plugin if necessary -void SearchEngine::updateNova() const{ +void SearchEngine::updateNova() { qDebug("Updating nova"); - float provided_nova_version = getNovaVersion(":/search_engine/nova.py"); + // create search_engine directory if necessary + QDir search_dir(misc::qBittorrentPath()+"search_engine"); + if(!search_dir.exists()){ + search_dir.mkdir(misc::qBittorrentPath()+"search_engine"); + } + QFile package_file(search_dir.path()+QDir::separator()+"__init__.py"); + package_file.open(QIODevice::WriteOnly | QIODevice::Text); + package_file.close(); + if(!search_dir.exists("engines")){ + search_dir.mkdir("engines"); + } + QFile package_file2(search_dir.path()+QDir::separator()+"engines"+QDir::separator()+"__init__.py"); + package_file2.open(QIODevice::WriteOnly | QIODevice::Text); + package_file2.close(); + // Copy search plugin files (if necessary) + QString filePath = misc::qBittorrentPath()+"search_engine"+QDir::separator()+"nova2.py"; + if(misc::getPluginVersion(":/search_engine/nova2.py") > misc::getPluginVersion(filePath)) { + if(QFile::exists(filePath)) + QFile::remove(filePath); + QFile::copy(":/search_engine/nova2.py", misc::qBittorrentPath()+"search_engine"+QDir::separator()+"nova2.py"); + } + // Set permissions QFile::Permissions perm=QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadUser | QFile::WriteUser | QFile::ExeUser | QFile::ReadGroup | QFile::ReadGroup; - QFile(misc::qBittorrentPath()+"nova.py").setPermissions(perm); - if(provided_nova_version > getNovaVersion(misc::qBittorrentPath()+"nova.py")){ - qDebug("updating local search plugin with shipped one"); - // nova.py needs update - QFile::remove(misc::qBittorrentPath()+"nova.py"); - qDebug("Old nova removed"); - QFile::copy(":/search_engine/nova.py", misc::qBittorrentPath()+"nova.py"); - qDebug("New nova copied"); - QFile::Permissions perm=QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadUser | QFile::WriteUser | QFile::ExeUser | QFile::ReadGroup | QFile::ReadGroup; - QFile(misc::qBittorrentPath()+"nova.py").setPermissions(perm); - qDebug("local search plugin updated"); - } -} - -void SearchEngine::novaUpdateDownloaded(QString url, QString filePath){ - float version_on_server = getNovaVersion(filePath); - qDebug("Version on qbittorrent.org: %.2f", version_on_server); - float my_version = getNovaVersion(misc::qBittorrentPath()+"nova.py"); - if(version_on_server > my_version){ - if(QMessageBox::question(this, - tr("Search plugin update -- qBittorrent"), - tr("Search plugin can be updated, do you want to update it?\n\nChangelog:\n")+getNovaChangelog(filePath, my_version), - tr("&Yes"), tr("&No"), - QString(), 0, 1)){ - return; - }else{ - qDebug("Updating search plugin from qbittorrent.org"); - QFile::remove(misc::qBittorrentPath()+"nova.py"); - QFile::copy(filePath, misc::qBittorrentPath()+"nova.py"); - QFile::Permissions perm=QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadUser | QFile::WriteUser | QFile::ExeUser | QFile::ReadGroup | QFile::ReadGroup; - QFile(misc::qBittorrentPath()+"nova.py").setPermissions(perm); - } - }else{ - if(version_on_server == 0.0){ - if(url == "http://www.dchris.eu/nova/nova.zip"){ - qDebug("*Warning: Search plugin update download from primary server failed, trying secondary server..."); - downloader->downloadUrl("http://hydr0g3n.free.fr/nova/nova.py"); - }else{ - QMessageBox::information(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), - tr("Sorry, update server is temporarily unavailable.")); + QFile(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"nova2.py").setPermissions(perm); + if(!QFile::exists(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"novaprinter.py")){ + QFile::copy(":/search_engine/novaprinter.py", misc::qBittorrentPath()+"search_engine"+QDir::separator()+"novaprinter.py"); + } + QString subDir = misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator(); + QDir search_subDir(":/search_engine/engines"); + QStringList files = search_subDir.entryList(); + QString file; + foreach(file, files){ + filePath = search_subDir.path()+QDir::separator()+file; + // Copy python classes + if(file.endsWith(".py")) { + if(misc::getPluginVersion(filePath) > misc::getPluginVersion(subDir+file) ) { + if(QFile::exists(filePath)) + QFile::remove(filePath); + QFile::copy(filePath, subDir+file); + } + } else { + // Copy icons + if(file.endsWith(".png")) { + if(!QFile::exists(subDir+file)) { + QFile::copy(filePath, subDir+file); + } } - }else{ - QMessageBox::information(this, tr("Search plugin update -- qBittorrent"), - tr("Your search plugin is already up to date.")); } } - // Delete tmp file - QFile::remove(filePath); -} - -void SearchEngine::handleNovaDownloadFailure(QString url, QString reason){ - if(url == "http://www.dchris.eu/nova/nova.zip"){ - qDebug("*Warning: Search plugin update download from primary server failed, trying secondary server..."); - downloader->downloadUrl("http://hydr0g3n.free.fr/nova/nova.py"); - }else{ - // Display a message box - QMessageBox::critical(0, tr("Search plugin download error"), tr("Couldn't download search plugin update at url: %1, reason: %2.").arg(url).arg(reason)); - } } -// Download nova.py from qbittorrent.org -// Check if our nova.py is outdated and -// ask user for action. -void SearchEngine::on_update_nova_button_clicked(){ - qDebug("Checking for search plugin updates on qbittorrent.org"); - downloader->downloadUrl("http://www.dchris.eu/nova/nova.zip"); -} +// void SearchEngine::novaUpdateDownloaded(QString url, QString filePath){ +// float version_on_server = getNovaVersion(filePath); +// qDebug("Version on qbittorrent.org: %.2f", version_on_server); +// float my_version = getNovaVersion(misc::qBittorrentPath()+"nova.py"); +// if(version_on_server > my_version){ +// if(QMessageBox::question(this, +// tr("Search plugin update -- qBittorrent"), +// tr("Search plugin can be updated, do you want to update it?\n\nChangelog:\n")+getNovaChangelog(filePath, my_version), +// tr("&Yes"), tr("&No"), +// QString(), 0, 1)){ +// return; +// }else{ +// qDebug("Updating search plugin from qbittorrent.org"); +// QFile::remove(misc::qBittorrentPath()+"nova.py"); +// QFile::copy(filePath, misc::qBittorrentPath()+"nova.py"); +// QFile::Permissions perm=QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadUser | QFile::WriteUser | QFile::ExeUser | QFile::ReadGroup | QFile::ReadGroup; +// QFile(misc::qBittorrentPath()+"nova.py").setPermissions(perm); +// } +// }else{ +// if(version_on_server == 0.0){ +// if(url == "http://www.dchris.eu/nova/nova.zip"){ +// qDebug("*Warning: Search plugin update download from primary server failed, trying secondary server..."); +// downloader->downloadUrl("http://hydr0g3n.free.fr/nova/nova.py"); +// }else{ +// QMessageBox::information(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), +// tr("Sorry, update server is temporarily unavailable.")); +// } +// }else{ +// QMessageBox::information(this, tr("Search plugin update -- qBittorrent"), +// tr("Your search plugin is already up to date.")); +// } +// } +// // Delete tmp file +// QFile::remove(filePath); +// } +// +// void SearchEngine::handleNovaDownloadFailure(QString url, QString reason){ +// if(url == "http://www.dchris.eu/nova/nova.zip"){ +// qDebug("*Warning: Search plugin update download from primary server failed, trying secondary server..."); +// downloader->downloadUrl("http://hydr0g3n.free.fr/nova/nova.py"); +// }else{ +// // Display a message box +// QMessageBox::critical(0, tr("Search plugin download error"), tr("Couldn't download search plugin update at url: %1, reason: %2.").arg(url).arg(reason)); +// } +// } // Slot called when search is Finished // Search can be finished for 3 reasons : diff --git a/src/searchEngine.h b/src/searchEngine.h index 447b3a544..27ff97465 100644 --- a/src/searchEngine.h +++ b/src/searchEngine.h @@ -26,6 +26,7 @@ #include #include "ui_search.h" +#include "engineSelectDlg.h" class QStandardItemModel; class SearchListDelegate; @@ -52,21 +53,20 @@ class SearchEngine : public QWidget, public Ui::search_engine{ QSystemTrayIcon *myTrayIcon; bool systrayIntegration; downloadThread *downloader; + QStringList enabled_engines; public: SearchEngine(bittorrent *BTSession, QSystemTrayIcon *myTrayIcon, bool systrayIntegration); ~SearchEngine(); - float getNovaVersion(QString novaPath) const; - QByteArray getNovaChangelog(QString novaPath, float my_version) const; + float getPluginVersion(QString filePath) const; bool loadColWidthSearchList(); - public slots: + protected slots: // Search slots void on_search_button_clicked(); void on_stop_search_button_clicked(); void on_clear_button_clicked(); void on_download_button_clicked(); - void on_update_nova_button_clicked(); void appendSearchResult(QString line); void searchFinished(int exitcode,QProcess::ExitStatus); void readSearchOutput(); @@ -74,16 +74,14 @@ class SearchEngine : public QWidget, public Ui::search_engine{ void searchStarted(); void downloadSelectedItem(const QModelIndex& index); void startSearchHistory(); - void loadCheckedSearchEngines(); - void updateNova() const; + void updateNova(); void saveSearchHistory(); void saveColWidthSearchList() const; - void saveCheckedSearchEngines(int) const; void sortSearchList(int index); void sortSearchListInt(int index, Qt::SortOrder sortOrder); void sortSearchListString(int index, Qt::SortOrder sortOrder); - void novaUpdateDownloaded(QString url, QString path); - void handleNovaDownloadFailure(QString url, QString reason); + void on_enginesButton_clicked(); + void loadEngineSettings(); }; #endif diff --git a/src/search_engine/__init__.py b/src/search_engine/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/search_engine/engines/__init__.py b/src/search_engine/engines/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/search_engine/engines/btjunkie.png b/src/search_engine/engines/btjunkie.png new file mode 100644 index 000000000..a31617580 Binary files /dev/null and b/src/search_engine/engines/btjunkie.png differ diff --git a/src/search_engine/engines/btjunkie.py b/src/search_engine/engines/btjunkie.py new file mode 100644 index 000000000..ac84674a8 --- /dev/null +++ b/src/search_engine/engines/btjunkie.py @@ -0,0 +1,29 @@ +#VERSION: 1.00 +#AUTHORS: Fabien Devaux (fab@gnux.info) +from novaprinter import prettyPrinter +import urllib +import re + +# TODO: add multipage +class btjunkie(object): + url = 'http://btjunkie.org' + name = 'btjunkie' + + def search(self, what): + dat = urllib.urlopen(self.url+'/search?q=%s'%what).read().decode('utf8', 'replace') + # I know it's not very readable, but the SGML parser feels in pain + section_re = re.compile('(?s)href="/torrent\?do=download.*?') + torrent_re = re.compile('(?s)href="(?P.*?do=download[^"]+).*?' + 'class="BlckUnd">(?P.*?).*?' + '>(?P\d+MB).*?' + '>(?P\d+).*?' + '>(?P\d+)') + for match in section_re.finditer(dat): + txt = match.group(0) + m = torrent_re.search(txt) + if m: + torrent_infos = m.groupdict() + torrent_infos['name'] = re.sub('', '', torrent_infos['name']) + torrent_infos['engine_url'] = self.url + torrent_infos['link'] = self.url+torrent_infos['link'] + prettyPrinter(torrent_infos) \ No newline at end of file diff --git a/src/search_engine/engines/isohunt.png b/src/search_engine/engines/isohunt.png new file mode 100644 index 000000000..e71fb1ca2 Binary files /dev/null and b/src/search_engine/engines/isohunt.png differ diff --git a/src/search_engine/engines/isohunt.py b/src/search_engine/engines/isohunt.py new file mode 100644 index 000000000..12f1b2619 --- /dev/null +++ b/src/search_engine/engines/isohunt.py @@ -0,0 +1,78 @@ +#VERSION: 1.00 +#AUTHORS: Gekko Dam Beer (gekko04@users.sourceforge.net) +from novaprinter import prettyPrinter +import sgmllib +import urllib + +class isohunt(object): + url = 'http://isohunt.com' + name = 'isoHunt' + + class SimpleSGMLParser(sgmllib.SGMLParser): + def __init__(self, results, url, *args): + sgmllib.SGMLParser.__init__(self) + self.td_counter = None + self.current_item = None + self.results = results + self.url = url + + def start_tr(self, attr): + params = dict(attr) + if 'onclick' in params: + Durl='http://isohunt.com/download' + self.current_item = {} + self.td_counter = 0 + try: + self.current_item['link'] = '%s/%s'%(Durl, params['onclick'].split('/')[2]) + except IndexError: + self.current_item['link'] = None + + def handle_data(self, data): + if self.td_counter == 3: + if not self.current_item.has_key('name'): + self.current_item['name'] = '' + self.current_item['name']+= data.strip() + if self.td_counter == 4: + if not self.current_item.has_key('size'): + self.current_item['size'] = '' + self.current_item['size']+= data.strip() + if self.td_counter == 5: + if not self.current_item.has_key('seeds'): + self.current_item['seeds'] = '' + self.current_item['seeds']+= data.strip() + if self.td_counter == 6: + if not self.current_item.has_key('leech'): + self.current_item['leech'] = '' + self.current_item['leech']+= data.strip() + + def start_td(self,attr): + if isinstance(self.td_counter,int): + self.td_counter += 1 + if self.td_counter > 7: + self.td_counter = None + # add item to results + if self.current_item: + self.current_item['engine_url'] = self.url + if not self.current_item.has_key('seeds') or not self.current_item['seeds'].isdigit(): + self.current_item['seeds'] = 0 + if not self.current_item.has_key('leech') or not self.current_item['leech'].isdigit(): + self.current_item['leech'] = 0 + if self.current_item['link'] is not None: + prettyPrinter(self.current_item) + self.results.append('a') + + def __init__(self): + self.results = [] + self.parser = self.SimpleSGMLParser(self.results, self.url) + + def search(self, what): + i = 1 + while True: + results = [] + parser = self.SimpleSGMLParser(results, self.url) + dat = urllib.urlopen(self.url+'/torrents.php?ihq=%s&ihp=%s'%(what,i)).read().decode('utf-8', 'replace') + parser.feed(dat) + parser.close() + if len(results) <= 0: + break + i += 1 \ No newline at end of file diff --git a/src/search_engine/engines/mininova.png b/src/search_engine/engines/mininova.png new file mode 100644 index 000000000..1513376b6 Binary files /dev/null and b/src/search_engine/engines/mininova.png differ diff --git a/src/search_engine/engines/mininova.py b/src/search_engine/engines/mininova.py new file mode 100644 index 000000000..5f03cbbe0 --- /dev/null +++ b/src/search_engine/engines/mininova.py @@ -0,0 +1,50 @@ +#VERSION: 1.00 +#AUTHORS: Fabien Devaux (fab@gnux.info) +from novaprinter import prettyPrinter +import urllib +from xml.dom import minidom +import re + +class mininova(object): + url = 'http://www.mininova.org' + name = 'Mininova' + table_items = 'added cat name size seeds leech'.split() + + def search(self, what): + order = 'seeds' # must be one in self.table_items + + def get_link(lnk): + lnks = lnk.getElementsByTagName('a') + if lnks.item(0).attributes.get('href').value.startswith('/faq'): + if len(lnks) > 1: + return self.url+lnks.item(1).attributes.get('href').value + else: + return self.url+lnks.item(0).attributes.get('href').value + + def get_text(txt): + if txt.nodeType == txt.TEXT_NODE: + return txt.toxml() + else: + return ''.join([ get_text(n) for n in txt.childNodes]) + dat = urllib.urlopen(self.url+'/search/%s/seeds'%(what,)).read().decode('utf-8', 'replace') + dat = re.sub(" 7: + self.td_counter = None + # add item to results + if self.current_item: + self.current_item['link']='http://download.torrentreactor.net/download.php?id=%s&name=%s'%(self.id, urllib.quote(self.current_item['name'])) + self.current_item['engine_url'] = self.url + if not self.current_item['seeds'].isdigit(): + self.current_item['seeds'] = 0 + if not self.current_item['leech'].isdigit(): + self.current_item['leech'] = 0 + prettyPrinter(self.current_item) + self.has_results = True + self.results.append('a') + + def __init__(self): + self.results = [] + self.parser = self.SimpleSGMLParser(self.results, self.url) + + def search(self, what): + i = 0 + while True: + results = [] + parser = self.SimpleSGMLParser(results, self.url) + dat = urllib.urlopen(self.url+'/search.php?search=&words=%s&cid=&sid=&type=2&orderby=a.seeds&asc=0&skip=%s'%(what,(i*35))).read().decode('utf-8', 'replace') + parser.feed(dat) + parser.close() + if len(results) <= 0: + break + i += 1 \ No newline at end of file diff --git a/src/search_engine/engines/versions.txt b/src/search_engine/engines/versions.txt new file mode 100644 index 000000000..2f54561ba --- /dev/null +++ b/src/search_engine/engines/versions.txt @@ -0,0 +1,5 @@ +isohunt: 1.00 +torrentreactor: 1.00 +btjunkie: 1.00 +mininova: 1.00 +piratebay: 1.00 \ No newline at end of file diff --git a/src/search_engine/nova.py b/src/search_engine/nova.py deleted file mode 100755 index 47a798fe7..000000000 --- a/src/search_engine/nova.py +++ /dev/null @@ -1,503 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Version: 2.04 -# Changelog: -# - Fixed TorrentReactor search engine - -# Version: 2.03 -# Changelog: -# - Little fix for mininova search engine when file name contain '<=' - -# Version: 2.02 -# Changelog: -# - Fixed mininova search engine - -# Version: 2.01 -# Changelog: -# - Use multiple threads to optimize speed - -# Version: 2.00 -# Changelog: -# - Fixed ThePirateBay search engine -# - Fixed Meganova search engine -# - Fixed Mininova search engine - -# Version: 1.90 -# Changelog: -# - Various fixes - -# Version: 1.80 -# Changelog: -# - Fixed links from isohunt - -# Version: 1.70 -# Changelog: -# - merged with qbittorrent branch (code cleanup, indentation mistakes) -# - separate standalone and slave mode -# - added btjunkie -# - added meganova -# - added multithreaded mode - -# End Changelog - -# Author: -# Fabien Devaux -# Contributors: -# Christophe Dumez (qbittorrent integration) -# Thanks to gab #gcu @ irc.freenode.net (multipage support on PirateBay) -# Thanks to Elias (torrentreactor and isohunt search engines) -# -# Licence: BSD - -import sys -import urllib -import sgmllib -from xml.dom import minidom -import re -import os -import cgi -import traceback -import threading - -STANDALONE = False -THREADED = True - -if os.environ.has_key('QBITTORRENT'): - STANDALONE = False - -best_ratios = [] - -def prettyPrinter(dictionnary): - print "%(link)s|%(name)s|%(size)s|%(seeds)s|%(leech)s|%(engine_url)s"%dictionnary - -if STANDALONE: - def termPrettyPrinter(dictionnary): - if isinstance( dictionnary['size'], int): - dictionnary['size'] = bytesToHuman(dictionnary['size']) - try: - print "%(seeds)5s/%(leech)5s | %(size)10s | %(name)s"%dictionnary - except (UnicodeDecodeError, UnicodeEncodeError): - print "%(seeds)5s/%(leech)5s | %(size)10s | "%dictionnary - try: - print "wget '%s'"%dictionnary['link'].replace("'","\\'") - except: - pass - dictionnary['seeds'] = int( dictionnary['seeds'] ) or 0.00000001 - dictionnary['leech'] = int( dictionnary['leech'] ) or 0.00000001 - best_ratios.append(dictionnary) - - globals()['prettyPrinter'] = termPrettyPrinter - -def bytesToHuman(filesize): - """ - Convert float (size in bytes) to readable string - """ - decimators = ('k','M','G','T') - unit = '' - for n in range(len(decimators)): - if filesize > 1100.0: - filesize /= 1024.0 - unit = decimators[n] - return '%.1f%sB'%(filesize, unit) - -def anySizeToBytes(size_string): - """ - Convert a string like '1 KB' to '1024' (bytes) - """ - # separate integer from unit - try: - size, unit = size_string.split() - except (ValueError, TypeError): - try: - size = size_string.strip() - unit = ''.join([c for c in size if c.isalpha()]) - size = size[:-len(unit)] - except(ValueError, TypeError): - return -1 - - size = float(size) - short_unit = unit.upper()[0] - - # convert - units_dict = { 'T': 40, 'G': 30, 'M': 20, 'K': 10 } - if units_dict.has_key( short_unit ): - size = size * 2**units_dict[short_unit] - return int(size) - -################################################################################ -# Every engine should have a "search" method taking -# a space-free string as parameter (ex. "family+guy") -# it should call prettyPrinter() with a dict as parameter -# see above for dict keys -# As a convention, try to list results by decrasing number of seeds or similar -################################################################################ - -class PirateBay(object): - url = 'http://thepiratebay.org' - - def __init__(self): - self.results = [] - self.parser = self.SimpleSGMLParser(self.results, self.url) - - class SimpleSGMLParser(sgmllib.SGMLParser): - def __init__(self, results, url, *args): - sgmllib.SGMLParser.__init__(self) - self.td_counter = None - self.current_item = None - self.results = results - self.url = url - self.code = 0 - - def start_a(self, attr): - params = dict(attr) - if params['href'].startswith('/browse'): - self.current_item = {} - self.td_counter = 0 - elif params['href'].startswith('/tor'): - self.code = params['href'].split('/')[2] - elif params['href'].startswith('http://torrents.thepiratebay.org/%s'%self.code): - self.current_item['link']=params['href'].strip() - self.td_counter = self.td_counter+1 - - def handle_data(self, data): - if self.td_counter == 1: - if not self.current_item.has_key('name'): - self.current_item['name'] = '' - self.current_item['name']+= data.strip() - if self.td_counter == 5: - if not self.current_item.has_key('size'): - self.current_item['size'] = '' - self.current_item['size']+= data.strip() - elif self.td_counter == 6: - if not self.current_item.has_key('seeds'): - self.current_item['seeds'] = '' - self.current_item['seeds']+= data.strip() - elif self.td_counter == 7: - if not self.current_item.has_key('leech'): - self.current_item['leech'] = '' - self.current_item['leech']+= data.strip() - - def start_td(self,attr): - if isinstance(self.td_counter,int): - self.td_counter += 1 - if self.td_counter > 7: - self.td_counter = None - # Display item - if self.current_item: - self.current_item['engine_url'] = self.url - self.current_item['size']= anySizeToBytes(self.current_item['size']) - if not self.current_item['seeds'].isdigit(): - self.current_item['seeds'] = 0 - if not self.current_item['leech'].isdigit(): - self.current_item['leech'] = 0 - prettyPrinter(self.current_item) - self.results.append('a') - def search(self, what): - ret = [] - i = 0 - order = 'se' - while True: - results = [] - parser = self.SimpleSGMLParser(results, self.url) - dat = urllib.urlopen(self.url+'/search/%s/%u/0/0' % (what, i)).read() - parser.feed(dat) - parser.close() - if len(results) <= 0: - break - i += 1 - -class Mininova(object): - url = 'http://www.mininova.org' - table_items = 'added cat name size seeds leech'.split() - - def search(self, what): - order = 'seeds' # must be one in self.table_items - - def get_link(lnk): - lnks = lnk.getElementsByTagName('a') - if lnks.item(0).attributes.get('href').value.startswith('/faq'): - if len(lnks) > 1: - return self.url+lnks.item(1).attributes.get('href').value - else: - return self.url+lnks.item(0).attributes.get('href').value - - def get_text(txt): - if txt.nodeType == txt.TEXT_NODE: - return txt.toxml() - else: - return ''.join([ get_text(n) for n in txt.childNodes]) - dat = urllib.urlopen(self.url+'/search/%s/seeds'%(what,)).read().decode('utf-8', 'replace') - dat = re.sub("') - torrent_re = re.compile('(?s)href="(?P.*?do=download[^"]+).*?' - 'class="BlckUnd">(?P.*?).*?' - '>(?P\d+MB).*?' - '>(?P\d+).*?' - '>(?P\d+)') - for match in section_re.finditer(dat): - txt = match.group(0) - m = torrent_re.search(txt) - if m: - torrent_infos = m.groupdict() - torrent_infos['name'] = re.sub('', '', torrent_infos['name']) - torrent_infos['engine_url'] = self.url - torrent_infos['size'] = anySizeToBytes(torrent_infos['size']) - torrent_infos['link'] = self.url+torrent_infos['link'] - prettyPrinter(torrent_infos) - -class MegaNova(object): - url = 'http://www.meganova.org' - - def search(self, what): - dat = urllib.urlopen(self.url+'/find/%s/4/1.html'%what).read().decode('utf8', 'replace') - print 'url is ' + self.url+'/find/%s/4/1.html'%what - # I know it's not very readable, but the SGML parser feels in pain - - section_re = re.compile('(?s)/torrent/.*?)".*?' - '(?P.*?).*?' - '>(?P[0-9.]+\s+.B).*?' - '>(?P\d+)<.*?' - '>(?P\d+)<') - - for match in section_re.finditer(dat): - txt = match.group(0) - m = torrent_re.search(txt) - if m: - torrent_infos = m.groupdict() - torrent_infos['engine_url'] = self.url - torrent_infos['size'] = anySizeToBytes(torrent_infos['size']) - torrent_infos['link'] = self.url+torrent_infos['link'] - prettyPrinter(torrent_infos) - -class Reactor(object): - url = 'http://www.torrentreactor.net' - - class SimpleSGMLParser(sgmllib.SGMLParser): - def __init__(self, results, url, *args): - sgmllib.SGMLParser.__init__(self) - self.td_counter = None - self.current_item = None - self.results = results - self.id = None - self.url = url - - def start_a(self, attr): - params = dict(attr) - if params['href'].startswith('http://dl.torrentreactor.net/download.php'): - self.current_item = {} - self.td_counter = 0 - equal = params['href'].find("=") - amp = params['href'].find("&", equal+1) - self.id = str(int(params['href'][equal+1:amp])) - - def handle_data(self, data): - if self.td_counter == 0: - if not self.current_item.has_key('name'): - self.current_item['name'] = '' - self.current_item['name']+= data.strip() - if self.td_counter == 1: - if not self.current_item.has_key('size'): - self.current_item['size'] = '' - self.current_item['size']+= data.strip() - elif self.td_counter == 2: - if not self.current_item.has_key('seeds'): - self.current_item['seeds'] = '' - self.current_item['seeds']+= data.strip() - elif self.td_counter == 3: - if not self.current_item.has_key('leech'): - self.current_item['leech'] = '' - self.current_item['leech']+= data.strip() - - def start_td(self,attr): - if isinstance(self.td_counter,int): - self.td_counter += 1 - if self.td_counter > 7: - self.td_counter = None - # add item to results - if self.current_item: - self.current_item['link']='http://download.torrentreactor.net/download.php?id=%s&name=%s'%(self.id, urllib.quote(self.current_item['name'])) - self.current_item['engine_url'] = self.url - self.current_item['size']= anySizeToBytes(self.current_item['size']) - if not self.current_item['seeds'].isdigit(): - self.current_item['seeds'] = 0 - if not self.current_item['leech'].isdigit(): - self.current_item['leech'] = 0 - prettyPrinter(self.current_item) - self.has_results = True - self.results.append('a') - - def __init__(self): - self.results = [] - self.parser = self.SimpleSGMLParser(self.results, self.url) - - def search(self, what): - i = 0 - while True: - results = [] - parser = self.SimpleSGMLParser(results, self.url) - dat = urllib.urlopen(self.url+'/search.php?search=&words=%s&cid=&sid=&type=2&orderby=a.seeds&asc=0&skip=%s'%(what,(i*35))).read().decode('utf-8', 'replace') - parser.feed(dat) - parser.close() - if len(results) <= 0: - break - i += 1 - -class Isohunt(object): - url = 'http://isohunt.com' - - class SimpleSGMLParser(sgmllib.SGMLParser): - def __init__(self, results, url, *args): - sgmllib.SGMLParser.__init__(self) - self.td_counter = None - self.current_item = None - self.results = results - self.url = url - - def start_tr(self, attr): - params = dict(attr) - if 'onclick' in params: - Durl='http://isohunt.com/download' - self.current_item = {} - self.td_counter = 0 - try: - self.current_item['link'] = '%s/%s'%(Durl, params['onclick'].split('/')[2]) - except IndexError: - self.current_item['link'] = None - - def handle_data(self, data): - if self.td_counter == 3: - if not self.current_item.has_key('name'): - self.current_item['name'] = '' - self.current_item['name']+= data.strip() - if self.td_counter == 4: - if not self.current_item.has_key('size'): - self.current_item['size'] = '' - self.current_item['size']+= data.strip() - if self.td_counter == 5: - if not self.current_item.has_key('seeds'): - self.current_item['seeds'] = '' - self.current_item['seeds']+= data.strip() - if self.td_counter == 6: - if not self.current_item.has_key('leech'): - self.current_item['leech'] = '' - self.current_item['leech']+= data.strip() - - def start_td(self,attr): - if isinstance(self.td_counter,int): - self.td_counter += 1 - if self.td_counter > 7: - self.td_counter = None - # add item to results - if self.current_item: - self.current_item['engine_url'] = self.url - self.current_item['size']= anySizeToBytes(self.current_item['size']) - if not self.current_item.has_key('seeds') or not self.current_item['seeds'].isdigit(): - self.current_item['seeds'] = 0 - if not self.current_item.has_key('leech') or not self.current_item['leech'].isdigit(): - self.current_item['leech'] = 0 - if self.current_item['link'] is not None: - prettyPrinter(self.current_item) - self.results.append('a') - - def __init__(self): - self.results = [] - self.parser = self.SimpleSGMLParser(self.results, self.url) - - def search(self, what): - i = 1 - while True: - results = [] - parser = self.SimpleSGMLParser(results, self.url) - dat = urllib.urlopen(self.url+'/torrents.php?ihq=%s&ihp=%s'%(what,i)).read().decode('utf-8', 'replace') - parser.feed(dat) - parser.close() - if len(results) <= 0: - break - i += 1 - -class EngineLauncher(threading.Thread): - def __init__(self, engine, what): - threading.Thread.__init__(self) - self.engine = engine - self.what = what - def run(self): - self.engine.search(self.what) - -if __name__ == '__main__': - available_engines_list = BtJunkie, MegaNova, Mininova, PirateBay, Reactor, Isohunt - - if len(sys.argv) < 2: - raise SystemExit('./nova.py [all|engine1[,engine2]*] \navailable engines: %s'% - (','.join(e.__name__ for e in available_engines_list))) - - engines_list = [e.lower() for e in sys.argv[1].strip().split(',')] - - if 'all' in engines_list: - engines_list = [e.__name__.lower() for e in available_engines_list] - - selected_engines = set(e for e in available_engines_list if e.__name__.lower() in engines_list) - - if not selected_engines: - selected_engines = [BtJunkie] - what = '+'.join(sys.argv[1:]) - else: - what = '+'.join(sys.argv[2:]) - - threads = [] - for engine in selected_engines: - try: - if THREADED: - l = EngineLauncher( engine(), what ) - threads.append(l) - l.start() - else: - engine().search(what) - except: - if STANDALONE: - traceback.print_exc() - if THREADED: - for t in threads: - t.join() - - best_ratios.sort(lambda a,b : cmp(a['seeds']-a['leech'], b['seeds']-b['leech'])) - - max_results = 10 - - print "########## TOP %d RATIOS ##########"%max_results - - for br in best_ratios: - if br['seeds'] > 1: # avoid those with 0 leech to be max rated - prettyPrinter(br) - max_results -= 1 - if not max_results: - break diff --git a/src/search_engine/nova2.py b/src/search_engine/nova2.py new file mode 100755 index 000000000..d0bdcb4e8 --- /dev/null +++ b/src/search_engine/nova2.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +#VERSION: 1.00 + +# Author: +# Fabien Devaux +# Contributors: +# Christophe Dumez (qbittorrent integration) +# Thanks to gab #gcu @ irc.freenode.net (multipage support on PirateBay) +# Thanks to Elias (torrentreactor and isohunt search engines) +# +# Licence: BSD + +import sys +import threading +import os +import glob + +THREADED = True + +################################################################################ +# Every engine should have a "search" method taking +# a space-free string as parameter (ex. "family+guy") +# it should call prettyPrinter() with a dict as parameter. +# The keys in the dict must be: link,name,size,seeds,leech,engine_url +# As a convention, try to list results by decrasing number of seeds or similar +################################################################################ + +supported_engines = [] + +engines = glob.glob(os.path.join(os.path.dirname(__file__), 'engines','*.py')) +for engine in engines: + e = engine.split(os.sep)[-1][:-3] + if len(e.strip()) == 0: continue + if e.startswith('_'): continue + try: + exec "from engines.%s import %s"%(e,e) + supported_engines.append(e) + except: + pass + +class EngineLauncher(threading.Thread): + def __init__(self, engine, what): + threading.Thread.__init__(self) + self.engine = engine + self.what = what + def run(self): + self.engine.search(self.what) + +if __name__ == '__main__': + if len(sys.argv) < 2: + raise SystemExit('./nova.py [all|engine1[,engine2]*] \navailable engines: %s'% + (','.join(supported_engines))) + + if len(sys.argv) == 2: + if sys.argv[1] == "--supported_engines": + print ','.join(supported_engines) + sys.exit(0) + elif sys.argv[1] == "--supported_engines_infos": + res = [] + for e in supported_engines: + exec "res.append(%s().name+'|'+%s().url)"%(e,e) + print ','.join(res) + sys.exit(0) + else: + raise SystemExit('./nova.py [all|engine1[,engine2]*] \navailable engines: %s'% + (','.join(supported_engines))) + + engines_list = [e.lower() for e in sys.argv[1].strip().split(',')] + + if 'all' in engines_list: + engines_list = supported_engines + + what = '+'.join(sys.argv[2:]) + + threads = [] + for engine in engines_list: + try: + if THREADED: + exec "l = EngineLauncher(%s(), what)" % engine + threads.append(l) + l.start() + else: + engine().search(what) + except: + pass + if THREADED: + for t in threads: + t.join() diff --git a/src/search_engine/novaprinter.py b/src/search_engine/novaprinter.py new file mode 100644 index 000000000..47e6e7561 --- /dev/null +++ b/src/search_engine/novaprinter.py @@ -0,0 +1,27 @@ +def prettyPrinter(dictionnary): + dictionnary['size'] = anySizeToBytes(dictionnary['size']) + print "%(link)s|%(name)s|%(size)s|%(seeds)s|%(leech)s|%(engine_url)s" % dictionnary + +def anySizeToBytes(size_string): + """ + Convert a string like '1 KB' to '1024' (bytes) + """ + # separate integer from unit + try: + size, unit = size_string.split() + except (ValueError, TypeError): + try: + size = size_string.strip() + unit = ''.join([c for c in size if c.isalpha()]) + size = size[:-len(unit)] + except(ValueError, TypeError): + return -1 + + size = float(size) + short_unit = unit.upper()[0] + + # convert + units_dict = { 'T': 40, 'G': 30, 'M': 20, 'K': 10 } + if units_dict.has_key( short_unit ): + size = size * 2**units_dict[short_unit] + return int(size) \ No newline at end of file diff --git a/src/src.pro b/src/src.pro index 28f3d4365..2fbd07f0d 100644 --- a/src/src.pro +++ b/src/src.pro @@ -150,12 +150,13 @@ HEADERS += GUI.h misc.h options_imp.h about_imp.h \ bittorrent.h searchEngine.h \ rss.h rss_imp.h FinishedTorrents.h \ allocationDlg.h FinishedListDelegate.h \ - qtorrenthandle.h downloadingTorrents.h + qtorrenthandle.h downloadingTorrents.h \ + engineSelectDlg.h FORMS += MainWindow.ui options.ui about.ui \ properties.ui createtorrent.ui preview.ui \ login.ui downloadFromURL.ui addTorrentDialog.ui \ search.ui rss.ui seeding.ui bandwidth_limit.ui \ - download.ui + download.ui engineSelect.ui SOURCES += GUI.cpp \ main.cpp \ options_imp.cpp \ @@ -166,5 +167,6 @@ SOURCES += GUI.cpp \ rss_imp.cpp \ FinishedTorrents.cpp \ qtorrenthandle.cpp \ - downloadingTorrents.cpp + downloadingTorrents.cpp \ + engineSelectDlg.cpp diff --git a/src/update_qrc_files.py b/src/update_qrc_files.py index 03e6dcc2c..e93a9dce1 100755 --- a/src/update_qrc_files.py +++ b/src/update_qrc_files.py @@ -20,6 +20,27 @@ lang_file = open('lang.qrc', 'w') lang_file.write(output) lang_file.close() +# update search_engine directory +search_list = [] +for root, dirs, files in os.walk('search_engine'): + for file in files: + if file.startswith("__"): + continue + if splitext(file)[-1] in ('.py', '.png'): + search_list.append(join(root, file)) + +output = ''' + +''' +for file in search_list: + output += ' %s'%(file) + output += os.linesep +output += ''' +''' +search_file = open('search.qrc', 'w') +search_file.write(output) +search_file.close() + # update icons files directory icons_list = [] for root, dirs, files in os.walk('Icons'):