Change project directory structure.

Change project directory structure according to application structure.
Change 'nox' configuration option to something more meaningful 'nogui'.
Rename 'Icons' folder to 'icons' (similar to other folders).
Partially add 'nowebui' option support.
Remove QConf project file.
This commit is contained in:
Vladimir Golovnev (Glassez)
2015-01-18 15:13:06 +03:00
parent e4c7f52bb3
commit ff9a281b72
797 changed files with 841 additions and 829 deletions

View File

@@ -0,0 +1,130 @@
<?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: &lt;a href=&quot;http://plugins.qbittorrent.org&quot;&gt;http://plugins.qbittorrent.org&lt;/a&gt;</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="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Enabled</string>
</property>
</action>
<action name="actionUninstall">
<property name="text">
<string>Uninstall</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,471 @@
/*
* 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 "fs_utils.h"
#include "misc.h"
#include "ico.h"
#include "searchengine.h"
#include "pluginsource.h"
#include "iconprovider.h"
#include "autoexpandabledialog.h"
#include <QProcess>
#include <QHeaderView>
#include <QMenu>
#include <QMessageBox>
#include <QFileDialog>
#include <QDropEvent>
#include <QTemporaryFile>
#include <QMimeData>
enum EngineColumns {ENGINE_NAME, ENGINE_URL, ENGINE_STATE, ENGINE_ID};
const QString UPDATE_URL = QString("https://raw.github.com/qbittorrent/qBittorrent/master/src/searchengine/") + (misc::pythonVersion() >= 3 ? "nova3" : "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);
actionUninstall->setIcon(IconProvider::instance()->getIcon("list-remove"));
connect(actionEnable, SIGNAL(toggled(bool)), this, SLOT(enableSelection(bool)));
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"));
foreach (QString file, files) {
qDebug("dropped %s", qPrintable(file));
if (misc::isUrl(file)) {
setCursor(QCursor(Qt::WaitCursor));
downloader->downloadUrl(file);
continue;
}
if (file.endsWith(".py", Qt::CaseInsensitive)) {
if (file.startsWith("file:", Qt::CaseInsensitive))
file = QUrl(file).toLocalFile();
QString plugin_name = fsutils::fileName(file);
plugin_name.chop(3); // Remove extension
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();
if (items.isEmpty()) return;
QString first_id = items.first()->text(ENGINE_ID);
actionEnable->setChecked(supported_engines->value(first_id)->isEnabled());
myContextMenu.addAction(actionEnable);
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 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(fsutils::searchEngineLocation() + "/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;
}
}
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(bool enable) {
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(enable);
if (enable) {
item->setText(ENGINE_STATE, tr("Yes"));
setRowColor(index, "green");
} else {
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, qreal new_version) const {
qreal old_version = SearchEngine::getPluginVersion(fsutils::searchEngineLocation() + "/engines/" + 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));
qreal 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")+" -- 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 = fsutils::searchEngineLocation() + "/engines/" + plugin_name + ".py";
bool update = false;
if (QFile::exists(dest_path)) {
// Backup in case install fails
QFile::copy(dest_path, dest_path+".bak");
fsutils::forceRemove(dest_path);
fsutils::forceRemove(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
fsutils::forceRemove(dest_path);
// restore backup
QFile::copy(dest_path+".bak", dest_path);
fsutils::forceRemove(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
fsutils::forceRemove(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) {
fsutils::forceRemove(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 = fsutils::searchEngineLocation() + "/engines/" + engine->getName() + ".png";
if (QFile::exists(iconPath)) {
// Good, we already have the icon
item->setData(ENGINE_NAME, Qt::DecorationRole, QVariant(QIcon(iconPath)));
} else {
iconPath = fsutils::searchEngineLocation() + "/engines/" + 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(false);
QString url = AutoExpandableDialog::getText(this, tr("New search engine plugin URL"),
tr("URL:"), QLineEdit::Normal,
"http://", &ok);
while(true) {
if (!ok || url.isEmpty())
return;
if (!url.endsWith(".py")) {
QMessageBox::warning(this, tr("Invalid link"), tr("The link doesn't seem to point to a search engine plugin."));
url = AutoExpandableDialog::getText(this, tr("New search engine plugin URL"),
tr("URL:"), QLineEdit::Normal,
url, &ok);
}
else
break;
}
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("/").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;
qreal 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
fsutils::forceRemove(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) {
filePath = fsutils::fromNativePath(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 = fsutils::searchEngineLocation() + "/engines/" + id + ".ico";
else
iconPath = fsutils::searchEngineLocation() + "/engines/" + id + ".png";
QFile::copy(filePath, iconPath);
item->setData(ENGINE_NAME, Qt::DecorationRole, QVariant(QIcon(iconPath)));
}
}
// Delete tmp file
fsutils::forceRemove(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."));
}
fsutils::forceRemove(filePath);
return;
}
if (url.endsWith(".py", Qt::CaseInsensitive)) {
QString plugin_name = fsutils::fileName(url);
plugin_name.chop(3); // Remove extension
installPlugin(filePath, plugin_name);
fsutils::forceRemove(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));
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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;
QT_BEGIN_NAMESPACE
class QDropEvent;
QT_END_NAMESPACE
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, qreal 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(bool enable);
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

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#VERSION: 1.23
#AUTHORS: BTDigg team (research@btdigg.org)
#
# GNU GENERAL PUBLIC LICENSE
# Version 3, 29 June 2007
#
# <http://www.gnu.org/licenses/>
#
# 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 3 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.
import urllib
import urllib2
import sys
if sys.platform == 'win32':
import httplib
import socket
import ssl
class HTTPSConnection(httplib.HTTPConnection):
"This class allows communication via SSL."
default_port = httplib.HTTPS_PORT
def __init__(self, host, port=None, key_file=None, cert_file=None,
strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None):
httplib.HTTPConnection.__init__(self, host, port, strict, timeout,
source_address)
self.key_file = key_file
self.cert_file = cert_file
def connect(self):
"Connect to a host on a given (SSL) port."
sock = socket.create_connection((self.host, self.port),
self.timeout, self.source_address)
if self._tunnel_host:
self.sock = sock
self._tunnel()
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_TLSv1)
httplib.HTTPSConnection = HTTPSConnection
from novaprinter import prettyPrinter
class btdigg(object):
url = 'https://btdigg.org'
name = 'BTDigg'
supported_categories = {'all': ''}
def __init__(self):
pass
def search(self, what, cat='all'):
req = urllib.unquote(what)
u = urllib2.urlopen('https://api.btdigg.org/api/public-8e9a50f8335b964f/s01?%s' % (urllib.urlencode(dict(q = req)),))
try:
for line in u:
if line.startswith('#'):
continue
info_hash, name, files, size, dl, seen = line.strip().split('\t')[:6]
name = name.translate(None, '|')
res = dict(link = 'magnet:?xt=urn:btih:%s&dn=%s' % (info_hash, urllib.quote(name)),
name = name,
size = size,
seeds = int(dl),
leech = int(dl),
engine_url = self.url,
desc_link = '%s/search?%s' % (self.url, urllib.urlencode(dict(info_hash = info_hash, q = req)),))
prettyPrinter(res)
finally:
u.close()
if __name__ == "__main__":
s = btdigg()
s.search(sys.argv[1])

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -0,0 +1,116 @@
#VERSION: 1.2
#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 extratorrent(object):
url = 'http://extratorrent.cc'
name = 'extratorrent'
supported_categories = {'all': '', 'movies': '4', 'tv': '8', 'music': '5', 'games': '3', 'anime': '1', 'software': '7', 'books': '2', 'pictures': '6'}
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.start_name = False
self.results = results
def start_a(self, attr):
params = dict(attr)
#print params
if params.has_key('href') and params['href'].startswith("/torrent_download/"):
self.current_item = {}
self.td_counter = 0
self.start_name = False
torrent_id = '/'.join(params['href'].split('/')[2:])
self.current_item['link']=self.url+'/download/'+torrent_id
elif params.has_key('href') and params['href'].startswith("/torrent/") and params['href'].endswith(".html"):
self.current_item['desc_link'] = self.url + params['href'].strip()
self.start_name = True
def handle_data(self, data):
if self.td_counter == 2:
if not self.current_item.has_key('name') and self.start_name:
self.current_item['name'] = data.strip()
elif self.td_counter == 3:
if not self.current_item.has_key('size'):
self.current_item['size'] = ''
self.current_item['size']+= data.replace("&nbsp;", " ").strip()
elif self.td_counter == 4:
if not self.current_item.has_key('seeds'):
self.current_item['seeds'] = ''
self.current_item['seeds']+= data.strip()
elif self.td_counter == 5:
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 > 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 = 1
while True and i<11:
results = []
parser = self.SimpleSGMLParser(results, self.url)
dat = retrieve_url(self.url+'/advanced_search/?with=%s&s_cat=%s&page=%d'%(what, self.supported_categories[cat], i))
results_re = re.compile('(?s)<table class="tl"><thead>.*')
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -0,0 +1,72 @@
#VERSION: 1.25
#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 json
class kickasstorrents(object):
url = 'https://kickass.so'
name = 'kickasstorrents'
supported_categories = {'all': '', 'movies': 'Movies', 'tv': 'TV', 'music': 'Music', 'games': 'Games', 'software': 'Applications'}
def __init__(self):
self.results = []
def download_torrent(self, info):
print download_file(info, info)
def search(self, what, cat='all'):
ret = []
i = 1
while True and i<11:
results = []
json_data = retrieve_url(self.url+'/json.php?q=%s&page=%d'%(what, i))
try:
json_dict = json.loads(json_data)
except:
i += 1
continue
if int(json_dict['total_results']) <= 0: return
results = json_dict['list']
for r in results:
try:
if cat != 'all' and self.supported_categories[cat] != r['category']: continue
res_dict = dict()
res_dict['name'] = r['title']
res_dict['size'] = str(r['size'])
res_dict['seeds'] = r['seeds']
res_dict['leech'] = r['leechs']
res_dict['link'] = r['torrentLink']
res_dict['desc_link'] = r['link']
res_dict['engine_url'] = self.url
prettyPrinter(res_dict)
except:
pass
i += 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

View File

@@ -0,0 +1,109 @@
#VERSION: 1.02
#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 legittorrents(object):
url = 'http://www.legittorrents.info'
name = 'legittorrents'
supported_categories = {'all': '', 'movies': '1', 'tv': '13', 'music': '2', 'games': '3', 'anime': '5', 'books': '6'}
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.start_name = False
self.results = results
def start_a(self, attr):
params = dict(attr)
if params.has_key('href') and params['href'].startswith('download.php?'):
self.current_item['link'] = self.url + '/' + params['href'].strip()
elif params.has_key('href') and params['href'].startswith('index.php?page=torrent-details'):
self.current_item = {}
self.td_counter = 0
self.current_item['desc_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'] = data.strip()
elif self.td_counter == 3:
if not self.current_item.has_key('seeds'):
self.current_item['seeds'] = ''
self.current_item['seeds']+= data.strip()
elif self.td_counter == 4:
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 > 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
self.current_item['size'] = ''
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+'/index.php?page=torrents&search=%s&category=%s&active=1&order=3&by=2&pages=%d'%(what, self.supported_categories[cat], i))
results_re = re.compile('(?s)<table width="100%" class="lista">.*')
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,114 @@
#VERSION: 1.50
#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'):
if params['href'].startswith("/get/"):
self.current_item = {}
self.td_counter = 0
self.current_item['link']=self.url+params['href'].strip()
elif params['href'].startswith("/tor/"):
self.current_item['desc_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

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,139 @@
#VERSION: 2.01
#AUTHORS: Fabien Devaux (fab@gnux.info)
#CONTRIBUTORS: Christophe Dumez (chris@qbittorrent.org)
# Arthur (custparasite@gmx.se)
# 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 HTMLParser import HTMLParser
from helpers import download_file
import urllib2
PREVIOUS_IDS = set()
class piratebay(object):
url = 'https://thepiratebay.se'
name = 'The Pirate Bay'
supported_categories = {'all': '0', 'music': '100', 'movies': '200', 'games': '400', 'software': '300'}
def download_torrent(self, info):
print(download_file(info))
class MyHtmlParseWithBlackJack(HTMLParser):
def __init__(self, results, url):
HTMLParser.__init__(self)
self.url = url
self.results = results
self.current_item = None
self.size_found = False
self.unit_found = False
self.seed_found = False
self.skip_td = False
self.leech_found = False
self.dispatcher = {'a' : self.handle_tag_a_ref,
'font' : self.handle_tag_font_size,
'td' : self.handle_tag_td_sl }
def handle_tag_a_ref(self, attrs):
params = dict(attrs)
#1
if params['href'].startswith('/torrent/'):
get_id = params['href'].split('/')[2]
if not get_id in PREVIOUS_IDS:
self.current_item = {}
self.current_item['desc_link'] = self.url + params['href'].strip()
self.current_item['name'] = params['title'][12:].strip()
self.current_item['id'] = get_id
#2
elif (not self.current_item is None) and (params['href'].startswith('magnet:')):
self.current_item['link'] = params['href'].strip()
def handle_tag_font_size(self, attrs):
if not self.current_item is None:
params = dict(attrs)
#3
if params['class'] == "detDesc":
self.size_found = True
def handle_tag_td_sl(self, attrs):
if not self.current_item is None:
params = dict(attrs)
if not self.current_item is None:
if self.seed_found:
#5
self.current_item['leech'] = ''
self.leech_found = True
self.seed_found = False
else:
#4
self.current_item['seeds'] = ''
self.seed_found = True
def handle_starttag(self, tag, attrs):
if tag in self.dispatcher:
self.dispatcher[tag](attrs)
def handle_data(self, data):
if not self.current_item is None:
if self.size_found:
#with utf-8 you're going to have something like that: ['Uploaded', '10-02'], ['15:31,', 'Size', '240.34'], ['MiB,', 'ULed', 'by']
temp = data.split()
if 'Size' in temp:
sizeIn = temp.index('Size')
self.current_item['size'] = temp[sizeIn + 1]
self.size_found = False
self.unit_found = True
elif self.unit_found:
temp = data.split()
self.current_item['size'] = ' '.join((self.current_item['size'], temp[0]))
self.unit_found = False
elif self.seed_found:
self.current_item['seeds'] += data.rstrip()
elif self.leech_found:
self.current_item['leech'] += data.rstrip()
self.current_item['engine_url'] = self.url
prettyPrinter(self.current_item)
PREVIOUS_IDS.add(self.current_item['id'])
self.results.append('a')
self.current_item = None
self.size_found = False
self.unit_found = False
self.seed_found = False
self.leech_found = False
def search(self, what, cat='all'):
ret = []
i = 0
while i < 11:
results = []
parser = self.MyHtmlParseWithBlackJack(results, self.url)
query = '%s/search/%s/%d/99/%s' % (self.url, what, i, self.supported_categories[cat])
dat = urllib2.urlopen(query)
parser.feed(dat.read().decode('utf-8'))
parser.close()
if len(results) <= 0:
break
i += 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

View File

@@ -0,0 +1,119 @@
#VERSION: 1.33
#AUTHORS: Gekko Dam Beer (gekko04@users.sourceforge.net)
#CONTRIBUTORS: Christophe Dumez (chris@qbittorrent.org)
# Bruno Barbieri (brunorex@gmail.com)
# 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
from urllib2 import HTTPError
from HTMLParser import HTMLParser
import urllib
import re
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 SimpleHTMLParser(HTMLParser):
def __init__(self, results, url, *args):
HTMLParser.__init__(self)
self.td_counter = None
self.current_item = None
self.results = results
self.id = None
self.url = url
self.dispatcher = { 'a' : self.start_a, 'td' : self.start_td }
def handle_starttag(self, tag, attrs):
if tag in self.dispatcher:
self.dispatcher[tag](attrs)
def start_a(self, attr):
params = dict(attr)
if re.match("/torrents/\d+.*", params['href']):
self.current_item = {}
self.current_item['desc_link'] = self.url+params['href'].strip()
elif 'torrentreactor.net/download.php' in params['href']:
self.td_counter = 0
self.current_item['link'] = params['href'].strip()
self.current_item['name'] = urllib.unquote_plus(params['href'].split('&')[1].split('name=')[1])
def handle_data(self, data):
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.SimpleHTMLParser(self.results, self.url)
def search(self, what, cat='all'):
i = 0
dat = ''
while True and i<11:
results = []
parser = self.SimpleHTMLParser(results, self.url)
try:
dat = retrieve_url(self.url+'/torrent-search/%s/%s?sort=seeders.desc&type=all&period=none&categories=%s'%(what, (i*35), self.supported_categories[cat]))
except HTTPError:
break
parser.feed(dat)
parser.close()
if len(results) <= 0:
break
i += 1

View File

@@ -0,0 +1,7 @@
torrentreactor: 1.33
mininova: 1.50
piratebay: 2.01
extratorrent: 1.2
kickasstorrents: 1.25
btdigg: 1.23
legittorrents: 1.02

View File

@@ -0,0 +1,371 @@
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Collection of functions and classes to fix various encoding problems on
multiple platforms with python.
"""
import codecs
import locale
import os
import sys
# Prevents initializing multiple times.
_SYS_ARGV_PROCESSED = False
def complain(message):
"""If any exception occurs in this file, we'll probably try to print it
on stderr, which makes for frustrating debugging if stderr is directed
to our wrapper. So be paranoid about catching errors and reporting them
to sys.__stderr__, so that the user has a higher chance to see them.
"""
print >> sys.__stderr__, (
isinstance(message, str) and message or repr(message))
def fix_default_encoding():
"""Forces utf8 solidly on all platforms.
By default python execution environment is lazy and defaults to ascii
encoding.
http://uucode.com/blog/2007/03/23/shut-up-you-dummy-7-bit-python/
"""
if sys.getdefaultencoding() == 'utf-8':
return False
# Regenerate setdefaultencoding.
reload(sys)
# Module 'sys' has no 'setdefaultencoding' member
# pylint: disable=E1101
sys.setdefaultencoding('utf-8')
for attr in dir(locale):
if attr[0:3] != 'LC_':
continue
aref = getattr(locale, attr)
try:
locale.setlocale(aref, '')
except locale.Error:
continue
try:
lang = locale.getlocale(aref)[0]
except (TypeError, ValueError):
continue
if lang:
try:
locale.setlocale(aref, (lang, 'UTF-8'))
except locale.Error:
os.environ[attr] = lang + '.UTF-8'
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error:
pass
return True
###############################
# Windows specific
def fix_win_sys_argv(encoding):
"""Converts sys.argv to 'encoding' encoded string.
utf-8 is recommended.
Works around <http://bugs.python.org/issue2128>.
"""
global _SYS_ARGV_PROCESSED
if _SYS_ARGV_PROCESSED:
return False
# These types are available on linux but not Mac.
# pylint: disable=E0611,F0401
from ctypes import byref, c_int, POINTER, windll, WINFUNCTYPE
from ctypes.wintypes import LPCWSTR, LPWSTR
# <http://msdn.microsoft.com/en-us/library/ms683156.aspx>
GetCommandLineW = WINFUNCTYPE(LPWSTR)(('GetCommandLineW', windll.kernel32))
# <http://msdn.microsoft.com/en-us/library/bb776391.aspx>
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
('CommandLineToArgvW', windll.shell32))
argc = c_int(0)
argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
argv = [
argv_unicode[i].encode(encoding, 'replace')
for i in xrange(0, argc.value)]
if not hasattr(sys, 'frozen'):
# If this is an executable produced by py2exe or bbfreeze, then it
# will have been invoked directly. Otherwise, unicode_argv[0] is the
# Python interpreter, so skip that.
argv = argv[1:]
# Also skip option arguments to the Python interpreter.
while len(argv) > 0:
arg = argv[0]
if not arg.startswith(u'-') or arg == u'-':
break
argv = argv[1:]
if arg == u'-m':
# sys.argv[0] should really be the absolute path of the
# module source, but never mind.
break
if arg == u'-c':
argv[0] = u'-c'
break
sys.argv = argv
_SYS_ARGV_PROCESSED = True
return True
def fix_win_codec():
"""Works around <http://bugs.python.org/issue6058>."""
# <http://msdn.microsoft.com/en-us/library/dd317756.aspx>
try:
codecs.lookup('cp65001')
return False
except LookupError:
codecs.register(
lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
return True
class WinUnicodeOutputBase(object):
"""Base class to adapt sys.stdout or sys.stderr to behave correctly on
Windows.
Setting encoding to utf-8 is recommended.
"""
def __init__(self, fileno, name, encoding):
# Corresponding file handle.
self._fileno = fileno
self.encoding = encoding
self.name = name
self.closed = False
self.softspace = False
self.mode = 'w'
@staticmethod
def isatty():
return False
def close(self):
# Don't really close the handle, that would only cause problems.
self.closed = True
def fileno(self):
return self._fileno
def flush(self):
raise NotImplementedError()
def write(self, text):
raise NotImplementedError()
def writelines(self, lines):
try:
for line in lines:
self.write(line)
except Exception, e:
complain('%s.writelines: %r' % (self.name, e))
raise
class WinUnicodeConsoleOutput(WinUnicodeOutputBase):
"""Output adapter to a Windows Console.
Understands how to use the win32 console API.
"""
def __init__(self, console_handle, fileno, stream_name, encoding):
super(WinUnicodeConsoleOutput, self).__init__(
fileno, '<Unicode console %s>' % stream_name, encoding)
# Handle to use for WriteConsoleW
self._console_handle = console_handle
# Loads the necessary function.
# These types are available on linux but not Mac.
# pylint: disable=E0611,F0401
from ctypes import byref, GetLastError, POINTER, windll, WINFUNCTYPE
from ctypes.wintypes import BOOL, DWORD, HANDLE, LPWSTR
from ctypes.wintypes import LPVOID # pylint: disable=E0611
self._DWORD = DWORD
self._byref = byref
# <http://msdn.microsoft.com/en-us/library/ms687401.aspx>
self._WriteConsoleW = WINFUNCTYPE(
BOOL, HANDLE, LPWSTR, DWORD, POINTER(DWORD), LPVOID)(
('WriteConsoleW', windll.kernel32))
self._GetLastError = GetLastError
def flush(self):
# No need to flush the console since it's immediate.
pass
def write(self, text):
try:
if not isinstance(text, unicode):
# Convert to unicode.
text = str(text).decode(self.encoding, 'replace')
remaining = len(text)
while remaining > 0:
n = self._DWORD(0)
# There is a shorter-than-documented limitation on the length of the
# string passed to WriteConsoleW. See
# <http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1232>.
retval = self._WriteConsoleW(
self._console_handle, text,
min(remaining, 10000),
self._byref(n), None)
if retval == 0 or n.value == 0:
raise IOError(
'WriteConsoleW returned %r, n.value = %r, last error = %r' % (
retval, n.value, self._GetLastError()))
remaining -= n.value
if not remaining:
break
text = text[n.value:]
except Exception, e:
complain('%s.write: %r' % (self.name, e))
raise
class WinUnicodeOutput(WinUnicodeOutputBase):
"""Output adaptor to a file output on Windows.
If the standard FileWrite function is used, it will be encoded in the current
code page. WriteConsoleW() permits writting any character.
"""
def __init__(self, stream, fileno, encoding):
super(WinUnicodeOutput, self).__init__(
fileno, '<Unicode redirected %s>' % stream.name, encoding)
# Output stream
self._stream = stream
# Flush right now.
self.flush()
def flush(self):
try:
self._stream.flush()
except Exception, e:
complain('%s.flush: %r from %r' % (self.name, e, self._stream))
raise
def write(self, text):
try:
if isinstance(text, unicode):
# Replace characters that cannot be printed instead of failing.
text = text.encode(self.encoding, 'replace')
self._stream.write(text)
except Exception, e:
complain('%s.write: %r' % (self.name, e))
raise
def win_handle_is_a_console(handle):
"""Returns True if a Windows file handle is a handle to a console."""
# These types are available on linux but not Mac.
# pylint: disable=E0611,F0401
from ctypes import byref, POINTER, windll, WINFUNCTYPE
from ctypes.wintypes import BOOL, DWORD, HANDLE
FILE_TYPE_CHAR = 0x0002
FILE_TYPE_REMOTE = 0x8000
INVALID_HANDLE_VALUE = DWORD(-1).value
# <http://msdn.microsoft.com/en-us/library/ms683167.aspx>
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
('GetConsoleMode', windll.kernel32))
# <http://msdn.microsoft.com/en-us/library/aa364960.aspx>
GetFileType = WINFUNCTYPE(DWORD, DWORD)(('GetFileType', windll.kernel32))
# GetStdHandle returns INVALID_HANDLE_VALUE, NULL, or a valid handle.
if handle == INVALID_HANDLE_VALUE or handle is None:
return False
return (
(GetFileType(handle) & ~FILE_TYPE_REMOTE) == FILE_TYPE_CHAR and
GetConsoleMode(handle, byref(DWORD())))
def win_get_unicode_stream(stream, excepted_fileno, output_handle, encoding):
"""Returns a unicode-compatible stream.
This function will return a direct-Console writing object only if:
- the file number is the expected console file number
- the handle the expected file handle
- the 'real' handle is in fact a handle to a console.
"""
old_fileno = getattr(stream, 'fileno', lambda: None)()
if old_fileno == excepted_fileno:
# These types are available on linux but not Mac.
# pylint: disable=E0611,F0401
from ctypes import windll, WINFUNCTYPE
from ctypes.wintypes import DWORD, HANDLE
# <http://msdn.microsoft.com/en-us/library/ms683231.aspx>
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(('GetStdHandle', windll.kernel32))
real_output_handle = GetStdHandle(DWORD(output_handle))
if win_handle_is_a_console(real_output_handle):
# It's a console.
return WinUnicodeConsoleOutput(
real_output_handle, old_fileno, stream.name, encoding)
# It's something else. Create an auto-encoding stream.
return WinUnicodeOutput(stream, old_fileno, encoding)
def fix_win_console(encoding):
"""Makes Unicode console output work independently of the current code page.
This also fixes <http://bugs.python.org/issue1602>.
Credit to Michael Kaplan
<http://blogs.msdn.com/b/michkap/archive/2010/04/07/9989346.aspx> and
TZOmegaTZIOY
<http://stackoverflow.com/questions/878972/windows-cmd-encoding-change-causes-python-crash/1432462#1432462>.
"""
if (isinstance(sys.stdout, WinUnicodeOutputBase) or
isinstance(sys.stderr, WinUnicodeOutputBase)):
return False
try:
# SetConsoleCP and SetConsoleOutputCP could be used to change the code page
# but it's not really useful since the code here is using WriteConsoleW().
# Also, changing the code page is 'permanent' to the console and needs to be
# reverted manually.
# In practice one needs to set the console font to a TTF font to be able to
# see all the characters but it failed for me in practice. In any case, it
# won't throw any exception when printing, which is the important part.
# -11 and -12 are defined in stdio.h
sys.stdout = win_get_unicode_stream(sys.stdout, 1, -11, encoding)
sys.stderr = win_get_unicode_stream(sys.stderr, 2, -12, encoding)
# TODO(maruel): Do sys.stdin with ReadConsoleW(). Albeit the limitation is
# "It doesn't appear to be possible to read Unicode characters in UTF-8
# mode" and this appears to be a limitation of cmd.exe.
except Exception, e:
complain('exception %r while fixing up sys.stdout and sys.stderr' % e)
return True
def fix_encoding():
"""Fixes various encoding problems on all platforms.
Should be called at the very begining of the process.
"""
ret = True
if sys.platform == 'win32':
ret &= fix_win_codec()
ret &= fix_default_encoding()
if sys.platform == 'win32':
encoding = sys.getdefaultencoding()
ret &= fix_win_sys_argv(encoding)
ret &= fix_win_console(encoding)
return ret

View File

@@ -0,0 +1,109 @@
# 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.34
# Author:
# Christophe DUMEZ (chris@qbittorrent.org)
import re, htmlentitydefs
import tempfile
import os
import StringIO, gzip, urllib2
import socket
import socks
import re
# Some sites blocks default python User-agent
user_agent = 'Mozilla/5.0'
headers = {'User-Agent': user_agent}
# 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 &eacute;)
# (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 &#233;)
t = re.sub(u'&#(\d+);', lambda x: unichr(int(x.group(1))), t)
# Then convert hexa entities (such as &#x00E9;)
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, headers = headers)
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
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, headers = headers)
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

View File

@@ -0,0 +1,163 @@
#!/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.32
# 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
import urllib
import fix_encoding
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__':
# Make sure we enforce utf-8 encoding
fix_encoding.fix_encoding()
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 = urllib.quote(' '.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()

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

View File

@@ -0,0 +1,69 @@
#VERSION: 1.43
# 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'])
if dictionary.has_key('desc_link'):
print u"%s|%s|%s|%s|%s|%s|%s"%(dictionary['link'],dictionary['name'].replace('|',' '),dictionary['size'],dictionary['seeds'],dictionary['leech'],dictionary['engine_url'],dictionary['desc_link'])
else:
print u"%s|%s|%s|%s|%s|%s"%(dictionary['link'],dictionary['name'].replace('|',' '),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)

View File

@@ -0,0 +1,391 @@
"""SocksiPy - Python SOCKS module.
Version 1.01
Copyright 2006 Dan-Haim. All rights reserved.
Various fixes by Christophe DUMEZ <chris@qbittorrent.org> - 2010
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:
d = self.recv(bytes-len(data))
if not d:
raise GeneralProxyError("connection closed unexpectedly")
data = data + d
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(ord(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]))

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#VERSION: 1.23
#AUTHORS: BTDigg team (research@btdigg.org)
#
# GNU GENERAL PUBLIC LICENSE
# Version 3, 29 June 2007
#
# <http://www.gnu.org/licenses/>
#
# 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 3 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.
import urllib.request, urllib.parse, urllib.error
import urllib.request, urllib.error, urllib.parse
import sys
from novaprinter import prettyPrinter
class btdigg(object):
url = 'https://btdigg.org'
name = 'BTDigg'
supported_categories = {'all': ''}
def __init__(self):
pass
def search(self, what, cat='all'):
req = urllib.parse.unquote(what)
u = urllib.request.urlopen('https://api.btdigg.org/api/public-8e9a50f8335b964f/s01?%s' % (urllib.parse.urlencode(dict(q = req)),))
try:
for line in u:
line = line.decode('utf-8')
if line.startswith('#'):
continue
info_hash, name, files, size, dl, seen = line.strip().split('\t')[:6]
name = name.replace('|', '')
res = dict(link = 'magnet:?xt=urn:btih:%s&dn=%s' % (info_hash, urllib.parse.quote(name)),
name = name,
size = size,
seeds = int(dl),
leech = int(dl),
engine_url = self.url,
desc_link = '%s/search?%s' % (self.url, urllib.parse.urlencode(dict(info_hash = info_hash, q = req)),))
prettyPrinter(res)
finally:
u.close()
if __name__ == "__main__":
s = btdigg()
s.search(sys.argv[1])

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -0,0 +1,116 @@
#VERSION: 1.2
#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 sgmllib3
import re
class extratorrent(object):
url = 'http://extratorrent.cc'
name = 'extratorrent'
supported_categories = {'all': '', 'movies': '4', 'tv': '8', 'music': '5', 'games': '3', 'anime': '1', 'software': '7', 'books': '2', 'pictures': '6'}
def __init__(self):
self.results = []
self.parser = self.SimpleSGMLParser(self.results, self.url)
def download_torrent(self, info):
print(download_file(info))
class SimpleSGMLParser(sgmllib3.SGMLParser):
def __init__(self, results, url, *args):
sgmllib3.SGMLParser.__init__(self)
self.url = url
self.td_counter = None
self.current_item = None
self.start_name = False
self.results = results
def start_a(self, attr):
params = dict(attr)
#print params
if 'href' in params and params['href'].startswith("/torrent_download/"):
self.current_item = {}
self.td_counter = 0
self.start_name = False
torrent_id = '/'.join(params['href'].split('/')[2:])
self.current_item['link']=self.url+'/download/'+torrent_id
elif 'href' in params and params['href'].startswith("/torrent/") and params['href'].endswith(".html"):
self.current_item['desc_link'] = self.url + params['href'].strip()
self.start_name = True
def handle_data(self, data):
if self.td_counter == 2:
if 'name' not in self.current_item and self.start_name:
self.current_item['name'] = data.strip()
elif self.td_counter == 3:
if 'size' not in self.current_item:
self.current_item['size'] = ''
self.current_item['size']+= data.replace("&nbsp;", " ").strip()
elif self.td_counter == 4:
if 'seeds' not in self.current_item:
self.current_item['seeds'] = ''
self.current_item['seeds']+= data.strip()
elif self.td_counter == 5:
if 'leech' not in self.current_item:
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 > 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 = 1
while True and i<11:
results = []
parser = self.SimpleSGMLParser(results, self.url)
dat = retrieve_url(self.url+'/advanced_search/?with=%s&s_cat=%s&page=%d'%(what, self.supported_categories[cat], i))
results_re = re.compile('(?s)<table class="tl"><thead>.*')
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -0,0 +1,72 @@
#VERSION: 1.25
#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 json
class kickasstorrents(object):
url = 'https://kickass.so'
name = 'kickasstorrents'
supported_categories = {'all': '', 'movies': 'Movies', 'tv': 'TV', 'music': 'Music', 'games': 'Games', 'software': 'Applications'}
def __init__(self):
self.results = []
def download_torrent(self, info):
print(download_file(info, info))
def search(self, what, cat='all'):
ret = []
i = 1
while True and i<11:
results = []
json_data = retrieve_url(self.url+'/json.php?q=%s&page=%d'%(what, i))
try:
json_dict = json.loads(json_data)
except:
i += 1
continue
if int(json_dict['total_results']) <= 0: return
results = json_dict['list']
for r in results:
try:
if cat != 'all' and self.supported_categories[cat] != r['category']: continue
res_dict = dict()
res_dict['name'] = r['title']
res_dict['size'] = str(r['size'])
res_dict['seeds'] = r['seeds']
res_dict['leech'] = r['leechs']
res_dict['link'] = r['torrentLink']
res_dict['desc_link'] = r['link']
res_dict['engine_url'] = self.url
prettyPrinter(res_dict)
except:
pass
i += 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

View File

@@ -0,0 +1,109 @@
#VERSION: 1.03
#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 sgmllib3 as sgmllib
import re
class legittorrents(object):
url = 'http://www.legittorrents.info'
name = 'legittorrents'
supported_categories = {'all': '', 'movies': '1', 'tv': '13', 'music': '2', 'games': '3', 'anime': '5', 'books': '6'}
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.start_name = False
self.results = results
def start_a(self, attr):
params = dict(attr)
if 'href' in params and params['href'].startswith('download.php?'):
self.current_item['link'] = self.url + '/' + params['href'].strip()
elif 'href' in params and params['href'].startswith('index.php?page=torrent-details'):
self.current_item = {}
self.td_counter = 0
self.current_item['desc_link'] = self.url + '/' + params['href'].strip()
def handle_data(self, data):
if self.td_counter == 0:
if 'name' not in self.current_item:
self.current_item['name'] = data.strip()
elif self.td_counter == 3:
if 'seeds' not in self.current_item:
self.current_item['seeds'] = ''
self.current_item['seeds']+= data.strip()
elif self.td_counter == 4:
if 'leech' not in self.current_item:
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 > 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
self.current_item['size'] = ''
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+'/index.php?page=torrents&search=%s&category=%s&active=1&order=3&by=2&pages=%d'%(what, self.supported_categories[cat], i))
results_re = re.compile('(?s)<table width="100%" class="lista">.*')
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,114 @@
#VERSION: 1.50
#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 sgmllib3
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(sgmllib3.SGMLParser):
def __init__(self, results, url, *args):
sgmllib3.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 'href' in params:
if params['href'].startswith("/get/"):
self.current_item = {}
self.td_counter = 0
self.current_item['link']=self.url+params['href'].strip()
elif params['href'].startswith("/tor/"):
self.current_item['desc_link']=self.url+params['href'].strip()
def handle_data(self, data):
if self.td_counter == 0:
if 'name' not in self.current_item:
self.current_item['name'] = ''
self.current_item['name']+= data
elif self.td_counter == 1:
if 'size' not in self.current_item:
self.current_item['size'] = ''
self.current_item['size']+= data.strip()
elif self.td_counter == 2:
if 'seeds' not in self.current_item:
self.current_item['seeds'] = ''
self.current_item['seeds']+= data.strip()
elif self.td_counter == 3:
if 'leech' not in self.current_item:
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,139 @@
#VERSION: 2.01
#AUTHORS: Fabien Devaux (fab@gnux.info)
#CONTRIBUTORS: Christophe Dumez (chris@qbittorrent.org)
# Arthur (custparasite@gmx.se)
# 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 html.parser import HTMLParser
from helpers import download_file
import urllib.request
PREVIOUS_IDS = set()
class piratebay(object):
url = 'https://thepiratebay.se'
name = 'The Pirate Bay'
supported_categories = {'all': '0', 'music': '100', 'movies': '200', 'games': '400', 'software': '300'}
def download_torrent(self, info):
print(download_file(info))
class MyHtmlParseWithBlackJack(HTMLParser):
def __init__(self, results, url):
super().__init__()
self.url = url
self.results = results
self.current_item = None
self.size_found = False
self.unit_found = False
self.seed_found = False
self.skip_td = False
self.leech_found = False
self.dispatcher = {'a' : self.handle_tag_a_ref,
'font' : self.handle_tag_font_size,
'td' : self.handle_tag_td_sl }
def handle_tag_a_ref(self, attrs):
params = dict(attrs)
#1
if params['href'].startswith('/torrent/'):
get_id = params['href'].split('/')[2]
if not get_id in PREVIOUS_IDS:
self.current_item = {}
self.current_item['desc_link'] = self.url + params['href'].strip()
self.current_item['name'] = params['title'][12:].strip()
self.current_item['id'] = get_id
#2
elif (not self.current_item is None) and (params['href'].startswith('magnet:')):
self.current_item['link'] = params['href'].strip()
def handle_tag_font_size(self, attrs):
if not self.current_item is None:
params = dict(attrs)
#3
if params['class'] == "detDesc":
self.size_found = True
def handle_tag_td_sl(self, attrs):
if not self.current_item is None:
params = dict(attrs)
if not self.current_item is None:
if self.seed_found:
#5
self.current_item['leech'] = ''
self.leech_found = True
self.seed_found = False
else:
#4
self.current_item['seeds'] = ''
self.seed_found = True
def handle_starttag(self, tag, attrs):
if tag in self.dispatcher:
self.dispatcher[tag](attrs)
def handle_data(self, data):
if not self.current_item is None:
if self.size_found:
#with utf-8 you're going to have something like that: ['Uploaded', '10-02'], ['15:31,', 'Size', '240.34'], ['MiB,', 'ULed', 'by']
temp = data.split()
if 'Size' in temp:
sizeIn = temp.index('Size')
self.current_item['size'] = temp[sizeIn + 1]
self.size_found = False
self.unit_found = True
elif self.unit_found:
temp = data.split()
self.current_item['size'] = ' '.join((self.current_item['size'], temp[0]))
self.unit_found = False
elif self.seed_found:
self.current_item['seeds'] += data.rstrip()
elif self.leech_found:
self.current_item['leech'] += data.rstrip()
self.current_item['engine_url'] = self.url
prettyPrinter(self.current_item)
PREVIOUS_IDS.add(self.current_item['id'])
self.results.append('a')
self.current_item = None
self.size_found = False
self.unit_found = False
self.seed_found = False
self.leech_found = False
def search(self, what, cat='all'):
ret = []
i = 0
while i < 11:
results = []
parser = self.MyHtmlParseWithBlackJack(results, self.url)
query = '%s/search/%s/%d/99/%s' % (self.url, what, i, self.supported_categories[cat])
dat = urllib.request.urlopen(query)
parser.feed(dat.read().decode('utf-8'))
parser.close()
if len(results) <= 0:
break
i += 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

View File

@@ -0,0 +1,118 @@
#VERSION: 1.33
#AUTHORS: Gekko Dam Beer (gekko04@users.sourceforge.net)
#CONTRIBUTORS: Christophe Dumez (chris@qbittorrent.org)
# Bruno Barbieri (brunorex@gmail.com)
# 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
from urllib import error, parse
from html.parser import HTMLParser
import re
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 SimpleHTMLParser(HTMLParser):
def __init__(self, results, url, *args):
HTMLParser.__init__(self)
self.td_counter = None
self.current_item = None
self.results = results
self.id = None
self.url = url
self.dispatcher = { 'a' : self.start_a, 'td' : self.start_td }
def handle_starttag(self, tag, attrs):
if tag in self.dispatcher:
self.dispatcher[tag](attrs)
def start_a(self, attr):
params = dict(attr)
if re.match("/torrents/\d+.*", params['href']):
self.current_item = {}
self.current_item['desc_link'] = self.url+params['href'].strip()
elif 'torrentreactor.net/download.php' in params['href']:
self.td_counter = 0
self.current_item['link'] = params['href'].strip()
self.current_item['name'] = parse.unquote_plus(params['href'].split('&')[1].split('name=')[1])
def handle_data(self, data):
if self.td_counter == 1:
if 'size' not in self.current_item:
self.current_item['size'] = ''
self.current_item['size']+= data.strip()
elif self.td_counter == 2:
if 'seeds' not in self.current_item:
self.current_item['seeds'] = ''
self.current_item['seeds']+= data.strip()
elif self.td_counter == 3:
if 'leech' not in self.current_item:
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.SimpleHTMLParser(self.results, self.url)
def search(self, what, cat='all'):
i = 0
dat = ''
while True and i<11:
results = []
parser = self.SimpleHTMLParser(results, self.url)
try:
dat = retrieve_url(self.url+'/torrent-search/%s/%s?sort=seeders.desc&type=all&period=none&categories=%s'%(what, (i*35), self.supported_categories[cat]))
except error.HTTPError:
break
parser.feed(dat)
parser.close()
if len(results) <= 0:
break
i += 1

View File

@@ -0,0 +1,7 @@
torrentreactor: 1.33
mininova: 1.50
piratebay: 2.01
extratorrent: 1.2
kickasstorrents: 1.25
btdigg: 1.23
legittorrents: 1.03

View File

@@ -0,0 +1,110 @@
# 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.35
# Author:
# Christophe DUMEZ (chris@qbittorrent.org)
import re, html.entities
import tempfile
import os
import io, gzip, urllib.request, urllib.error, urllib.parse
import socket
import socks
import re
# Some sites blocks default python User-agent
user_agent = 'Mozilla/5.0'
headers = {'User-Agent': user_agent}
# SOCKS5 Proxy support
if "sock_proxy" in os.environ 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 &eacute;)
# (Inspired from http://mail.python.org/pipermail/python-list/2007-June/443813.html)
def entity2char(m):
entity = m.group(1)
if entity in html.entities.name2codepoint:
return chr(html.entities.name2codepoint[entity])
return " " # Unknown entity: We replace with a space.
t = re.sub('&(%s);' % '|'.join(html.entities.name2codepoint), entity2char, s)
# Then convert numerical entities (such as &#233;)
t = re.sub('&#(\d+);', lambda x: chr(int(x.group(1))), t)
# Then convert hexa entities (such as &#x00E9;)
return re.sub('&#x(\w+);', lambda x: chr(int(x.group(1),16)), t)
def retrieve_url(url):
""" Return the content of the url page as a string """
req = urllib.request.Request(url, headers = headers)
response = urllib.request.urlopen(req)
dat = response.read()
# Check if it is gzipped
if dat[:2] == b'\x1f\x8b':
# Data is gzip encoded, decode it
compressedstream = io.BytesIO(dat)
gzipper = gzip.GzipFile(fileobj=compressedstream)
extracted_data = gzipper.read()
dat = extracted_data
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')
return dat
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, "wb")
# Download url
req = urllib.request.Request(url, headers = headers)
if referer is not None:
req.add_header('referer', referer)
response = urllib.request.urlopen(req)
dat = response.read()
# Check if it is gzipped
if dat[:2] == b'\x1f\x8b':
# Data is gzip encoded, decode it
compressedstream = io.BytesIO(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

View File

@@ -0,0 +1,158 @@
#!/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.24
# 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
import urllib.parse
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("search_engine = %s()"%short_name, globals())
xml += "<name>%s</name>\n"%search_engine.name
xml += "<url>%s</url>\n"%search_engine.url
xml += "<categories>"
if hasattr(search_engine, 'supported_categories'):
supported_categories = list(search_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 list(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 = urllib.parse.quote(' '.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 list(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()

View 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 list(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)

View File

@@ -0,0 +1,66 @@
#VERSION: 1.44
# 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.
def prettyPrinter(dictionary):
outtext = ''
dictionary['size'] = anySizeToBytes(dictionary['size'])
if 'desc_link' in dictionary:
outtext = '%s|%s|%s|%s|%s|%s|%s'%(dictionary['link'],dictionary['name'].replace('|',' '),dictionary['size'],dictionary['seeds'],dictionary['leech'],dictionary['engine_url'],dictionary['desc_link'])
else:
outtext = '%s|%s|%s|%s|%s|%s'%(dictionary['link'],dictionary['name'].replace('|',' '),dictionary['size'],dictionary['seeds'],dictionary['leech'],dictionary['engine_url'])
# fd 1 is stdout
with open(1, 'w', encoding='utf-8', closefd=False) as utf8stdout:
print(outtext, file=utf8stdout)
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 short_unit in units_dict:
size = size * 2**units_dict[short_unit]
return int(size)

View File

@@ -0,0 +1,547 @@
"""A parser for SGML, using the derived class as a static DTD."""
# XXX This only supports those SGML features used by HTML.
# XXX There should be a way to distinguish between PCDATA (parsed
# character data -- the normal case), RCDATA (replaceable character
# data -- only char and entity references and end tags are special)
# and CDATA (character data -- only end tags are special). RCDATA is
# not supported at all.
import _markupbase
import re
__all__ = ["SGMLParser", "SGMLParseError"]
# Regular expressions used for parsing
interesting = re.compile('[&<]')
incomplete = re.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|'
'<([a-zA-Z][^<>]*|'
'/([a-zA-Z][^<>]*)?|'
'![^<>]*)?')
entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
charref = re.compile('&#([0-9]+)[^0-9]')
starttagopen = re.compile('<[>a-zA-Z]')
shorttagopen = re.compile('<[a-zA-Z][-.a-zA-Z0-9]*/')
shorttag = re.compile('<([a-zA-Z][-.a-zA-Z0-9]*)/([^/]*)/')
piclose = re.compile('>')
endbracket = re.compile('[<>]')
tagfind = re.compile('[a-zA-Z][-_.a-zA-Z0-9]*')
attrfind = re.compile(
r'\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)(\s*=\s*'
r'(\'[^\']*\'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?')
class SGMLParseError(RuntimeError):
"""Exception raised for all parse errors."""
pass
# SGML parser base class -- find tags and call handler functions.
# Usage: p = SGMLParser(); p.feed(data); ...; p.close().
# The dtd is defined by deriving a class which defines methods
# with special names to handle tags: start_foo and end_foo to handle
# <foo> and </foo>, respectively, or do_foo to handle <foo> by itself.
# (Tags are converted to lower case for this purpose.) The data
# between tags is passed to the parser by calling self.handle_data()
# with some data as argument (the data may be split up in arbitrary
# chunks). Entity references are passed by calling
# self.handle_entityref() with the entity reference as argument.
class SGMLParser(_markupbase.ParserBase):
# Definition of entities -- derived classes may override
entity_or_charref = re.compile('&(?:'
'([a-zA-Z][-.a-zA-Z0-9]*)|#([0-9]+)'
')(;?)')
def __init__(self, verbose=0):
"""Initialize and reset this instance."""
self.verbose = verbose
self.reset()
def reset(self):
"""Reset this instance. Loses all unprocessed data."""
self.__starttag_text = None
self.rawdata = ''
self.stack = []
self.lasttag = '???'
self.nomoretags = 0
self.literal = 0
_markupbase.ParserBase.reset(self)
def setnomoretags(self):
"""Enter literal mode (CDATA) till EOF.
Intended for derived classes only.
"""
self.nomoretags = self.literal = 1
def setliteral(self, *args):
"""Enter literal mode (CDATA).
Intended for derived classes only.
"""
self.literal = 1
def feed(self, data):
"""Feed some data to the parser.
Call this as often as you want, with as little or as much text
as you want (may include '\n'). (This just saves the text,
all the processing is done by goahead().)
"""
self.rawdata = self.rawdata + data
self.goahead(0)
def close(self):
"""Handle the remaining data."""
self.goahead(1)
def error(self, message):
raise SGMLParseError(message)
# Internal -- handle data as far as reasonable. May leave state
# and data to be processed by a subsequent call. If 'end' is
# true, force handling all data as if followed by EOF marker.
def goahead(self, end):
rawdata = self.rawdata
i = 0
n = len(rawdata)
while i < n:
if self.nomoretags:
self.handle_data(rawdata[i:n])
i = n
break
match = interesting.search(rawdata, i)
if match: j = match.start()
else: j = n
if i < j:
self.handle_data(rawdata[i:j])
i = j
if i == n: break
if rawdata[i] == '<':
if starttagopen.match(rawdata, i):
if self.literal:
self.handle_data(rawdata[i])
i = i+1
continue
k = self.parse_starttag(i)
if k < 0: break
i = k
continue
if rawdata.startswith("</", i):
k = self.parse_endtag(i)
if k < 0: break
i = k
self.literal = 0
continue
if self.literal:
if n > (i + 1):
self.handle_data("<")
i = i+1
else:
# incomplete
break
continue
if rawdata.startswith("<!--", i):
# Strictly speaking, a comment is --.*--
# within a declaration tag <!...>.
# This should be removed,
# and comments handled only in parse_declaration.
k = self.parse_comment(i)
if k < 0: break
i = k
continue
if rawdata.startswith("<?", i):
k = self.parse_pi(i)
if k < 0: break
i = i+k
continue
if rawdata.startswith("<!", i):
# This is some sort of declaration; in "HTML as
# deployed," this should only be the document type
# declaration ("<!DOCTYPE html...>").
k = self.parse_declaration(i)
if k < 0: break
i = k
continue
elif rawdata[i] == '&':
if self.literal:
self.handle_data(rawdata[i])
i = i+1
continue
match = charref.match(rawdata, i)
if match:
name = match.group(1)
self.handle_charref(name)
i = match.end(0)
if rawdata[i-1] != ';': i = i-1
continue
match = entityref.match(rawdata, i)
if match:
name = match.group(1)
self.handle_entityref(name)
i = match.end(0)
if rawdata[i-1] != ';': i = i-1
continue
else:
self.error('neither < nor & ??')
# We get here only if incomplete matches but
# nothing else
match = incomplete.match(rawdata, i)
if not match:
self.handle_data(rawdata[i])
i = i+1
continue
j = match.end(0)
if j == n:
break # Really incomplete
self.handle_data(rawdata[i:j])
i = j
# end while
if end and i < n:
self.handle_data(rawdata[i:n])
i = n
self.rawdata = rawdata[i:]
# XXX if end: check for empty stack
# Extensions for the DOCTYPE scanner:
_decl_otherchars = '='
# Internal -- parse processing instr, return length or -1 if not terminated
def parse_pi(self, i):
rawdata = self.rawdata
if rawdata[i:i+2] != '<?':
self.error('unexpected call to parse_pi()')
match = piclose.search(rawdata, i+2)
if not match:
return -1
j = match.start(0)
self.handle_pi(rawdata[i+2: j])
j = match.end(0)
return j-i
def get_starttag_text(self):
return self.__starttag_text
# Internal -- handle starttag, return length or -1 if not terminated
def parse_starttag(self, i):
self.__starttag_text = None
start_pos = i
rawdata = self.rawdata
if shorttagopen.match(rawdata, i):
# SGML shorthand: <tag/data/ == <tag>data</tag>
# XXX Can data contain &... (entity or char refs)?
# XXX Can data contain < or > (tag characters)?
# XXX Can there be whitespace before the first /?
match = shorttag.match(rawdata, i)
if not match:
return -1
tag, data = match.group(1, 2)
self.__starttag_text = '<%s/' % tag
tag = tag.lower()
k = match.end(0)
self.finish_shorttag(tag, data)
self.__starttag_text = rawdata[start_pos:match.end(1) + 1]
return k
# XXX The following should skip matching quotes (' or ")
# As a shortcut way to exit, this isn't so bad, but shouldn't
# be used to locate the actual end of the start tag since the
# < or > characters may be embedded in an attribute value.
match = endbracket.search(rawdata, i+1)
if not match:
return -1
j = match.start(0)
# Now parse the data between i+1 and j into a tag and attrs
attrs = []
if rawdata[i:i+2] == '<>':
# SGML shorthand: <> == <last open tag seen>
k = j
tag = self.lasttag
else:
match = tagfind.match(rawdata, i+1)
if not match:
self.error('unexpected call to parse_starttag')
k = match.end(0)
tag = rawdata[i+1:k].lower()
self.lasttag = tag
while k < j:
match = attrfind.match(rawdata, k)
if not match: break
attrname, rest, attrvalue = match.group(1, 2, 3)
if not rest:
attrvalue = attrname
else:
if (attrvalue[:1] == "'" == attrvalue[-1:] or
attrvalue[:1] == '"' == attrvalue[-1:]):
# strip quotes
attrvalue = attrvalue[1:-1]
attrvalue = self.entity_or_charref.sub(
self._convert_ref, attrvalue)
attrs.append((attrname.lower(), attrvalue))
k = match.end(0)
if rawdata[j] == '>':
j = j+1
self.__starttag_text = rawdata[start_pos:j]
self.finish_starttag(tag, attrs)
return j
# Internal -- convert entity or character reference
def _convert_ref(self, match):
if match.group(2):
return self.convert_charref(match.group(2)) or \
'&#%s%s' % match.groups()[1:]
elif match.group(3):
return self.convert_entityref(match.group(1)) or \
'&%s;' % match.group(1)
else:
return '&%s' % match.group(1)
# Internal -- parse endtag
def parse_endtag(self, i):
rawdata = self.rawdata
match = endbracket.search(rawdata, i+1)
if not match:
return -1
j = match.start(0)
tag = rawdata[i+2:j].strip().lower()
if rawdata[j] == '>':
j = j+1
self.finish_endtag(tag)
return j
# Internal -- finish parsing of <tag/data/ (same as <tag>data</tag>)
def finish_shorttag(self, tag, data):
self.finish_starttag(tag, [])
self.handle_data(data)
self.finish_endtag(tag)
# Internal -- finish processing of start tag
# Return -1 for unknown tag, 0 for open-only tag, 1 for balanced tag
def finish_starttag(self, tag, attrs):
try:
method = getattr(self, 'start_' + tag)
except AttributeError:
try:
method = getattr(self, 'do_' + tag)
except AttributeError:
self.unknown_starttag(tag, attrs)
return -1
else:
self.handle_starttag(tag, method, attrs)
return 0
else:
self.stack.append(tag)
self.handle_starttag(tag, method, attrs)
return 1
# Internal -- finish processing of end tag
def finish_endtag(self, tag):
if not tag:
found = len(self.stack) - 1
if found < 0:
self.unknown_endtag(tag)
return
else:
if tag not in self.stack:
try:
method = getattr(self, 'end_' + tag)
except AttributeError:
self.unknown_endtag(tag)
else:
self.report_unbalanced(tag)
return
found = len(self.stack)
for i in range(found):
if self.stack[i] == tag: found = i
while len(self.stack) > found:
tag = self.stack[-1]
try:
method = getattr(self, 'end_' + tag)
except AttributeError:
method = None
if method:
self.handle_endtag(tag, method)
else:
self.unknown_endtag(tag)
del self.stack[-1]
# Overridable -- handle start tag
def handle_starttag(self, tag, method, attrs):
method(attrs)
# Overridable -- handle end tag
def handle_endtag(self, tag, method):
method()
# Example -- report an unbalanced </...> tag.
def report_unbalanced(self, tag):
if self.verbose:
print('*** Unbalanced </' + tag + '>')
print('*** Stack:', self.stack)
def convert_charref(self, name):
"""Convert character reference, may be overridden."""
try:
n = int(name)
except ValueError:
return
if not 0 <= n <= 127:
return
return self.convert_codepoint(n)
def convert_codepoint(self, codepoint):
return chr(codepoint)
def handle_charref(self, name):
"""Handle character reference, no need to override."""
replacement = self.convert_charref(name)
if replacement is None:
self.unknown_charref(name)
else:
self.handle_data(replacement)
# Definition of entities -- derived classes may override
entitydefs = \
{'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': '\''}
def convert_entityref(self, name):
"""Convert entity references.
As an alternative to overriding this method; one can tailor the
results by setting up the self.entitydefs mapping appropriately.
"""
table = self.entitydefs
if name in table:
return table[name]
else:
return
def handle_entityref(self, name):
"""Handle entity references, no need to override."""
replacement = self.convert_entityref(name)
if replacement is None:
self.unknown_entityref(name)
else:
self.handle_data(replacement)
# Example -- handle data, should be overridden
def handle_data(self, data):
pass
# Example -- handle comment, could be overridden
def handle_comment(self, data):
pass
# Example -- handle declaration, could be overridden
def handle_decl(self, decl):
pass
# Example -- handle processing instruction, could be overridden
def handle_pi(self, data):
pass
# To be overridden -- handlers for unknown objects
def unknown_starttag(self, tag, attrs): pass
def unknown_endtag(self, tag): pass
def unknown_charref(self, ref): pass
def unknown_entityref(self, ref): pass
class TestSGMLParser(SGMLParser):
def __init__(self, verbose=0):
self.testdata = ""
SGMLParser.__init__(self, verbose)
def handle_data(self, data):
self.testdata = self.testdata + data
if len(repr(self.testdata)) >= 70:
self.flush()
def flush(self):
data = self.testdata
if data:
self.testdata = ""
print('data:', repr(data))
def handle_comment(self, data):
self.flush()
r = repr(data)
if len(r) > 68:
r = r[:32] + '...' + r[-32:]
print('comment:', r)
def unknown_starttag(self, tag, attrs):
self.flush()
if not attrs:
print('start tag: <' + tag + '>')
else:
print('start tag: <' + tag, end=' ')
for name, value in attrs:
print(name + '=' + '"' + value + '"', end=' ')
print('>')
def unknown_endtag(self, tag):
self.flush()
print('end tag: </' + tag + '>')
def unknown_entityref(self, ref):
self.flush()
print('*** unknown entity ref: &' + ref + ';')
def unknown_charref(self, ref):
self.flush()
print('*** unknown char ref: &#' + ref + ';')
def unknown_decl(self, data):
self.flush()
print('*** unknown decl: [' + data + ']')
def close(self):
SGMLParser.close(self)
self.flush()
def test(args = None):
import sys
if args is None:
args = sys.argv[1:]
if args and args[0] == '-s':
args = args[1:]
klass = SGMLParser
else:
klass = TestSGMLParser
if args:
file = args[0]
else:
file = 'test.html'
if file == '-':
f = sys.stdin
else:
try:
f = open(file, 'r')
except IOError as msg:
print(file, ":", msg)
sys.exit(1)
data = f.read()
if f is not sys.stdin:
f.close()
x = klass()
for c in data:
x.feed(c)
x.close()
if __name__ == '__main__':
test()

View File

@@ -0,0 +1,391 @@
"""SocksiPy - Python SOCKS module.
Version 1.01
Copyright 2006 Dan-Haim. All rights reserved.
Various fixes by Christophe DUMEZ <chris@qbittorrent.org> - 2010
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:
d = self.recv(bytes-len(data))
if not d:
raise GeneralProxyError("connection closed unexpectedly")
data = data + d
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(ord(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]))

View 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

View File

@@ -0,0 +1,52 @@
<ui version="4.0" >
<class>pluginSourceDlg</class>
<widget class="QDialog" name="pluginSourceDlg" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>207</width>
<height>76</height>
</rect>
</property>
<property name="windowTitle" >
<string>Plugin source</string>
</property>
<layout class="QVBoxLayout" >
<item>
<widget class="QLabel" name="source_lbl" >
<property name="font" >
<font>
<weight>75</weight>
<bold>true</bold>
<underline>true</underline>
</font>
</property>
<property name="text" >
<string>Search plugin source:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" >
<item>
<widget class="QPushButton" name="localButton" >
<property name="text" >
<string>Local file</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="urlButton" >
<property name="text" >
<string>Web link</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,44 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>nova/fix_encoding.py</file>
<file>nova/helpers.py</file>
<file>nova/nova2.py</file>
<file>nova/nova2dl.py</file>
<file>nova/novaprinter.py</file>
<file>nova/socks.py</file>
<file>nova/engines/btdigg.png</file>
<file>nova/engines/btdigg.py</file>
<file>nova/engines/extratorrent.png</file>
<file>nova/engines/extratorrent.py</file>
<file>nova/engines/kickasstorrents.png</file>
<file>nova/engines/kickasstorrents.py</file>
<file>nova/engines/legittorrents.png</file>
<file>nova/engines/legittorrents.py</file>
<file>nova/engines/mininova.png</file>
<file>nova/engines/mininova.py</file>
<file>nova/engines/piratebay.png</file>
<file>nova/engines/piratebay.py</file>
<file>nova/engines/torrentreactor.png</file>
<file>nova/engines/torrentreactor.py</file>
<file>nova3/helpers.py</file>
<file>nova3/nova2.py</file>
<file>nova3/nova2dl.py</file>
<file>nova3/novaprinter.py</file>
<file>nova3/sgmllib3.py</file>
<file>nova3/socks.py</file>
<file>nova3/engines/btdigg.png</file>
<file>nova3/engines/btdigg.py</file>
<file>nova3/engines/extratorrent.png</file>
<file>nova3/engines/extratorrent.py</file>
<file>nova3/engines/kickasstorrents.png</file>
<file>nova3/engines/kickasstorrents.py</file>
<file>nova3/engines/legittorrents.png</file>
<file>nova3/engines/legittorrents.py</file>
<file>nova3/engines/mininova.png</file>
<file>nova3/engines/mininova.py</file>
<file>nova3/engines/piratebay.png</file>
<file>nova3/engines/piratebay.py</file>
<file>nova3/engines/torrentreactor.png</file>
<file>nova3/engines/torrentreactor.py</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,155 @@
<?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="searchBarLayout">
<item>
<widget class="QComboBox" name="comboCategory"/>
</item>
<item>
<widget class="QComboBox" name="selectEngine"/>
</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">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QPushButton" name="download_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Download</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="goToDescBtn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Go to description page</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/>
</ui>

View File

@@ -0,0 +1,561 @@
/*
* 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 <QMessageBox>
#include <QTemporaryFile>
#include <QSystemTrayIcon>
#include <iostream>
#include <QTimer>
#include <QDir>
#include <QMenu>
#include <QClipboard>
#include <QMimeData>
#include <QSortFilterProxyModel>
#include <QFileDialog>
#include <QDesktopServices>
#ifdef Q_OS_WIN
#include <stdlib.h>
#endif
#include "searchengine.h"
#include "qbtsession.h"
#include "fs_utils.h"
#include "misc.h"
#include "preferences.h"
#include "searchlistdelegate.h"
#include "mainwindow.h"
#include "iconprovider.h"
#include "lineedit.h"
#define SEARCHHISTORY_MAXSIZE 50
/*SEARCH ENGINE START*/
SearchEngine::SearchEngine(MainWindow* parent)
: QWidget(parent)
, search_pattern(new LineEdit)
, mp_mainWindow(parent)
{
setupUi(this);
searchBarLayout->insertWidget(0, search_pattern);
connect(search_pattern, SIGNAL(returnPressed()), search_button, SLOT(click()));
// Icons
search_button->setIcon(IconProvider::instance()->getIcon("edit-find"));
download_button->setIcon(IconProvider::instance()->getIcon("download"));
goToDescBtn->setIcon(IconProvider::instance()->getIcon("application-x-mswinurl"));
enginesButton->setIcon(IconProvider::instance()->getIcon("preferences-system-network"));
tabWidget->setTabsClosable(true);
connect(tabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int)));
// Boolean initialization
search_stopped = false;
// Creating Search Process
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();
fillEngineComboBox();
connect(search_pattern, SIGNAL(textEdited(QString)), this, SLOT(searchTextEdited(QString)));
connect(selectEngine, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(selectMultipleBox(const 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));
}
}
void SearchEngine::fillEngineComboBox() {
selectEngine->clear();
selectEngine->addItem("All enabled", QVariant("enabled"));
selectEngine->addItem("All engines", QVariant("all"));
foreach (QString engi, supported_engines->enginesEnabled())
selectEngine->addItem(engi, QVariant(engi));
selectEngine->addItem("Multiple...", QVariant("multi"));
}
QString SearchEngine::selectedCategory() const {
return comboCategory->itemData(comboCategory->currentIndex()).toString();
}
QString SearchEngine::selectedEngine() const {
return selectEngine->itemData(selectEngine->currentIndex()).toString();
}
SearchEngine::~SearchEngine() {
qDebug("Search destruction");
searchProcess->kill();
searchProcess->waitForFinished();
foreach (QProcess *downloader, downloaders) {
// Make sure we disconnect the SIGNAL/SLOT first
// To avoid qreal free
downloader->disconnect();
downloader->kill();
downloader->waitForFinished();
delete downloader;
}
delete search_pattern;
delete searchTimeout;
delete searchProcess;
delete supported_engines;
}
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);
goToDescBtn->setEnabled(true);
}
else {
download_button->setEnabled(false);
goToDescBtn->setEnabled(false);
}
}
}
void SearchEngine::selectMultipleBox(const QString &text) {
if (text == "Multiple...") on_enginesButton_clicked();
}
void SearchEngine::on_enginesButton_clicked() {
engineSelectDlg *dlg = new engineSelectDlg(this, supported_engines);
connect(dlg, SIGNAL(enginesChanged()), this, SLOT(fillCatCombobox()));
connect(dlg, SIGNAL(enginesChanged()), this, SLOT(fillEngineComboBox()));
}
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() {
if (searchProcess->state() != QProcess::NotRunning) {
#ifdef Q_OS_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());
const 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);
QString tabName = pattern;
tabName.replace(QRegExp("&{1}"), "&&");
tabWidget->addTab(currentSearchTab, tabName);
tabWidget->setCurrentWidget(currentSearchTab);
// Getting checked search engines
QStringList params;
search_stopped = false;
params << fsutils::toNativePath(fsutils::searchEngineLocation() + "/nova2.py");
if (selectedEngine() == "all") params << supported_engines->enginesAll().join(",");
else if (selectedEngine() == "enabled") params << supported_engines->enginesEnabled().join(",");
else if (selectedEngine() == "multi") params << supported_engines->enginesEnabled().join(",");
else params << selectedEngine();
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::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();
Preferences* const pref = Preferences::instance();
QStringList width_list;
QStringList new_width_list;
short nbColumns = all_tab.first()->getCurrentSearchListModel()->columnCount();
QString line = pref->getSearchColsWidth();
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));
}
}
pref->setSearchColsWidth(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);
}
qDebug() << Q_FUNC_INFO << torrent_url;
if (torrent_url.startsWith("magnet:")) {
QStringList urls;
urls << torrent_url;
mp_mainWindow->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 << fsutils::toNativePath(fsutils::searchEngineLocation() + "/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(tr("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];
QBtSession::instance()->processDownloadedFile(url, path);
}
}
qDebug("Deleting downloadProcess");
downloaders.removeOne(downloadProcess);
delete downloadProcess;
}
static void removePythonScriptIfExists(const QString& script_path)
{
fsutils::forceRemove(script_path);
fsutils::forceRemove(script_path + "c");
}
// Update nova.py search plugin if necessary
void SearchEngine::updateNova() {
qDebug("Updating nova");
// create nova directory if necessary
QDir search_dir(fsutils::searchEngineLocation());
QString nova_folder = misc::pythonVersion() >= 3 ? "nova3" : "nova";
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() + "/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_folder+"/nova2.py") > getPluginVersion(filePath)) {
removePythonScriptIfExists(filePath);
QFile::copy(":/"+nova_folder+"/nova2.py", filePath);
}
filePath = search_dir.absoluteFilePath("nova2dl.py");
if (getPluginVersion(":/"+nova_folder+"/nova2dl.py") > getPluginVersion(filePath)) {
removePythonScriptIfExists(filePath);
QFile::copy(":/"+nova_folder+"/nova2dl.py", filePath);
}
filePath = search_dir.absoluteFilePath("novaprinter.py");
if (getPluginVersion(":/"+nova_folder+"/novaprinter.py") > getPluginVersion(filePath)) {
removePythonScriptIfExists(filePath);
QFile::copy(":/"+nova_folder+"/novaprinter.py", filePath);
}
filePath = search_dir.absoluteFilePath("helpers.py");
if (getPluginVersion(":/"+nova_folder+"/helpers.py") > getPluginVersion(filePath)) {
removePythonScriptIfExists(filePath);
QFile::copy(":/"+nova_folder+"/helpers.py", filePath);
}
filePath = search_dir.absoluteFilePath("socks.py");
removePythonScriptIfExists(filePath);
QFile::copy(":/"+nova_folder+"/socks.py", filePath);
if (nova_folder == "nova") {
filePath = search_dir.absoluteFilePath("fix_encoding.py");
removePythonScriptIfExists(filePath);
QFile::copy(":/"+nova_folder+"/fix_encoding.py", filePath);
}
if (nova_folder == "nova3") {
filePath = search_dir.absoluteFilePath("sgmllib3.py");
removePythonScriptIfExists(filePath);
QFile::copy(":/"+nova_folder+"/sgmllib3.py", filePath);
}
QDir destDir(QDir(fsutils::searchEngineLocation()).absoluteFilePath("engines"));
QDir shipped_subDir(":/"+nova_folder+"/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));
removePythonScriptIfExists(dest_file);
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));
}
}
}
}
}
// 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();
}
bool useNotificationBalloons = Preferences::instance()->useProgramNotification();
if (useNotificationBalloons && mp_mainWindow->getCurrentTabWidget() != this) {
mp_mainWindow->showNotificationBaloon(tr("Search Engine"), tr("Search has finished"));
}
if (exitcode) {
#ifdef Q_OS_WIN
search_status->setText(tr("Search aborted"));
#else
search_status->setText(tr("An error occurred 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(tr("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(const QString &line) {
if (!currentSearchTab) {
if (searchProcess->state() != QProcess::NotRunning) {
searchProcess->terminate();
}
if (searchTimeout->isActive()) {
searchTimeout->stop();
}
search_stopped = true;
return;
}
const QStringList parts = line.split("|");
const int nb_fields = parts.size();
if (nb_fields < NB_PLUGIN_COLUMNS-1) { //-1 because desc_link is optional
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, SearchSortModel::DL_LINK), parts.at(PL_DL_LINK).trimmed()); // download URL
cur_model->setData(cur_model->index(row, SearchSortModel::NAME), parts.at(PL_NAME).trimmed()); // Name
cur_model->setData(cur_model->index(row, SearchSortModel::SIZE), parts.at(PL_SIZE).trimmed().toLongLong()); // Size
bool ok = false;
qlonglong nb_seeders = parts.at(PL_SEEDS).trimmed().toLongLong(&ok);
if (!ok || nb_seeders < 0) {
cur_model->setData(cur_model->index(row, SearchSortModel::SEEDS), tr("Unknown")); // Seeders
} else {
cur_model->setData(cur_model->index(row, SearchSortModel::SEEDS), nb_seeders); // Seeders
}
qlonglong nb_leechers = parts.at(PL_LEECHS).trimmed().toLongLong(&ok);
if (!ok || nb_leechers < 0) {
cur_model->setData(cur_model->index(row, SearchSortModel::LEECHS), tr("Unknown")); // Leechers
} else {
cur_model->setData(cur_model->index(row, SearchSortModel::LEECHS), nb_leechers); // Leechers
}
cur_model->setData(cur_model->index(row, SearchSortModel::ENGINE_URL), parts.at(PL_ENGINE_URL).trimmed()); // Engine URL
// Description Link
if (nb_fields == NB_PLUGIN_COLUMNS)
cur_model->setData(cur_model->index(row, SearchSortModel::DESC_LINK), parts.at(PL_DESC_LINK).trimmed());
no_search_results = false;
++nb_search_results;
// Enable clear & download buttons
download_button->setEnabled(true);
goToDescBtn->setEnabled(true);
}
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);
goToDescBtn->setEnabled(false);
}
}
// 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() == SearchSortModel::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");
}
}
}
void SearchEngine::on_goToDescBtn_clicked()
{
QModelIndexList selectedIndexes = all_tab.at(tabWidget->currentIndex())->getCurrentTreeView()->selectionModel()->selectedIndexes();
foreach (const QModelIndex &index, selectedIndexes) {
if (index.column() == SearchSortModel::NAME) {
QSortFilterProxyModel* model = all_tab.at(tabWidget->currentIndex())->getCurrentSearchListProxy();
const QString desc_url = model->data(model->index(index.row(), SearchSortModel::DESC_LINK)).toString();
if (!desc_url.isEmpty())
QDesktopServices::openUrl(QUrl::fromEncoded(desc_url.toUtf8()));
}
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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 SearchEngine;
class MainWindow;
class LineEdit;
QT_BEGIN_NAMESPACE
class QTimer;
QT_END_NAMESPACE
class SearchEngine : public QWidget, public Ui::search_engine{
Q_OBJECT
Q_DISABLE_COPY(SearchEngine)
private:
enum PluginColumn { PL_DL_LINK, PL_NAME, PL_SIZE, PL_SEEDS, PL_LEECHS, PL_ENGINE_URL, PL_DESC_LINK, NB_PLUGIN_COLUMNS };
public:
SearchEngine(MainWindow *mp_mainWindow);
~SearchEngine();
QString selectedCategory() const;
QString selectedEngine() const;
static qreal 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;
}
qreal 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();
void closeTab(int index);
void appendSearchResult(const QString &line);
void searchFinished(int exitcode,QProcess::ExitStatus);
void readSearchOutput();
void searchStarted();
void updateNova();
void selectMultipleBox(const QString &text);
void on_enginesButton_clicked();
void propagateSectionResized(int index, int oldsize , int newsize);
void saveResultsColumnsWidth();
void downloadFinished(int exitcode, QProcess::ExitStatus);
void fillCatCombobox();
void fillEngineComboBox();
void searchTextEdited(QString);
private slots:
void on_goToDescBtn_clicked();
private:
// Search related
LineEdit* search_pattern;
QProcess *searchProcess;
QList<QProcess*> downloaders;
bool search_stopped;
bool no_search_results;
QByteArray search_result_line_truncated;
unsigned long nb_search_results;
SupportedEngines *supported_engines;
QTimer *searchTimeout;
QPointer<SearchTab> currentSearchTab;
QList<QPointer<SearchTab> > all_tab; // To store all tabs
const SearchCategories full_cat_names;
MainWindow *mp_mainWindow;
};
#endif

View File

@@ -0,0 +1,19 @@
INCLUDEPATH += $$PWD
FORMS += $$PWD/search.ui \
$$PWD/engineselect.ui \
$$PWD/pluginsource.ui
HEADERS += $$PWD/searchengine.h \
$$PWD/searchtab.h \
$$PWD/engineselectdlg.h \
$$PWD/pluginsource.h \
$$PWD/searchlistdelegate.h \
$$PWD/supportedengines.h \
$$PWD/searchsortmodel.h
SOURCES += $$PWD/searchengine.cpp \
$$PWD/searchtab.cpp \
$$PWD/engineselectdlg.cpp
RESOURCES += $$PWD/search.qrc

View File

@@ -0,0 +1,70 @@
/*
* 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 <QStyleOptionViewItemV2>
#include <QModelIndex>
#include <QPainter>
#include <QProgressBar>
#include "misc.h"
#include "searchengine.h"
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 SearchSortModel::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

View File

@@ -0,0 +1,35 @@
#ifndef SEARCHSORTMODEL_H
#define SEARCHSORTMODEL_H
#include <QSortFilterProxyModel>
#include "misc.h"
class SearchSortModel : public QSortFilterProxyModel {
Q_OBJECT
public:
enum SearchColumn { NAME, SIZE, SEEDS, LEECHS, ENGINE_URL, DL_LINK, DESC_LINK, NB_SEARCH_COLUMNS };
SearchSortModel(QObject *parent = 0) : QSortFilterProxyModel(parent) {}
protected:
virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const {
if (sortColumn() == NAME || sortColumn() == ENGINE_URL) {
QVariant vL = sourceModel()->data(left);
QVariant vR = sourceModel()->data(right);
if (!(vL.isValid() && vR.isValid()))
return QSortFilterProxyModel::lessThan(left, right);
Q_ASSERT(vL.isValid());
Q_ASSERT(vR.isValid());
bool res = false;
if (misc::naturalSort(vL.toString(), vR.toString(), res))
return res;
return QSortFilterProxyModel::lessThan(left, right);
}
return QSortFilterProxyModel::lessThan(left, right);
}
};
#endif // SEARCHSORTMODEL_H

View File

@@ -0,0 +1,151 @@
/*
* 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 "preferences.h"
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, SearchSortModel::NB_SEARCH_COLUMNS);
SearchListModel->setHeaderData(SearchSortModel::NAME, Qt::Horizontal, tr("Name", "i.e: file name"));
SearchListModel->setHeaderData(SearchSortModel::SIZE, Qt::Horizontal, tr("Size", "i.e: file size"));
SearchListModel->setHeaderData(SearchSortModel::SEEDS, Qt::Horizontal, tr("Seeders", "i.e: Number of full sources"));
SearchListModel->setHeaderData(SearchSortModel::LEECHS, Qt::Horizontal, tr("Leechers", "i.e: Number of partial sources"));
SearchListModel->setHeaderData(SearchSortModel::ENGINE_URL, Qt::Horizontal, tr("Search engine"));
proxyModel = new SearchSortModel();
proxyModel->setDynamicSortFilter(true);
proxyModel->setSourceModel(SearchListModel);
resultsBrowser->setModel(proxyModel);
SearchDelegate = new SearchListDelegate();
resultsBrowser->setItemDelegate(SearchDelegate);
resultsBrowser->hideColumn(SearchSortModel::DL_LINK); // Hide url column
resultsBrowser->hideColumn(SearchSortModel::DESC_LINK);
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(SearchSortModel::SEEDS, Qt::DescendingOrder);
}
void SearchTab::downloadSelectedItem(const QModelIndex& index) {
QString engine_url = proxyModel->data(proxyModel->index(index.row(), SearchSortModel::ENGINE_URL)).toString();
QString torrent_url = proxyModel->data(proxyModel->index(index.row(), SearchSortModel::DL_LINK)).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() {
QString line = Preferences::instance()->getSearchColsWidth();
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);
}

View File

@@ -0,0 +1,79 @@
/*
* 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"
#include "searchsortmodel.h"
#define ENGINE_URL_COLUMN 4
#define URL_COLUMN 5
class SearchListDelegate;
class SearchEngine;
QT_BEGIN_NAMESPACE
class QTreeView;
class QHeaderView;
class QStandardItemModel;
QT_END_NAMESPACE
class SearchTab: public QWidget, public Ui::search_engine {
Q_OBJECT
private:
QVBoxLayout *box;
QLabel *results_lbl;
QTreeView *resultsBrowser;
QStandardItemModel *SearchListModel;
SearchSortModel *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

View File

@@ -0,0 +1,185 @@
/*
* 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 <QDebug>
#include "fs_utils.h"
#include "preferences.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(" ");
QStringList disabled_engines = Preferences::instance()->getSearchEngDisabled();
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
Preferences* const pref = Preferences::instance();
QStringList disabled_engines = pref->getSearchEngDisabled();
if (enabled) {
disabled_engines.removeAll(name);
} else {
disabled_engines.append(name);
}
pref->setSearchEngDisabled(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 enginesAll() const {
QStringList engines;
foreach (const SupportedEngine *engine, values()) engines << engine->getName();
return engines;
}
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 << fsutils::toNativePath(fsutils::searchEngineLocation()+"/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)) {
qWarning() << "Could not parse Nova search engine capabilities, msg: " << capabilities.toLocal8Bit().data();
qWarning() << "Error: " << nova.readAllStandardError().constData();
return;
}
QDomElement root = xml_doc.documentElement();
if (root.tagName() != "capabilities") {
qWarning() << "Invalid XML file for Nova search engine capabilities, msg: " << capabilities.toLocal8Bit().data();
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