Moved search code into a subfolder
132
src/searchengine/engineselect.ui
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>engineSelect</class>
|
||||
<widget class="QDialog" name="engineSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>541</width>
|
||||
<height>254</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</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>
|
||||
<column>
|
||||
<property name="text">
|
||||
<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: <a href="http://plugins.qbittorrent.org">http://plugins.qbittorrent.org</a></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::AutoText</enum>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</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>
|
||||
482
src/searchengine/engineselectdlg.cpp
Normal file
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#include "engineselectdlg.h"
|
||||
#include "downloadthread.h"
|
||||
#include "misc.h"
|
||||
#include "ico.h"
|
||||
#include "searchengine.h"
|
||||
#include "pluginsource.h"
|
||||
#include <QProcess>
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QFileDialog>
|
||||
#include <QDropEvent>
|
||||
#include <QInputDialog>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
enum EngineColumns {ENGINE_NAME, ENGINE_URL, ENGINE_STATE, ENGINE_ID};
|
||||
#define UPDATE_URL "http://qbittorrent.svn.sourceforge.net/viewvc/qbittorrent/trunk/src/nova/engines/"
|
||||
|
||||
engineSelectDlg::engineSelectDlg(QWidget *parent, SupportedEngines *supported_engines) : QDialog(parent), supported_engines(supported_engines) {
|
||||
setupUi(this);
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
pluginsTree->header()->resizeSection(0, 170);
|
||||
pluginsTree->header()->resizeSection(1, 220);
|
||||
pluginsTree->hideColumn(ENGINE_ID);
|
||||
actionEnable->setIcon(QIcon(QString::fromUtf8(":/Icons/oxygen/button_ok.png")));
|
||||
actionDisable->setIcon(QIcon(QString::fromUtf8(":/Icons/oxygen/button_cancel.png")));
|
||||
actionUninstall->setIcon(QIcon(QString::fromUtf8(":/Icons/oxygen/list-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)));
|
||||
loadSupportedSearchEngines();
|
||||
connect(supported_engines, SIGNAL(newSupportedEngine(QString)), this, SLOT(addNewEngine(QString)));
|
||||
connect(pluginsTree, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(toggleEngineState(QTreeWidgetItem*, int)));
|
||||
show();
|
||||
}
|
||||
|
||||
engineSelectDlg::~engineSelectDlg() {
|
||||
qDebug("Destroying engineSelectDlg");
|
||||
emit enginesChanged();
|
||||
qDebug("Before deleting downloader");
|
||||
delete downloader;
|
||||
qDebug("Engine plugins dialog destroyed");
|
||||
}
|
||||
|
||||
void engineSelectDlg::dropEvent(QDropEvent *event) {
|
||||
event->acceptProposedAction();
|
||||
QStringList files=event->mimeData()->text().split(QString::fromUtf8("\n"));
|
||||
QString file;
|
||||
foreach(file, files) {
|
||||
qDebug("dropped %s", qPrintable(file));
|
||||
#ifdef Q_WS_WIN
|
||||
file = file.replace("file:///", "");
|
||||
#else
|
||||
file = file.replace("file://", "");
|
||||
#endif
|
||||
if(file.startsWith("http://", Qt::CaseInsensitive) || file.startsWith("https://", Qt::CaseInsensitive) || file.startsWith("ftp://", Qt::CaseInsensitive)) {
|
||||
setCursor(QCursor(Qt::WaitCursor));
|
||||
downloader->downloadUrl(file);
|
||||
continue;
|
||||
}
|
||||
if(file.endsWith(".py", Qt::CaseInsensitive)) {
|
||||
QString plugin_name = file.split(QDir::separator()).last();
|
||||
plugin_name.replace(".py", "");
|
||||
installPlugin(file, plugin_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decode if we accept drag 'n drop or not
|
||||
void engineSelectDlg::dragEnterEvent(QDragEnterEvent *event) {
|
||||
QString mime;
|
||||
foreach(mime, event->mimeData()->formats()){
|
||||
qDebug("mimeData: %s", qPrintable(mime));
|
||||
}
|
||||
if (event->mimeData()->hasFormat(QString::fromUtf8("text/plain")) || event->mimeData()->hasFormat(QString::fromUtf8("text/uri-list"))) {
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
}
|
||||
|
||||
void engineSelectDlg::on_updateButton_clicked() {
|
||||
// Download version file from update server on sourceforge
|
||||
setCursor(QCursor(Qt::WaitCursor));
|
||||
downloader->downloadUrl(QString(UPDATE_URL)+"versions.txt");
|
||||
}
|
||||
|
||||
void engineSelectDlg::toggleEngineState(QTreeWidgetItem *item, int) {
|
||||
SupportedEngine *engine = supported_engines->value(item->text(ENGINE_ID));
|
||||
engine->setEnabled(!engine->isEnabled());
|
||||
if(engine->isEnabled()) {
|
||||
item->setText(ENGINE_STATE, tr("Yes"));
|
||||
setRowColor(pluginsTree->indexOfTopLevelItem(item), "green");
|
||||
} else {
|
||||
item->setText(ENGINE_STATE, tr("No"));
|
||||
setRowColor(pluginsTree->indexOfTopLevelItem(item), "red");
|
||||
}
|
||||
}
|
||||
|
||||
void engineSelectDlg::displayContextMenu(const QPoint&) {
|
||||
QMenu myContextMenu(this);
|
||||
// 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) {
|
||||
QString id = item->text(ENGINE_ID);
|
||||
if(supported_engines->value(id)->isEnabled() && !has_disable) {
|
||||
myContextMenu.addAction(actionDisable);
|
||||
has_disable = true;
|
||||
}
|
||||
if(!supported_engines->value(id)->isEnabled() && !has_enable) {
|
||||
myContextMenu.addAction(actionEnable);
|
||||
has_enable = true;
|
||||
}
|
||||
if(has_enable && has_disable) break;
|
||||
}
|
||||
myContextMenu.addSeparator();
|
||||
myContextMenu.addAction(actionUninstall);
|
||||
myContextMenu.exec(QCursor::pos());
|
||||
}
|
||||
|
||||
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 id = item->text(ENGINE_ID);
|
||||
if(QFile::exists(":/nova/engines/"+id+".py")) {
|
||||
error = true;
|
||||
// Disable it instead
|
||||
supported_engines->value(id)->setEnabled(false);
|
||||
item->setText(ENGINE_STATE, tr("No"));
|
||||
setRowColor(index, "red");
|
||||
continue;
|
||||
}else {
|
||||
// Proceed with uninstall
|
||||
// remove it from hard drive
|
||||
QDir enginesFolder(misc::searchEngineLocation()+QDir::separator()+"engines");
|
||||
QStringList filters;
|
||||
filters << id+".*";
|
||||
QStringList files = enginesFolder.entryList(filters, QDir::Files, QDir::Unsorted);
|
||||
QString file;
|
||||
foreach(file, files) {
|
||||
enginesFolder.remove(file);
|
||||
}
|
||||
// Remove it from supported engines
|
||||
delete supported_engines->take(id);
|
||||
delete item;
|
||||
change = true;
|
||||
}
|
||||
}
|
||||
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 successfully"));
|
||||
}
|
||||
|
||||
void engineSelectDlg::enableSelection() {
|
||||
QList<QTreeWidgetItem *> items = pluginsTree->selectedItems();
|
||||
QTreeWidgetItem *item;
|
||||
foreach(item, items) {
|
||||
int index = pluginsTree->indexOfTopLevelItem(item);
|
||||
Q_ASSERT(index != -1);
|
||||
QString id = item->text(ENGINE_ID);
|
||||
supported_engines->value(id)->setEnabled(true);
|
||||
item->setText(ENGINE_STATE, tr("Yes"));
|
||||
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);
|
||||
QString id = item->text(ENGINE_ID);
|
||||
supported_engines->value(id)->setEnabled(false);
|
||||
item->setText(ENGINE_STATE, tr("No"));
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
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), Qt::CaseInsensitive))
|
||||
res << item;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
QTreeWidgetItem* engineSelectDlg::findItemWithID(QString id){
|
||||
QList<QTreeWidgetItem*> res;
|
||||
for(int i=0; i<pluginsTree->topLevelItemCount(); ++i) {
|
||||
QTreeWidgetItem *item = pluginsTree->topLevelItem(i);
|
||||
if(id == item->text(ENGINE_ID))
|
||||
return item;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool engineSelectDlg::isUpdateNeeded(QString plugin_name, float new_version) const {
|
||||
float old_version = SearchEngine::getPluginVersion(misc::searchEngineLocation()+QDir::separator()+"engines"+QDir::separator()+plugin_name+".py");
|
||||
qDebug("IsUpdate needed? tobeinstalled: %.2f, alreadyinstalled: %.2f", new_version, old_version);
|
||||
return (new_version > old_version);
|
||||
}
|
||||
|
||||
void engineSelectDlg::installPlugin(QString path, QString plugin_name) {
|
||||
qDebug("Asked to install plugin at %s", qPrintable(path));
|
||||
float new_version = SearchEngine::getPluginVersion(path);
|
||||
qDebug("Version to be installed: %.2f", new_version);
|
||||
if(!isUpdateNeeded(plugin_name, new_version)) {
|
||||
qDebug("Apparently update is not needed, we have a more recent 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));
|
||||
return;
|
||||
}
|
||||
// Process with install
|
||||
QString dest_path = misc::searchEngineLocation()+QDir::separator()+"engines"+QDir::separator()+plugin_name+".py";
|
||||
bool update = false;
|
||||
if(QFile::exists(dest_path)) {
|
||||
// Backup in case install fails
|
||||
QFile::copy(dest_path, dest_path+".bak");
|
||||
misc::safeRemove(dest_path);
|
||||
misc::safeRemove(dest_path+"c");
|
||||
update = true;
|
||||
}
|
||||
// Copy the plugin
|
||||
QFile::copy(path, dest_path);
|
||||
// Update supported plugins
|
||||
supported_engines->update();
|
||||
// Check if this was correctly installed
|
||||
if(!supported_engines->contains(plugin_name)) {
|
||||
if(update) {
|
||||
// Remove broken file
|
||||
misc::safeRemove(dest_path);
|
||||
// restore backup
|
||||
QFile::copy(dest_path+".bak", dest_path);
|
||||
misc::safeRemove(dest_path+".bak");
|
||||
QMessageBox::warning(this, tr("Search plugin install")+" -- "+tr("qBittorrent"), tr("%1 search engine plugin could not be updated, keeping old version.", "%1 is the name of the search engine").arg(plugin_name));
|
||||
return;
|
||||
} else {
|
||||
// Remove broken file
|
||||
misc::safeRemove(dest_path);
|
||||
QMessageBox::warning(this, tr("Search plugin install")+" -- "+tr("qBittorrent"), tr("%1 search engine plugin could not be installed.", "%1 is the name of the search engine").arg(plugin_name));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Install was successful, remove backup
|
||||
if(update) {
|
||||
misc::safeRemove(dest_path+".bak");
|
||||
}
|
||||
if(update) {
|
||||
QMessageBox::information(this, tr("Search plugin install")+" -- "+tr("qBittorrent"), tr("%1 search engine plugin was successfully updated.", "%1 is the name of the search engine").arg(plugin_name));
|
||||
return;
|
||||
} else {
|
||||
QMessageBox::information(this, tr("Search plugin install")+" -- "+tr("qBittorrent"), tr("%1 search engine plugin was successfully installed.", "%1 is the name of the search engine").arg(plugin_name));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void engineSelectDlg::loadSupportedSearchEngines() {
|
||||
// Some clean up first
|
||||
pluginsTree->clear();
|
||||
foreach(QString name, supported_engines->keys()) {
|
||||
addNewEngine(name);
|
||||
}
|
||||
}
|
||||
|
||||
void engineSelectDlg::addNewEngine(QString engine_name) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(pluginsTree);
|
||||
SupportedEngine *engine = supported_engines->value(engine_name);
|
||||
item->setText(ENGINE_NAME, engine->getFullName());
|
||||
item->setText(ENGINE_URL, engine->getUrl());
|
||||
item->setText(ENGINE_ID, engine->getName());
|
||||
if(engine->isEnabled()) {
|
||||
item->setText(ENGINE_STATE, tr("Yes"));
|
||||
setRowColor(pluginsTree->indexOfTopLevelItem(item), "green");
|
||||
} else {
|
||||
item->setText(ENGINE_STATE, tr("No"));
|
||||
setRowColor(pluginsTree->indexOfTopLevelItem(item), "red");
|
||||
}
|
||||
// Handle icon
|
||||
QString iconPath = misc::searchEngineLocation()+QDir::separator()+"engines"+QDir::separator()+engine->getName()+".png";
|
||||
if(QFile::exists(iconPath)) {
|
||||
// Good, we already have the icon
|
||||
item->setData(ENGINE_NAME, Qt::DecorationRole, QVariant(QIcon(iconPath)));
|
||||
} else {
|
||||
iconPath = misc::searchEngineLocation()+QDir::separator()+"engines"+QDir::separator()+engine->getName()+".ico";
|
||||
if(QFile::exists(iconPath)) { // ICO support
|
||||
item->setData(ENGINE_NAME, Qt::DecorationRole, QVariant(QIcon(iconPath)));
|
||||
} else {
|
||||
// Icon is missing, we must download it
|
||||
downloader->downloadUrl(engine->getUrl()+"/favicon.ico");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void engineSelectDlg::on_installButton_clicked() {
|
||||
pluginSourceDlg *dlg = new pluginSourceDlg(this);
|
||||
connect(dlg, SIGNAL(askForLocalFile()), this, SLOT(askForLocalPlugin()));
|
||||
connect(dlg, SIGNAL(askForUrl()), this, SLOT(askForPluginUrl()));
|
||||
}
|
||||
|
||||
void engineSelectDlg::askForPluginUrl() {
|
||||
bool ok;
|
||||
QString url = QInputDialog::getText(this, tr("New search engine plugin URL"),
|
||||
tr("URL:"), QLineEdit::Normal,
|
||||
"http://", &ok);
|
||||
if (ok && !url.isEmpty()) {
|
||||
setCursor(QCursor(Qt::WaitCursor));
|
||||
downloader->downloadUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
void engineSelectDlg::askForLocalPlugin() {
|
||||
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", Qt::CaseInsensitive)) {
|
||||
QString plugin_name = path.split(QDir::separator()).last();
|
||||
plugin_name.replace(".py", "", Qt::CaseInsensitive);
|
||||
installPlugin(path, plugin_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool engineSelectDlg::parseVersionsFile(QString versions_file) {
|
||||
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;
|
||||
}
|
||||
bool updated = 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", qPrintable(plugin_name), version);
|
||||
if(!ok) continue;
|
||||
file_correct = true;
|
||||
if(isUpdateNeeded(plugin_name, version)) {
|
||||
qDebug("Plugin: %s is outdated", qPrintable(plugin_name));
|
||||
// Downloading update
|
||||
setCursor(QCursor(Qt::WaitCursor));
|
||||
downloader->downloadUrl(UPDATE_URL+plugin_name+".py");
|
||||
//downloader->downloadUrl(UPDATE_URL+plugin_name+".png");
|
||||
updated = true;
|
||||
}else {
|
||||
qDebug("Plugin: %s is up to date", qPrintable(plugin_name));
|
||||
}
|
||||
}
|
||||
// Close file
|
||||
versions.close();
|
||||
// Clean up tmp file
|
||||
misc::safeRemove(versions_file);
|
||||
if(file_correct && !updated) {
|
||||
QMessageBox::information(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), tr("All your plugins are already up to date."));
|
||||
}
|
||||
return file_correct;
|
||||
}
|
||||
|
||||
void engineSelectDlg::processDownloadedFile(QString url, QString filePath) {
|
||||
setCursor(QCursor(Qt::ArrowCursor));
|
||||
qDebug("engineSelectDlg received %s", qPrintable(url));
|
||||
if(url.endsWith("favicon.ico", Qt::CaseInsensitive)){
|
||||
// Icon downloaded
|
||||
QImage fileIcon;
|
||||
if(fileIcon.load(filePath)) {
|
||||
QList<QTreeWidgetItem*> items = findItemsWithUrl(url);
|
||||
QTreeWidgetItem *item;
|
||||
foreach(item, items){
|
||||
QString id = item->text(ENGINE_ID);
|
||||
QString iconPath;
|
||||
QFile icon(filePath);
|
||||
icon.open(QIODevice::ReadOnly);
|
||||
if(ICOHandler::canRead(&icon))
|
||||
iconPath = misc::searchEngineLocation()+QDir::separator()+"engines"+QDir::separator()+id+".ico";
|
||||
else
|
||||
iconPath = misc::searchEngineLocation()+QDir::separator()+"engines"+QDir::separator()+id+".png";
|
||||
QFile::copy(filePath, iconPath);
|
||||
item->setData(ENGINE_NAME, Qt::DecorationRole, QVariant(QIcon(iconPath)));
|
||||
}
|
||||
}
|
||||
// Delete tmp file
|
||||
misc::safeRemove(filePath);
|
||||
return;
|
||||
}
|
||||
if(url.endsWith("versions.txt")) {
|
||||
if(!parseVersionsFile(filePath)) {
|
||||
QMessageBox::warning(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), tr("Sorry, update server is temporarily unavailable."));
|
||||
}
|
||||
misc::safeRemove(filePath);
|
||||
return;
|
||||
}
|
||||
if(url.endsWith(".py", Qt::CaseInsensitive)) {
|
||||
QString plugin_name = url.split('/').last();
|
||||
plugin_name.replace(".py", "");
|
||||
installPlugin(filePath, plugin_name);
|
||||
misc::safeRemove(filePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void engineSelectDlg::handleDownloadFailure(QString url, QString reason) {
|
||||
setCursor(QCursor(Qt::ArrowCursor));
|
||||
if(url.endsWith("favicon.ico", Qt::CaseInsensitive)){
|
||||
qDebug("Could not download favicon: %s, reason: %s", qPrintable(url), qPrintable(reason));
|
||||
return;
|
||||
}
|
||||
if(url.endsWith("versions.txt")) {
|
||||
QMessageBox::warning(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), tr("Sorry, update server is temporarily unavailable."));
|
||||
return;
|
||||
}
|
||||
if(url.endsWith(".py", Qt::CaseInsensitive)) {
|
||||
// a plugin update download has been failed
|
||||
QString plugin_name = url.split('/').last();
|
||||
plugin_name.replace(".py", "", Qt::CaseInsensitive);
|
||||
QMessageBox::warning(this, tr("Search plugin update")+" -- "+tr("qBittorrent"), tr("Sorry, %1 search plugin install failed.", "%1 is the name of the search engine").arg(plugin_name));
|
||||
}
|
||||
}
|
||||
81
src/searchengine/engineselectdlg.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#ifndef ENGINE_SELECT_DLG_H
|
||||
#define ENGINE_SELECT_DLG_H
|
||||
|
||||
#include "ui_engineselect.h"
|
||||
#include "supportedengines.h"
|
||||
|
||||
class downloadThread;
|
||||
class QDropEvent;
|
||||
|
||||
class engineSelectDlg : public QDialog, public Ui::engineSelect{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
downloadThread *downloader;
|
||||
SupportedEngines *supported_engines;
|
||||
|
||||
public:
|
||||
engineSelectDlg(QWidget *parent, SupportedEngines *supported_engines);
|
||||
~engineSelectDlg();
|
||||
QList<QTreeWidgetItem*> findItemsWithUrl(QString url);
|
||||
QTreeWidgetItem* findItemWithID(QString id);
|
||||
|
||||
protected:
|
||||
bool parseVersionsFile(QString versions_file);
|
||||
bool isUpdateNeeded(QString plugin_name, float new_version) const;
|
||||
|
||||
signals:
|
||||
void enginesChanged();
|
||||
|
||||
protected slots:
|
||||
void on_closeButton_clicked();
|
||||
void loadSupportedSearchEngines();
|
||||
void addNewEngine(QString engine_name);
|
||||
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();
|
||||
void dropEvent(QDropEvent *event);
|
||||
void dragEnterEvent(QDragEnterEvent *event);
|
||||
void installPlugin(QString plugin_path, QString plugin_name);
|
||||
void askForLocalPlugin();
|
||||
void askForPluginUrl();
|
||||
};
|
||||
|
||||
#endif
|
||||
0
src/searchengine/nova/__init__.py
Normal file
0
src/searchengine/nova/engines/__init__.py
Normal file
BIN
src/searchengine/nova/engines/btjunkie.png
Normal file
|
After Width: | Height: | Size: 622 B |
119
src/searchengine/nova/engines/btjunkie.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#VERSION: 2.23
|
||||
#AUTHORS: Christophe Dumez (chris@qbittorrent.org)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from novaprinter import prettyPrinter
|
||||
from helpers import retrieve_url, download_file
|
||||
import sgmllib
|
||||
import re
|
||||
|
||||
class btjunkie(object):
|
||||
url = 'http://btjunkie.org'
|
||||
name = 'btjunkie'
|
||||
supported_categories = {'all': '0', 'movies': '6', 'tv': '4', 'music': '1', 'games': '2', 'anime': '7', 'software': '3'}
|
||||
|
||||
def __init__(self):
|
||||
self.results = []
|
||||
self.parser = self.SimpleSGMLParser(self.results, self.url)
|
||||
|
||||
def download_torrent(self, info):
|
||||
print download_file(info)
|
||||
|
||||
class SimpleSGMLParser(sgmllib.SGMLParser):
|
||||
def __init__(self, results, url, *args):
|
||||
sgmllib.SGMLParser.__init__(self)
|
||||
self.url = url
|
||||
self.th_counter = None
|
||||
self.current_item = None
|
||||
self.results = results
|
||||
|
||||
def start_a(self, attr):
|
||||
params = dict(attr)
|
||||
#print params
|
||||
if params.has_key('href') and params['href'].startswith("http://dl.btjunkie.org/torrent"):
|
||||
self.current_item = {}
|
||||
self.th_counter = 0
|
||||
self.current_item['link']=params['href'].strip()
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.th_counter == 0:
|
||||
if not self.current_item.has_key('name'):
|
||||
self.current_item['name'] = ''
|
||||
self.current_item['name']+= data.strip()
|
||||
elif self.th_counter == 3:
|
||||
if not self.current_item.has_key('size'):
|
||||
self.current_item['size'] = ''
|
||||
self.current_item['size']+= data.strip()
|
||||
elif self.th_counter == 5:
|
||||
if not self.current_item.has_key('seeds'):
|
||||
self.current_item['seeds'] = ''
|
||||
self.current_item['seeds']+= data.strip()
|
||||
elif self.th_counter == 6:
|
||||
if not self.current_item.has_key('leech'):
|
||||
self.current_item['leech'] = ''
|
||||
self.current_item['leech']+= data.strip()
|
||||
|
||||
def start_th(self,attr):
|
||||
if isinstance(self.th_counter,int):
|
||||
self.th_counter += 1
|
||||
if self.th_counter > 6:
|
||||
self.th_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, cat='all'):
|
||||
# Remove {} since btjunkie does not seem
|
||||
# to handle those very well
|
||||
what = what.replace('{', '').replace('}', '')
|
||||
ret = []
|
||||
i = 1
|
||||
while True and i<11:
|
||||
results = []
|
||||
parser = self.SimpleSGMLParser(results, self.url)
|
||||
dat = retrieve_url(self.url+'/search?q=%s&c=%s&o=52&p=%d'%(what, self.supported_categories[cat], i))
|
||||
# Remove <font> tags from page
|
||||
p = re.compile( '<[/]?font.*?>')
|
||||
dat = p.sub('', dat)
|
||||
#print dat
|
||||
#return
|
||||
results_re = re.compile('(?s)class="tab_results">.*')
|
||||
for match in results_re.finditer(dat):
|
||||
res_tab = match.group(0)
|
||||
parser.feed(res_tab)
|
||||
parser.close()
|
||||
break
|
||||
if len(results) <= 0:
|
||||
break
|
||||
i += 1
|
||||
|
||||
BIN
src/searchengine/nova/engines/isohunt.png
Normal file
|
After Width: | Height: | Size: 633 B |
67
src/searchengine/nova/engines/isohunt.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#VERSION: 1.32
|
||||
#AUTHORS: Christophe Dumez (chris@qbittorrent.org)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from novaprinter import prettyPrinter
|
||||
import re
|
||||
from helpers import retrieve_url, download_file
|
||||
|
||||
class isohunt(object):
|
||||
url = 'http://isohunt.com'
|
||||
name = 'isoHunt'
|
||||
supported_categories = {'all': '', 'movies': '1', 'tv': '3', 'music': '2', 'games': '4', 'anime': '7', 'software': '5', 'pictures': '6', 'books': '9'}
|
||||
|
||||
def download_torrent(self, info):
|
||||
print download_file(info)
|
||||
|
||||
def search(self, what, cat='all'):
|
||||
# Remove {} since isohunt does not seem
|
||||
# to handle those very well
|
||||
what = what.replace('{', '').replace('}', '')
|
||||
i = 1
|
||||
while True and i<11:
|
||||
res = 0
|
||||
dat = retrieve_url(self.url+'/torrents.php?ihq=%s&iht=%s&ihp=%s&ihs1=2&iho1=d'%(what, self.supported_categories[cat],i))
|
||||
# I know it's not very readable, but the SGML parser feels in pain
|
||||
section_re = re.compile('(?s)id=link.*?</tr><tr')
|
||||
torrent_re = re.compile('(?s)torrent_details/(?P<link>.*?[^/]+).*?'
|
||||
'>(?P<name>.*?)</a>.*?'
|
||||
'>(?P<size>[\d,\.]+\s+MB)</td>.*?'
|
||||
'>(?P<seeds>\d+)</td>.*?'
|
||||
'>(?P<leech>\d+)</td>')
|
||||
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'] = 'http://isohunt.com/download/'+torrent_infos['link']
|
||||
prettyPrinter(torrent_infos)
|
||||
res = res + 1
|
||||
if res == 0:
|
||||
break
|
||||
i = i + 1
|
||||
BIN
src/searchengine/nova/engines/mininova.png
Normal file
|
After Width: | Height: | Size: 365 B |
111
src/searchengine/nova/engines/mininova.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#VERSION: 1.40
|
||||
#AUTHORS: Christophe Dumez (chris@qbittorrent.org)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from novaprinter import prettyPrinter
|
||||
from helpers import retrieve_url, download_file
|
||||
import sgmllib
|
||||
import re
|
||||
|
||||
class mininova(object):
|
||||
# Mandatory properties
|
||||
url = 'http://www.mininova.org'
|
||||
name = 'Mininova'
|
||||
supported_categories = {'all': '0', 'movies': '4', 'tv': '8', 'music': '5', 'games': '3', 'anime': '1', 'software': '7', 'pictures': '6', 'books': '2'}
|
||||
|
||||
def __init__(self):
|
||||
self.results = []
|
||||
self.parser = self.SimpleSGMLParser(self.results, self.url)
|
||||
|
||||
def download_torrent(self, info):
|
||||
print download_file(info)
|
||||
|
||||
class SimpleSGMLParser(sgmllib.SGMLParser):
|
||||
def __init__(self, results, url, *args):
|
||||
sgmllib.SGMLParser.__init__(self)
|
||||
self.url = url
|
||||
self.td_counter = None
|
||||
self.current_item = None
|
||||
self.results = results
|
||||
|
||||
def start_a(self, attr):
|
||||
params = dict(attr)
|
||||
#print params
|
||||
if params.has_key('href') and params['href'].startswith("/get/"):
|
||||
self.current_item = {}
|
||||
self.td_counter = 0
|
||||
self.current_item['link']=self.url+params['href'].strip()
|
||||
|
||||
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
|
||||
elif 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 > 4:
|
||||
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, cat='all'):
|
||||
ret = []
|
||||
i = 1
|
||||
while True and i<11:
|
||||
results = []
|
||||
parser = self.SimpleSGMLParser(results, self.url)
|
||||
dat = retrieve_url(self.url+'/search/%s/%s/seeds/%d'%(what, self.supported_categories[cat], i))
|
||||
results_re = re.compile('(?s)<h1>Search results for.*')
|
||||
for match in results_re.finditer(dat):
|
||||
res_tab = match.group(0)
|
||||
parser.feed(res_tab)
|
||||
parser.close()
|
||||
break
|
||||
if len(results) <= 0:
|
||||
break
|
||||
i += 1
|
||||
|
||||
BIN
src/searchengine/nova/engines/piratebay.png
Normal file
|
After Width: | Height: | Size: 609 B |
113
src/searchengine/nova/engines/piratebay.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#VERSION: 1.30
|
||||
#AUTHORS: Fabien Devaux (fab@gnux.info)
|
||||
#CONTRIBUTORS: Christophe Dumez (chris@qbittorrent.org)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from novaprinter import prettyPrinter
|
||||
import sgmllib
|
||||
from helpers import retrieve_url, download_file
|
||||
|
||||
class piratebay(object):
|
||||
url = 'http://thepiratebay.org'
|
||||
name = 'The Pirate Bay'
|
||||
supported_categories = {'all': '0', 'movies': '200', 'music': '100', 'games': '400', 'software': '300'}
|
||||
|
||||
def __init__(self):
|
||||
self.results = []
|
||||
self.parser = self.SimpleSGMLParser(self.results, self.url)
|
||||
|
||||
def download_torrent(self, info):
|
||||
print download_file(info)
|
||||
|
||||
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
|
||||
self.in_name = None
|
||||
|
||||
def start_a(self, attr):
|
||||
params = dict(attr)
|
||||
if params['href'].startswith('/torrent/'):
|
||||
self.current_item = {}
|
||||
self.td_counter = 0
|
||||
self.code = params['href'].split('/')[2]
|
||||
self.in_name = True
|
||||
elif params['href'].startswith('http://torrents.thepiratebay.org/%s'%self.code):
|
||||
self.current_item['link']=params['href'].strip()
|
||||
self.in_name = False
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.td_counter == 0:
|
||||
if self.in_name:
|
||||
if not self.current_item.has_key('name'):
|
||||
self.current_item['name'] = ''
|
||||
self.current_item['name']+= data.strip()
|
||||
else:
|
||||
#Parse size
|
||||
if 'Size' in data:
|
||||
self.current_item['size'] = data[data.index("Size")+5:]
|
||||
self.current_item['size'] = self.current_item['size'][:self.current_item['size'].index(',')]
|
||||
elif self.td_counter == 1:
|
||||
if not self.current_item.has_key('seeds'):
|
||||
self.current_item['seeds'] = ''
|
||||
self.current_item['seeds']+= data.strip()
|
||||
elif self.td_counter == 2:
|
||||
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 > 3:
|
||||
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, cat='all'):
|
||||
ret = []
|
||||
i = 0
|
||||
order = 'se'
|
||||
while True and i<11:
|
||||
results = []
|
||||
parser = self.SimpleSGMLParser(results, self.url)
|
||||
dat = retrieve_url(self.url+'/search/%s/%u/7/%s' % (what, i, self.supported_categories[cat]))
|
||||
print self.url+'/search/%s/%u/7/%s' % (what, i, self.supported_categories[cat])
|
||||
parser.feed(dat)
|
||||
parser.close()
|
||||
if len(results) <= 0:
|
||||
break
|
||||
i += 1
|
||||
BIN
src/searchengine/nova/engines/torrentdownloads.png
Normal file
|
After Width: | Height: | Size: 423 B |
140
src/searchengine/nova/engines/torrentdownloads.py
Normal file
@@ -0,0 +1,140 @@
|
||||
#VERSION: 1.06
|
||||
#AUTHORS: Christophe Dumez (chris@qbittorrent.org)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from novaprinter import prettyPrinter
|
||||
from helpers import retrieve_url
|
||||
import StringIO, gzip, urllib2, tempfile
|
||||
import sgmllib
|
||||
import re
|
||||
import os
|
||||
|
||||
class torrentdownloads(object):
|
||||
url = 'http://www.torrentdownloads.net'
|
||||
name = 'TorrentDownloads'
|
||||
supported_categories = {'all': '0', 'movies': '4', 'tv': '8', 'music': '5', 'games': '3', 'anime': '1', 'software': '7', 'books': '2'}
|
||||
|
||||
def __init__(self):
|
||||
self.results = []
|
||||
#self.parser = self.SimpleSGMLParser(self.results, self.url)
|
||||
|
||||
def download_torrent(self, url):
|
||||
""" Download file at url and write it to a file, return the path to the file and the url """
|
||||
file, path = tempfile.mkstemp()
|
||||
file = os.fdopen(file, "w")
|
||||
# Download url
|
||||
req = urllib2.Request(url)
|
||||
response = urllib2.urlopen(req)
|
||||
dat = response.read()
|
||||
# Check if it is gzipped
|
||||
if dat[:2] == '\037\213':
|
||||
# Data is gzip encoded, decode it
|
||||
compressedstream = StringIO.StringIO(dat)
|
||||
gzipper = gzip.GzipFile(fileobj=compressedstream)
|
||||
extracted_data = gzipper.read()
|
||||
dat = extracted_data
|
||||
|
||||
# Write it to a file
|
||||
file.write(dat.strip())
|
||||
file.close()
|
||||
# return file path
|
||||
print path+" "+url
|
||||
|
||||
class SimpleSGMLParser(sgmllib.SGMLParser):
|
||||
def __init__(self, results, url, what, *args):
|
||||
sgmllib.SGMLParser.__init__(self)
|
||||
self.url = url
|
||||
self.li_counter = None
|
||||
self.current_item = None
|
||||
self.results = results
|
||||
self.what = what.upper().split('+')
|
||||
if len(self.what) == 0:
|
||||
self.what = None
|
||||
|
||||
def start_a(self, attr):
|
||||
params = dict(attr)
|
||||
#print params
|
||||
if params.has_key('href') and params['href'].startswith("http://www.torrentdownloads.net/torrent/"):
|
||||
self.current_item = {}
|
||||
self.li_counter = 0
|
||||
self.current_item['link']=params['href'].strip().replace('/torrent', '/download', 1)
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.li_counter == 0:
|
||||
if not self.current_item.has_key('name'):
|
||||
self.current_item['name'] = ''
|
||||
self.current_item['name']+= data
|
||||
elif self.li_counter == 1:
|
||||
if not self.current_item.has_key('size'):
|
||||
self.current_item['size'] = ''
|
||||
self.current_item['size']+= data.strip().replace(" ", " ")
|
||||
elif self.li_counter == 2:
|
||||
if not self.current_item.has_key('seeds'):
|
||||
self.current_item['seeds'] = ''
|
||||
self.current_item['seeds']+= data.strip()
|
||||
elif self.li_counter == 3:
|
||||
if not self.current_item.has_key('leech'):
|
||||
self.current_item['leech'] = ''
|
||||
self.current_item['leech']+= data.strip()
|
||||
|
||||
def start_li(self,attr):
|
||||
if isinstance(self.li_counter,int):
|
||||
self.li_counter += 1
|
||||
if self.li_counter > 3:
|
||||
self.li_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
|
||||
# Search should use AND operator as a default
|
||||
tmp = self.current_item['name'].upper();
|
||||
if self.what is not None:
|
||||
for w in self.what:
|
||||
if tmp.find(w) < 0: return
|
||||
prettyPrinter(self.current_item)
|
||||
self.results.append('a')
|
||||
|
||||
def search(self, what, cat='all'):
|
||||
ret = []
|
||||
i = 1
|
||||
while i<11:
|
||||
results = []
|
||||
parser = self.SimpleSGMLParser(results, self.url, what)
|
||||
dat = retrieve_url(self.url+'/search/?page=%d&search=%s&s_cat=%s&srt=seeds&pp=50&order=desc'%(i, what, self.supported_categories[cat]))
|
||||
results_re = re.compile('(?s)<div class="torrentlist">.*')
|
||||
for match in results_re.finditer(dat):
|
||||
res_tab = match.group(0)
|
||||
parser.feed(res_tab)
|
||||
parser.close()
|
||||
break
|
||||
if len(results) <= 0:
|
||||
break
|
||||
i += 1
|
||||
|
||||
BIN
src/searchengine/nova/engines/torrentreactor.png
Normal file
|
After Width: | Height: | Size: 529 B |
105
src/searchengine/nova/engines/torrentreactor.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#VERSION: 1.21
|
||||
#AUTHORS: Gekko Dam Beer (gekko04@users.sourceforge.net)
|
||||
#CONTRIBUTORS: Christophe Dumez (chris@qbittorrent.org)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from novaprinter import prettyPrinter
|
||||
import sgmllib
|
||||
from helpers import retrieve_url, download_file
|
||||
|
||||
class torrentreactor(object):
|
||||
url = 'http://www.torrentreactor.net'
|
||||
name = 'TorrentReactor.Net'
|
||||
supported_categories = {'all': '', 'movies': '5', 'tv': '8', 'music': '6', 'games': '3', 'anime': '1', 'software': '2'}
|
||||
|
||||
def download_torrent(self, info):
|
||||
print download_file(info)
|
||||
|
||||
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 'torrentreactor.net/download.php' in params['href']:
|
||||
self.current_item = {}
|
||||
self.td_counter = 0
|
||||
self.current_item['link'] = params['href'].strip()
|
||||
|
||||
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 > 3:
|
||||
self.td_counter = None
|
||||
# add item to results
|
||||
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.has_results = True
|
||||
self.results.append('a')
|
||||
|
||||
def __init__(self):
|
||||
self.results = []
|
||||
self.parser = self.SimpleSGMLParser(self.results, self.url)
|
||||
|
||||
def search(self, what, cat='all'):
|
||||
i = 0
|
||||
while True and i<11:
|
||||
results = []
|
||||
parser = self.SimpleSGMLParser(results, self.url)
|
||||
dat = retrieve_url(self.url+'/search.php?search=&words=%s&cid=%s&sid=&type=2&orderby=a.seeds&asc=0&skip=%s'%(what, self.supported_categories[cat], (i*35)))
|
||||
parser.feed(dat)
|
||||
parser.close()
|
||||
if len(results) <= 0:
|
||||
break
|
||||
i += 1
|
||||
7
src/searchengine/nova/engines/versions.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
isohunt: 1.32
|
||||
torrentreactor: 1.21
|
||||
btjunkie: 2.23
|
||||
mininova: 1.40
|
||||
piratebay: 1.30
|
||||
vertor: 1.1
|
||||
torrentdownloads: 1.06
|
||||
BIN
src/searchengine/nova/engines/vertor.png
Normal file
|
After Width: | Height: | Size: 643 B |
124
src/searchengine/nova/engines/vertor.py
Executable file
@@ -0,0 +1,124 @@
|
||||
#VERSION: 1.1
|
||||
#AUTHORS: Christophe Dumez (chris@qbittorrent.org)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from novaprinter import prettyPrinter
|
||||
from helpers import retrieve_url, download_file
|
||||
import sgmllib
|
||||
import re
|
||||
|
||||
class vertor(object):
|
||||
url = 'http://www.vertor.com'
|
||||
name = 'vertor'
|
||||
supported_categories = {'all': '0', 'movies': '5', 'tv': '8', 'music': '6', 'games': '3', 'anime': '1', 'software': '2'}
|
||||
|
||||
def __init__(self):
|
||||
self.results = []
|
||||
self.parser = self.SimpleSGMLParser(self.results, self.url)
|
||||
|
||||
def download_torrent(self, info):
|
||||
print download_file(info)
|
||||
|
||||
class SimpleSGMLParser(sgmllib.SGMLParser):
|
||||
def __init__(self, results, url, *args):
|
||||
sgmllib.SGMLParser.__init__(self)
|
||||
self.url = url
|
||||
self.td_counter = None
|
||||
self.current_item = None
|
||||
self.results = results
|
||||
self.in_name = False
|
||||
self.outside_td = True;
|
||||
|
||||
def start_a(self, attr):
|
||||
params = dict(attr)
|
||||
#print params
|
||||
if params.has_key('href') and params['href'].startswith("http://www.vertor.com/index.php?mod=download"):
|
||||
self.current_item = {}
|
||||
self.td_counter = 0
|
||||
self.current_item['link']=params['href'].strip()
|
||||
elif self.td_counter == 0 and params.has_key('href') and params['href'].startswith("/torrents/") \
|
||||
and not self.current_item.has_key('name'):
|
||||
self.in_name = True
|
||||
|
||||
def end_a(self):
|
||||
if self.in_name:
|
||||
self.in_name = False
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.in_name:
|
||||
if not self.current_item.has_key('name'):
|
||||
self.current_item['name'] = ''
|
||||
self.current_item['name']+= data.strip()
|
||||
elif self.td_counter == 2 and not self.outside_td:
|
||||
if not self.current_item.has_key('size'):
|
||||
self.current_item['size'] = ''
|
||||
self.current_item['size']+= data.strip()
|
||||
elif self.td_counter == 4 and not self.outside_td:
|
||||
if not self.current_item.has_key('seeds'):
|
||||
self.current_item['seeds'] = ''
|
||||
self.current_item['seeds']+= data.strip()
|
||||
elif self.td_counter == 5 and not self.outside_td:
|
||||
if not self.current_item.has_key('leech'):
|
||||
self.current_item['leech'] = ''
|
||||
self.current_item['leech']+= data.strip()
|
||||
|
||||
def end_td(self):
|
||||
self.outside_td = True
|
||||
|
||||
def start_td(self,attr):
|
||||
if isinstance(self.td_counter,int):
|
||||
self.outside_td = False
|
||||
self.td_counter += 1
|
||||
if self.td_counter > 5:
|
||||
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, cat='all'):
|
||||
ret = []
|
||||
i = 0
|
||||
while True and i<11:
|
||||
results = []
|
||||
parser = self.SimpleSGMLParser(results, self.url)
|
||||
dat = retrieve_url(self.url+'/index.php?mod=search&words=%s&cid=%s&orderby=a.seeds&asc=0&search=&exclude=&p=%d'%(what, self.supported_categories[cat], i))
|
||||
results_re = re.compile('(?s)<table class="listing">.*')
|
||||
for match in results_re.finditer(dat):
|
||||
res_tab = match.group(0)
|
||||
parser.feed(res_tab)
|
||||
parser.close()
|
||||
break
|
||||
if len(results) <= 0:
|
||||
break
|
||||
i += 1
|
||||
|
||||
99
src/searchengine/nova/helpers.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#VERSION: 1.31
|
||||
|
||||
# Author:
|
||||
# Christophe DUMEZ (chris@qbittorrent.org)
|
||||
|
||||
import re, htmlentitydefs
|
||||
import tempfile
|
||||
import os
|
||||
import StringIO, gzip, urllib2
|
||||
import socket
|
||||
import socks
|
||||
import re
|
||||
|
||||
# SOCKS5 Proxy support
|
||||
if os.environ.has_key("sock_proxy") and len(os.environ["sock_proxy"].strip()) > 0:
|
||||
proxy_str = os.environ["sock_proxy"].strip()
|
||||
m=re.match(r"^(?:(?P<username>[^:]+):(?P<password>[^@]+)@)?(?P<host>[^:]+):(?P<port>\w+)$", proxy_str)
|
||||
if m is not None:
|
||||
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, m.group('host'), int(m.group('port')), True, m.group('username'), m.group('password'))
|
||||
socket.socket = socks.socksocket
|
||||
|
||||
def htmlentitydecode(s):
|
||||
# First convert alpha entities (such as é)
|
||||
# (Inspired from http://mail.python.org/pipermail/python-list/2007-June/443813.html)
|
||||
def entity2char(m):
|
||||
entity = m.group(1)
|
||||
if entity in htmlentitydefs.name2codepoint:
|
||||
return unichr(htmlentitydefs.name2codepoint[entity])
|
||||
return u" " # Unknown entity: We replace with a space.
|
||||
t = re.sub(u'&(%s);' % u'|'.join(htmlentitydefs.name2codepoint), entity2char, s)
|
||||
|
||||
# Then convert numerical entities (such as é)
|
||||
t = re.sub(u'&#(\d+);', lambda x: unichr(int(x.group(1))), t)
|
||||
|
||||
# Then convert hexa entities (such as é)
|
||||
return re.sub(u'&#x(\w+);', lambda x: unichr(int(x.group(1),16)), t)
|
||||
|
||||
def retrieve_url(url):
|
||||
""" Return the content of the url page as a string """
|
||||
req = urllib2.Request(url)
|
||||
response = urllib2.urlopen(req)
|
||||
dat = response.read()
|
||||
info = response.info()
|
||||
charset = 'utf-8'
|
||||
try:
|
||||
ignore, charset = info['Content-Type'].split('charset=')
|
||||
except:
|
||||
pass
|
||||
dat = dat.decode(charset, 'replace')
|
||||
dat = htmlentitydecode(dat)
|
||||
return dat.encode('utf-8', 'replace')
|
||||
|
||||
def download_file(url, referer=None):
|
||||
""" Download file at url and write it to a file, return the path to the file and the url """
|
||||
file, path = tempfile.mkstemp()
|
||||
file = os.fdopen(file, "w")
|
||||
# Download url
|
||||
req = urllib2.Request(url)
|
||||
if referer is not None:
|
||||
req.add_header('referer', referer)
|
||||
response = urllib2.urlopen(req)
|
||||
dat = response.read()
|
||||
# Check if it is gzipped
|
||||
if dat[:2] == '\037\213':
|
||||
# Data is gzip encoded, decode it
|
||||
compressedstream = StringIO.StringIO(dat)
|
||||
gzipper = gzip.GzipFile(fileobj=compressedstream)
|
||||
extracted_data = gzipper.read()
|
||||
dat = extracted_data
|
||||
|
||||
# Write it to a file
|
||||
file.write(dat)
|
||||
file.close()
|
||||
# return file path
|
||||
return path+" "+url
|
||||
157
src/searchengine/nova/nova2.py
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
#VERSION: 1.23
|
||||
|
||||
# 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
|
||||
CATEGORIES = ('all', 'movies', 'tv', 'music', 'games', 'anime', 'software', 'pictures', 'books')
|
||||
|
||||
################################################################################
|
||||
# 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
|
||||
|
||||
def engineToXml(short_name):
|
||||
xml = "<%s>\n"%short_name
|
||||
exec "engine = %s()"%short_name
|
||||
xml += "<name>%s</name>\n"%engine.name
|
||||
xml += "<url>%s</url>\n"%engine.url
|
||||
xml += "<categories>"
|
||||
if hasattr(engine, 'supported_categories'):
|
||||
supported_categories = engine.supported_categories.keys()
|
||||
supported_categories.remove('all')
|
||||
xml += " ".join(supported_categories)
|
||||
xml += "</categories>\n"
|
||||
xml += "</%s>\n"%short_name
|
||||
return xml
|
||||
|
||||
def displayCapabilities():
|
||||
"""
|
||||
Display capabilities in XML format
|
||||
<capabilities>
|
||||
<engine_short_name>
|
||||
<name>long name</name>
|
||||
<url>http://example.com</url>
|
||||
<categories>movies music games</categories>
|
||||
</engine_short_name>
|
||||
</capabilities>
|
||||
"""
|
||||
xml = "<capabilities>"
|
||||
for short_name in supported_engines:
|
||||
xml += engineToXml(short_name)
|
||||
xml += "</capabilities>"
|
||||
print xml
|
||||
|
||||
class EngineLauncher(threading.Thread):
|
||||
def __init__(self, engine, what, cat='all'):
|
||||
threading.Thread.__init__(self)
|
||||
self.engine = engine
|
||||
self.what = what
|
||||
self.cat = cat
|
||||
def run(self):
|
||||
if hasattr(self.engine, 'supported_categories'):
|
||||
if self.cat == 'all' or self.cat in self.engine.supported_categories.keys():
|
||||
self.engine.search(self.what, self.cat)
|
||||
elif self.cat == 'all':
|
||||
self.engine.search(self.what)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit('./nova2.py [all|engine1[,engine2]*] <category> <keywords>\navailable engines: %s'%
|
||||
(','.join(supported_engines)))
|
||||
|
||||
if len(sys.argv) == 2:
|
||||
if sys.argv[1] == "--capabilities":
|
||||
displayCapabilities()
|
||||
sys.exit(0)
|
||||
else:
|
||||
raise SystemExit('./nova.py [all|engine1[,engine2]*] <category> <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
|
||||
|
||||
cat = sys.argv[2].lower()
|
||||
|
||||
if cat not in CATEGORIES:
|
||||
raise SystemExit('Invalid category!')
|
||||
|
||||
what = '+'.join(sys.argv[3:])
|
||||
|
||||
threads = []
|
||||
for engine in engines_list:
|
||||
try:
|
||||
if THREADED:
|
||||
exec "l = EngineLauncher(%s(), what, cat)"%engine
|
||||
threads.append(l)
|
||||
l.start()
|
||||
else:
|
||||
exec "e = %s()"%engine
|
||||
if hasattr(engine, 'supported_categories'):
|
||||
if cat == 'all' or cat in e.supported_categories.keys():
|
||||
e.search(what, cat)
|
||||
elif self.cat == 'all':
|
||||
e.search(what)
|
||||
engine().search(what, cat)
|
||||
except:
|
||||
pass
|
||||
if THREADED:
|
||||
for t in threads:
|
||||
t.join()
|
||||
64
src/searchengine/nova/nova2dl.py
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#VERSION: 1.10
|
||||
|
||||
# Author:
|
||||
# Christophe DUMEZ (chris@qbittorrent.org)
|
||||
|
||||
import sys
|
||||
import os
|
||||
import glob
|
||||
from helpers import download_file
|
||||
|
||||
supported_engines = dict()
|
||||
|
||||
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)
|
||||
exec "engine_url = %s.url"%e
|
||||
supported_engines[engine_url] = e
|
||||
except:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 3:
|
||||
raise SystemExit('./nova2dl.py engine_url download_parameter')
|
||||
engine_url = sys.argv[1].strip()
|
||||
download_param = sys.argv[2].strip()
|
||||
if engine_url not in supported_engines.keys():
|
||||
raise SystemExit('./nova2dl.py: this engine_url was not recognized')
|
||||
exec "engine = %s()"%supported_engines[engine_url]
|
||||
if hasattr(engine, 'download_torrent'):
|
||||
engine.download_torrent(download_param)
|
||||
else:
|
||||
print download_file(download_param)
|
||||
sys.exit(0)
|
||||
66
src/searchengine/nova/novaprinter.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#VERSION: 1.33
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors may be
|
||||
# used to endorse or promote products derived from this software without
|
||||
# specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import sys, codecs
|
||||
|
||||
# Force UTF-8 printing
|
||||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
|
||||
|
||||
def prettyPrinter(dictionary):
|
||||
# Convert everything to unicode for safe printing
|
||||
for key,value in dictionary.items():
|
||||
if isinstance(dictionary[key], str):
|
||||
dictionary[key] = unicode(dictionary[key], 'utf-8')
|
||||
dictionary['size'] = anySizeToBytes(dictionary['size'])
|
||||
print u"%s|%s|%s|%s|%s|%s"%(dictionary['link'],dictionary['name'],dictionary['size'],dictionary['seeds'],dictionary['leech'],dictionary['engine_url'])
|
||||
|
||||
def anySizeToBytes(size_string):
|
||||
"""
|
||||
Convert a string like '1 KB' to '1024' (bytes)
|
||||
"""
|
||||
# separate integer from unit
|
||||
try:
|
||||
size, unit = size_string.split()
|
||||
except:
|
||||
try:
|
||||
size = size_string.strip()
|
||||
unit = ''.join([c for c in size if c.isalpha()])
|
||||
if len(unit) > 0:
|
||||
size = size[:-len(unit)]
|
||||
except:
|
||||
return -1
|
||||
if len(size) == 0:
|
||||
return -1
|
||||
size = float(size)
|
||||
if len(unit) == 0:
|
||||
return int(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)
|
||||
387
src/searchengine/nova/socks.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""SocksiPy - Python SOCKS module.
|
||||
Version 1.00
|
||||
|
||||
Copyright 2006 Dan-Haim. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of Dan Haim nor the names of his contributors may be used
|
||||
to endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
|
||||
|
||||
|
||||
This module provides a standard socket-like interface for Python
|
||||
for tunneling connections through SOCKS proxies.
|
||||
|
||||
"""
|
||||
|
||||
import socket
|
||||
import struct
|
||||
|
||||
PROXY_TYPE_SOCKS4 = 1
|
||||
PROXY_TYPE_SOCKS5 = 2
|
||||
PROXY_TYPE_HTTP = 3
|
||||
|
||||
_defaultproxy = None
|
||||
_orgsocket = socket.socket
|
||||
|
||||
class ProxyError(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
class GeneralProxyError(ProxyError):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
class Socks5AuthError(ProxyError):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
class Socks5Error(ProxyError):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
class Socks4Error(ProxyError):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
class HTTPError(ProxyError):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
_generalerrors = ("success",
|
||||
"invalid data",
|
||||
"not connected",
|
||||
"not available",
|
||||
"bad proxy type",
|
||||
"bad input")
|
||||
|
||||
_socks5errors = ("succeeded",
|
||||
"general SOCKS server failure",
|
||||
"connection not allowed by ruleset",
|
||||
"Network unreachable",
|
||||
"Host unreachable",
|
||||
"Connection refused",
|
||||
"TTL expired",
|
||||
"Command not supported",
|
||||
"Address type not supported",
|
||||
"Unknown error")
|
||||
|
||||
_socks5autherrors = ("succeeded",
|
||||
"authentication is required",
|
||||
"all offered authentication methods were rejected",
|
||||
"unknown username or invalid password",
|
||||
"unknown error")
|
||||
|
||||
_socks4errors = ("request granted",
|
||||
"request rejected or failed",
|
||||
"request rejected because SOCKS server cannot connect to identd on the client",
|
||||
"request rejected because the client program and identd report different user-ids",
|
||||
"unknown error")
|
||||
|
||||
def setdefaultproxy(proxytype=None,addr=None,port=None,rdns=True,username=None,password=None):
|
||||
"""setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
|
||||
Sets a default proxy which all further socksocket objects will use,
|
||||
unless explicitly changed.
|
||||
"""
|
||||
global _defaultproxy
|
||||
_defaultproxy = (proxytype,addr,port,rdns,username,password)
|
||||
|
||||
class socksocket(socket.socket):
|
||||
"""socksocket([family[, type[, proto]]]) -> socket object
|
||||
|
||||
Open a SOCKS enabled socket. The parameters are the same as
|
||||
those of the standard socket init. In order for SOCKS to work,
|
||||
you must specify family=AF_INET, type=SOCK_STREAM and proto=0.
|
||||
"""
|
||||
|
||||
def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None):
|
||||
_orgsocket.__init__(self,family,type,proto,_sock)
|
||||
if _defaultproxy != None:
|
||||
self.__proxy = _defaultproxy
|
||||
else:
|
||||
self.__proxy = (None, None, None, None, None, None)
|
||||
self.__proxysockname = None
|
||||
self.__proxypeername = None
|
||||
|
||||
def __recvall(self, bytes):
|
||||
"""__recvall(bytes) -> data
|
||||
Receive EXACTLY the number of bytes requested from the socket.
|
||||
Blocks until the required number of bytes have been received.
|
||||
"""
|
||||
data = ""
|
||||
while len(data) < bytes:
|
||||
data = data + self.recv(bytes-len(data))
|
||||
return data
|
||||
|
||||
def setproxy(self,proxytype=None,addr=None,port=None,rdns=True,username=None,password=None):
|
||||
"""setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
|
||||
Sets the proxy to be used.
|
||||
proxytype - The type of the proxy to be used. Three types
|
||||
are supported: PROXY_TYPE_SOCKS4 (including socks4a),
|
||||
PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
|
||||
addr - The address of the server (IP or DNS).
|
||||
port - The port of the server. Defaults to 1080 for SOCKS
|
||||
servers and 8080 for HTTP proxy servers.
|
||||
rdns - Should DNS queries be preformed on the remote side
|
||||
(rather than the local side). The default is True.
|
||||
Note: This has no effect with SOCKS4 servers.
|
||||
username - Username to authenticate with to the server.
|
||||
The default is no authentication.
|
||||
password - Password to authenticate with to the server.
|
||||
Only relevant when username is also provided.
|
||||
"""
|
||||
self.__proxy = (proxytype,addr,port,rdns,username,password)
|
||||
|
||||
def __negotiatesocks5(self,destaddr,destport):
|
||||
"""__negotiatesocks5(self,destaddr,destport)
|
||||
Negotiates a connection through a SOCKS5 server.
|
||||
"""
|
||||
# First we'll send the authentication packages we support.
|
||||
if (self.__proxy[4]!=None) and (self.__proxy[5]!=None):
|
||||
# The username/password details were supplied to the
|
||||
# setproxy method so we support the USERNAME/PASSWORD
|
||||
# authentication (in addition to the standard none).
|
||||
self.sendall("\x05\x02\x00\x02")
|
||||
else:
|
||||
# No username/password were entered, therefore we
|
||||
# only support connections with no authentication.
|
||||
self.sendall("\x05\x01\x00")
|
||||
# We'll receive the server's response to determine which
|
||||
# method was selected
|
||||
chosenauth = self.__recvall(2)
|
||||
if chosenauth[0] != "\x05":
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
# Check the chosen authentication method
|
||||
if chosenauth[1] == "\x00":
|
||||
# No authentication is required
|
||||
pass
|
||||
elif chosenauth[1] == "\x02":
|
||||
# Okay, we need to perform a basic username/password
|
||||
# authentication.
|
||||
self.sendall("\x01" + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.proxy[5])) + self.__proxy[5])
|
||||
authstat = self.__recvall(2)
|
||||
if authstat[0] != "\x01":
|
||||
# Bad response
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
if authstat[1] != "\x00":
|
||||
# Authentication failed
|
||||
self.close()
|
||||
raise Socks5AuthError,((3,_socks5autherrors[3]))
|
||||
# Authentication succeeded
|
||||
else:
|
||||
# Reaching here is always bad
|
||||
self.close()
|
||||
if chosenauth[1] == "\xFF":
|
||||
raise Socks5AuthError((2,_socks5autherrors[2]))
|
||||
else:
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
# Now we can request the actual connection
|
||||
req = "\x05\x01\x00"
|
||||
# If the given destination address is an IP address, we'll
|
||||
# use the IPv4 address request even if remote resolving was specified.
|
||||
try:
|
||||
ipaddr = socket.inet_aton(destaddr)
|
||||
req = req + "\x01" + ipaddr
|
||||
except socket.error:
|
||||
# Well it's not an IP number, so it's probably a DNS name.
|
||||
if self.__proxy[3]==True:
|
||||
# Resolve remotely
|
||||
ipaddr = None
|
||||
req = req + "\x03" + chr(len(destaddr)) + destaddr
|
||||
else:
|
||||
# Resolve locally
|
||||
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
|
||||
req = req + "\x01" + ipaddr
|
||||
req = req + struct.pack(">H",destport)
|
||||
self.sendall(req)
|
||||
# Get the response
|
||||
resp = self.__recvall(4)
|
||||
if resp[0] != "\x05":
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
elif resp[1] != "\x00":
|
||||
# Connection failed
|
||||
self.close()
|
||||
if ord(resp[1])<=8:
|
||||
raise Socks5Error(ord(resp[1]),_generalerrors[ord(resp[1])])
|
||||
else:
|
||||
raise Socks5Error(9,_generalerrors[9])
|
||||
# Get the bound address/port
|
||||
elif resp[3] == "\x01":
|
||||
boundaddr = self.__recvall(4)
|
||||
elif resp[3] == "\x03":
|
||||
resp = resp + self.recv(1)
|
||||
boundaddr = self.__recvall(resp[4])
|
||||
else:
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
boundport = struct.unpack(">H",self.__recvall(2))[0]
|
||||
self.__proxysockname = (boundaddr,boundport)
|
||||
if ipaddr != None:
|
||||
self.__proxypeername = (socket.inet_ntoa(ipaddr),destport)
|
||||
else:
|
||||
self.__proxypeername = (destaddr,destport)
|
||||
|
||||
def getproxysockname(self):
|
||||
"""getsockname() -> address info
|
||||
Returns the bound IP address and port number at the proxy.
|
||||
"""
|
||||
return self.__proxysockname
|
||||
|
||||
def getproxypeername(self):
|
||||
"""getproxypeername() -> address info
|
||||
Returns the IP and port number of the proxy.
|
||||
"""
|
||||
return _orgsocket.getpeername(self)
|
||||
|
||||
def getpeername(self):
|
||||
"""getpeername() -> address info
|
||||
Returns the IP address and port number of the destination
|
||||
machine (note: getproxypeername returns the proxy)
|
||||
"""
|
||||
return self.__proxypeername
|
||||
|
||||
def __negotiatesocks4(self,destaddr,destport):
|
||||
"""__negotiatesocks4(self,destaddr,destport)
|
||||
Negotiates a connection through a SOCKS4 server.
|
||||
"""
|
||||
# Check if the destination address provided is an IP address
|
||||
rmtrslv = False
|
||||
try:
|
||||
ipaddr = socket.inet_aton(destaddr)
|
||||
except socket.error:
|
||||
# It's a DNS name. Check where it should be resolved.
|
||||
if self.__proxy[3]==True:
|
||||
ipaddr = "\x00\x00\x00\x01"
|
||||
rmtrslv = True
|
||||
else:
|
||||
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
|
||||
# Construct the request packet
|
||||
req = "\x04\x01" + struct.pack(">H",destport) + ipaddr
|
||||
# The username parameter is considered userid for SOCKS4
|
||||
if self.__proxy[4] != None:
|
||||
req = req + self.__proxy[4]
|
||||
req = req + "\x00"
|
||||
# DNS name if remote resolving is required
|
||||
# NOTE: This is actually an extension to the SOCKS4 protocol
|
||||
# called SOCKS4A and may not be supported in all cases.
|
||||
if rmtrslv==True:
|
||||
req = req + destaddr + "\x00"
|
||||
self.sendall(req)
|
||||
# Get the response from the server
|
||||
resp = self.__recvall(8)
|
||||
if resp[0] != "\x00":
|
||||
# Bad data
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
if resp[1] != "\x5A":
|
||||
# Server returned an error
|
||||
self.close()
|
||||
if ord(resp[1]) in (91,92,93):
|
||||
self.close()
|
||||
raise Socks4Error((ord(resp[1]),_socks4errors[ord(resp[1])-90]))
|
||||
else:
|
||||
raise Socks4Error((94,_socks4errors[4]))
|
||||
# Get the bound address/port
|
||||
self.__proxysockname = (socket.inet_ntoa(resp[4:]),struct.unpack(">H",resp[2:4])[0])
|
||||
if rmtrslv != None:
|
||||
self.__proxypeername = (socket.inet_ntoa(ipaddr),destport)
|
||||
else:
|
||||
self.__proxypeername = (destaddr,destport)
|
||||
|
||||
def __negotiatehttp(self,destaddr,destport):
|
||||
"""__negotiatehttp(self,destaddr,destport)
|
||||
Negotiates a connection through an HTTP server.
|
||||
"""
|
||||
# If we need to resolve locally, we do this now
|
||||
if self.__proxy[3] == False:
|
||||
addr = socket.gethostbyname(destaddr)
|
||||
else:
|
||||
addr = destaddr
|
||||
self.sendall("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n")
|
||||
# We read the response until we get the string "\r\n\r\n"
|
||||
resp = self.recv(1)
|
||||
while resp.find("\r\n\r\n")==-1:
|
||||
resp = resp + self.recv(1)
|
||||
# We just need the first line to check if the connection
|
||||
# was successful
|
||||
statusline = resp.splitlines()[0].split(" ",2)
|
||||
if statusline[0] not in ("HTTP/1.0","HTTP/1.1"):
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
try:
|
||||
statuscode = int(statusline[1])
|
||||
except ValueError:
|
||||
self.close()
|
||||
raise GeneralProxyError((1,_generalerrors[1]))
|
||||
if statuscode != 200:
|
||||
self.close()
|
||||
raise HTTPError((statuscode,statusline[2]))
|
||||
self.__proxysockname = ("0.0.0.0",0)
|
||||
self.__proxypeername = (addr,destport)
|
||||
|
||||
def connect(self,destpair):
|
||||
"""connect(self,despair)
|
||||
Connects to the specified destination through a proxy.
|
||||
destpar - A tuple of the IP/DNS address and the port number.
|
||||
(identical to socket's connect).
|
||||
To select the proxy server use setproxy().
|
||||
"""
|
||||
# Do a minimal input check first
|
||||
if (type(destpair) in (list,tuple)==False) or (len(destpair)<2) or (type(destpair[0])!=str) or (type(destpair[1])!=int):
|
||||
raise GeneralProxyError((5,_generalerrors[5]))
|
||||
if self.__proxy[0] == PROXY_TYPE_SOCKS5:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 1080
|
||||
_orgsocket.connect(self,(self.__proxy[1],portnum))
|
||||
self.__negotiatesocks5(destpair[0],destpair[1])
|
||||
elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 1080
|
||||
_orgsocket.connect(self,(self.__proxy[1],portnum))
|
||||
self.__negotiatesocks4(destpair[0],destpair[1])
|
||||
elif self.__proxy[0] == PROXY_TYPE_HTTP:
|
||||
if self.__proxy[2] != None:
|
||||
portnum = self.__proxy[2]
|
||||
else:
|
||||
portnum = 8080
|
||||
_orgsocket.connect(self,(self.__proxy[1],portnum))
|
||||
self.__negotiatehttp(destpair[0],destpair[1])
|
||||
elif self.__proxy[0] == None:
|
||||
_orgsocket.connect(self,(destpair[0],destpair[1]))
|
||||
else:
|
||||
raise GeneralProxyError((4,_generalerrors[4]))
|
||||
65
src/searchengine/pluginsource.h
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#ifndef PLUGIN_SOURCE_H
|
||||
#define PLUGIN_SOURCE_H
|
||||
|
||||
#include <QDialog>
|
||||
#include "ui_pluginsource.h"
|
||||
|
||||
class pluginSourceDlg: public QDialog, private Ui::pluginSourceDlg {
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void askForUrl();
|
||||
void askForLocalFile();
|
||||
|
||||
protected slots:
|
||||
void on_localButton_clicked() {
|
||||
emit askForLocalFile();
|
||||
close();
|
||||
}
|
||||
|
||||
void on_urlButton_clicked() {
|
||||
emit askForUrl();
|
||||
close();
|
||||
}
|
||||
|
||||
public:
|
||||
pluginSourceDlg(QWidget* parent): QDialog(parent){
|
||||
setupUi(this);
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
show();
|
||||
}
|
||||
|
||||
~pluginSourceDlg(){}
|
||||
};
|
||||
|
||||
#endif
|
||||
23
src/searchengine/search.qrc
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource>
|
||||
<file>nova/nova2.py</file>
|
||||
<file>nova/novaprinter.py</file>
|
||||
<file>nova/socks.py</file>
|
||||
<file>nova/nova2dl.py</file>
|
||||
<file>nova/helpers.py</file>
|
||||
<file>nova/engines/vertor.png</file>
|
||||
<file>nova/engines/mininova.png</file>
|
||||
<file>nova/engines/mininova.py</file>
|
||||
<file>nova/engines/torrentdownloads.png</file>
|
||||
<file>nova/engines/isohunt.png</file>
|
||||
<file>nova/engines/torrentreactor.py</file>
|
||||
<file>nova/engines/btjunkie.png</file>
|
||||
<file>nova/engines/piratebay.py</file>
|
||||
<file>nova/engines/torrentdownloads.py</file>
|
||||
<file>nova/engines/torrentreactor.png</file>
|
||||
<file>nova/engines/isohunt.py</file>
|
||||
<file>nova/engines/btjunkie.py</file>
|
||||
<file>nova/engines/piratebay.png</file>
|
||||
<file>nova/engines/vertor.py</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
184
src/searchengine/search.ui
Normal file
@@ -0,0 +1,184 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>search_engine</class>
|
||||
<widget class="QWidget" name="search_engine">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>820</width>
|
||||
<height>453</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search_pattern">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboCategory"/>
|
||||
</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>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="status_lbl">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>35</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Status:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="search_status">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>35</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<italic>true</italic>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Stopped</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>188</width>
|
||||
<height>21</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="download_button">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Download</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>601</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="enginesButton">
|
||||
<property name="text">
|
||||
<string>Search engines...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>search_pattern</sender>
|
||||
<signal>returnPressed()</signal>
|
||||
<receiver>search_button</receiver>
|
||||
<slot>click()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>421</x>
|
||||
<y>37</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>685</x>
|
||||
<y>45</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
699
src/searchengine/searchengine.cpp
Normal file
@@ -0,0 +1,699 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#include <QStandardItemModel>
|
||||
#include <QHeaderView>
|
||||
#include <QCompleter>
|
||||
#include <QMessageBox>
|
||||
#include <QTemporaryFile>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <iostream>
|
||||
#include <QTimer>
|
||||
#include <QDir>
|
||||
#include <QMenu>
|
||||
#include <QClipboard>
|
||||
#include <QMimeData>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QFileDialog>
|
||||
|
||||
#ifdef Q_WS_WIN
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
#include "searchengine.h"
|
||||
#include "qbtsession.h"
|
||||
#include "downloadthread.h"
|
||||
#include "misc.h"
|
||||
#include "preferences.h"
|
||||
#include "searchlistdelegate.h"
|
||||
#include "qinisettings.h"
|
||||
#include "GUI.h"
|
||||
|
||||
#define SEARCHHISTORY_MAXSIZE 50
|
||||
|
||||
/*SEARCH ENGINE START*/
|
||||
SearchEngine::SearchEngine(GUI *parent, QBtSession *BTSession) : QWidget(parent), BTSession(BTSession), parent(parent) {
|
||||
setupUi(this);
|
||||
// new qCompleter to the search pattern
|
||||
startSearchHistory();
|
||||
createCompleter();
|
||||
#if QT_VERSION >= 0x040500
|
||||
tabWidget->setTabsClosable(true);
|
||||
connect(tabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int)));
|
||||
#else
|
||||
// Add close tab button
|
||||
closeTab_button = new QPushButton();
|
||||
closeTab_button->setIcon(QIcon(QString::fromUtf8(":/Icons/oxygen/tab-close.png")));
|
||||
closeTab_button->setFlat(true);
|
||||
tabWidget->setCornerWidget(closeTab_button);
|
||||
connect(closeTab_button, SIGNAL(clicked()), this, SLOT(closeTab_button_clicked()));
|
||||
#endif
|
||||
// Boolean initialization
|
||||
search_stopped = false;
|
||||
// Creating Search Process
|
||||
#ifdef Q_WS_WIN
|
||||
has_python = addPythonPathToEnv();
|
||||
#endif
|
||||
searchProcess = new QProcess(this);
|
||||
searchProcess->setEnvironment(QProcess::systemEnvironment());
|
||||
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)));
|
||||
connect(tabWidget, SIGNAL(currentChanged(int)), this, SLOT(tab_changed(int)));
|
||||
searchTimeout = new QTimer(this);
|
||||
searchTimeout->setSingleShot(true);
|
||||
connect(searchTimeout, SIGNAL(timeout()), this, SLOT(on_search_button_clicked()));
|
||||
// Update nova.py search plugin if necessary
|
||||
updateNova();
|
||||
supported_engines = new SupportedEngines();
|
||||
// Fill in category combobox
|
||||
fillCatCombobox();
|
||||
connect(search_pattern, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(displayPatternContextMenu(QPoint)));
|
||||
connect(search_pattern, SIGNAL(textEdited(QString)), this, SLOT(searchTextEdited(QString)));
|
||||
}
|
||||
|
||||
void SearchEngine::fillCatCombobox() {
|
||||
comboCategory->clear();
|
||||
comboCategory->addItem(full_cat_names["all"], QVariant("all"));
|
||||
QStringList supported_cat = supported_engines->supportedCategories();
|
||||
foreach(QString cat, supported_cat) {
|
||||
qDebug("Supported category: %s", qPrintable(cat));
|
||||
comboCategory->addItem(full_cat_names[cat], QVariant(cat));
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_WS_WIN
|
||||
bool SearchEngine::addPythonPathToEnv() {
|
||||
QString python_path = Preferences::getPythonPath();
|
||||
if(!python_path.isEmpty()) {
|
||||
// Add it to PATH envvar
|
||||
QString path_envar = QString::fromLocal8Bit(getenv("PATH"));
|
||||
if(path_envar.isNull()) {
|
||||
path_envar = "";
|
||||
}
|
||||
path_envar = python_path+";"+path_envar;
|
||||
qDebug("New PATH envvar is: %s", qPrintable(path_envar));
|
||||
QString envar = "PATH="+path_envar;
|
||||
putenv(envar.toLocal8Bit().data());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SearchEngine::installPython() {
|
||||
setCursor(QCursor(Qt::WaitCursor));
|
||||
// Download python
|
||||
downloadThread *pydownloader = new downloadThread(this);
|
||||
connect(pydownloader, SIGNAL(downloadFinished(QString,QString)), this, SLOT(pythonDownloadSuccess(QString,QString)));
|
||||
connect(pydownloader, SIGNAL(downloadFailure(QString,QString)), this, SLOT(pythonDownloadFailure(QString,QString)));
|
||||
pydownloader->downloadUrl("http://python.org/ftp/python/2.6.5/python-2.6.5.msi");
|
||||
}
|
||||
|
||||
void SearchEngine::pythonDownloadSuccess(QString url, QString file_path) {
|
||||
setCursor(QCursor(Qt::ArrowCursor));
|
||||
Q_UNUSED(url);
|
||||
QFile::rename(file_path, file_path+".msi");
|
||||
QProcess installer;
|
||||
qDebug("Launching Python installer in passive mode...");
|
||||
|
||||
installer.start("msiexec.exe /passive /i "+file_path.replace("/", "\\")+".msi");
|
||||
// Wait for setup to complete
|
||||
installer.waitForFinished();
|
||||
|
||||
qDebug("Installer stdout: %s", installer.readAllStandardOutput().data());
|
||||
qDebug("Installer stderr: %s", installer.readAllStandardError().data());
|
||||
qDebug("Setup should be complete!");
|
||||
// Reload search engine
|
||||
has_python = addPythonPathToEnv();
|
||||
if(has_python) {
|
||||
supported_engines->update();
|
||||
// Launch the search again
|
||||
on_search_button_clicked();
|
||||
}
|
||||
// Delete temp file
|
||||
misc::safeRemove(file_path+".msi");
|
||||
}
|
||||
|
||||
void SearchEngine::pythonDownloadFailure(QString url, QString error) {
|
||||
Q_UNUSED(url);
|
||||
setCursor(QCursor(Qt::ArrowCursor));
|
||||
QMessageBox::warning(this, tr("Download error"), tr("Python setup could not be downloaded, reason: %1.\nPlease install it manually.").arg(error));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
QString SearchEngine::selectedCategory() const {
|
||||
return comboCategory->itemData(comboCategory->currentIndex()).toString();
|
||||
}
|
||||
|
||||
SearchEngine::~SearchEngine(){
|
||||
qDebug("Search destruction");
|
||||
// save the searchHistory for later uses
|
||||
saveSearchHistory();
|
||||
searchProcess->kill();
|
||||
searchProcess->waitForFinished();
|
||||
foreach(QProcess *downloader, downloaders) {
|
||||
// Make sure we disconnect the SIGNAL/SLOT first
|
||||
// To avoid double free
|
||||
downloader->disconnect();
|
||||
downloader->kill();
|
||||
downloader->waitForFinished();
|
||||
delete downloader;
|
||||
}
|
||||
#if QT_VERSION < 0x040500
|
||||
delete closeTab_button;
|
||||
#endif
|
||||
delete searchTimeout;
|
||||
delete searchProcess;
|
||||
delete supported_engines;
|
||||
if(searchCompleter)
|
||||
delete searchCompleter;
|
||||
}
|
||||
|
||||
void SearchEngine::displayPatternContextMenu(QPoint) {
|
||||
QMenu myMenu(this);
|
||||
QAction cutAct(QIcon(":/Icons/oxygen/edit-cut.png"), tr("Cut"), &myMenu);
|
||||
QAction copyAct(QIcon(":/Icons/oxygen/edit-copy.png"), tr("Copy"), &myMenu);
|
||||
QAction pasteAct(QIcon(":/Icons/oxygen/edit-paste.png"), tr("Paste"), &myMenu);
|
||||
QAction clearAct(QIcon(":/Icons/oxygen/edit_clear.png"), tr("Clear field"), &myMenu);
|
||||
QAction clearHistoryAct(QIcon(":/Icons/oxygen/edit-clear.png"), tr("Clear completion history"), &myMenu);
|
||||
bool hasCopyAct = false;
|
||||
if(search_pattern->hasSelectedText()) {
|
||||
myMenu.addAction(&cutAct);
|
||||
myMenu.addAction(©Act);
|
||||
hasCopyAct = true;
|
||||
}
|
||||
if(qApp->clipboard()->mimeData()->hasText()) {
|
||||
myMenu.addAction(&pasteAct);
|
||||
hasCopyAct = true;
|
||||
}
|
||||
if(hasCopyAct)
|
||||
myMenu.addSeparator();
|
||||
myMenu.addAction(&clearHistoryAct);
|
||||
myMenu.addAction(&clearAct);
|
||||
QAction *act = myMenu.exec(QCursor::pos());
|
||||
if(act != 0) {
|
||||
if(act == &clearHistoryAct) {
|
||||
// Ask for confirmation
|
||||
if(QMessageBox::question(this, tr("Confirmation"), tr("Are you sure you want to clear the history?"), QMessageBox::Yes|QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) {
|
||||
// Clear history
|
||||
searchHistory.setStringList(QStringList());
|
||||
}
|
||||
}
|
||||
else if (act == &pasteAct) {
|
||||
search_pattern->paste();
|
||||
}
|
||||
else if (act == &cutAct) {
|
||||
search_pattern->cut();
|
||||
}
|
||||
else if (act == ©Act) {
|
||||
search_pattern->copy();
|
||||
}
|
||||
else if (act == &clearAct) {
|
||||
search_pattern->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SearchEngine::tab_changed(int t)
|
||||
{//when we switch from a tab that is not empty to another that is empty the download button
|
||||
//doesn't have to be available
|
||||
if(t>-1)
|
||||
{//-1 = no more tab
|
||||
if(all_tab.at(tabWidget->currentIndex())->getCurrentSearchListModel()->rowCount()) {
|
||||
download_button->setEnabled(true);
|
||||
} else {
|
||||
download_button->setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SearchEngine::on_enginesButton_clicked() {
|
||||
engineSelectDlg *dlg = new engineSelectDlg(this, supported_engines);
|
||||
connect(dlg, SIGNAL(enginesChanged()), this, SLOT(fillCatCombobox()));
|
||||
}
|
||||
|
||||
// get the last searchs from a QIniSettings to a QStringList
|
||||
void SearchEngine::startSearchHistory(){
|
||||
QIniSettings settings("qBittorrent", "qBittorrent");
|
||||
searchHistory.setStringList(settings.value("Search/searchHistory",QStringList()).toStringList());
|
||||
}
|
||||
|
||||
// Save the history list into the QIniSettings for the next session
|
||||
void SearchEngine::saveSearchHistory() {
|
||||
QIniSettings settings("qBittorrent", "qBittorrent");
|
||||
settings.setValue("Search/searchHistory",searchHistory.stringList());
|
||||
}
|
||||
|
||||
void SearchEngine::searchTextEdited(QString) {
|
||||
// Enable search button
|
||||
search_button->setText(tr("Search"));
|
||||
}
|
||||
|
||||
void SearchEngine::giveFocusToSearchInput() {
|
||||
search_pattern->setFocus();
|
||||
}
|
||||
|
||||
// Function called when we click on search button
|
||||
void SearchEngine::on_search_button_clicked(){
|
||||
#ifdef Q_WS_WIN
|
||||
if(!has_python) {
|
||||
if(QMessageBox::question(this, tr("Missing Python Interpreter"),
|
||||
tr("Python 2.x is required to use the search engine but it does not seem to be installed.\nDo you want to install it now?"),
|
||||
QMessageBox::Yes|QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) {
|
||||
// Download and Install Python
|
||||
installPython();
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if(searchProcess->state() != QProcess::NotRunning){
|
||||
#ifdef Q_WS_WIN
|
||||
searchProcess->kill();
|
||||
#else
|
||||
searchProcess->terminate();
|
||||
#endif
|
||||
search_stopped = true;
|
||||
if(searchTimeout->isActive()) {
|
||||
searchTimeout->stop();
|
||||
}
|
||||
if(search_button->text() != tr("Search")) {
|
||||
search_button->setText(tr("Search"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
searchProcess->waitForFinished();
|
||||
// Reload environment variables (proxy)
|
||||
searchProcess->setEnvironment(QProcess::systemEnvironment());
|
||||
|
||||
QString pattern = search_pattern->text().trimmed();
|
||||
// No search pattern entered
|
||||
if(pattern.isEmpty()){
|
||||
QMessageBox::critical(0, tr("Empty search pattern"), tr("Please type a search pattern first"));
|
||||
return;
|
||||
}
|
||||
// Tab Addition
|
||||
currentSearchTab=new SearchTab(this);
|
||||
connect(currentSearchTab->header(), SIGNAL(sectionResized(int, int, int)), this, SLOT(propagateSectionResized(int,int,int)));
|
||||
all_tab.append(currentSearchTab);
|
||||
tabWidget->addTab(currentSearchTab, pattern);
|
||||
tabWidget->setCurrentWidget(currentSearchTab);
|
||||
#if QT_VERSION < 0x040500
|
||||
closeTab_button->setEnabled(true);
|
||||
#endif
|
||||
// if the pattern is not in the pattern
|
||||
QStringList wordList = searchHistory.stringList();
|
||||
if(wordList.indexOf(pattern) == -1){
|
||||
//update the searchHistory list
|
||||
wordList.append(pattern);
|
||||
// verify the max size of the history
|
||||
if(wordList.size() > SEARCHHISTORY_MAXSIZE)
|
||||
wordList = wordList.mid(wordList.size()/2);
|
||||
searchHistory.setStringList(wordList);
|
||||
}
|
||||
|
||||
// Getting checked search engines
|
||||
QStringList params;
|
||||
search_stopped = false;
|
||||
params << misc::searchEngineLocation()+QDir::separator()+"nova2.py";
|
||||
params << supported_engines->enginesEnabled().join(",");
|
||||
qDebug("Search with category: %s", qPrintable(selectedCategory()));
|
||||
params << selectedCategory();
|
||||
params << pattern.split(" ");
|
||||
// Update SearchEngine widgets
|
||||
no_search_results = true;
|
||||
nb_search_results = 0;
|
||||
search_result_line_truncated.clear();
|
||||
//on change le texte du label courrant
|
||||
currentSearchTab->getCurrentLabel()->setText(tr("Results")+" <i>(0)</i>:");
|
||||
// Launch search
|
||||
searchProcess->start("python", params, QIODevice::ReadOnly);
|
||||
searchTimeout->start(180000); // 3min
|
||||
}
|
||||
|
||||
void SearchEngine::createCompleter() {
|
||||
if(searchCompleter)
|
||||
delete searchCompleter;
|
||||
searchCompleter = new QCompleter(&searchHistory);
|
||||
searchCompleter->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
search_pattern->setCompleter(searchCompleter);
|
||||
}
|
||||
|
||||
void SearchEngine::propagateSectionResized(int index, int , int newsize) {
|
||||
foreach(SearchTab * tab, all_tab) {
|
||||
tab->getCurrentTreeView()->setColumnWidth(index, newsize);
|
||||
}
|
||||
saveResultsColumnsWidth();
|
||||
}
|
||||
|
||||
void SearchEngine::saveResultsColumnsWidth() {
|
||||
if(all_tab.size() > 0) {
|
||||
QTreeView* treeview = all_tab.first()->getCurrentTreeView();
|
||||
QIniSettings settings("qBittorrent", "qBittorrent");
|
||||
QStringList width_list;
|
||||
QStringList new_width_list;
|
||||
short nbColumns = all_tab.first()->getCurrentSearchListModel()->columnCount();
|
||||
|
||||
QString line = settings.value("SearchResultsColsWidth", QString()).toString();
|
||||
if(!line.isEmpty()) {
|
||||
width_list = line.split(' ');
|
||||
}
|
||||
for(short i=0; i<nbColumns; ++i){
|
||||
if(treeview->columnWidth(i)<1 && width_list.size() == nbColumns && width_list.at(i).toInt()>=1) {
|
||||
// load the former width
|
||||
new_width_list << width_list.at(i);
|
||||
} else if(treeview->columnWidth(i)>=1) {
|
||||
// usual case, save the current width
|
||||
new_width_list << QString::number(treeview->columnWidth(i));
|
||||
} else {
|
||||
// default width
|
||||
treeview->resizeColumnToContents(i);
|
||||
new_width_list << QString::number(treeview->columnWidth(i));
|
||||
}
|
||||
}
|
||||
settings.setValue("SearchResultsColsWidth", new_width_list.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
void SearchEngine::downloadTorrent(QString engine_url, QString torrent_url) {
|
||||
if(torrent_url.startsWith("bc://bt/", Qt::CaseInsensitive)) {
|
||||
qDebug("Converting bc link to magnet link");
|
||||
torrent_url = misc::bcLinkToMagnet(torrent_url);
|
||||
}
|
||||
if(torrent_url.startsWith("magnet:")) {
|
||||
QStringList urls;
|
||||
urls << torrent_url;
|
||||
parent->downloadFromURLList(urls);
|
||||
} else {
|
||||
QProcess *downloadProcess = new QProcess(this);
|
||||
downloadProcess->setEnvironment(QProcess::systemEnvironment());
|
||||
connect(downloadProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(downloadFinished(int,QProcess::ExitStatus)));
|
||||
downloaders << downloadProcess;
|
||||
QStringList params;
|
||||
params << misc::searchEngineLocation()+QDir::separator()+"nova2dl.py";
|
||||
params << engine_url;
|
||||
params << torrent_url;
|
||||
// Launch search
|
||||
downloadProcess->start("python", params, QIODevice::ReadOnly);
|
||||
}
|
||||
}
|
||||
|
||||
void SearchEngine::searchStarted(){
|
||||
// Update SearchEngine widgets
|
||||
search_status->setText(tr("Searching..."));
|
||||
search_status->repaint();
|
||||
search_button->setText("Stop");
|
||||
}
|
||||
|
||||
// search Qprocess return output as soon as it gets new
|
||||
// stuff to read. We split it into lines and add each
|
||||
// line to search results calling appendSearchResult().
|
||||
void SearchEngine::readSearchOutput(){
|
||||
QByteArray output = searchProcess->readAllStandardOutput();
|
||||
output.replace("\r", "");
|
||||
QList<QByteArray> lines_list = output.split('\n');
|
||||
if(!search_result_line_truncated.isEmpty()){
|
||||
QByteArray end_of_line = lines_list.takeFirst();
|
||||
lines_list.prepend(search_result_line_truncated+end_of_line);
|
||||
}
|
||||
search_result_line_truncated = lines_list.takeLast().trimmed();
|
||||
foreach(const QByteArray &line, lines_list){
|
||||
appendSearchResult(QString::fromUtf8(line));
|
||||
}
|
||||
if(currentSearchTab)
|
||||
currentSearchTab->getCurrentLabel()->setText(tr("Results")+QString::fromUtf8(" <i>(")+QString::number(nb_search_results)+QString::fromUtf8(")</i>:"));
|
||||
}
|
||||
|
||||
void SearchEngine::downloadFinished(int exitcode, QProcess::ExitStatus) {
|
||||
QProcess *downloadProcess = (QProcess*)sender();
|
||||
if(exitcode == 0) {
|
||||
QString line = QString::fromUtf8(downloadProcess->readAllStandardOutput()).trimmed();
|
||||
QStringList parts = line.split(' ');
|
||||
if(parts.size() == 2) {
|
||||
QString path = parts[0];
|
||||
QString url = parts[1];
|
||||
BTSession->processDownloadedFile(url, path);
|
||||
}
|
||||
}
|
||||
qDebug("Deleting downloadProcess");
|
||||
downloaders.removeOne(downloadProcess);
|
||||
delete downloadProcess;
|
||||
}
|
||||
|
||||
// Update nova.py search plugin if necessary
|
||||
void SearchEngine::updateNova() {
|
||||
qDebug("Updating nova");
|
||||
// create nova directory if necessary
|
||||
QDir search_dir(misc::searchEngineLocation());
|
||||
QFile package_file(search_dir.absoluteFilePath("__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.absolutePath().replace("\\", "/")+"/engines/__init__.py");
|
||||
package_file2.open(QIODevice::WriteOnly | QIODevice::Text);
|
||||
package_file2.close();
|
||||
// Copy search plugin files (if necessary)
|
||||
QString filePath = search_dir.absoluteFilePath("nova2.py");
|
||||
if(getPluginVersion(":/nova/nova2.py") > getPluginVersion(filePath)) {
|
||||
if(QFile::exists(filePath)) {
|
||||
misc::safeRemove(filePath);
|
||||
misc::safeRemove(filePath+"c");
|
||||
}
|
||||
QFile::copy(":/nova/nova2.py", filePath);
|
||||
}
|
||||
|
||||
filePath = search_dir.absoluteFilePath("nova2dl.py");
|
||||
if(getPluginVersion(":/nova/nova2dl.py") > getPluginVersion(filePath)) {
|
||||
if(QFile::exists(filePath)){
|
||||
misc::safeRemove(filePath);
|
||||
misc::safeRemove(filePath+"c");
|
||||
}
|
||||
QFile::copy(":/nova/nova2dl.py", filePath);
|
||||
}
|
||||
|
||||
filePath = search_dir.absoluteFilePath("novaprinter.py");
|
||||
if(getPluginVersion(":/nova/novaprinter.py") > getPluginVersion(filePath)) {
|
||||
if(QFile::exists(filePath)){
|
||||
misc::safeRemove(filePath);
|
||||
misc::safeRemove(filePath+"c");
|
||||
}
|
||||
QFile::copy(":/nova/novaprinter.py", filePath);
|
||||
}
|
||||
|
||||
filePath = search_dir.absoluteFilePath("helpers.py");
|
||||
if(getPluginVersion(":/nova/helpers.py") > getPluginVersion(filePath)) {
|
||||
if(QFile::exists(filePath)){
|
||||
misc::safeRemove(filePath);
|
||||
misc::safeRemove(filePath+"c");
|
||||
}
|
||||
QFile::copy(":/nova/helpers.py", filePath);
|
||||
}
|
||||
|
||||
filePath = search_dir.absoluteFilePath("socks.py");
|
||||
if(!QFile::exists(filePath)) {
|
||||
QFile::copy(":/nova/socks.py", filePath);
|
||||
}
|
||||
QDir destDir(QDir(misc::searchEngineLocation()).absoluteFilePath("engines"));
|
||||
QDir shipped_subDir(":/nova/engines/");
|
||||
QStringList files = shipped_subDir.entryList();
|
||||
foreach(const QString &file, files){
|
||||
QString shipped_file = shipped_subDir.absoluteFilePath(file);
|
||||
// Copy python classes
|
||||
if(file.endsWith(".py")) {
|
||||
const QString dest_file = destDir.absoluteFilePath(file);
|
||||
if(getPluginVersion(shipped_file) > getPluginVersion(dest_file) ) {
|
||||
qDebug("shipped %s is more recent then local plugin, updating...", qPrintable(file));
|
||||
if(QFile::exists(dest_file)) {
|
||||
qDebug("Removing old %s", qPrintable(dest_file));
|
||||
misc::safeRemove(dest_file);
|
||||
misc::safeRemove(dest_file+"c");
|
||||
}
|
||||
qDebug("%s copied to %s", qPrintable(shipped_file), qPrintable(dest_file));
|
||||
QFile::copy(shipped_file, dest_file);
|
||||
}
|
||||
} else {
|
||||
// Copy icons
|
||||
if(file.endsWith(".png")) {
|
||||
if(!QFile::exists(destDir.absoluteFilePath(file))) {
|
||||
QFile::copy(shipped_file, destDir.absoluteFilePath(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifndef Q_WS_WIN
|
||||
// Fix permissions
|
||||
misc::chmod644(QDir(misc::searchEngineLocation()));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Slot called when search is Finished
|
||||
// Search can be finished for 3 reasons :
|
||||
// Error | Stopped by user | Finished normally
|
||||
void SearchEngine::searchFinished(int exitcode,QProcess::ExitStatus){
|
||||
if(searchTimeout->isActive()) {
|
||||
searchTimeout->stop();
|
||||
}
|
||||
QIniSettings settings("qBittorrent", "qBittorrent");
|
||||
bool useNotificationBalloons = settings.value("Preferences/General/NotificationBaloons", true).toBool();
|
||||
if(useNotificationBalloons && parent->getCurrentTabWidget() != this) {
|
||||
parent->showNotificationBaloon(tr("Search Engine"), tr("Search has finished"));
|
||||
}
|
||||
if(exitcode){
|
||||
#ifdef Q_WS_WIN
|
||||
search_status->setText(tr("Search aborted"));
|
||||
#else
|
||||
search_status->setText(tr("An error occured during search..."));
|
||||
#endif
|
||||
}else{
|
||||
if(search_stopped){
|
||||
search_status->setText(tr("Search aborted"));
|
||||
}else{
|
||||
if(no_search_results){
|
||||
search_status->setText(tr("Search returned no results"));
|
||||
}else{
|
||||
search_status->setText(tr("Search has finished"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if(currentSearchTab)
|
||||
currentSearchTab->getCurrentLabel()->setText(tr("Results", "i.e: Search results")+QString::fromUtf8(" <i>(")+QString::number(nb_search_results)+QString::fromUtf8(")</i>:"));
|
||||
search_button->setText("Search");
|
||||
}
|
||||
|
||||
// SLOT to append one line to search results list
|
||||
// Line is in the following form :
|
||||
// file url | file name | file size | nb seeds | nb leechers | Search engine url
|
||||
void SearchEngine::appendSearchResult(QString line){
|
||||
if(!currentSearchTab) {
|
||||
if(searchProcess->state() != QProcess::NotRunning){
|
||||
searchProcess->terminate();
|
||||
}
|
||||
if(searchTimeout->isActive()) {
|
||||
searchTimeout->stop();
|
||||
}
|
||||
search_stopped = true;
|
||||
return;
|
||||
}
|
||||
QStringList parts = line.split("|");
|
||||
if(parts.size() != 6){
|
||||
return;
|
||||
}
|
||||
Q_ASSERT(currentSearchTab);
|
||||
// Add item to search result list
|
||||
QStandardItemModel* cur_model = currentSearchTab->getCurrentSearchListModel();
|
||||
Q_ASSERT(cur_model);
|
||||
int row = cur_model->rowCount();
|
||||
cur_model->insertRow(row);
|
||||
|
||||
cur_model->setData(cur_model->index(row, 5), parts.at(0).trimmed()); // download URL
|
||||
cur_model->setData(cur_model->index(row, 0), parts.at(1).trimmed()); // Name
|
||||
cur_model->setData(cur_model->index(row, 1), parts.at(2).trimmed().toLongLong()); // Size
|
||||
bool ok = false;
|
||||
qlonglong nb_seeders = parts.at(3).trimmed().toLongLong(&ok);
|
||||
if(!ok || nb_seeders < 0) {
|
||||
cur_model->setData(cur_model->index(row, 2), tr("Unknown")); // Seeders
|
||||
} else {
|
||||
cur_model->setData(cur_model->index(row, 2), nb_seeders); // Seeders
|
||||
}
|
||||
qlonglong nb_leechers = parts.at(4).trimmed().toLongLong(&ok);
|
||||
if(!ok || nb_leechers < 0) {
|
||||
cur_model->setData(cur_model->index(row, 3), tr("Unknown")); // Leechers
|
||||
} else {
|
||||
cur_model->setData(cur_model->index(row, 3), nb_leechers); // Leechers
|
||||
}
|
||||
cur_model->setData(cur_model->index(row, 4), parts.at(5).trimmed()); // Engine URL
|
||||
|
||||
no_search_results = false;
|
||||
++nb_search_results;
|
||||
// Enable clear & download buttons
|
||||
download_button->setEnabled(true);
|
||||
}
|
||||
|
||||
#if QT_VERSION >= 0x040500
|
||||
void SearchEngine::closeTab(int index) {
|
||||
if(index == tabWidget->indexOf(currentSearchTab)) {
|
||||
qDebug("Deleted current search Tab");
|
||||
if(searchProcess->state() != QProcess::NotRunning){
|
||||
searchProcess->terminate();
|
||||
}
|
||||
if(searchTimeout->isActive()) {
|
||||
searchTimeout->stop();
|
||||
}
|
||||
search_stopped = true;
|
||||
currentSearchTab = 0;
|
||||
}
|
||||
delete all_tab.takeAt(index);
|
||||
if(!all_tab.size()) {
|
||||
download_button->setEnabled(false);
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Clear search results list
|
||||
void SearchEngine::closeTab_button_clicked(){
|
||||
if(all_tab.size()) {
|
||||
qDebug("currentTab rank: %d", tabWidget->currentIndex());
|
||||
qDebug("currentSearchTab rank: %d", tabWidget->indexOf(currentSearchTab));
|
||||
if(tabWidget->currentIndex() == tabWidget->indexOf(currentSearchTab)) {
|
||||
qDebug("Deleted current search Tab");
|
||||
if(searchProcess->state() != QProcess::NotRunning){
|
||||
searchProcess->terminate();
|
||||
}
|
||||
if(searchTimeout->isActive()) {
|
||||
searchTimeout->stop();
|
||||
}
|
||||
search_stopped = true;
|
||||
currentSearchTab = 0;
|
||||
}
|
||||
delete all_tab.takeAt(tabWidget->currentIndex());
|
||||
if(!all_tab.size()) {
|
||||
closeTab_button->setEnabled(false);
|
||||
download_button->setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Download selected items in search results list
|
||||
void SearchEngine::on_download_button_clicked(){
|
||||
//QModelIndexList selectedIndexes = currentSearchTab->getCurrentTreeView()->selectionModel()->selectedIndexes();
|
||||
QModelIndexList selectedIndexes = all_tab.at(tabWidget->currentIndex())->getCurrentTreeView()->selectionModel()->selectedIndexes();
|
||||
foreach(const QModelIndex &index, selectedIndexes){
|
||||
if(index.column() == NAME){
|
||||
// Get Item url
|
||||
QSortFilterProxyModel* model = all_tab.at(tabWidget->currentIndex())->getCurrentSearchListProxy();
|
||||
QString torrent_url = model->data(model->index(index.row(), URL_COLUMN)).toString();
|
||||
QString engine_url = model->data(model->index(index.row(), ENGINE_URL_COLUMN)).toString();
|
||||
downloadTorrent(engine_url, torrent_url);
|
||||
all_tab.at(tabWidget->currentIndex())->setRowColor(index.row(), "red");
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/searchengine/searchengine.h
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#ifndef SEARCH_H
|
||||
#define SEARCH_H
|
||||
|
||||
#include <QProcess>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QPointer>
|
||||
#include <QStringListModel>
|
||||
#include "ui_search.h"
|
||||
#include "engineselectdlg.h"
|
||||
#include "searchtab.h"
|
||||
#include "supportedengines.h"
|
||||
|
||||
class QBtSession;
|
||||
class downloadThread;
|
||||
class QTimer;
|
||||
class SearchEngine;
|
||||
class GUI;
|
||||
|
||||
class SearchEngine : public QWidget, public Ui::search_engine{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
// Search related
|
||||
QProcess *searchProcess;
|
||||
QList<QProcess*> downloaders;
|
||||
bool search_stopped;
|
||||
bool no_search_results;
|
||||
QByteArray search_result_line_truncated;
|
||||
unsigned long nb_search_results;
|
||||
QPointer<QCompleter> searchCompleter;
|
||||
QStringListModel searchHistory;
|
||||
QBtSession *BTSession;
|
||||
SupportedEngines *supported_engines;
|
||||
QTimer *searchTimeout;
|
||||
QPointer<SearchTab> currentSearchTab;
|
||||
#if QT_VERSION < 0x040500
|
||||
QPushButton *closeTab_button;
|
||||
#endif
|
||||
QList<QPointer<SearchTab> > all_tab; // To store all tabs
|
||||
const SearchCategories full_cat_names;
|
||||
GUI *parent;
|
||||
#ifdef Q_WS_WIN
|
||||
bool has_python;
|
||||
#endif
|
||||
|
||||
public:
|
||||
SearchEngine(GUI *parent, QBtSession *BTSession);
|
||||
~SearchEngine();
|
||||
QString selectedCategory() const;
|
||||
|
||||
static float getPluginVersion(QString filePath) {
|
||||
QFile plugin(filePath);
|
||||
if(!plugin.exists()){
|
||||
qDebug("%s plugin does not exist, returning 0.0", qPrintable(filePath));
|
||||
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().trimmed();
|
||||
version = line.toFloat();
|
||||
qDebug("plugin %s version: %.2f", qPrintable(filePath), version);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void on_download_button_clicked();
|
||||
void downloadTorrent(QString engine_url, QString torrent_url);
|
||||
void giveFocusToSearchInput();
|
||||
|
||||
protected slots:
|
||||
// Search slots
|
||||
void tab_changed(int);//to prevent the use of the download button when the tab is empty
|
||||
void on_search_button_clicked();
|
||||
#if QT_VERSION < 0x040500
|
||||
void closeTab_button_clicked();
|
||||
#else
|
||||
void closeTab(int index);
|
||||
#endif
|
||||
void appendSearchResult(QString line);
|
||||
void searchFinished(int exitcode,QProcess::ExitStatus);
|
||||
void readSearchOutput();
|
||||
void searchStarted();
|
||||
void startSearchHistory();
|
||||
void updateNova();
|
||||
void saveSearchHistory();
|
||||
void on_enginesButton_clicked();
|
||||
void propagateSectionResized(int index, int oldsize , int newsize);
|
||||
void saveResultsColumnsWidth();
|
||||
void downloadFinished(int exitcode, QProcess::ExitStatus);
|
||||
void displayPatternContextMenu(QPoint);
|
||||
void createCompleter();
|
||||
void fillCatCombobox();
|
||||
void searchTextEdited(QString);
|
||||
#ifdef Q_WS_WIN
|
||||
bool addPythonPathToEnv();
|
||||
void installPython();
|
||||
void pythonDownloadSuccess(QString url, QString file_path);
|
||||
void pythonDownloadFailure(QString url, QString error);
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif
|
||||
17
src/searchengine/searchengine.pri
Normal file
@@ -0,0 +1,17 @@
|
||||
INCLUDEPATH += $$PWD
|
||||
|
||||
FORMS += $$PWD/search.ui \
|
||||
$$PWD/engineselect.ui
|
||||
|
||||
HEADERS += $$PWD/searchengine.h \
|
||||
$$PWD/searchtab.h \
|
||||
$$PWD/engineselectdlg.h \
|
||||
$$PWD/pluginsource.h \
|
||||
$$PWD/searchlistdelegate.h \
|
||||
$$PWD/supportedengines.h
|
||||
|
||||
SOURCES += $$PWD/searchengine.cpp \
|
||||
$$PWD/searchtab.cpp \
|
||||
$$PWD/engineselectdlg.cpp
|
||||
|
||||
RESOURCES += $$PWD/search.qrc
|
||||
77
src/searchengine/searchlistdelegate.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#ifndef SEARCHLISTDELEGATE_H
|
||||
#define SEARCHLISTDELEGATE_H
|
||||
|
||||
#include <QItemDelegate>
|
||||
#include <QStyleOptionProgressBarV2>
|
||||
#include <QStyleOptionViewItemV2>
|
||||
#include <QModelIndex>
|
||||
#include <QPainter>
|
||||
#include <QProgressBar>
|
||||
#include "misc.h"
|
||||
|
||||
// Defines for search results list columns
|
||||
#define NAME 0
|
||||
#define SIZE 1
|
||||
#define SEEDERS 2
|
||||
#define LEECHERS 3
|
||||
#define ENGINE 4
|
||||
|
||||
class SearchListDelegate: public QItemDelegate {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SearchListDelegate(QObject *parent=0) : QItemDelegate(parent){}
|
||||
|
||||
~SearchListDelegate(){}
|
||||
|
||||
void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const{
|
||||
painter->save();
|
||||
QStyleOptionViewItemV2 opt = QItemDelegate::setOptions(index, option);
|
||||
switch(index.column()){
|
||||
case SIZE:
|
||||
QItemDelegate::drawBackground(painter, opt, index);
|
||||
QItemDelegate::drawDisplay(painter, opt, option.rect, misc::friendlyUnit(index.data().toLongLong()));
|
||||
break;
|
||||
default:
|
||||
QItemDelegate::paint(painter, option, index);
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QWidget* createEditor(QWidget*, const QStyleOptionViewItem &, const QModelIndex &) const {
|
||||
// No editor here
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
157
src/searchengine/searchtab.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#include <QDir>
|
||||
#include <QTreeView>
|
||||
#include <QStandardItemModel>
|
||||
#include <QHeaderView>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "searchtab.h"
|
||||
#include "searchlistdelegate.h"
|
||||
#include "misc.h"
|
||||
#include "searchengine.h"
|
||||
#include "qinisettings.h"
|
||||
|
||||
#define SEARCH_NAME 0
|
||||
#define SEARCH_SIZE 1
|
||||
#define SEARCH_SEEDERS 2
|
||||
#define SEARCH_LEECHERS 3
|
||||
#define SEARCH_ENGINE 4
|
||||
|
||||
SearchTab::SearchTab(SearchEngine *parent) : QWidget(), parent(parent)
|
||||
{
|
||||
box=new QVBoxLayout();
|
||||
results_lbl=new QLabel();
|
||||
resultsBrowser = new QTreeView();
|
||||
resultsBrowser->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
box->addWidget(results_lbl);
|
||||
box->addWidget(resultsBrowser);
|
||||
|
||||
setLayout(box);
|
||||
// Set Search results list model
|
||||
SearchListModel = new QStandardItemModel(0,6);
|
||||
SearchListModel->setHeaderData(SEARCH_NAME, Qt::Horizontal, tr("Name", "i.e: file name"));
|
||||
SearchListModel->setHeaderData(SEARCH_SIZE, Qt::Horizontal, tr("Size", "i.e: file size"));
|
||||
SearchListModel->setHeaderData(SEARCH_SEEDERS, Qt::Horizontal, tr("Seeders", "i.e: Number of full sources"));
|
||||
SearchListModel->setHeaderData(SEARCH_LEECHERS, Qt::Horizontal, tr("Leechers", "i.e: Number of partial sources"));
|
||||
SearchListModel->setHeaderData(SEARCH_ENGINE, Qt::Horizontal, tr("Search engine"));
|
||||
|
||||
proxyModel = new QSortFilterProxyModel();
|
||||
proxyModel->setDynamicSortFilter(true);
|
||||
proxyModel->setSourceModel(SearchListModel);
|
||||
resultsBrowser->setModel(proxyModel);
|
||||
|
||||
SearchDelegate = new SearchListDelegate();
|
||||
resultsBrowser->setItemDelegate(SearchDelegate);
|
||||
|
||||
resultsBrowser->hideColumn(URL_COLUMN); // Hide url column
|
||||
|
||||
resultsBrowser->setRootIsDecorated(false);
|
||||
resultsBrowser->setAllColumnsShowFocus(true);
|
||||
resultsBrowser->setSortingEnabled(true);
|
||||
|
||||
// Connect signals to slots (search part)
|
||||
connect(resultsBrowser, SIGNAL(doubleClicked(const QModelIndex&)), this, SLOT(downloadSelectedItem(const QModelIndex&)));
|
||||
|
||||
// Load last columns width for search results list
|
||||
if(!loadColWidthResultsList()){
|
||||
resultsBrowser->header()->resizeSection(0, 275);
|
||||
}
|
||||
|
||||
// Sort by Seeds
|
||||
resultsBrowser->sortByColumn(SEEDERS, Qt::DescendingOrder);
|
||||
}
|
||||
|
||||
void SearchTab::downloadSelectedItem(const QModelIndex& index) {
|
||||
QString engine_url = proxyModel->data(proxyModel->index(index.row(), ENGINE_URL_COLUMN)).toString();
|
||||
QString torrent_url = proxyModel->data(proxyModel->index(index.row(), URL_COLUMN)).toString();
|
||||
setRowColor(index.row(), "red");
|
||||
parent->downloadTorrent(engine_url, torrent_url);
|
||||
}
|
||||
|
||||
SearchTab::~SearchTab() {
|
||||
delete box;
|
||||
delete results_lbl;
|
||||
delete resultsBrowser;
|
||||
delete SearchListModel;
|
||||
delete proxyModel;
|
||||
delete SearchDelegate;
|
||||
}
|
||||
|
||||
QHeaderView* SearchTab::header() const {
|
||||
return resultsBrowser->header();
|
||||
}
|
||||
|
||||
bool SearchTab::loadColWidthResultsList() {
|
||||
QIniSettings settings("qBittorrent", "qBittorrent");
|
||||
QString line = settings.value("SearchResultsColsWidth", QString()).toString();
|
||||
if(line.isEmpty())
|
||||
return false;
|
||||
QStringList width_list = line.split(' ');
|
||||
if(width_list.size() < SearchListModel->columnCount())
|
||||
return false;
|
||||
unsigned int listSize = width_list.size();
|
||||
for(unsigned int i=0; i<listSize; ++i){
|
||||
resultsBrowser->header()->resizeSection(i, width_list.at(i).toInt());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QLabel* SearchTab::getCurrentLabel()
|
||||
{
|
||||
return results_lbl;
|
||||
}
|
||||
|
||||
QTreeView* SearchTab::getCurrentTreeView()
|
||||
{
|
||||
return resultsBrowser;
|
||||
}
|
||||
|
||||
QSortFilterProxyModel* SearchTab::getCurrentSearchListProxy() const
|
||||
{
|
||||
return proxyModel;
|
||||
}
|
||||
|
||||
QStandardItemModel* SearchTab::getCurrentSearchListModel() const
|
||||
{
|
||||
return SearchListModel;
|
||||
}
|
||||
|
||||
// Set the color of a row in data model
|
||||
void SearchTab::setRowColor(int row, QString color){
|
||||
proxyModel->setDynamicSortFilter(false);
|
||||
for(int i=0; i<proxyModel->columnCount(); ++i){
|
||||
proxyModel->setData(proxyModel->index(row, i), QVariant(QColor(color)), Qt::ForegroundRole);
|
||||
}
|
||||
proxyModel->setDynamicSortFilter(true);
|
||||
}
|
||||
|
||||
|
||||
76
src/searchengine/searchtab.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#ifndef SEARCH_TAB_H
|
||||
#define SEARCH_TAB_H
|
||||
|
||||
#include "ui_search.h"
|
||||
|
||||
#define ENGINE_URL_COLUMN 4
|
||||
#define URL_COLUMN 5
|
||||
|
||||
class SearchListDelegate;
|
||||
class SearchEngine;
|
||||
class QTreeView;
|
||||
class QHeaderView;
|
||||
class QStandardItemModel;
|
||||
class QSortFilterProxyModel;
|
||||
|
||||
class SearchTab: public QWidget, public Ui::search_engine {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QVBoxLayout *box;
|
||||
QLabel *results_lbl;
|
||||
QTreeView *resultsBrowser;
|
||||
QStandardItemModel *SearchListModel;
|
||||
QSortFilterProxyModel *proxyModel;
|
||||
SearchListDelegate *SearchDelegate;
|
||||
SearchEngine *parent;
|
||||
|
||||
protected slots:
|
||||
void downloadSelectedItem(const QModelIndex& index);
|
||||
|
||||
public:
|
||||
SearchTab(SearchEngine *parent);
|
||||
~SearchTab();
|
||||
bool loadColWidthResultsList();
|
||||
QLabel * getCurrentLabel();
|
||||
QStandardItemModel* getCurrentSearchListModel() const;
|
||||
QSortFilterProxyModel* getCurrentSearchListProxy() const;
|
||||
QTreeView * getCurrentTreeView();
|
||||
void setRowColor(int row, QString color);
|
||||
QHeaderView* header() const;
|
||||
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
179
src/searchengine/supportedengines.h
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*
|
||||
* Contact : chris@qbittorrent.org
|
||||
*/
|
||||
|
||||
#ifndef SEARCHENGINES_H
|
||||
#define SEARCHENGINES_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QStringList>
|
||||
#include <QDomDocument>
|
||||
#include <QDomNode>
|
||||
#include <QDomElement>
|
||||
#include <QProcess>
|
||||
#include <QDir>
|
||||
#include <QApplication>
|
||||
|
||||
#include "misc.h"
|
||||
#include "qinisettings.h"
|
||||
|
||||
class SearchCategories: public QObject, public QHash<QString, QString> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SearchCategories() {
|
||||
(*this)["all"] = tr("All categories");
|
||||
(*this)["movies"] = tr("Movies");
|
||||
(*this)["tv"] = tr("TV shows");
|
||||
(*this)["music"] = tr("Music");
|
||||
(*this)["games"] = tr("Games");
|
||||
(*this)["anime"] = tr("Anime");
|
||||
(*this)["software"] = tr("Software");
|
||||
(*this)["pictures"] = tr("Pictures");
|
||||
(*this)["books"] = tr("Books");
|
||||
}
|
||||
};
|
||||
|
||||
class SupportedEngine {
|
||||
private:
|
||||
QString name;
|
||||
QString full_name;
|
||||
QString url;
|
||||
QStringList supported_categories;
|
||||
bool enabled;
|
||||
|
||||
public:
|
||||
SupportedEngine(QDomElement engine_elem) {
|
||||
name = engine_elem.tagName();
|
||||
full_name = engine_elem.elementsByTagName("name").at(0).toElement().text();
|
||||
url = engine_elem.elementsByTagName("url").at(0).toElement().text();
|
||||
supported_categories = engine_elem.elementsByTagName("categories").at(0).toElement().text().split(" ");
|
||||
QIniSettings settings(QString::fromUtf8("qBittorrent"), QString::fromUtf8("qBittorrent"));
|
||||
QStringList disabled_engines = settings.value(QString::fromUtf8("SearchEngines/disabledEngines"), QStringList()).toStringList();
|
||||
enabled = !disabled_engines.contains(name);
|
||||
}
|
||||
|
||||
QString getName() const { return name; }
|
||||
QString getUrl() const { return url; }
|
||||
QString getFullName() const { return full_name; }
|
||||
QStringList getSupportedCategories() const { return supported_categories; }
|
||||
bool isEnabled() const { return enabled; }
|
||||
void setEnabled(bool _enabled) {
|
||||
enabled = _enabled;
|
||||
// Save to Hard disk
|
||||
QIniSettings settings(QString::fromUtf8("qBittorrent"), QString::fromUtf8("qBittorrent"));
|
||||
QStringList disabled_engines = settings.value(QString::fromUtf8("SearchEngines/disabledEngines"), QStringList()).toStringList();
|
||||
if(enabled) {
|
||||
disabled_engines.removeAll(name);
|
||||
} else {
|
||||
disabled_engines.append(name);
|
||||
}
|
||||
settings.setValue("SearchEngines/disabledEngines", disabled_engines);
|
||||
}
|
||||
};
|
||||
|
||||
class SupportedEngines: public QObject, public QHash<QString, SupportedEngine*> {
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void newSupportedEngine(QString name);
|
||||
|
||||
public:
|
||||
SupportedEngines() {
|
||||
update();
|
||||
}
|
||||
|
||||
~SupportedEngines() {
|
||||
qDeleteAll(this->values());
|
||||
}
|
||||
|
||||
QStringList enginesEnabled() const {
|
||||
QStringList engines;
|
||||
foreach(const SupportedEngine *engine, values()) {
|
||||
if(engine->isEnabled())
|
||||
engines << engine->getName();
|
||||
}
|
||||
return engines;
|
||||
}
|
||||
|
||||
QStringList supportedCategories() const {
|
||||
QStringList supported_cat;
|
||||
foreach(const SupportedEngine *engine, values()) {
|
||||
if(engine->isEnabled()) {
|
||||
const QStringList &s = engine->getSupportedCategories();
|
||||
foreach(QString cat, s) {
|
||||
cat = cat.trimmed();
|
||||
if(!cat.isEmpty() && !supported_cat.contains(cat))
|
||||
supported_cat << cat;
|
||||
}
|
||||
}
|
||||
}
|
||||
return supported_cat;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void update() {
|
||||
QProcess nova;
|
||||
nova.setEnvironment(QProcess::systemEnvironment());
|
||||
QStringList params;
|
||||
params << misc::searchEngineLocation()+QDir::separator()+"nova2.py";
|
||||
params << "--capabilities";
|
||||
nova.start("python", params, QIODevice::ReadOnly);
|
||||
nova.waitForStarted();
|
||||
nova.waitForFinished();
|
||||
QString capabilities = QString(nova.readAll());
|
||||
QDomDocument xml_doc;
|
||||
if(!xml_doc.setContent(capabilities)) {
|
||||
std::cerr << "Could not parse Nova search engine capabilities, msg: " << capabilities.toLocal8Bit().data() << std::endl;
|
||||
std::cerr << "Error: " << nova.readAllStandardError().constData() << std::endl;
|
||||
return;
|
||||
}
|
||||
QDomElement root = xml_doc.documentElement();
|
||||
if(root.tagName() != "capabilities") {
|
||||
std::cout << "Invalid XML file for Nova search engine capabilities, msg: " << capabilities.toLocal8Bit().data() << std::endl;
|
||||
return;
|
||||
}
|
||||
for(QDomNode engine_node = root.firstChild(); !engine_node.isNull(); engine_node = engine_node.nextSibling()) {
|
||||
QDomElement engine_elem = engine_node.toElement();
|
||||
if(!engine_elem.isNull()) {
|
||||
SupportedEngine *s = new SupportedEngine(engine_elem);
|
||||
if(this->contains(s->getName())) {
|
||||
// Already in the list
|
||||
delete s;
|
||||
} else {
|
||||
qDebug("Supported search engine: %s", s->getFullName().toLocal8Bit().data());
|
||||
(*this)[s->getName()] = s;
|
||||
emit newSupportedEngine(s->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // SEARCHENGINES_H
|
||||