Browse Source

- Merged custom-search branch. New search plugins management system

adaptive-webui-19844
Christophe Dumez 17 years ago
parent
commit
ff4ab915a2
  1. 1
      Changelog
  2. 2
      TODO
  3. 117
      src/engineSelect.ui
  4. 443
      src/engineSelectDlg.cpp
  5. 69
      src/engineSelectDlg.h
  6. 22
      src/misc.h
  7. 13
      src/search.qrc
  8. 402
      src/search.ui
  9. 310
      src/searchEngine.cpp
  10. 16
      src/searchEngine.h
  11. 0
      src/search_engine/__init__.py
  12. 0
      src/search_engine/engines/__init__.py
  13. BIN
      src/search_engine/engines/btjunkie.png
  14. 29
      src/search_engine/engines/btjunkie.py
  15. BIN
      src/search_engine/engines/isohunt.png
  16. 78
      src/search_engine/engines/isohunt.py
  17. BIN
      src/search_engine/engines/mininova.png
  18. 50
      src/search_engine/engines/mininova.py
  19. BIN
      src/search_engine/engines/piratebay.png
  20. 79
      src/search_engine/engines/piratebay.py
  21. BIN
      src/search_engine/engines/torrentreactor.png
  22. 78
      src/search_engine/engines/torrentreactor.py
  23. 5
      src/search_engine/engines/versions.txt
  24. 503
      src/search_engine/nova.py
  25. 90
      src/search_engine/nova2.py
  26. 27
      src/search_engine/novaprinter.py
  27. 8
      src/src.pro
  28. 21
      src/update_qrc_files.py

1
Changelog

@ -5,6 +5,7 @@
- FEATURE: Bittorrent FAST extension support - FEATURE: Bittorrent FAST extension support
- FEATURE: Added RSS support - FEATURE: Added RSS support
- FEATURE: Support files prioritizing in a torrent - 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: Finished torrents are now moved to another tab for seeding
- FEATURE: Display more infos about the torrent in its properties - FEATURE: Display more infos about the torrent in its properties
- FEATURE: Allow the user to edit torrents' trackers - FEATURE: Allow the user to edit torrents' trackers

2
TODO

@ -38,7 +38,6 @@
- Allow to limit the number of downloading torrents simultaneously (other are paused until a download finishes) - Allow to limit the number of downloading torrents simultaneously (other are paused until a download finishes)
- Add "Mark all as read" feature for RSS - Add "Mark all as read" feature for RSS
- Allow to customize lists refreshing interval (in options) - 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 // in v1.0.0 (partial) - WIP
- Check storage st creation + hasher in torrent creation - 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: A lot of code optimization (CPU & memory usage)
- FEATURE: Added support for .ico format (useful for RSS favicons) - FEATURE: Added support for .ico format (useful for RSS favicons)
- FEATURE: Replaced Meganova search engine by TorrentReactor - FEATURE: Replaced Meganova search engine by TorrentReactor
- FEATURE: Brand new search engine plugins system
- I18N: Updated Greek, Dutch and Romanian translation - I18N: Updated Greek, Dutch and Romanian translation
- I18N: Removed no longer maintained Traditional chinese translation - I18N: Removed no longer maintained Traditional chinese translation
- BUGFIX: Made torrent deletion from hard-drive safer - BUGFIX: Made torrent deletion from hard-drive safer

117
src/engineSelect.ui

@ -0,0 +1,117 @@
<ui version="4.0" >
<class>engineSelect</class>
<widget class="QDialog" name="engineSelect" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>527</width>
<height>254</height>
</rect>
</property>
<property name="windowTitle" >
<string>Search plugins</string>
</property>
<layout class="QVBoxLayout" >
<item>
<widget class="QLabel" name="lbl_engines" >
<property name="font" >
<font>
<weight>75</weight>
<bold>true</bold>
<underline>true</underline>
</font>
</property>
<property name="text" >
<string>Installed search engines:</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="pluginsTree" >
<property name="contextMenuPolicy" >
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="selectionMode" >
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="uniformRowHeights" >
<bool>true</bool>
</property>
<property name="itemsExpandable" >
<bool>false</bool>
</property>
<column>
<property name="text" >
<string>Name</string>
</property>
</column>
<column>
<property name="text" >
<string>Url</string>
</property>
</column>
<column>
<property name="text" >
<string>Enabled</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QLabel" name="getNewEngine_lbl" >
<property name="font" >
<font>
<italic>true</italic>
</font>
</property>
<property name="text" >
<string>You can get new search engine plugins here: &lt;a href="http:plugins.qbittorrent.org">http://plugins.qbittorrent.org&lt;/a></string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" >
<item>
<widget class="QPushButton" name="installButton" >
<property name="text" >
<string>Install a new one</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="updateButton" >
<property name="text" >
<string>Check for updates</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton" >
<property name="text" >
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
<action name="actionEnable" >
<property name="text" >
<string>Enable</string>
</property>
</action>
<action name="actionDisable" >
<property name="text" >
<string>Disable</string>
</property>
</action>
<action name="actionUninstall" >
<property name="text" >
<string>Uninstall</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

443
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 <QProcess>
#include <QHeaderView>
#include <QSettings>
#include <QMenu>
#include <QMessageBox>
#include <QFileDialog>
#ifdef HAVE_MAGICK
#include <Magick++.h>
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<QVariant>()).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<QTreeWidgetItem *> 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<QTreeWidgetItem *> 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<QTreeWidgetItem *> 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<QTreeWidgetItem *> 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; i<pluginsTree->columnCount(); ++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<QTreeWidgetItem*> engineSelectDlg::findItemsWithUrl(QString url){
QList<QTreeWidgetItem*> res;
for(int i=0; i<pluginsTree->topLevelItemCount(); ++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<QByteArray> 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<QTreeWidgetItem*> 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()));
}
}

69
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<QTreeWidgetItem*> 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

22
src/misc.h

@ -328,6 +328,28 @@ class misc : public QObject{
list.insert(i, value); 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 // Take a number of seconds and return an user-friendly
// time duration like "1d 2h 10m". // time duration like "1d 2h 10m".
static QString userFriendlyDuration(qlonglong seconds) { static QString userFriendlyDuration(qlonglong seconds) {

13
src/search.qrc

@ -1,5 +1,16 @@
<!DOCTYPE RCC><RCC version="1.0"> <!DOCTYPE RCC><RCC version="1.0">
<qresource> <qresource>
<file>search_engine/nova.py</file> <file>search_engine/nova2.py</file>
<file>search_engine/novaprinter.py</file>
<file>search_engine/engines/isohunt.py</file>
<file>search_engine/engines/btjunkie.py</file>
<file>search_engine/engines/torrentreactor.py</file>
<file>search_engine/engines/mininova.py</file>
<file>search_engine/engines/piratebay.py</file>
<file>search_engine/engines/torrentreactor.png</file>
<file>search_engine/engines/mininova.png</file>
<file>search_engine/engines/piratebay.png</file>
<file>search_engine/engines/btjunkie.png</file>
<file>search_engine/engines/isohunt.png</file>
</qresource> </qresource>
</RCC> </RCC>

402
src/search.ui

@ -13,256 +13,200 @@
<string>Search</string> <string>Search</string>
</property> </property>
<layout class="QVBoxLayout" > <layout class="QVBoxLayout" >
<property name="margin" >
<number>9</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item> <item>
<layout class="QHBoxLayout" > <layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" > <property name="spacing" >
<number>6</number> <number>6</number>
</property> </property>
<property name="leftMargin" >
<number>0</number>
</property>
<property name="topMargin" >
<number>0</number>
</property>
<property name="rightMargin" >
<number>0</number>
</property>
<property name="bottomMargin" >
<number>0</number>
</property>
<item>
<widget class="QLabel" name="search_lbl" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>35</height>
</size>
</property>
<property name="font" >
<font>
<family>Sans Serif</family>
<pointsize>9</pointsize>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
<underline>false</underline>
<strikeout>false</strikeout>
</font>
</property>
<property name="text" >
<string>Search Pattern:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="search_pattern" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="search_button" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>29</height>
</size>
</property>
<property name="text" >
<string>Search</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stop_search_button" >
<property name="enabled" >
<bool>false</bool>
</property>
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>29</height>
</size>
</property>
<property name="text" >
<string>Stop</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="enginesButton" >
<property name="text" >
<string>Search engines...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" >
<item>
<widget class="QLabel" name="status_lbl" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>35</height>
</size>
</property>
<property name="font" >
<font>
<family>Sans Serif</family>
<pointsize>9</pointsize>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
<underline>false</underline>
<strikeout>false</strikeout>
</font>
</property>
<property name="text" >
<string>Status:</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QGroupBox" name="groupEngines" > <widget class="QLabel" name="search_status" >
<property name="minimumSize" > <property name="minimumSize" >
<size> <size>
<width>131</width> <width>400</width>
<height>132</height> <height>0</height>
</size> </size>
</property> </property>
<property name="maximumSize" > <property name="maximumSize" >
<size> <size>
<width>125</width> <width>16777215</width>
<height>132</height> <height>35</height>
</size> </size>
</property> </property>
<property name="title" > <property name="font" >
<string>Search Engines</string> <font>
</property> <family>Sans Serif</family>
<layout class="QVBoxLayout" > <pointsize>9</pointsize>
<property name="margin" > <weight>50</weight>
<number>9</number> <italic>true</italic>
</property> <bold>false</bold>
<property name="spacing" > <underline>false</underline>
<number>6</number> <strikeout>false</strikeout>
</property> </font>
<item> </property>
<widget class="QCheckBox" name="mininova" > <property name="text" >
<property name="text" > <string>Stopped</string>
<string/> </property>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="piratebay" >
<property name="text" >
<string/>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="isohunt" >
<property name="text" >
<string/>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="reactor" >
<property name="text" >
<string/>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QVBoxLayout" > <spacer>
<property name="margin" > <property name="orientation" >
<number>0</number> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="spacing" > <property name="sizeHint" >
<number>6</number> <size>
<width>188</width>
<height>21</height>
</size>
</property> </property>
<item> </spacer>
<layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="QLabel" name="search_lbl" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>35</height>
</size>
</property>
<property name="font" >
<font>
<family>Sans Serif</family>
<pointsize>9</pointsize>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
<underline>false</underline>
<strikeout>false</strikeout>
</font>
</property>
<property name="text" >
<string>Search Pattern:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="search_pattern" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="search_button" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>29</height>
</size>
</property>
<property name="text" >
<string>Search</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stop_search_button" >
<property name="enabled" >
<bool>false</bool>
</property>
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>29</height>
</size>
</property>
<property name="text" >
<string>Stop</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="QLabel" name="status_lbl" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>35</height>
</size>
</property>
<property name="font" >
<font>
<family>Sans Serif</family>
<pointsize>9</pointsize>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
<underline>false</underline>
<strikeout>false</strikeout>
</font>
</property>
<property name="text" >
<string>Status:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="search_status" >
<property name="minimumSize" >
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>35</height>
</size>
</property>
<property name="font" >
<font>
<family>Sans Serif</family>
<pointsize>9</pointsize>
<weight>50</weight>
<italic>true</italic>
<bold>false</bold>
<underline>false</underline>
<strikeout>false</strikeout>
</font>
</property>
<property name="text" >
<string>Stopped</string>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" >
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item> </item>
</layout> </layout>
</item> </item>
<item> <item>
<layout class="QVBoxLayout" > <layout class="QVBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" > <property name="spacing" >
<number>6</number> <number>6</number>
</property> </property>
<property name="leftMargin" >
<number>0</number>
</property>
<property name="topMargin" >
<number>0</number>
</property>
<property name="rightMargin" >
<number>0</number>
</property>
<property name="bottomMargin" >
<number>0</number>
</property>
<item> <item>
<layout class="QHBoxLayout" > <layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" > <property name="spacing" >
<number>6</number> <number>6</number>
</property> </property>
<property name="leftMargin" >
<number>0</number>
</property>
<property name="topMargin" >
<number>0</number>
</property>
<property name="rightMargin" >
<number>0</number>
</property>
<property name="bottomMargin" >
<number>0</number>
</property>
<item> <item>
<widget class="QLabel" name="results_lbl" > <widget class="QLabel" name="results_lbl" >
<property name="maximumSize" > <property name="maximumSize" >
@ -329,12 +273,21 @@
</item> </item>
<item> <item>
<layout class="QHBoxLayout" > <layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" > <property name="spacing" >
<number>6</number> <number>6</number>
</property> </property>
<property name="leftMargin" >
<number>0</number>
</property>
<property name="topMargin" >
<number>0</number>
</property>
<property name="rightMargin" >
<number>0</number>
</property>
<property name="bottomMargin" >
<number>0</number>
</property>
<item> <item>
<widget class="QPushButton" name="download_button" > <widget class="QPushButton" name="download_button" >
<property name="enabled" > <property name="enabled" >
@ -368,13 +321,6 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QPushButton" name="update_nova_button" >
<property name="text" >
<string>Update search plugin</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>

310
src/searchEngine.cpp

@ -45,8 +45,6 @@
SearchEngine::SearchEngine(bittorrent *BTSession, QSystemTrayIcon *myTrayIcon, bool systrayIntegration) : QWidget(), BTSession(BTSession), myTrayIcon(myTrayIcon), systrayIntegration(systrayIntegration){ SearchEngine::SearchEngine(bittorrent *BTSession, QSystemTrayIcon *myTrayIcon, bool systrayIntegration) : QWidget(), BTSession(BTSession), myTrayIcon(myTrayIcon), systrayIntegration(systrayIntegration){
setupUi(this); setupUi(this);
downloader = new downloadThread(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 // Set Search results list model
SearchListModel = new QStandardItemModel(0,5); SearchListModel = new QStandardItemModel(0,5);
SearchListModel->setHeaderData(SEARCH_NAME, Qt::Horizontal, tr("Name", "i.e: file name")); 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(started()), this, SLOT(searchStarted()));
connect(searchProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(readSearchOutput())); connect(searchProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(readSearchOutput()));
connect(searchProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(searchFinished(int,QProcess::ExitStatus))); connect(searchProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(searchFinished(int,QProcess::ExitStatus)));
// Set search engines names // Check last enabled search engines
mininova->setText("Mininova"); loadEngineSettings();
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)));
// Update nova.py search plugin if necessary // Update nova.py search plugin if necessary
updateNova(); updateNova();
} }
@ -183,18 +168,6 @@ void SearchEngine::sortSearchListString(int index, Qt::SortOrder sortOrder){
SearchListModel->removeRows(0, nbRows_old); 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 // Save columns width in a file to remember them
// (download list) // (download list)
void SearchEngine::saveColWidthSearchList() const{ void SearchEngine::saveColWidthSearchList() const{
@ -208,6 +181,11 @@ void SearchEngine::saveColWidthSearchList() const{
qDebug("Search list columns width saved"); 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 // Load columns width in a file that were saved previously
// (search list) // (search list)
bool SearchEngine::loadColWidthSearchList(){ bool SearchEngine::loadColWidthSearchList(){
@ -226,19 +204,6 @@ bool SearchEngine::loadColWidthSearchList(){
return true; 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 // get the last searchs from a QSettings to a QStringList
void SearchEngine::startSearchHistory(){ void SearchEngine::startSearchHistory(){
QSettings settings("qBittorrent", "qBittorrent"); QSettings settings("qBittorrent", "qBittorrent");
@ -247,6 +212,24 @@ void SearchEngine::startSearchHistory(){
settings.endGroup(); 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<QVariant>()).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 // Save the history list into the QSettings for the next session
void SearchEngine::saveSearchHistory() void SearchEngine::saveSearchHistory()
{ {
@ -282,33 +265,12 @@ void SearchEngine::on_search_button_clicked(){
// Getting checked search engines // Getting checked search engines
if(!mininova->isChecked() && ! piratebay->isChecked() && !reactor->isChecked() && !isohunt->isChecked()/* && !btjunkie->isChecked()*/ /*&& !meganova->isChecked()*/){ Q_ASSERT(!enabled_engines.empty());
QMessageBox::critical(0, tr("No search engine selected"), tr("You must select at least one search engine."));
return;
}
QStringList params; QStringList params;
QStringList engineNames; QStringList engineNames;
search_stopped = false; search_stopped = false;
// Get checked search engines
if(mininova->isChecked()){ params << enabled_engines.join(",");
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 << pattern.split(" "); params << pattern.split(" ");
// Update SearchEngine widgets // Update SearchEngine widgets
no_search_results = true; no_search_results = true;
@ -316,7 +278,7 @@ void SearchEngine::on_search_button_clicked(){
search_result_line_truncated.clear(); search_result_line_truncated.clear();
results_lbl->setText(tr("Results")+" <i>(0)</i>:"); results_lbl->setText(tr("Results")+" <i>(0)</i>:");
// Launch search // 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(){ void SearchEngine::searchStarted(){
@ -360,137 +322,105 @@ void SearchEngine::readSearchOutput(){
results_lbl->setText(tr("Results")+QString::fromUtf8(" <i>(")+misc::toQString(nb_search_results)+QString::fromUtf8(")</i>:")); results_lbl->setText(tr("Results")+QString::fromUtf8(" <i>(")+misc::toQString(nb_search_results)+QString::fromUtf8(")</i>:"));
} }
// 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 // Update nova.py search plugin if necessary
void SearchEngine::updateNova() const{ void SearchEngine::updateNova() {
qDebug("Updating nova"); 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::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); QFile(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"nova2.py").setPermissions(perm);
if(provided_nova_version > getNovaVersion(misc::qBittorrentPath()+"nova.py")){ if(!QFile::exists(misc::qBittorrentPath()+"search_engine"+QDir::separator()+"novaprinter.py")){
qDebug("updating local search plugin with shipped one"); QFile::copy(":/search_engine/novaprinter.py", misc::qBittorrentPath()+"search_engine"+QDir::separator()+"novaprinter.py");
// nova.py needs update }
QFile::remove(misc::qBittorrentPath()+"nova.py"); QString subDir = misc::qBittorrentPath()+"search_engine"+QDir::separator()+"engines"+QDir::separator();
qDebug("Old nova removed"); QDir search_subDir(":/search_engine/engines");
QFile::copy(":/search_engine/nova.py", misc::qBittorrentPath()+"nova.py"); QStringList files = search_subDir.entryList();
qDebug("New nova copied"); QString file;
QFile::Permissions perm=QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadUser | QFile::WriteUser | QFile::ExeUser | QFile::ReadGroup | QFile::ReadGroup; foreach(file, files){
QFile(misc::qBittorrentPath()+"nova.py").setPermissions(perm); filePath = search_subDir.path()+QDir::separator()+file;
qDebug("local search plugin updated"); // Copy python classes
} if(file.endsWith(".py")) {
} if(misc::getPluginVersion(filePath) > misc::getPluginVersion(subDir+file) ) {
if(QFile::exists(filePath))
void SearchEngine::novaUpdateDownloaded(QString url, QString filePath){ QFile::remove(filePath);
float version_on_server = getNovaVersion(filePath); QFile::copy(filePath, subDir+file);
qDebug("Version on qbittorrent.org: %.2f", version_on_server); }
float my_version = getNovaVersion(misc::qBittorrentPath()+"nova.py"); } else {
if(version_on_server > my_version){ // Copy icons
if(QMessageBox::question(this, if(file.endsWith(".png")) {
tr("Search plugin update -- qBittorrent"), if(!QFile::exists(subDir+file)) {
tr("Search plugin can be updated, do you want to update it?\n\nChangelog:\n")+getNovaChangelog(filePath, my_version), QFile::copy(filePath, subDir+file);
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));
}
} }
// Download nova.py from qbittorrent.org // void SearchEngine::novaUpdateDownloaded(QString url, QString filePath){
// Check if our nova.py is outdated and // float version_on_server = getNovaVersion(filePath);
// ask user for action. // qDebug("Version on qbittorrent.org: %.2f", version_on_server);
void SearchEngine::on_update_nova_button_clicked(){ // float my_version = getNovaVersion(misc::qBittorrentPath()+"nova.py");
qDebug("Checking for search plugin updates on qbittorrent.org"); // if(version_on_server > my_version){
downloader->downloadUrl("http://www.dchris.eu/nova/nova.zip"); // 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 // Slot called when search is Finished
// Search can be finished for 3 reasons : // Search can be finished for 3 reasons :

16
src/searchEngine.h

@ -26,6 +26,7 @@
#include <QProcess> #include <QProcess>
#include "ui_search.h" #include "ui_search.h"
#include "engineSelectDlg.h"
class QStandardItemModel; class QStandardItemModel;
class SearchListDelegate; class SearchListDelegate;
@ -52,21 +53,20 @@ class SearchEngine : public QWidget, public Ui::search_engine{
QSystemTrayIcon *myTrayIcon; QSystemTrayIcon *myTrayIcon;
bool systrayIntegration; bool systrayIntegration;
downloadThread *downloader; downloadThread *downloader;
QStringList enabled_engines;
public: public:
SearchEngine(bittorrent *BTSession, QSystemTrayIcon *myTrayIcon, bool systrayIntegration); SearchEngine(bittorrent *BTSession, QSystemTrayIcon *myTrayIcon, bool systrayIntegration);
~SearchEngine(); ~SearchEngine();
float getNovaVersion(QString novaPath) const; float getPluginVersion(QString filePath) const;
QByteArray getNovaChangelog(QString novaPath, float my_version) const;
bool loadColWidthSearchList(); bool loadColWidthSearchList();
public slots: protected slots:
// Search slots // Search slots
void on_search_button_clicked(); void on_search_button_clicked();
void on_stop_search_button_clicked(); void on_stop_search_button_clicked();
void on_clear_button_clicked(); void on_clear_button_clicked();
void on_download_button_clicked(); void on_download_button_clicked();
void on_update_nova_button_clicked();
void appendSearchResult(QString line); void appendSearchResult(QString line);
void searchFinished(int exitcode,QProcess::ExitStatus); void searchFinished(int exitcode,QProcess::ExitStatus);
void readSearchOutput(); void readSearchOutput();
@ -74,16 +74,14 @@ class SearchEngine : public QWidget, public Ui::search_engine{
void searchStarted(); void searchStarted();
void downloadSelectedItem(const QModelIndex& index); void downloadSelectedItem(const QModelIndex& index);
void startSearchHistory(); void startSearchHistory();
void loadCheckedSearchEngines(); void updateNova();
void updateNova() const;
void saveSearchHistory(); void saveSearchHistory();
void saveColWidthSearchList() const; void saveColWidthSearchList() const;
void saveCheckedSearchEngines(int) const;
void sortSearchList(int index); void sortSearchList(int index);
void sortSearchListInt(int index, Qt::SortOrder sortOrder); void sortSearchListInt(int index, Qt::SortOrder sortOrder);
void sortSearchListString(int index, Qt::SortOrder sortOrder); void sortSearchListString(int index, Qt::SortOrder sortOrder);
void novaUpdateDownloaded(QString url, QString path); void on_enginesButton_clicked();
void handleNovaDownloadFailure(QString url, QString reason); void loadEngineSettings();
}; };
#endif #endif

0
src/search_engine/__init__.py

0
src/search_engine/engines/__init__.py

BIN
src/search_engine/engines/btjunkie.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

29
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.*?<tr>')
torrent_re = re.compile('(?s)href="(?P<link>.*?do=download[^"]+).*?'
'class="BlckUnd">(?P<name>.*?)</a>.*?'
'>(?P<size>\d+MB)</font>.*?'
'>(?P<seeds>\d+)</font>.*?'
'>(?P<leech>\d+)</font>')
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('</?font.*?>', '', torrent_infos['name'])
torrent_infos['engine_url'] = self.url
torrent_infos['link'] = self.url+torrent_infos['link']
prettyPrinter(torrent_infos)

BIN
src/search_engine/engines/isohunt.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

78
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

BIN
src/search_engine/engines/mininova.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

50
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("<a href=\"http://www.boardreader.com/index.php.*\"", "<a href=\"plop\"", dat)
dat = re.sub("<=", "&lt;=", dat)
x = minidom.parseString(dat.encode('utf-8', 'replace'))
table = x.getElementsByTagName('table').item(0)
if not table: return
for tr in table.getElementsByTagName('tr'):
tds = tr.getElementsByTagName('td')
if tds:
i = 0
vals = {}
for td in tds:
if self.table_items[i] == 'name':
vals['link'] = get_link(td).strip()
vals[self.table_items[i]] = get_text(td).strip()
i += 1
vals['engine_url'] = self.url
if not vals['seeds'].isdigit():
vals['seeds'] = 0
if not vals['leech'].isdigit():
vals['leech'] = 0
prettyPrinter(vals)

BIN
src/search_engine/engines/piratebay.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

79
src/search_engine/engines/piratebay.py

@ -0,0 +1,79 @@
#VERSION: 1.00
#AUTHORS: Fabien Devaux (fab@gnux.info)
from novaprinter import prettyPrinter
import sgmllib
import urllib
class piratebay(object):
url = 'http://thepiratebay.org'
name = 'The Pirate Bay'
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
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

BIN
src/search_engine/engines/torrentreactor.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

78
src/search_engine/engines/torrentreactor.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 torrentreactor(object):
url = 'http://www.torrentreactor.net'
name = '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
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

5
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

503
src/search_engine/nova.py

@ -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 <fab AT gnux DOT info>
# Contributors:
# Christophe Dumez <chris@qbittorrent.org> (qbittorrent integration)
# Thanks to gab #gcu @ irc.freenode.net (multipage support on PirateBay)
# Thanks to Elias <gekko04@users.sourceforge.net> (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 | <unprintable title>"%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("<a href=\"http://www.boardreader.com/index.php.*\"", "<a href=\"plop\"", dat)
dat = re.sub("<=", "&lt;=", dat)
x = minidom.parseString(dat.encode('utf-8', 'replace'))
table = x.getElementsByTagName('table').item(0)
if not table: return
for tr in table.getElementsByTagName('tr'):
tds = tr.getElementsByTagName('td')
if tds:
i = 0
vals = {}
for td in tds:
if self.table_items[i] == 'name':
vals['link'] = get_link(td).strip()
vals[self.table_items[i]] = get_text(td).strip()
i += 1
vals['engine_url'] = self.url
vals['size']= anySizeToBytes(vals['size'])
if not vals['seeds'].isdigit():
vals['seeds'] = 0
if not vals['leech'].isdigit():
vals['leech'] = 0
prettyPrinter(vals)
# TODO: add multipage
class BtJunkie(object):
url = 'http://btjunkie.org'
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.*?<tr>')
torrent_re = re.compile('(?s)href="(?P<link>.*?do=download[^"]+).*?'
'class="BlckUnd">(?P<name>.*?)</a>.*?'
'>(?P<size>\d+MB)</font>.*?'
'>(?P<seeds>\d+)</font>.*?'
'>(?P<leech>\d+)</font>')
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('</?font.*?>', '', 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)<td><a class="name".*?</tr')
torrent_re = re.compile('(?s)href="(?P<link>/torrent/.*?)".*?'
'<span.*?>(?P<name>.*?)</span>.*?'
'>(?P<size>[0-9.]+\s+.B).*?'
'>(?P<seeds>\d+)<.*?'
'>(?P<leech>\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]*] <keywords>\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

90
src/search_engine/nova2.py

@ -0,0 +1,90 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#VERSION: 1.00
# Author:
# Fabien Devaux <fab AT gnux DOT info>
# Contributors:
# Christophe Dumez <chris@qbittorrent.org> (qbittorrent integration)
# Thanks to gab #gcu @ irc.freenode.net (multipage support on PirateBay)
# Thanks to Elias <gekko04@users.sourceforge.net> (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]*] <keywords>\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]*] <keywords>\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()

27
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)

8
src/src.pro

@ -150,12 +150,13 @@ HEADERS += GUI.h misc.h options_imp.h about_imp.h \
bittorrent.h searchEngine.h \ bittorrent.h searchEngine.h \
rss.h rss_imp.h FinishedTorrents.h \ rss.h rss_imp.h FinishedTorrents.h \
allocationDlg.h FinishedListDelegate.h \ allocationDlg.h FinishedListDelegate.h \
qtorrenthandle.h downloadingTorrents.h qtorrenthandle.h downloadingTorrents.h \
engineSelectDlg.h
FORMS += MainWindow.ui options.ui about.ui \ FORMS += MainWindow.ui options.ui about.ui \
properties.ui createtorrent.ui preview.ui \ properties.ui createtorrent.ui preview.ui \
login.ui downloadFromURL.ui addTorrentDialog.ui \ login.ui downloadFromURL.ui addTorrentDialog.ui \
search.ui rss.ui seeding.ui bandwidth_limit.ui \ search.ui rss.ui seeding.ui bandwidth_limit.ui \
download.ui download.ui engineSelect.ui
SOURCES += GUI.cpp \ SOURCES += GUI.cpp \
main.cpp \ main.cpp \
options_imp.cpp \ options_imp.cpp \
@ -166,5 +167,6 @@ SOURCES += GUI.cpp \
rss_imp.cpp \ rss_imp.cpp \
FinishedTorrents.cpp \ FinishedTorrents.cpp \
qtorrenthandle.cpp \ qtorrenthandle.cpp \
downloadingTorrents.cpp downloadingTorrents.cpp \
engineSelectDlg.cpp

21
src/update_qrc_files.py

@ -20,6 +20,27 @@ lang_file = open('lang.qrc', 'w')
lang_file.write(output) lang_file.write(output)
lang_file.close() 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 = '''<!DOCTYPE RCC><RCC version="1.0">
<qresource>
'''
for file in search_list:
output += ' <file>%s</file>'%(file)
output += os.linesep
output += '''</qresource>
</RCC>'''
search_file = open('search.qrc', 'w')
search_file.write(output)
search_file.close()
# update icons files directory # update icons files directory
icons_list = [] icons_list = []
for root, dirs, files in os.walk('Icons'): for root, dirs, files in os.walk('Icons'):

Loading…
Cancel
Save