mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-08-19 12:59:56 -07:00
commit
a886dbf8ab
64 changed files with 5116 additions and 4727 deletions
|
@ -74,6 +74,8 @@
|
||||||
#include "base/net/proxyconfigurationmanager.h"
|
#include "base/net/proxyconfigurationmanager.h"
|
||||||
#include "base/bittorrent/session.h"
|
#include "base/bittorrent/session.h"
|
||||||
#include "base/bittorrent/torrenthandle.h"
|
#include "base/bittorrent/torrenthandle.h"
|
||||||
|
#include "base/rss/rss_autodownloader.h"
|
||||||
|
#include "base/rss/rss_session.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
@ -438,6 +440,9 @@ int Application::exec(const QStringList ¶ms)
|
||||||
m_webui = new WebUI;
|
m_webui = new WebUI;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
new RSS::Session; // create RSS::Session singleton
|
||||||
|
new RSS::AutoDownloader; // create RSS::AutoDownloader singleton
|
||||||
|
|
||||||
#ifdef DISABLE_GUI
|
#ifdef DISABLE_GUI
|
||||||
#ifndef DISABLE_WEBUI
|
#ifndef DISABLE_WEBUI
|
||||||
Preferences* const pref = Preferences::instance();
|
Preferences* const pref = Preferences::instance();
|
||||||
|
@ -629,6 +634,9 @@ void Application::cleanup()
|
||||||
delete m_webui;
|
delete m_webui;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
delete RSS::AutoDownloader::instance();
|
||||||
|
delete RSS::Session::instance();
|
||||||
|
|
||||||
ScanFoldersModel::freeInstance();
|
ScanFoldersModel::freeInstance();
|
||||||
BitTorrent::Session::freeInstance();
|
BitTorrent::Session::freeInstance();
|
||||||
#ifndef DISABLE_COUNTRIES_RESOLUTION
|
#ifndef DISABLE_COUNTRIES_RESOLUTION
|
||||||
|
|
|
@ -64,6 +64,12 @@ namespace BitTorrent
|
||||||
class TorrentHandle;
|
class TorrentHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace RSS
|
||||||
|
{
|
||||||
|
class Session;
|
||||||
|
class AutoDownloader;
|
||||||
|
}
|
||||||
|
|
||||||
class Application : public BaseApplication
|
class Application : public BaseApplication
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
88
src/base/asyncfilestorage.cpp
Normal file
88
src/base/asyncfilestorage.cpp
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "asyncfilestorage.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QMetaObject>
|
||||||
|
#include <QSaveFile>
|
||||||
|
|
||||||
|
AsyncFileStorage::AsyncFileStorage(const QString &storageFolderPath, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_storageDir(storageFolderPath)
|
||||||
|
, m_lockFile(m_storageDir.absoluteFilePath(QStringLiteral("storage.lock")))
|
||||||
|
{
|
||||||
|
if (!m_storageDir.mkpath(m_storageDir.absolutePath()))
|
||||||
|
throw AsyncFileStorageError(
|
||||||
|
QString("Could not create directory '%1'.").arg(m_storageDir.absolutePath()));
|
||||||
|
|
||||||
|
// TODO: This folder locking approach does not work for UNIX systems. Implement it.
|
||||||
|
if (!m_lockFile.open(QFile::WriteOnly))
|
||||||
|
throw AsyncFileStorageError(m_lockFile.errorString());
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFileStorage::~AsyncFileStorage()
|
||||||
|
{
|
||||||
|
m_lockFile.close();
|
||||||
|
m_lockFile.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsyncFileStorage::store(const QString &fileName, const QByteArray &data)
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(this, "store_impl", Qt::QueuedConnection
|
||||||
|
, Q_ARG(QString, fileName), Q_ARG(QByteArray, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir AsyncFileStorage::storageDir() const
|
||||||
|
{
|
||||||
|
return m_storageDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsyncFileStorage::store_impl(const QString &fileName, const QByteArray &data)
|
||||||
|
{
|
||||||
|
const QString filePath = m_storageDir.absoluteFilePath(fileName);
|
||||||
|
QSaveFile file(filePath);
|
||||||
|
qDebug() << "AsyncFileStorage: Saving data to" << filePath;
|
||||||
|
if (file.open(QIODevice::WriteOnly)) {
|
||||||
|
file.write(data);
|
||||||
|
if (!file.commit()) {
|
||||||
|
qDebug() << "AsyncFileStorage: Failed to save data";
|
||||||
|
emit failed(filePath, file.errorString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFileStorageError::AsyncFileStorageError(const QString &message)
|
||||||
|
: std::runtime_error(message.toUtf8().data())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AsyncFileStorageError::message() const
|
||||||
|
{
|
||||||
|
return what();
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -25,27 +24,41 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "rssfolder.h"
|
#pragma once
|
||||||
#include "rssfile.h"
|
|
||||||
|
|
||||||
using namespace Rss;
|
#include <stdexcept>
|
||||||
|
|
||||||
File::~File() {}
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
Folder *File::parentFolder() const
|
class AsyncFileStorageError: public std::runtime_error
|
||||||
{
|
{
|
||||||
return m_parent;
|
public:
|
||||||
}
|
explicit AsyncFileStorageError(const QString &message);
|
||||||
|
QString message() const;
|
||||||
|
};
|
||||||
|
|
||||||
QStringList File::pathHierarchy() const
|
class AsyncFileStorage: public QObject
|
||||||
{
|
{
|
||||||
QStringList path;
|
Q_OBJECT
|
||||||
if (m_parent)
|
|
||||||
path << m_parent->pathHierarchy();
|
public:
|
||||||
path << id();
|
explicit AsyncFileStorage(const QString &storageFolderPath, QObject *parent = nullptr);
|
||||||
return path;
|
~AsyncFileStorage() override;
|
||||||
}
|
|
||||||
|
void store(const QString &fileName, const QByteArray &data);
|
||||||
|
|
||||||
|
QDir storageDir() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void failed(const QString &fileName, const QString &errorString);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Q_INVOKABLE void store_impl(const QString &fileName, const QByteArray &data);
|
||||||
|
|
||||||
|
QDir m_storageDir;
|
||||||
|
QFile m_lockFile;
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
|
$$PWD/asyncfilestorage.h \
|
||||||
$$PWD/types.h \
|
$$PWD/types.h \
|
||||||
$$PWD/tristatebool.h \
|
$$PWD/tristatebool.h \
|
||||||
$$PWD/filesystemwatcher.h \
|
$$PWD/filesystemwatcher.h \
|
||||||
|
@ -40,14 +41,14 @@ HEADERS += \
|
||||||
$$PWD/bittorrent/private/filterparserthread.h \
|
$$PWD/bittorrent/private/filterparserthread.h \
|
||||||
$$PWD/bittorrent/private/statistics.h \
|
$$PWD/bittorrent/private/statistics.h \
|
||||||
$$PWD/bittorrent/private/resumedatasavingmanager.h \
|
$$PWD/bittorrent/private/resumedatasavingmanager.h \
|
||||||
$$PWD/rss/rssmanager.h \
|
$$PWD/rss/rss_article.h \
|
||||||
$$PWD/rss/rssfeed.h \
|
$$PWD/rss/rss_item.h \
|
||||||
$$PWD/rss/rssfolder.h \
|
$$PWD/rss/rss_feed.h \
|
||||||
$$PWD/rss/rssfile.h \
|
$$PWD/rss/rss_folder.h \
|
||||||
$$PWD/rss/rssarticle.h \
|
$$PWD/rss/rss_session.h \
|
||||||
$$PWD/rss/rssdownloadrule.h \
|
$$PWD/rss/rss_autodownloader.h \
|
||||||
$$PWD/rss/rssdownloadrulelist.h \
|
$$PWD/rss/rss_autodownloadrule.h \
|
||||||
$$PWD/rss/private/rssparser.h \
|
$$PWD/rss/private/rss_parser.h \
|
||||||
$$PWD/utils/fs.h \
|
$$PWD/utils/fs.h \
|
||||||
$$PWD/utils/gzip.h \
|
$$PWD/utils/gzip.h \
|
||||||
$$PWD/utils/misc.h \
|
$$PWD/utils/misc.h \
|
||||||
|
@ -64,6 +65,7 @@ HEADERS += \
|
||||||
$$PWD/searchengine.h
|
$$PWD/searchengine.h
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
|
$$PWD/asyncfilestorage.cpp \
|
||||||
$$PWD/tristatebool.cpp \
|
$$PWD/tristatebool.cpp \
|
||||||
$$PWD/filesystemwatcher.cpp \
|
$$PWD/filesystemwatcher.cpp \
|
||||||
$$PWD/logger.cpp \
|
$$PWD/logger.cpp \
|
||||||
|
@ -100,14 +102,14 @@ SOURCES += \
|
||||||
$$PWD/bittorrent/private/filterparserthread.cpp \
|
$$PWD/bittorrent/private/filterparserthread.cpp \
|
||||||
$$PWD/bittorrent/private/statistics.cpp \
|
$$PWD/bittorrent/private/statistics.cpp \
|
||||||
$$PWD/bittorrent/private/resumedatasavingmanager.cpp \
|
$$PWD/bittorrent/private/resumedatasavingmanager.cpp \
|
||||||
$$PWD/rss/rssmanager.cpp \
|
$$PWD/rss/rss_article.cpp \
|
||||||
$$PWD/rss/rssfeed.cpp \
|
$$PWD/rss/rss_item.cpp \
|
||||||
$$PWD/rss/rssfolder.cpp \
|
$$PWD/rss/rss_feed.cpp \
|
||||||
$$PWD/rss/rssarticle.cpp \
|
$$PWD/rss/rss_folder.cpp \
|
||||||
$$PWD/rss/rssdownloadrule.cpp \
|
$$PWD/rss/rss_session.cpp \
|
||||||
$$PWD/rss/rssdownloadrulelist.cpp \
|
$$PWD/rss/rss_autodownloader.cpp \
|
||||||
$$PWD/rss/rssfile.cpp \
|
$$PWD/rss/rss_autodownloadrule.cpp \
|
||||||
$$PWD/rss/private/rssparser.cpp \
|
$$PWD/rss/private/rss_parser.cpp \
|
||||||
$$PWD/utils/fs.cpp \
|
$$PWD/utils/fs.cpp \
|
||||||
$$PWD/utils/gzip.cpp \
|
$$PWD/utils/gzip.cpp \
|
||||||
$$PWD/utils/misc.cpp \
|
$$PWD/utils/misc.cpp \
|
||||||
|
|
|
@ -3372,8 +3372,8 @@ void Session::createTorrentHandle(const libt::torrent_handle &nativeHandle)
|
||||||
bool fromMagnetUri = !torrent->hasMetadata();
|
bool fromMagnetUri = !torrent->hasMetadata();
|
||||||
|
|
||||||
if (data.resumed) {
|
if (data.resumed) {
|
||||||
if (fromMagnetUri && !data.addPaused)
|
if (fromMagnetUri && (data.addPaused != TriStateBool::True))
|
||||||
torrent->resume(data.addForced);
|
torrent->resume(data.addForced == TriStateBool::True);
|
||||||
|
|
||||||
logger->addMessage(tr("'%1' resumed. (fast resume)", "'torrent name' was resumed. (fast resume)")
|
logger->addMessage(tr("'%1' resumed. (fast resume)", "'torrent name' was resumed. (fast resume)")
|
||||||
.arg(torrent->name()));
|
.arg(torrent->name()));
|
||||||
|
@ -3399,7 +3399,7 @@ void Session::createTorrentHandle(const libt::torrent_handle &nativeHandle)
|
||||||
if (isAddTrackersEnabled() && !torrent->isPrivate())
|
if (isAddTrackersEnabled() && !torrent->isPrivate())
|
||||||
torrent->addTrackers(m_additionalTrackerList);
|
torrent->addTrackers(m_additionalTrackerList);
|
||||||
|
|
||||||
bool addPaused = data.addPaused;
|
bool addPaused = (data.addPaused == TriStateBool::True);
|
||||||
if (data.addPaused == TriStateBool::Undefined)
|
if (data.addPaused == TriStateBool::Undefined)
|
||||||
addPaused = isAddTorrentPaused();
|
addPaused = isAddTorrentPaused();
|
||||||
|
|
||||||
|
@ -3664,8 +3664,8 @@ namespace
|
||||||
torrentData.hasRootFolder = fast.dict_find_int_value("qBt-hasRootFolder");
|
torrentData.hasRootFolder = fast.dict_find_int_value("qBt-hasRootFolder");
|
||||||
|
|
||||||
magnetUri = MagnetUri(QString::fromStdString(fast.dict_find_string_value("qBt-magnetUri")));
|
magnetUri = MagnetUri(QString::fromStdString(fast.dict_find_string_value("qBt-magnetUri")));
|
||||||
torrentData.addPaused = fast.dict_find_int_value("qBt-paused");
|
torrentData.addPaused = TriStateBool(fast.dict_find_int_value("qBt-paused"));
|
||||||
torrentData.addForced = fast.dict_find_int_value("qBt-forced");
|
torrentData.addForced = TriStateBool(fast.dict_find_int_value("qBt-forced"));
|
||||||
|
|
||||||
prio = fast.dict_find_int_value("qBt-queuePosition");
|
prio = fast.dict_find_int_value("qBt-queuePosition");
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ DownloadHandler *DownloadManager::downloadUrl(const QString &url, bool saveToFil
|
||||||
|
|
||||||
// Process download request
|
// Process download request
|
||||||
qDebug("url is %s", qPrintable(url));
|
qDebug("url is %s", qPrintable(url));
|
||||||
const QUrl qurl = QUrl::fromEncoded(url.toUtf8());
|
const QUrl qurl = QUrl(url);
|
||||||
QNetworkRequest request(qurl);
|
QNetworkRequest request(qurl);
|
||||||
|
|
||||||
if (userAgent.isEmpty())
|
if (userAgent.isEmpty())
|
||||||
|
|
|
@ -1241,32 +1241,32 @@ void Preferences::setRssHSplitterSizes(const QByteArray &sizes)
|
||||||
|
|
||||||
QStringList Preferences::getRssOpenFolders() const
|
QStringList Preferences::getRssOpenFolders() const
|
||||||
{
|
{
|
||||||
return value("Rss/open_folders").toStringList();
|
return value("GUI/RSSWidget/OpenedFolders").toStringList();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preferences::setRssOpenFolders(const QStringList &folders)
|
void Preferences::setRssOpenFolders(const QStringList &folders)
|
||||||
{
|
{
|
||||||
setValue("Rss/open_folders", folders);
|
setValue("GUI/RSSWidget/OpenedFolders", folders);
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray Preferences::getRssSideSplitterState() const
|
QByteArray Preferences::getRssSideSplitterState() const
|
||||||
{
|
{
|
||||||
return value("Rss/qt5/splitter_h").toByteArray();
|
return value("GUI/RSSWidget/qt5/splitter_h").toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preferences::setRssSideSplitterState(const QByteArray &state)
|
void Preferences::setRssSideSplitterState(const QByteArray &state)
|
||||||
{
|
{
|
||||||
setValue("Rss/qt5/splitter_h", state);
|
setValue("GUI/RSSWidget/qt5/splitter_h", state);
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray Preferences::getRssMainSplitterState() const
|
QByteArray Preferences::getRssMainSplitterState() const
|
||||||
{
|
{
|
||||||
return value("Rss/qt5/splitterMain").toByteArray();
|
return value("GUI/RSSWidget/qt5/splitterMain").toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preferences::setRssMainSplitterState(const QByteArray &state)
|
void Preferences::setRssMainSplitterState(const QByteArray &state)
|
||||||
{
|
{
|
||||||
setValue("Rss/qt5/splitterMain", state);
|
setValue("GUI/RSSWidget/qt5/splitterMain", state);
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray Preferences::getSearchTabHeaderState() const
|
QByteArray Preferences::getSearchTabHeaderState() const
|
||||||
|
@ -1410,64 +1410,14 @@ void Preferences::setTransHeaderState(const QByteArray &state)
|
||||||
}
|
}
|
||||||
|
|
||||||
//From old RssSettings class
|
//From old RssSettings class
|
||||||
bool Preferences::isRSSEnabled() const
|
bool Preferences::isRSSWidgetEnabled() const
|
||||||
{
|
{
|
||||||
return value("Preferences/RSS/RSSEnabled", false).toBool();
|
return value("GUI/RSSWidget/Enabled", false).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preferences::setRSSEnabled(const bool enabled)
|
void Preferences::setRSSWidgetVisible(const bool enabled)
|
||||||
{
|
{
|
||||||
setValue("Preferences/RSS/RSSEnabled", enabled);
|
setValue("GUI/RSSWidget/Enabled", enabled);
|
||||||
}
|
|
||||||
|
|
||||||
uint Preferences::getRSSRefreshInterval() const
|
|
||||||
{
|
|
||||||
return value("Preferences/RSS/RSSRefresh", 30).toUInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Preferences::setRSSRefreshInterval(const uint &interval)
|
|
||||||
{
|
|
||||||
setValue("Preferences/RSS/RSSRefresh", interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
int Preferences::getRSSMaxArticlesPerFeed() const
|
|
||||||
{
|
|
||||||
return value("Preferences/RSS/RSSMaxArticlesPerFeed", 50).toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Preferences::setRSSMaxArticlesPerFeed(const int &nb)
|
|
||||||
{
|
|
||||||
setValue("Preferences/RSS/RSSMaxArticlesPerFeed", nb);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Preferences::isRssDownloadingEnabled() const
|
|
||||||
{
|
|
||||||
return value("Preferences/RSS/RssDownloading", true).toBool();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Preferences::setRssDownloadingEnabled(const bool b)
|
|
||||||
{
|
|
||||||
setValue("Preferences/RSS/RssDownloading", b);
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList Preferences::getRssFeedsUrls() const
|
|
||||||
{
|
|
||||||
return value("Rss/streamList").toStringList();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Preferences::setRssFeedsUrls(const QStringList &rssFeeds)
|
|
||||||
{
|
|
||||||
setValue("Rss/streamList", rssFeeds);
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList Preferences::getRssFeedsAliases() const
|
|
||||||
{
|
|
||||||
return value("Rss/streamAlias").toStringList();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Preferences::setRssFeedsAliases(const QStringList &rssAliases)
|
|
||||||
{
|
|
||||||
setValue("Rss/streamAlias", rssAliases);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int Preferences::getToolbarTextPosition() const
|
int Preferences::getToolbarTextPosition() const
|
||||||
|
@ -1522,24 +1472,6 @@ void Preferences::setSpeedWidgetGraphEnable(int id, const bool enable)
|
||||||
|
|
||||||
void Preferences::upgrade()
|
void Preferences::upgrade()
|
||||||
{
|
{
|
||||||
// Move RSS cookies to global storage
|
|
||||||
QList<QNetworkCookie> cookies = getNetworkCookies();
|
|
||||||
QVariantMap hostsTable = value("Rss/hosts_cookies").toMap();
|
|
||||||
foreach (const QString &key, hostsTable.keys()) {
|
|
||||||
QVariant value = hostsTable[key];
|
|
||||||
QList<QByteArray> rawCookies = value.toByteArray().split(':');
|
|
||||||
foreach (const QByteArray &rawCookie, rawCookies) {
|
|
||||||
foreach (QNetworkCookie cookie, QNetworkCookie::parseCookies(rawCookie)) {
|
|
||||||
cookie.setDomain(key);
|
|
||||||
cookie.setPath("/");
|
|
||||||
cookie.setExpirationDate(QDateTime::currentDateTime().addYears(10));
|
|
||||||
cookies << cookie;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setNetworkCookies(cookies);
|
|
||||||
|
|
||||||
QStringList labels = value("TransferListFilters/customLabels").toStringList();
|
QStringList labels = value("TransferListFilters/customLabels").toStringList();
|
||||||
if (!labels.isEmpty()) {
|
if (!labels.isEmpty()) {
|
||||||
QVariantMap categories = value("BitTorrent/Session/Categories").toMap();
|
QVariantMap categories = value("BitTorrent/Session/Categories").toMap();
|
||||||
|
@ -1551,7 +1483,6 @@ void Preferences::upgrade()
|
||||||
SettingsStorage::instance()->removeValue("TransferListFilters/customLabels");
|
SettingsStorage::instance()->removeValue("TransferListFilters/customLabels");
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsStorage::instance()->removeValue("Rss/hosts_cookies");
|
|
||||||
SettingsStorage::instance()->removeValue("Preferences/Downloads/AppendLabel");
|
SettingsStorage::instance()->removeValue("Preferences/Downloads/AppendLabel");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -333,18 +333,8 @@ public:
|
||||||
void setToolbarTextPosition(const int position);
|
void setToolbarTextPosition(const int position);
|
||||||
|
|
||||||
//From old RssSettings class
|
//From old RssSettings class
|
||||||
bool isRSSEnabled() const;
|
bool isRSSWidgetEnabled() const;
|
||||||
void setRSSEnabled(const bool enabled);
|
void setRSSWidgetVisible(const bool enabled);
|
||||||
uint getRSSRefreshInterval() const;
|
|
||||||
void setRSSRefreshInterval(const uint &interval);
|
|
||||||
int getRSSMaxArticlesPerFeed() const;
|
|
||||||
void setRSSMaxArticlesPerFeed(const int &nb);
|
|
||||||
bool isRssDownloadingEnabled() const;
|
|
||||||
void setRssDownloadingEnabled(const bool b);
|
|
||||||
QStringList getRssFeedsUrls() const;
|
|
||||||
void setRssFeedsUrls(const QStringList &rssFeeds);
|
|
||||||
QStringList getRssFeedsAliases() const;
|
|
||||||
void setRssFeedsAliases(const QStringList &rssAliases);
|
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
QList<QNetworkCookie> getNetworkCookies() const;
|
QList<QNetworkCookie> getNetworkCookies() const;
|
||||||
|
|
|
@ -29,15 +29,16 @@
|
||||||
* Contact : chris@qbittorrent.org
|
* Contact : chris@qbittorrent.org
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "rss_parser.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QMetaObject>
|
||||||
#include <QRegExp>
|
#include <QRegExp>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QXmlStreamReader>
|
#include <QXmlStreamReader>
|
||||||
|
|
||||||
#include "rssparser.h"
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
const char shortDay[][4] = {
|
const char shortDay[][4] = {
|
||||||
|
@ -206,10 +207,23 @@ namespace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using namespace Rss::Private;
|
using namespace RSS::Private;
|
||||||
|
|
||||||
|
const int ParsingResultTypeId = qRegisterMetaType<ParsingResult>();
|
||||||
|
|
||||||
|
Parser::Parser(QString lastBuildDate)
|
||||||
|
{
|
||||||
|
m_result.lastBuildDate = lastBuildDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::parse(const QByteArray &feedData)
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(this, "parse_impl", Qt::QueuedConnection
|
||||||
|
, Q_ARG(QByteArray, feedData));
|
||||||
|
}
|
||||||
|
|
||||||
// read and create items from a rss document
|
// read and create items from a rss document
|
||||||
void Parser::parse(const QByteArray &feedData)
|
void Parser::parse_impl(const QByteArray &feedData)
|
||||||
{
|
{
|
||||||
qDebug() << Q_FUNC_INFO;
|
qDebug() << Q_FUNC_INFO;
|
||||||
|
|
||||||
|
@ -243,18 +257,28 @@ void Parser::parse(const QByteArray &feedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xml.hasError())
|
if (xml.hasError())
|
||||||
emit finished(xml.errorString());
|
m_result.error = xml.errorString();
|
||||||
else if (!foundChannel)
|
else if (!foundChannel)
|
||||||
emit finished(tr("Invalid RSS feed."));
|
m_result.error = tr("Invalid RSS feed.");
|
||||||
else
|
else
|
||||||
emit finished(QString());
|
// Sort article list chronologically
|
||||||
|
// NOTE: We don't need to sort it here if articles are always
|
||||||
|
// sorted in fetched XML in reverse chronological order
|
||||||
|
std::sort(m_result.articles.begin(), m_result.articles.end()
|
||||||
|
, [](const QVariantHash &a1, const QVariantHash &a2)
|
||||||
|
{
|
||||||
|
return a1["date"].toDateTime() < a2["date"].toDateTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
emit finished(m_result);
|
||||||
|
m_result.articles.clear(); // clear articles only
|
||||||
}
|
}
|
||||||
|
|
||||||
void Parser::parseRssArticle(QXmlStreamReader &xml)
|
void Parser::parseRssArticle(QXmlStreamReader &xml)
|
||||||
{
|
{
|
||||||
QVariantHash article;
|
QVariantHash article;
|
||||||
|
|
||||||
while(!xml.atEnd()) {
|
while (!xml.atEnd()) {
|
||||||
xml.readNext();
|
xml.readNext();
|
||||||
|
|
||||||
if(xml.isEndElement() && xml.name() == "item")
|
if(xml.isEndElement() && xml.name() == "item")
|
||||||
|
@ -290,28 +314,7 @@ void Parser::parseRssArticle(QXmlStreamReader &xml)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!article.contains("torrent_url") && article.contains("news_link"))
|
m_result.articles.prepend(article);
|
||||||
article["torrent_url"] = article["news_link"];
|
|
||||||
|
|
||||||
if (!article.contains("id")) {
|
|
||||||
// Item does not have a guid, fall back to some other identifier
|
|
||||||
const QString link = article.value("news_link").toString();
|
|
||||||
if (!link.isEmpty()) {
|
|
||||||
article["id"] = link;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const QString title = article.value("title").toString();
|
|
||||||
if (!title.isEmpty()) {
|
|
||||||
article["id"] = title;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qWarning() << "Item has no guid, link or title, ignoring it...";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit newArticle(article);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Parser::parseRSSChannel(QXmlStreamReader &xml)
|
void Parser::parseRSSChannel(QXmlStreamReader &xml)
|
||||||
|
@ -319,22 +322,21 @@ void Parser::parseRSSChannel(QXmlStreamReader &xml)
|
||||||
qDebug() << Q_FUNC_INFO;
|
qDebug() << Q_FUNC_INFO;
|
||||||
Q_ASSERT(xml.isStartElement() && xml.name() == "channel");
|
Q_ASSERT(xml.isStartElement() && xml.name() == "channel");
|
||||||
|
|
||||||
while(!xml.atEnd()) {
|
while (!xml.atEnd()) {
|
||||||
xml.readNext();
|
xml.readNext();
|
||||||
|
|
||||||
if (xml.isStartElement()) {
|
if (xml.isStartElement()) {
|
||||||
if (xml.name() == "title") {
|
if (xml.name() == "title") {
|
||||||
QString title = xml.readElementText();
|
m_result.title = xml.readElementText();
|
||||||
emit feedTitle(title);
|
|
||||||
}
|
}
|
||||||
else if (xml.name() == "lastBuildDate") {
|
else if (xml.name() == "lastBuildDate") {
|
||||||
QString lastBuildDate = xml.readElementText();
|
QString lastBuildDate = xml.readElementText();
|
||||||
if (!lastBuildDate.isEmpty()) {
|
if (!lastBuildDate.isEmpty()) {
|
||||||
if (m_lastBuildDate == lastBuildDate) {
|
if (m_result.lastBuildDate == lastBuildDate) {
|
||||||
qDebug() << "The RSS feed has not changed since last time, aborting parsing.";
|
qDebug() << "The RSS feed has not changed since last time, aborting parsing.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_lastBuildDate = lastBuildDate;
|
m_result.lastBuildDate = lastBuildDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (xml.name() == "item") {
|
else if (xml.name() == "item") {
|
||||||
|
@ -349,10 +351,10 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
|
||||||
QVariantHash article;
|
QVariantHash article;
|
||||||
bool doubleContent = false;
|
bool doubleContent = false;
|
||||||
|
|
||||||
while(!xml.atEnd()) {
|
while (!xml.atEnd()) {
|
||||||
xml.readNext();
|
xml.readNext();
|
||||||
|
|
||||||
if(xml.isEndElement() && (xml.name() == "entry"))
|
if (xml.isEndElement() && (xml.name() == "entry"))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (xml.isStartElement()) {
|
if (xml.isStartElement()) {
|
||||||
|
@ -360,9 +362,9 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
|
||||||
article["title"] = xml.readElementText().trimmed();
|
article["title"] = xml.readElementText().trimmed();
|
||||||
}
|
}
|
||||||
else if (xml.name() == "link") {
|
else if (xml.name() == "link") {
|
||||||
QString link = ( xml.attributes().isEmpty() ?
|
QString link = (xml.attributes().isEmpty()
|
||||||
xml.readElementText().trimmed() :
|
? xml.readElementText().trimmed()
|
||||||
xml.attributes().value("href").toString() );
|
: xml.attributes().value("href").toString());
|
||||||
|
|
||||||
if (link.startsWith("magnet:", Qt::CaseInsensitive))
|
if (link.startsWith("magnet:", Qt::CaseInsensitive))
|
||||||
article["torrent_url"] = link; // magnet link instead of a news URL
|
article["torrent_url"] = link; // magnet link instead of a news URL
|
||||||
|
@ -370,7 +372,7 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
|
||||||
// Atom feeds can have relative links, work around this and
|
// Atom feeds can have relative links, work around this and
|
||||||
// take the stress of figuring article full URI from UI
|
// take the stress of figuring article full URI from UI
|
||||||
// Assemble full URI
|
// Assemble full URI
|
||||||
article["news_link"] = ( m_baseUrl.isEmpty() ? link : m_baseUrl + link );
|
article["news_link"] = (m_baseUrl.isEmpty() ? link : m_baseUrl + link);
|
||||||
|
|
||||||
}
|
}
|
||||||
else if ((xml.name() == "summary") || (xml.name() == "content")){
|
else if ((xml.name() == "summary") || (xml.name() == "content")){
|
||||||
|
@ -398,8 +400,8 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
|
||||||
}
|
}
|
||||||
else if (xml.name() == "author") {
|
else if (xml.name() == "author") {
|
||||||
xml.readNext();
|
xml.readNext();
|
||||||
while(xml.name() != "author") {
|
while (xml.name() != "author") {
|
||||||
if(xml.name() == "name")
|
if (xml.name() == "name")
|
||||||
article["author"] = xml.readElementText().trimmed();
|
article["author"] = xml.readElementText().trimmed();
|
||||||
xml.readNext();
|
xml.readNext();
|
||||||
}
|
}
|
||||||
|
@ -410,28 +412,7 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!article.contains("torrent_url") && article.contains("news_link"))
|
m_result.articles.prepend(article);
|
||||||
article["torrent_url"] = article["news_link"];
|
|
||||||
|
|
||||||
if (!article.contains("id")) {
|
|
||||||
// Item does not have a guid, fall back to some other identifier
|
|
||||||
const QString link = article.value("news_link").toString();
|
|
||||||
if (!link.isEmpty()) {
|
|
||||||
article["id"] = link;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const QString title = article.value("title").toString();
|
|
||||||
if (!title.isEmpty()) {
|
|
||||||
article["id"] = title;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qWarning() << "Item has no guid, link or title, ignoring it...";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit newArticle(article);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Parser::parseAtomChannel(QXmlStreamReader &xml)
|
void Parser::parseAtomChannel(QXmlStreamReader &xml)
|
||||||
|
@ -446,17 +427,16 @@ void Parser::parseAtomChannel(QXmlStreamReader &xml)
|
||||||
|
|
||||||
if (xml.isStartElement()) {
|
if (xml.isStartElement()) {
|
||||||
if (xml.name() == "title") {
|
if (xml.name() == "title") {
|
||||||
QString title = xml.readElementText();
|
m_result.title = xml.readElementText();
|
||||||
emit feedTitle(title);
|
|
||||||
}
|
}
|
||||||
else if (xml.name() == "updated") {
|
else if (xml.name() == "updated") {
|
||||||
QString lastBuildDate = xml.readElementText();
|
QString lastBuildDate = xml.readElementText();
|
||||||
if (!lastBuildDate.isEmpty()) {
|
if (!lastBuildDate.isEmpty()) {
|
||||||
if (m_lastBuildDate == lastBuildDate) {
|
if (m_result.lastBuildDate == lastBuildDate) {
|
||||||
qDebug() << "The RSS feed has not changed since last time, aborting parsing.";
|
qDebug() << "The RSS feed has not changed since last time, aborting parsing.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_lastBuildDate = lastBuildDate;
|
m_result.lastBuildDate = lastBuildDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (xml.name() == "entry") {
|
else if (xml.name() == "entry") {
|
|
@ -29,41 +29,49 @@
|
||||||
* Contact : chris@qbittorrent.org
|
* Contact : chris@qbittorrent.org
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef RSSPARSER_H
|
#pragma once
|
||||||
#define RSSPARSER_H
|
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariantHash>
|
#include <QVariantHash>
|
||||||
|
|
||||||
class QXmlStreamReader;
|
class QXmlStreamReader;
|
||||||
|
|
||||||
namespace Rss
|
namespace RSS
|
||||||
{
|
{
|
||||||
namespace Private
|
namespace Private
|
||||||
{
|
{
|
||||||
|
struct ParsingResult
|
||||||
|
{
|
||||||
|
QString error;
|
||||||
|
QString lastBuildDate;
|
||||||
|
QString title;
|
||||||
|
QList<QVariantHash> articles;
|
||||||
|
};
|
||||||
|
|
||||||
class Parser: public QObject
|
class Parser: public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public slots:
|
public:
|
||||||
|
explicit Parser(QString lastBuildDate);
|
||||||
void parse(const QByteArray &feedData);
|
void parse(const QByteArray &feedData);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void newArticle(const QVariantHash &rssArticle);
|
void finished(const RSS::Private::ParsingResult &result);
|
||||||
void feedTitle(const QString &title);
|
|
||||||
void finished(const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
Q_INVOKABLE void parse_impl(const QByteArray &feedData);
|
||||||
void parseRssArticle(QXmlStreamReader &xml);
|
void parseRssArticle(QXmlStreamReader &xml);
|
||||||
void parseRSSChannel(QXmlStreamReader &xml);
|
void parseRSSChannel(QXmlStreamReader &xml);
|
||||||
void parseAtomArticle(QXmlStreamReader &xml);
|
void parseAtomArticle(QXmlStreamReader &xml);
|
||||||
void parseAtomChannel(QXmlStreamReader &xml);
|
void parseAtomChannel(QXmlStreamReader &xml);
|
||||||
|
|
||||||
QString m_lastBuildDate; // Optimization
|
|
||||||
QString m_baseUrl;
|
QString m_baseUrl;
|
||||||
|
ParsingResult m_result;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // RSSPARSER_H
|
Q_DECLARE_METATYPE(RSS::Private::ParsingResult)
|
178
src/base/rss/rss_article.cpp
Normal file
178
src/base/rss/rss_article.cpp
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rss_article.h"
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
#include "rss_feed.h"
|
||||||
|
|
||||||
|
const QString Str_Id(QStringLiteral("id"));
|
||||||
|
const QString Str_Date(QStringLiteral("date"));
|
||||||
|
const QString Str_Title(QStringLiteral("title"));
|
||||||
|
const QString Str_Author(QStringLiteral("author"));
|
||||||
|
const QString Str_Description(QStringLiteral("description"));
|
||||||
|
const QString Str_TorrentURL(QStringLiteral("torrentURL"));
|
||||||
|
const QString Str_Torrent_Url(QStringLiteral("torrent_url"));
|
||||||
|
const QString Str_Link(QStringLiteral("link"));
|
||||||
|
const QString Str_News_Link(QStringLiteral("news_link"));
|
||||||
|
const QString Str_IsRead(QStringLiteral("isRead"));
|
||||||
|
const QString Str_Read(QStringLiteral("read"));
|
||||||
|
|
||||||
|
using namespace RSS;
|
||||||
|
|
||||||
|
Article::Article(Feed *feed, QString guid, QDateTime date, QString title, QString author
|
||||||
|
, QString description, QString torrentUrl, QString link, bool isRead)
|
||||||
|
: QObject(feed)
|
||||||
|
, m_feed(feed)
|
||||||
|
, m_guid(guid)
|
||||||
|
, m_date(date)
|
||||||
|
, m_title(title)
|
||||||
|
, m_author(author)
|
||||||
|
, m_description(description)
|
||||||
|
, m_torrentURL(torrentUrl)
|
||||||
|
, m_link(link)
|
||||||
|
, m_isRead(isRead)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Article::guid() const
|
||||||
|
{
|
||||||
|
return m_guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime Article::date() const
|
||||||
|
{
|
||||||
|
return m_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Article::title() const
|
||||||
|
{
|
||||||
|
return m_title;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Article::author() const
|
||||||
|
{
|
||||||
|
return m_author;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Article::description() const
|
||||||
|
{
|
||||||
|
return m_description;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Article::torrentUrl() const
|
||||||
|
{
|
||||||
|
return (m_torrentURL.isEmpty() ? m_link : m_torrentURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Article::link() const
|
||||||
|
{
|
||||||
|
return m_link;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Article::isRead() const
|
||||||
|
{
|
||||||
|
return m_isRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Article::markAsRead()
|
||||||
|
{
|
||||||
|
if (!m_isRead) {
|
||||||
|
m_isRead = true;
|
||||||
|
emit read(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject Article::toJsonObject() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{Str_Id, m_guid},
|
||||||
|
{Str_Date, m_date.toString(Qt::RFC2822Date)},
|
||||||
|
{Str_Title, m_title},
|
||||||
|
{Str_Author, m_author},
|
||||||
|
{Str_Description, m_description},
|
||||||
|
{Str_TorrentURL, m_torrentURL},
|
||||||
|
{Str_Link, m_link},
|
||||||
|
{Str_IsRead, m_isRead}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Article::articleDateRecentThan(Article *article, const QDateTime &date)
|
||||||
|
{
|
||||||
|
return article->date() > date;
|
||||||
|
}
|
||||||
|
|
||||||
|
Article *Article::fromJsonObject(Feed *feed, const QJsonObject &jsonObj)
|
||||||
|
{
|
||||||
|
QString guid = jsonObj.value(Str_Id).toString();
|
||||||
|
// If item does not have a guid, fall back to some other identifier
|
||||||
|
if (guid.isEmpty())
|
||||||
|
guid = jsonObj.value(Str_Torrent_Url).toString();
|
||||||
|
if (guid.isEmpty())
|
||||||
|
guid = jsonObj.value(Str_Title).toString();
|
||||||
|
if (guid.isEmpty()) return nullptr;
|
||||||
|
|
||||||
|
return new Article(
|
||||||
|
feed, guid
|
||||||
|
, QDateTime::fromString(jsonObj.value(Str_Date).toString(), Qt::RFC2822Date)
|
||||||
|
, jsonObj.value(Str_Title).toString()
|
||||||
|
, jsonObj.value(Str_Author).toString()
|
||||||
|
, jsonObj.value(Str_Description).toString()
|
||||||
|
, jsonObj.value(Str_TorrentURL).toString()
|
||||||
|
, jsonObj.value(Str_Link).toString()
|
||||||
|
, jsonObj.value(Str_IsRead).toBool(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Article *Article::fromVariantHash(Feed *feed, const QVariantHash &varHash)
|
||||||
|
{
|
||||||
|
QString guid = varHash[Str_Id].toString();
|
||||||
|
// If item does not have a guid, fall back to some other identifier
|
||||||
|
if (guid.isEmpty())
|
||||||
|
guid = varHash.value(Str_Torrent_Url).toString();
|
||||||
|
if (guid.isEmpty())
|
||||||
|
guid = varHash.value(Str_Title).toString();
|
||||||
|
if (guid.isEmpty()) nullptr;
|
||||||
|
|
||||||
|
return new Article(feed, guid
|
||||||
|
, varHash.value(Str_Date).toDateTime()
|
||||||
|
, varHash.value(Str_Title).toString()
|
||||||
|
, varHash.value(Str_Author).toString()
|
||||||
|
, varHash.value(Str_Description).toString()
|
||||||
|
, varHash.value(Str_Torrent_Url).toString()
|
||||||
|
, varHash.value(Str_News_Link).toString()
|
||||||
|
, varHash.value(Str_Read, false).toBool());
|
||||||
|
}
|
||||||
|
|
||||||
|
Feed *Article::feed() const
|
||||||
|
{
|
||||||
|
return m_feed;
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
*
|
*
|
||||||
|
@ -25,67 +26,59 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef RSSARTICLE_H
|
#pragma once
|
||||||
#define RSSARTICLE_H
|
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QVariantHash>
|
#include <QObject>
|
||||||
#include <QSharedPointer>
|
#include <QString>
|
||||||
|
|
||||||
namespace Rss
|
namespace RSS
|
||||||
{
|
{
|
||||||
class Feed;
|
class Feed;
|
||||||
class Article;
|
|
||||||
|
|
||||||
typedef QSharedPointer<Article> ArticlePtr;
|
|
||||||
|
|
||||||
// Item of a rss stream, single information
|
|
||||||
class Article: public QObject
|
class Article: public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(Article)
|
||||||
|
|
||||||
|
friend class Feed;
|
||||||
|
|
||||||
|
Article(Feed *feed, QString guid, QDateTime date, QString title, QString author
|
||||||
|
, QString description, QString torrentUrl, QString link, bool isRead = false);
|
||||||
|
static Article *fromJsonObject(Feed *feed, const QJsonObject &jsonObj);
|
||||||
|
static Article *fromVariantHash(Feed *feed, const QVariantHash &varHash);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Article(Feed *parent, const QString &guid);
|
Feed *feed() const;
|
||||||
|
QString guid() const;
|
||||||
// Accessors
|
QDateTime date() const;
|
||||||
bool hasAttachment() const;
|
QString title() const;
|
||||||
const QString &guid() const;
|
QString author() const;
|
||||||
Feed *parent() const;
|
|
||||||
const QString &title() const;
|
|
||||||
const QString &author() const;
|
|
||||||
const QString &torrentUrl() const;
|
|
||||||
const QString &link() const;
|
|
||||||
QString description() const;
|
QString description() const;
|
||||||
const QDateTime &date() const;
|
QString torrentUrl() const;
|
||||||
|
QString link() const;
|
||||||
bool isRead() const;
|
bool isRead() const;
|
||||||
// Setters
|
|
||||||
void markAsRead();
|
void markAsRead();
|
||||||
|
|
||||||
// Serialization
|
QJsonObject toJsonObject() const;
|
||||||
QVariantHash toHash() const;
|
|
||||||
static ArticlePtr fromHash(Feed *parent, const QVariantHash &hash);
|
static bool articleDateRecentThan(Article *article, const QDateTime &date);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void articleWasRead();
|
void read(Article *article = nullptr);
|
||||||
|
|
||||||
public slots:
|
|
||||||
void handleTorrentDownloadSuccess(const QString &url);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Feed *m_parent;
|
Feed *m_feed = nullptr;
|
||||||
QString m_guid;
|
QString m_guid;
|
||||||
QString m_title;
|
|
||||||
QString m_torrentUrl;
|
|
||||||
QString m_link;
|
|
||||||
QString m_description;
|
|
||||||
QDateTime m_date;
|
QDateTime m_date;
|
||||||
|
QString m_title;
|
||||||
QString m_author;
|
QString m_author;
|
||||||
bool m_read;
|
QString m_description;
|
||||||
|
QString m_torrentURL;
|
||||||
|
QString m_link;
|
||||||
|
bool m_isRead = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // RSSARTICLE_H
|
|
390
src/base/rss/rss_autodownloader.cpp
Normal file
390
src/base/rss/rss_autodownloader.cpp
Normal file
|
@ -0,0 +1,390 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rss_autodownloader.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QThread>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include "../bittorrent/magneturi.h"
|
||||||
|
#include "../bittorrent/session.h"
|
||||||
|
#include "../asyncfilestorage.h"
|
||||||
|
#include "../logger.h"
|
||||||
|
#include "../profile.h"
|
||||||
|
#include "../settingsstorage.h"
|
||||||
|
#include "../tristatebool.h"
|
||||||
|
#include "../utils/fs.h"
|
||||||
|
#include "rss_article.h"
|
||||||
|
#include "rss_autodownloadrule.h"
|
||||||
|
#include "rss_feed.h"
|
||||||
|
#include "rss_folder.h"
|
||||||
|
#include "rss_session.h"
|
||||||
|
|
||||||
|
struct ProcessingJob
|
||||||
|
{
|
||||||
|
QString feedURL;
|
||||||
|
QString articleGUID;
|
||||||
|
QString articleTitle;
|
||||||
|
QDateTime articleDate;
|
||||||
|
QString torrentURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
const QString ConfFolderName(QStringLiteral("rss"));
|
||||||
|
const QString RulesFileName(QStringLiteral("download_rules.json"));
|
||||||
|
|
||||||
|
const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing"));
|
||||||
|
|
||||||
|
using namespace RSS;
|
||||||
|
|
||||||
|
QPointer<AutoDownloader> AutoDownloader::m_instance = nullptr;
|
||||||
|
|
||||||
|
AutoDownloader::AutoDownloader()
|
||||||
|
: m_processingEnabled(SettingsStorage::instance()->loadValue(SettingsKey_ProcessingEnabled, false).toBool())
|
||||||
|
, m_processingTimer(new QTimer(this))
|
||||||
|
, m_ioThread(new QThread(this))
|
||||||
|
{
|
||||||
|
Q_ASSERT(!m_instance); // only one instance is allowed
|
||||||
|
m_instance = this;
|
||||||
|
|
||||||
|
m_fileStorage = new AsyncFileStorage(
|
||||||
|
Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Config) + ConfFolderName));
|
||||||
|
if (!m_fileStorage)
|
||||||
|
throw std::runtime_error("Directory for RSS AutoDownloader data is unavailable.");
|
||||||
|
|
||||||
|
m_fileStorage->moveToThread(m_ioThread);
|
||||||
|
connect(m_ioThread, &QThread::finished, m_fileStorage, &AsyncFileStorage::deleteLater);
|
||||||
|
connect(m_fileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
|
||||||
|
{
|
||||||
|
Logger::instance()->addMessage(QString("Couldn't save RSS AutoDownloader data in %1. Error: %2")
|
||||||
|
.arg(fileName).arg(errorString), Log::WARNING);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_ioThread->start();
|
||||||
|
|
||||||
|
connect(BitTorrent::Session::instance(), &BitTorrent::Session::downloadFromUrlFinished
|
||||||
|
, this, &AutoDownloader::handleTorrentDownloadFinished);
|
||||||
|
connect(BitTorrent::Session::instance(), &BitTorrent::Session::downloadFromUrlFailed
|
||||||
|
, this, &AutoDownloader::handleTorrentDownloadFailed);
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
m_processingTimer->setSingleShot(true);
|
||||||
|
connect(m_processingTimer, &QTimer::timeout, this, &AutoDownloader::process);
|
||||||
|
|
||||||
|
if (m_processingEnabled)
|
||||||
|
startProcessing();
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoDownloader::~AutoDownloader()
|
||||||
|
{
|
||||||
|
store();
|
||||||
|
|
||||||
|
m_ioThread->quit();
|
||||||
|
m_ioThread->wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoDownloader *AutoDownloader::instance()
|
||||||
|
{
|
||||||
|
return m_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloader::hasRule(const QString &ruleName) const
|
||||||
|
{
|
||||||
|
return m_rules.contains(ruleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoDownloadRule AutoDownloader::ruleByName(const QString &ruleName) const
|
||||||
|
{
|
||||||
|
return m_rules.value(ruleName, AutoDownloadRule("Unknown Rule"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<AutoDownloadRule> AutoDownloader::rules() const
|
||||||
|
{
|
||||||
|
return m_rules.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::insertRule(const AutoDownloadRule &rule)
|
||||||
|
{
|
||||||
|
if (!hasRule(rule.name())) {
|
||||||
|
// Insert new rule
|
||||||
|
setRule_impl(rule);
|
||||||
|
m_dirty = true;
|
||||||
|
store();
|
||||||
|
emit ruleAdded(rule.name());
|
||||||
|
resetProcessingQueue();
|
||||||
|
}
|
||||||
|
else if (ruleByName(rule.name()) != rule) {
|
||||||
|
// Update existing rule
|
||||||
|
setRule_impl(rule);
|
||||||
|
m_dirty = true;
|
||||||
|
storeDeferred();
|
||||||
|
emit ruleChanged(rule.name());
|
||||||
|
resetProcessingQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloader::renameRule(const QString &ruleName, const QString &newRuleName)
|
||||||
|
{
|
||||||
|
if (!hasRule(ruleName)) return false;
|
||||||
|
if (hasRule(newRuleName)) return false;
|
||||||
|
|
||||||
|
m_rules.insert(newRuleName, m_rules.take(ruleName));
|
||||||
|
m_dirty = true;
|
||||||
|
store();
|
||||||
|
emit ruleRenamed(newRuleName, ruleName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::removeRule(const QString &ruleName)
|
||||||
|
{
|
||||||
|
if (m_rules.contains(ruleName)) {
|
||||||
|
emit ruleAboutToBeRemoved(ruleName);
|
||||||
|
m_rules.remove(ruleName);
|
||||||
|
m_dirty = true;
|
||||||
|
store();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::process()
|
||||||
|
{
|
||||||
|
if (m_processingQueue.isEmpty()) return; // processing was disabled
|
||||||
|
|
||||||
|
processJob(m_processingQueue.takeFirst());
|
||||||
|
if (!m_processingQueue.isEmpty())
|
||||||
|
// Schedule to process the next torrent (if any)
|
||||||
|
m_processingTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::handleTorrentDownloadFinished(const QString &url)
|
||||||
|
{
|
||||||
|
auto job = m_waitingJobs.take(url);
|
||||||
|
if (!job) return;
|
||||||
|
|
||||||
|
if (auto feed = Session::instance()->feedByURL(job->feedURL))
|
||||||
|
if (auto article = feed->articleByGUID(job->articleGUID))
|
||||||
|
article->markAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::handleTorrentDownloadFailed(const QString &url)
|
||||||
|
{
|
||||||
|
m_waitingJobs.remove(url);
|
||||||
|
// TODO: Re-schedule job here.
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::handleNewArticle(Article *article)
|
||||||
|
{
|
||||||
|
if (!article->isRead() && !article->torrentUrl().isEmpty())
|
||||||
|
addJobForArticle(article);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::setRule_impl(const AutoDownloadRule &rule)
|
||||||
|
{
|
||||||
|
m_rules.insert(rule.name(), rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::addJobForArticle(Article *article)
|
||||||
|
{
|
||||||
|
const QString torrentURL = article->torrentUrl();
|
||||||
|
if (m_waitingJobs.contains(torrentURL)) return;
|
||||||
|
|
||||||
|
QSharedPointer<ProcessingJob> job(new ProcessingJob);
|
||||||
|
job->feedURL = article->feed()->url();
|
||||||
|
job->articleGUID = article->guid();
|
||||||
|
job->articleTitle = article->title();
|
||||||
|
job->articleDate = article->date();
|
||||||
|
job->torrentURL = torrentURL;
|
||||||
|
m_processingQueue.append(job);
|
||||||
|
if (!m_processingTimer->isActive())
|
||||||
|
m_processingTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::processJob(const QSharedPointer<ProcessingJob> &job)
|
||||||
|
{
|
||||||
|
for (AutoDownloadRule &rule: m_rules) {
|
||||||
|
if (!rule.isEnabled()) continue;
|
||||||
|
if (!rule.feedURLs().contains(job->feedURL)) continue;
|
||||||
|
if (!rule.matches(job->articleTitle)) continue;
|
||||||
|
|
||||||
|
// if rule is in ignoring state do nothing with matched torrent
|
||||||
|
if (rule.ignoreDays() > 0) {
|
||||||
|
if (rule.lastMatch().isValid()) {
|
||||||
|
if (job->articleDate < rule.lastMatch().addDays(rule.ignoreDays()))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.setLastMatch(job->articleDate);
|
||||||
|
m_dirty = true;
|
||||||
|
storeDeferred();
|
||||||
|
|
||||||
|
BitTorrent::AddTorrentParams params;
|
||||||
|
params.savePath = rule.savePath();
|
||||||
|
params.category = rule.assignedCategory();
|
||||||
|
params.addPaused = rule.addPaused();
|
||||||
|
BitTorrent::Session::instance()->addTorrent(job->torrentURL, params);
|
||||||
|
|
||||||
|
if (BitTorrent::MagnetUri(job->torrentURL).isValid()) {
|
||||||
|
if (auto feed = Session::instance()->feedByURL(job->feedURL)) {
|
||||||
|
if (auto article = feed->articleByGUID(job->articleGUID))
|
||||||
|
article->markAsRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// waiting for torrent file downloading
|
||||||
|
// normalize URL string via QUrl since DownloadManager do it
|
||||||
|
m_waitingJobs.insert(QUrl(job->torrentURL).toString(), job);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::load()
|
||||||
|
{
|
||||||
|
QFile rulesFile(m_fileStorage->storageDir().absoluteFilePath(RulesFileName));
|
||||||
|
|
||||||
|
if (!rulesFile.exists())
|
||||||
|
loadRulesLegacy();
|
||||||
|
else if (rulesFile.open(QFile::ReadOnly))
|
||||||
|
loadRules(rulesFile.readAll());
|
||||||
|
else
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't read RSS AutoDownloader rules from %1. Error: %2")
|
||||||
|
.arg(rulesFile.fileName()).arg(rulesFile.errorString()), Log::WARNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::loadRules(const QByteArray &data)
|
||||||
|
{
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
|
||||||
|
if (jsonError.error != QJsonParseError::NoError) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't parse RSS AutoDownloader rules. Error: %1")
|
||||||
|
.arg(jsonError.errorString()), Log::WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jsonDoc.isObject()) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't load RSS AutoDownloader rules. Invalid data format."), Log::WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
|
foreach (const QString &key, jsonObj.keys()) {
|
||||||
|
const QJsonValue jsonVal = jsonObj.value(key);
|
||||||
|
if (!jsonVal.isObject()) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't load RSS AutoDownloader rule '%1'. Invalid data format.")
|
||||||
|
.arg(key), Log::WARNING);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRule_impl(AutoDownloadRule::fromJsonObject(jsonVal.toObject(), key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::loadRulesLegacy()
|
||||||
|
{
|
||||||
|
SettingsPtr settings = Profile::instance().applicationSettings(QStringLiteral("qBittorrent-rss"));
|
||||||
|
QVariantHash rules = settings->value(QStringLiteral("download_rules")).toHash();
|
||||||
|
foreach (const QVariant &ruleVar, rules) {
|
||||||
|
auto rule = AutoDownloadRule::fromVariantHash(ruleVar.toHash());
|
||||||
|
if (!rule.name().isEmpty())
|
||||||
|
insertRule(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::store()
|
||||||
|
{
|
||||||
|
if (!m_dirty) return;
|
||||||
|
|
||||||
|
m_dirty = false;
|
||||||
|
m_savingTimer.stop();
|
||||||
|
|
||||||
|
QJsonObject jsonObj;
|
||||||
|
foreach (auto rule, m_rules)
|
||||||
|
jsonObj.insert(rule.name(), rule.toJsonObject());
|
||||||
|
|
||||||
|
m_fileStorage->store(RulesFileName, QJsonDocument(jsonObj).toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::storeDeferred()
|
||||||
|
{
|
||||||
|
if (!m_savingTimer.isActive())
|
||||||
|
m_savingTimer.start(5 * 1000, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloader::isProcessingEnabled() const
|
||||||
|
{
|
||||||
|
return m_processingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::resetProcessingQueue()
|
||||||
|
{
|
||||||
|
m_processingQueue.clear();
|
||||||
|
foreach (Article *article, Session::instance()->rootFolder()->articles()) {
|
||||||
|
if (!article->isRead() && !article->torrentUrl().isEmpty())
|
||||||
|
addJobForArticle(article);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::startProcessing()
|
||||||
|
{
|
||||||
|
resetProcessingQueue();
|
||||||
|
connect(Session::instance()->rootFolder(), &Folder::newArticle, this, &AutoDownloader::handleNewArticle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::setProcessingEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (m_processingEnabled != enabled) {
|
||||||
|
m_processingEnabled = enabled;
|
||||||
|
SettingsStorage::instance()->storeValue(SettingsKey_ProcessingEnabled, m_processingEnabled);
|
||||||
|
if (m_processingEnabled) {
|
||||||
|
startProcessing();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_processingQueue.clear();
|
||||||
|
disconnect(Session::instance()->rootFolder(), &Folder::newArticle, this, &AutoDownloader::handleNewArticle);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit processingStateChanged(m_processingEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloader::timerEvent(QTimerEvent *event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(event);
|
||||||
|
store();
|
||||||
|
}
|
114
src/base/rss/rss_autodownloader.h
Normal file
114
src/base/rss/rss_autodownloader.h
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QBasicTimer>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
class QThread;
|
||||||
|
class QTimer;
|
||||||
|
class Application;
|
||||||
|
class AsyncFileStorage;
|
||||||
|
struct ProcessingJob;
|
||||||
|
|
||||||
|
namespace RSS
|
||||||
|
{
|
||||||
|
class Article;
|
||||||
|
class Feed;
|
||||||
|
class Item;
|
||||||
|
|
||||||
|
class AutoDownloadRule;
|
||||||
|
|
||||||
|
class AutoDownloader final: public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(AutoDownloader)
|
||||||
|
|
||||||
|
friend class ::Application;
|
||||||
|
|
||||||
|
AutoDownloader();
|
||||||
|
~AutoDownloader() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static AutoDownloader *instance();
|
||||||
|
|
||||||
|
bool isProcessingEnabled() const;
|
||||||
|
void setProcessingEnabled(bool enabled);
|
||||||
|
|
||||||
|
bool hasRule(const QString &ruleName) const;
|
||||||
|
AutoDownloadRule ruleByName(const QString &ruleName) const;
|
||||||
|
QList<AutoDownloadRule> rules() const;
|
||||||
|
|
||||||
|
void insertRule(const AutoDownloadRule &rule);
|
||||||
|
bool renameRule(const QString &ruleName, const QString &newRuleName);
|
||||||
|
void removeRule(const QString &ruleName);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void processingStateChanged(bool enabled);
|
||||||
|
void ruleAdded(const QString &ruleName);
|
||||||
|
void ruleChanged(const QString &ruleName);
|
||||||
|
void ruleRenamed(const QString &ruleName, const QString &oldRuleName);
|
||||||
|
void ruleAboutToBeRemoved(const QString &ruleName);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void process();
|
||||||
|
void handleTorrentDownloadFinished(const QString &url);
|
||||||
|
void handleTorrentDownloadFailed(const QString &url);
|
||||||
|
void handleNewArticle(Article *article);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void timerEvent(QTimerEvent *event) override;
|
||||||
|
void setRule_impl(const AutoDownloadRule &rule);
|
||||||
|
void resetProcessingQueue();
|
||||||
|
void startProcessing();
|
||||||
|
void addJobForArticle(Article *article);
|
||||||
|
void processJob(const QSharedPointer<ProcessingJob> &job);
|
||||||
|
void load();
|
||||||
|
void loadRules(const QByteArray &data);
|
||||||
|
void loadRulesLegacy();
|
||||||
|
void store();
|
||||||
|
void storeDeferred();
|
||||||
|
|
||||||
|
static QPointer<AutoDownloader> m_instance;
|
||||||
|
|
||||||
|
bool m_processingEnabled;
|
||||||
|
QTimer *m_processingTimer;
|
||||||
|
QThread *m_ioThread;
|
||||||
|
AsyncFileStorage *m_fileStorage;
|
||||||
|
QHash<QString, AutoDownloadRule> m_rules;
|
||||||
|
QList<QSharedPointer<ProcessingJob>> m_processingQueue;
|
||||||
|
QHash<QString, QSharedPointer<ProcessingJob>> m_waitingJobs;
|
||||||
|
bool m_dirty = false;
|
||||||
|
QBasicTimer m_savingTimer;
|
||||||
|
};
|
||||||
|
}
|
538
src/base/rss/rss_autodownloadrule.cpp
Normal file
538
src/base/rss/rss_autodownloadrule.cpp
Normal file
|
@ -0,0 +1,538 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rss_autodownloadrule.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QRegExp>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSharedData>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
#include "../preferences.h"
|
||||||
|
#include "../tristatebool.h"
|
||||||
|
#include "../utils/fs.h"
|
||||||
|
#include "../utils/string.h"
|
||||||
|
#include "rss_feed.h"
|
||||||
|
#include "rss_article.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
TriStateBool jsonValueToTriStateBool(const QJsonValue &jsonVal)
|
||||||
|
{
|
||||||
|
if (jsonVal.isBool())
|
||||||
|
return TriStateBool(jsonVal.toBool());
|
||||||
|
|
||||||
|
if (!jsonVal.isNull())
|
||||||
|
qDebug() << Q_FUNC_INFO << "Incorrect value" << jsonVal.toVariant();
|
||||||
|
|
||||||
|
return TriStateBool::Undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonValue triStateBoolToJsonValue(const TriStateBool &triStateBool)
|
||||||
|
{
|
||||||
|
switch (static_cast<int>(triStateBool)) {
|
||||||
|
case 0: return false; break;
|
||||||
|
case 1: return true; break;
|
||||||
|
default: return QJsonValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString Str_Name(QStringLiteral("name"));
|
||||||
|
const QString Str_Enabled(QStringLiteral("enabled"));
|
||||||
|
const QString Str_UseRegex(QStringLiteral("useRegex"));
|
||||||
|
const QString Str_MustContain(QStringLiteral("mustContain"));
|
||||||
|
const QString Str_MustNotContain(QStringLiteral("mustNotContain"));
|
||||||
|
const QString Str_EpisodeFilter(QStringLiteral("episodeFilter"));
|
||||||
|
const QString Str_AffectedFeeds(QStringLiteral("affectedFeeds"));
|
||||||
|
const QString Str_SavePath(QStringLiteral("savePath"));
|
||||||
|
const QString Str_AssignedCategory(QStringLiteral("assignedCategory"));
|
||||||
|
const QString Str_LastMatch(QStringLiteral("lastMatch"));
|
||||||
|
const QString Str_IgnoreDays(QStringLiteral("ignoreDays"));
|
||||||
|
const QString Str_AddPaused(QStringLiteral("addPaused"));
|
||||||
|
|
||||||
|
namespace RSS
|
||||||
|
{
|
||||||
|
struct AutoDownloadRuleData: public QSharedData
|
||||||
|
{
|
||||||
|
QString name;
|
||||||
|
bool enabled = true;
|
||||||
|
|
||||||
|
QStringList mustContain;
|
||||||
|
QStringList mustNotContain;
|
||||||
|
QString episodeFilter;
|
||||||
|
QStringList feedURLs;
|
||||||
|
bool useRegex = false;
|
||||||
|
int ignoreDays = 0;
|
||||||
|
QDateTime lastMatch;
|
||||||
|
|
||||||
|
QString savePath;
|
||||||
|
QString category;
|
||||||
|
TriStateBool addPaused = TriStateBool::Undefined;
|
||||||
|
|
||||||
|
mutable QHash<QString, QRegularExpression> cachedRegexes;
|
||||||
|
|
||||||
|
bool operator==(const AutoDownloadRuleData &other) const
|
||||||
|
{
|
||||||
|
return (name == other.name)
|
||||||
|
&& (enabled == other.enabled)
|
||||||
|
&& (mustContain == other.mustContain)
|
||||||
|
&& (mustNotContain == other.mustNotContain)
|
||||||
|
&& (episodeFilter == other.episodeFilter)
|
||||||
|
&& (feedURLs == other.feedURLs)
|
||||||
|
&& (useRegex == other.useRegex)
|
||||||
|
&& (ignoreDays == other.ignoreDays)
|
||||||
|
&& (lastMatch == other.lastMatch)
|
||||||
|
&& (savePath == other.savePath)
|
||||||
|
&& (category == other.category)
|
||||||
|
&& (addPaused == other.addPaused);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace RSS;
|
||||||
|
|
||||||
|
AutoDownloadRule::AutoDownloadRule(const QString &name)
|
||||||
|
: m_dataPtr(new AutoDownloadRuleData)
|
||||||
|
{
|
||||||
|
setName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoDownloadRule::AutoDownloadRule(const AutoDownloadRule &other)
|
||||||
|
: m_dataPtr(other.m_dataPtr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoDownloadRule::~AutoDownloadRule() {}
|
||||||
|
|
||||||
|
QRegularExpression AutoDownloadRule::cachedRegex(const QString &expression, bool isRegex) const
|
||||||
|
{
|
||||||
|
// Use a cache of regexes so we don't have to continually recompile - big performance increase.
|
||||||
|
// The cache is cleared whenever the regex/wildcard, must or must not contain fields or
|
||||||
|
// episode filter are modified.
|
||||||
|
Q_ASSERT(!expression.isEmpty());
|
||||||
|
QRegularExpression regex(m_dataPtr->cachedRegexes[expression]);
|
||||||
|
|
||||||
|
if (!regex.pattern().isEmpty())
|
||||||
|
return regex;
|
||||||
|
|
||||||
|
return m_dataPtr->cachedRegexes[expression] = QRegularExpression(isRegex ? expression : Utils::String::wildcardToRegex(expression), QRegularExpression::CaseInsensitiveOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloadRule::matches(const QString &articleTitle, const QString &expression) const
|
||||||
|
{
|
||||||
|
static QRegularExpression whitespace("\\s+");
|
||||||
|
|
||||||
|
if (expression.isEmpty()) {
|
||||||
|
// A regex of the form "expr|" will always match, so do the same for wildcards
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (m_dataPtr->useRegex) {
|
||||||
|
QRegularExpression reg(cachedRegex(expression));
|
||||||
|
return reg.match(articleTitle).hasMatch();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Only match if every wildcard token (separated by spaces) is present in the article name.
|
||||||
|
// Order of wildcard tokens is unimportant (if order is important, they should have used *).
|
||||||
|
foreach (const QString &wildcard, expression.split(whitespace, QString::SplitBehavior::SkipEmptyParts)) {
|
||||||
|
QRegularExpression reg(cachedRegex(wildcard, false));
|
||||||
|
|
||||||
|
if (!reg.match(articleTitle).hasMatch())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloadRule::matches(const QString &articleTitle) const
|
||||||
|
{
|
||||||
|
if (!m_dataPtr->mustContain.empty()) {
|
||||||
|
bool logged = false;
|
||||||
|
bool foundMustContain = false;
|
||||||
|
|
||||||
|
// Each expression is either a regex, or a set of wildcards separated by whitespace.
|
||||||
|
// Accept if any complete expression matches.
|
||||||
|
foreach (const QString &expression, m_dataPtr->mustContain) {
|
||||||
|
if (!logged) {
|
||||||
|
// qDebug() << "Checking matching" << (m_dataPtr->useRegex ? "regex:" : "wildcard expressions:") << m_dataPtr->mustContain.join("|");
|
||||||
|
logged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A regex of the form "expr|" will always match, so do the same for wildcards
|
||||||
|
foundMustContain = matches(articleTitle, expression);
|
||||||
|
|
||||||
|
if (foundMustContain) {
|
||||||
|
// qDebug() << "Found matching" << (m_dataPtr->useRegex ? "regex:" : "wildcard expression:") << expression;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundMustContain)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_dataPtr->mustNotContain.empty()) {
|
||||||
|
bool logged = false;
|
||||||
|
|
||||||
|
// Each expression is either a regex, or a set of wildcards separated by whitespace.
|
||||||
|
// Reject if any complete expression matches.
|
||||||
|
foreach (const QString &expression, m_dataPtr->mustNotContain) {
|
||||||
|
if (!logged) {
|
||||||
|
// qDebug() << "Checking not matching" << (m_dataPtr->useRegex ? "regex:" : "wildcard expressions:") << m_dataPtr->mustNotContain.join("|");
|
||||||
|
logged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A regex of the form "expr|" will always match, so do the same for wildcards
|
||||||
|
if (matches(articleTitle, expression)) {
|
||||||
|
// qDebug() << "Found not matching" << (m_dataPtr->useRegex ? "regex:" : "wildcard expression:") << expression;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_dataPtr->episodeFilter.isEmpty()) {
|
||||||
|
// qDebug() << "Checking episode filter:" << m_dataPtr->episodeFilter;
|
||||||
|
QRegularExpression f(cachedRegex("(^\\d{1,4})x(.*;$)"));
|
||||||
|
QRegularExpressionMatch matcher = f.match(m_dataPtr->episodeFilter);
|
||||||
|
bool matched = matcher.hasMatch();
|
||||||
|
|
||||||
|
if (!matched)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
QString s = matcher.captured(1);
|
||||||
|
QStringList eps = matcher.captured(2).split(";");
|
||||||
|
int sOurs = s.toInt();
|
||||||
|
|
||||||
|
foreach (QString ep, eps) {
|
||||||
|
if (ep.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// We need to trim leading zeroes, but if it's all zeros then we want episode zero.
|
||||||
|
while (ep.size() > 1 && ep.startsWith("0"))
|
||||||
|
ep = ep.right(ep.size() - 1);
|
||||||
|
|
||||||
|
if (ep.indexOf('-') != -1) { // Range detected
|
||||||
|
QString partialPattern1 = "\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)";
|
||||||
|
QString partialPattern2 = "\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)";
|
||||||
|
QRegularExpression reg(cachedRegex(partialPattern1));
|
||||||
|
|
||||||
|
if (ep.endsWith('-')) { // Infinite range
|
||||||
|
int epOurs = ep.left(ep.size() - 1).toInt();
|
||||||
|
|
||||||
|
// Extract partial match from article and compare as digits
|
||||||
|
matcher = reg.match(articleTitle);
|
||||||
|
matched = matcher.hasMatch();
|
||||||
|
|
||||||
|
if (!matched) {
|
||||||
|
reg = QRegularExpression(cachedRegex(partialPattern2));
|
||||||
|
matcher = reg.match(articleTitle);
|
||||||
|
matched = matcher.hasMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
int sTheirs = matcher.captured(1).toInt();
|
||||||
|
int epTheirs = matcher.captured(2).toInt();
|
||||||
|
if (((sTheirs == sOurs) && (epTheirs >= epOurs)) || (sTheirs > sOurs)) {
|
||||||
|
// qDebug() << "Matched episode:" << ep;
|
||||||
|
// qDebug() << "Matched article:" << articleTitle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // Normal range
|
||||||
|
QStringList range = ep.split('-');
|
||||||
|
Q_ASSERT(range.size() == 2);
|
||||||
|
if (range.first().toInt() > range.last().toInt())
|
||||||
|
continue; // Ignore this subrule completely
|
||||||
|
|
||||||
|
int epOursFirst = range.first().toInt();
|
||||||
|
int epOursLast = range.last().toInt();
|
||||||
|
|
||||||
|
// Extract partial match from article and compare as digits
|
||||||
|
matcher = reg.match(articleTitle);
|
||||||
|
matched = matcher.hasMatch();
|
||||||
|
|
||||||
|
if (!matched) {
|
||||||
|
reg = QRegularExpression(cachedRegex(partialPattern2));
|
||||||
|
matcher = reg.match(articleTitle);
|
||||||
|
matched = matcher.hasMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
int sTheirs = matcher.captured(1).toInt();
|
||||||
|
int epTheirs = matcher.captured(2).toInt();
|
||||||
|
if ((sTheirs == sOurs) && ((epOursFirst <= epTheirs) && (epOursLast >= epTheirs))) {
|
||||||
|
// qDebug() << "Matched episode:" << ep;
|
||||||
|
// qDebug() << "Matched article:" << articleTitle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // Single number
|
||||||
|
QString expStr("\\b(?:s0?" + s + "[ -_\\.]?" + "e0?" + ep + "|" + s + "x" + "0?" + ep + ")(?:\\D|\\b)");
|
||||||
|
QRegularExpression reg(cachedRegex(expStr));
|
||||||
|
if (reg.match(articleTitle).hasMatch()) {
|
||||||
|
// qDebug() << "Matched episode:" << ep;
|
||||||
|
// qDebug() << "Matched article:" << articleTitle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// qDebug() << "Matched article:" << articleTitle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoDownloadRule &AutoDownloadRule::operator=(const AutoDownloadRule &other)
|
||||||
|
{
|
||||||
|
m_dataPtr = other.m_dataPtr;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloadRule::operator==(const AutoDownloadRule &other) const
|
||||||
|
{
|
||||||
|
return (m_dataPtr == other.m_dataPtr) // optimization
|
||||||
|
|| (*m_dataPtr == *other.m_dataPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloadRule::operator!=(const AutoDownloadRule &other) const
|
||||||
|
{
|
||||||
|
return !operator==(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject AutoDownloadRule::toJsonObject() const
|
||||||
|
{
|
||||||
|
return {{Str_Enabled, isEnabled()}
|
||||||
|
, {Str_UseRegex, useRegex()}
|
||||||
|
, {Str_MustContain, mustContain()}
|
||||||
|
, {Str_MustNotContain, mustNotContain()}
|
||||||
|
, {Str_EpisodeFilter, episodeFilter()}
|
||||||
|
, {Str_AffectedFeeds, QJsonArray::fromStringList(feedURLs())}
|
||||||
|
, {Str_SavePath, savePath()}
|
||||||
|
, {Str_AssignedCategory, assignedCategory()}
|
||||||
|
, {Str_LastMatch, lastMatch().toString(Qt::RFC2822Date)}
|
||||||
|
, {Str_IgnoreDays, ignoreDays()}
|
||||||
|
, {Str_AddPaused, triStateBoolToJsonValue(addPaused())}};
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, const QString &name)
|
||||||
|
{
|
||||||
|
AutoDownloadRule rule(name.isEmpty() ? jsonObj.value(Str_Name).toString() : name);
|
||||||
|
|
||||||
|
rule.setUseRegex(jsonObj.value(Str_UseRegex).toBool(false));
|
||||||
|
rule.setMustContain(jsonObj.value(Str_MustContain).toString());
|
||||||
|
rule.setMustNotContain(jsonObj.value(Str_MustNotContain).toString());
|
||||||
|
rule.setEpisodeFilter(jsonObj.value(Str_EpisodeFilter).toString());
|
||||||
|
rule.setEnabled(jsonObj.value(Str_Enabled).toBool(true));
|
||||||
|
rule.setSavePath(jsonObj.value(Str_SavePath).toString());
|
||||||
|
rule.setCategory(jsonObj.value(Str_AssignedCategory).toString());
|
||||||
|
rule.setAddPaused(jsonValueToTriStateBool(jsonObj.value(Str_AddPaused)));
|
||||||
|
rule.setLastMatch(QDateTime::fromString(jsonObj.value(Str_LastMatch).toString(), Qt::RFC2822Date));
|
||||||
|
rule.setIgnoreDays(jsonObj.value(Str_IgnoreDays).toInt());
|
||||||
|
|
||||||
|
const QJsonValue feedsVal = jsonObj.value(Str_AffectedFeeds);
|
||||||
|
QStringList feedURLs;
|
||||||
|
if (feedsVal.isString())
|
||||||
|
feedURLs << feedsVal.toString();
|
||||||
|
else foreach (const QJsonValue &urlVal, feedsVal.toArray())
|
||||||
|
feedURLs << urlVal.toString();
|
||||||
|
rule.setFeedURLs(feedURLs);
|
||||||
|
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoDownloadRule AutoDownloadRule::fromVariantHash(const QVariantHash &varHash)
|
||||||
|
{
|
||||||
|
AutoDownloadRule rule(varHash.value("name").toString());
|
||||||
|
|
||||||
|
rule.setUseRegex(varHash.value("use_regex", false).toBool());
|
||||||
|
rule.setMustContain(varHash.value("must_contain").toString());
|
||||||
|
rule.setMustNotContain(varHash.value("must_not_contain").toString());
|
||||||
|
rule.setEpisodeFilter(varHash.value("episode_filter").toString());
|
||||||
|
rule.setFeedURLs(varHash.value("affected_feeds").toStringList());
|
||||||
|
rule.setEnabled(varHash.value("enabled", false).toBool());
|
||||||
|
rule.setSavePath(varHash.value("save_path").toString());
|
||||||
|
rule.setCategory(varHash.value("category_assigned").toString());
|
||||||
|
rule.setAddPaused(TriStateBool(varHash.value("add_paused").toInt() - 1));
|
||||||
|
rule.setLastMatch(varHash.value("last_match").toDateTime());
|
||||||
|
rule.setIgnoreDays(varHash.value("ignore_days").toInt());
|
||||||
|
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setMustContain(const QString &tokens)
|
||||||
|
{
|
||||||
|
m_dataPtr->cachedRegexes.clear();
|
||||||
|
|
||||||
|
if (m_dataPtr->useRegex)
|
||||||
|
m_dataPtr->mustContain = QStringList() << tokens;
|
||||||
|
else
|
||||||
|
m_dataPtr->mustContain = tokens.split("|");
|
||||||
|
|
||||||
|
// Check for single empty string - if so, no condition
|
||||||
|
if ((m_dataPtr->mustContain.size() == 1) && m_dataPtr->mustContain[0].isEmpty())
|
||||||
|
m_dataPtr->mustContain.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setMustNotContain(const QString &tokens)
|
||||||
|
{
|
||||||
|
m_dataPtr->cachedRegexes.clear();
|
||||||
|
|
||||||
|
if (m_dataPtr->useRegex)
|
||||||
|
m_dataPtr->mustNotContain = QStringList() << tokens;
|
||||||
|
else
|
||||||
|
m_dataPtr->mustNotContain = tokens.split("|");
|
||||||
|
|
||||||
|
// Check for single empty string - if so, no condition
|
||||||
|
if ((m_dataPtr->mustNotContain.size() == 1) && m_dataPtr->mustNotContain[0].isEmpty())
|
||||||
|
m_dataPtr->mustNotContain.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList AutoDownloadRule::feedURLs() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->feedURLs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setFeedURLs(const QStringList &rssFeeds)
|
||||||
|
{
|
||||||
|
m_dataPtr->feedURLs = rssFeeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AutoDownloadRule::name() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setName(const QString &name)
|
||||||
|
{
|
||||||
|
m_dataPtr->name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AutoDownloadRule::savePath() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->savePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setSavePath(const QString &savePath)
|
||||||
|
{
|
||||||
|
m_dataPtr->savePath = Utils::Fs::fromNativePath(savePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
TriStateBool AutoDownloadRule::addPaused() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->addPaused;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setAddPaused(const TriStateBool &addPaused)
|
||||||
|
{
|
||||||
|
m_dataPtr->addPaused = addPaused;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AutoDownloadRule::assignedCategory() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->category;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setCategory(const QString &category)
|
||||||
|
{
|
||||||
|
m_dataPtr->category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloadRule::isEnabled() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setEnabled(bool enable)
|
||||||
|
{
|
||||||
|
m_dataPtr->enabled = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime AutoDownloadRule::lastMatch() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->lastMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setLastMatch(const QDateTime &lastMatch)
|
||||||
|
{
|
||||||
|
m_dataPtr->lastMatch = lastMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setIgnoreDays(int d)
|
||||||
|
{
|
||||||
|
m_dataPtr->ignoreDays = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AutoDownloadRule::ignoreDays() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->ignoreDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AutoDownloadRule::mustContain() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->mustContain.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AutoDownloadRule::mustNotContain() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->mustNotContain.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AutoDownloadRule::useRegex() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->useRegex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setUseRegex(bool enabled)
|
||||||
|
{
|
||||||
|
m_dataPtr->useRegex = enabled;
|
||||||
|
m_dataPtr->cachedRegexes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AutoDownloadRule::episodeFilter() const
|
||||||
|
{
|
||||||
|
return m_dataPtr->episodeFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoDownloadRule::setEpisodeFilter(const QString &e)
|
||||||
|
{
|
||||||
|
m_dataPtr->episodeFilter = e;
|
||||||
|
m_dataPtr->cachedRegexes.clear();
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2010 Christophe Dumez
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -24,91 +25,71 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact : chris@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef RSSDOWNLOADRULE_H
|
#pragma once
|
||||||
#define RSSDOWNLOADRULE_H
|
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QVariantHash>
|
#include <QSharedDataPointer>
|
||||||
#include <QSharedPointer>
|
#include <QVariant>
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
template <class T, class U> class QHash;
|
class QJsonObject;
|
||||||
class QRegularExpression;
|
class QRegularExpression;
|
||||||
|
class TriStateBool;
|
||||||
|
|
||||||
namespace Rss
|
namespace RSS
|
||||||
{
|
{
|
||||||
class Feed;
|
struct AutoDownloadRuleData;
|
||||||
typedef QSharedPointer<Feed> FeedPtr;
|
|
||||||
|
|
||||||
class DownloadRule;
|
class AutoDownloadRule
|
||||||
typedef QSharedPointer<DownloadRule> DownloadRulePtr;
|
|
||||||
|
|
||||||
class DownloadRule
|
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum AddPausedState
|
explicit AutoDownloadRule(const QString &name = "");
|
||||||
{
|
AutoDownloadRule(const AutoDownloadRule &other);
|
||||||
USE_GLOBAL = 0,
|
~AutoDownloadRule();
|
||||||
ALWAYS_PAUSED,
|
|
||||||
NEVER_PAUSED
|
|
||||||
};
|
|
||||||
|
|
||||||
DownloadRule();
|
|
||||||
~DownloadRule();
|
|
||||||
|
|
||||||
static DownloadRulePtr fromVariantHash(const QVariantHash &ruleHash);
|
|
||||||
QVariantHash toVariantHash() const;
|
|
||||||
bool matches(const QString &articleTitle) const;
|
|
||||||
void setMustContain(const QString &tokens);
|
|
||||||
void setMustNotContain(const QString &tokens);
|
|
||||||
QStringList rssFeeds() const;
|
|
||||||
void setRssFeeds(const QStringList &rssFeeds);
|
|
||||||
QString name() const;
|
QString name() const;
|
||||||
void setName(const QString &name);
|
void setName(const QString &name);
|
||||||
QString savePath() const;
|
|
||||||
void setSavePath(const QString &savePath);
|
|
||||||
AddPausedState addPaused() const;
|
|
||||||
void setAddPaused(const AddPausedState &aps);
|
|
||||||
QString category() const;
|
|
||||||
void setCategory(const QString &category);
|
|
||||||
bool isEnabled() const;
|
bool isEnabled() const;
|
||||||
void setEnabled(bool enable);
|
void setEnabled(bool enable);
|
||||||
void setLastMatch(const QDateTime &d);
|
|
||||||
QDateTime lastMatch() const;
|
|
||||||
void setIgnoreDays(int d);
|
|
||||||
int ignoreDays() const;
|
|
||||||
QString mustContain() const;
|
QString mustContain() const;
|
||||||
|
void setMustContain(const QString &tokens);
|
||||||
QString mustNotContain() const;
|
QString mustNotContain() const;
|
||||||
|
void setMustNotContain(const QString &tokens);
|
||||||
|
QStringList feedURLs() const;
|
||||||
|
void setFeedURLs(const QStringList &feedURLs);
|
||||||
|
int ignoreDays() const;
|
||||||
|
void setIgnoreDays(int d);
|
||||||
|
QDateTime lastMatch() const;
|
||||||
|
void setLastMatch(const QDateTime &lastMatch);
|
||||||
bool useRegex() const;
|
bool useRegex() const;
|
||||||
void setUseRegex(bool enabled);
|
void setUseRegex(bool enabled);
|
||||||
QString episodeFilter() const;
|
QString episodeFilter() const;
|
||||||
void setEpisodeFilter(const QString &e);
|
void setEpisodeFilter(const QString &e);
|
||||||
QStringList findMatchingArticles(const FeedPtr &feed) const;
|
|
||||||
// Operators
|
QString savePath() const;
|
||||||
bool operator==(const DownloadRule &other) const;
|
void setSavePath(const QString &savePath);
|
||||||
|
TriStateBool addPaused() const;
|
||||||
|
void setAddPaused(const TriStateBool &addPaused);
|
||||||
|
QString assignedCategory() const;
|
||||||
|
void setCategory(const QString &assignedCategory);
|
||||||
|
|
||||||
|
bool matches(const QString &articleTitle) const;
|
||||||
|
|
||||||
|
AutoDownloadRule &operator=(const AutoDownloadRule &other);
|
||||||
|
bool operator==(const AutoDownloadRule &other) const;
|
||||||
|
bool operator!=(const AutoDownloadRule &other) const;
|
||||||
|
|
||||||
|
QJsonObject toJsonObject() const;
|
||||||
|
static AutoDownloadRule fromJsonObject(const QJsonObject &jsonObj, const QString &name = "");
|
||||||
|
static AutoDownloadRule fromVariantHash(const QVariantHash &varHash);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool matches(const QString &articleTitle, const QString &expression) const;
|
bool matches(const QString &articleTitle, const QString &expression) const;
|
||||||
QRegularExpression cachedRegex(const QString &expression, bool isRegex = true) const;
|
QRegularExpression cachedRegex(const QString &expression, bool isRegex = true) const;
|
||||||
|
|
||||||
QString m_name;
|
QSharedDataPointer<AutoDownloadRuleData> m_dataPtr;
|
||||||
QStringList m_mustContain;
|
|
||||||
QStringList m_mustNotContain;
|
|
||||||
QString m_episodeFilter;
|
|
||||||
QString m_savePath;
|
|
||||||
QString m_category;
|
|
||||||
bool m_enabled;
|
|
||||||
QStringList m_rssFeeds;
|
|
||||||
bool m_useRegex;
|
|
||||||
AddPausedState m_apstate;
|
|
||||||
QDateTime m_lastMatch;
|
|
||||||
int m_ignoreDays;
|
|
||||||
mutable QHash<QString, QRegularExpression> *m_cachedRegexes;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // RSSDOWNLOADRULE_H
|
|
437
src/base/rss/rss_feed.cpp
Normal file
437
src/base/rss/rss_feed.cpp
Normal file
|
@ -0,0 +1,437 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2015, 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rss_feed.h"
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "../asyncfilestorage.h"
|
||||||
|
#include "../logger.h"
|
||||||
|
#include "../net/downloadhandler.h"
|
||||||
|
#include "../net/downloadmanager.h"
|
||||||
|
#include "../profile.h"
|
||||||
|
#include "../utils/fs.h"
|
||||||
|
#include "private/rss_parser.h"
|
||||||
|
#include "rss_article.h"
|
||||||
|
#include "rss_session.h"
|
||||||
|
|
||||||
|
const QString Str_Url(QStringLiteral("url"));
|
||||||
|
const QString Str_Title(QStringLiteral("title"));
|
||||||
|
const QString Str_LastBuildDate(QStringLiteral("lastBuildDate"));
|
||||||
|
const QString Str_IsLoading(QStringLiteral("isLoading"));
|
||||||
|
const QString Str_HasError(QStringLiteral("hasError"));
|
||||||
|
const QString Str_Articles(QStringLiteral("articles"));
|
||||||
|
|
||||||
|
using namespace RSS;
|
||||||
|
|
||||||
|
Feed::Feed(const QString &url, const QString &path, Session *session)
|
||||||
|
: Item(path)
|
||||||
|
, m_session(session)
|
||||||
|
, m_url(url)
|
||||||
|
{
|
||||||
|
m_dataFileName = QString("%1.json").arg(Utils::Fs::toValidFileSystemName(m_url, false, QLatin1String("_")));
|
||||||
|
|
||||||
|
m_parser = new Private::Parser(m_lastBuildDate);
|
||||||
|
m_parser->moveToThread(m_session->workingThread());
|
||||||
|
connect(this, &Feed::destroyed, m_parser, &Private::Parser::deleteLater);
|
||||||
|
connect(m_parser, &Private::Parser::finished, this, &Feed::handleParsingFinished);
|
||||||
|
|
||||||
|
connect(m_session, &Session::maxArticlesPerFeedChanged, this, &Feed::handleMaxArticlesPerFeedChanged);
|
||||||
|
|
||||||
|
if (m_session->isProcessingEnabled())
|
||||||
|
downloadIcon();
|
||||||
|
else
|
||||||
|
connect(m_session, &Session::processingStateChanged, this, &Feed::handleSessionProcessingEnabledChanged);
|
||||||
|
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Feed::~Feed()
|
||||||
|
{
|
||||||
|
emit aboutToBeDestroyed(this);
|
||||||
|
Utils::Fs::forceRemove(m_iconPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Article *> Feed::articles() const
|
||||||
|
{
|
||||||
|
return m_articlesByDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::markAsRead()
|
||||||
|
{
|
||||||
|
auto oldUnreadCount = m_unreadCount;
|
||||||
|
foreach (Article *article, m_articles) {
|
||||||
|
if (!article->isRead()) {
|
||||||
|
article->disconnect(this);
|
||||||
|
article->markAsRead();
|
||||||
|
--m_unreadCount;
|
||||||
|
emit articleRead(article);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_unreadCount != oldUnreadCount) {
|
||||||
|
m_dirty = true;
|
||||||
|
store();
|
||||||
|
emit unreadCountChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::refresh()
|
||||||
|
{
|
||||||
|
if (isLoading()) return;
|
||||||
|
|
||||||
|
// NOTE: Should we allow manually refreshing for disabled session?
|
||||||
|
|
||||||
|
Net::DownloadHandler *handler = Net::DownloadManager::instance()->downloadUrl(m_url);
|
||||||
|
connect(handler
|
||||||
|
, static_cast<void (Net::DownloadHandler::*)(const QString &, const QByteArray &)>(&Net::DownloadHandler::downloadFinished)
|
||||||
|
, this, &Feed::handleDownloadFinished);
|
||||||
|
connect(handler, &Net::DownloadHandler::downloadFailed, this, &Feed::handleDownloadFailed);
|
||||||
|
|
||||||
|
m_isLoading = true;
|
||||||
|
emit stateChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Feed::url() const
|
||||||
|
{
|
||||||
|
return m_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Feed::title() const
|
||||||
|
{
|
||||||
|
return m_title;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Feed::isLoading() const
|
||||||
|
{
|
||||||
|
return m_isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Feed::lastBuildDate() const
|
||||||
|
{
|
||||||
|
return m_lastBuildDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Feed::unreadCount() const
|
||||||
|
{
|
||||||
|
return m_unreadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Article *Feed::articleByGUID(const QString &guid) const
|
||||||
|
{
|
||||||
|
return m_articles.value(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::handleMaxArticlesPerFeedChanged(int n)
|
||||||
|
{
|
||||||
|
while (m_articlesByDate.size() > n)
|
||||||
|
removeOldestArticle();
|
||||||
|
// We don't need store articles here
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::handleIconDownloadFinished(const QString &url, const QString &filePath)
|
||||||
|
{
|
||||||
|
Q_UNUSED(url);
|
||||||
|
|
||||||
|
m_iconPath = Utils::Fs::fromNativePath(filePath);
|
||||||
|
emit iconLoaded(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Feed::hasError() const
|
||||||
|
{
|
||||||
|
return m_hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::handleDownloadFinished(const QString &url, const QByteArray &data)
|
||||||
|
{
|
||||||
|
qDebug() << "Successfully downloaded RSS feed at" << url;
|
||||||
|
// Parse the download RSS
|
||||||
|
m_parser->parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::handleDownloadFailed(const QString &url, const QString &error)
|
||||||
|
{
|
||||||
|
m_isLoading = false;
|
||||||
|
m_hasError = true;
|
||||||
|
emit stateChanged(this);
|
||||||
|
qWarning() << "Failed to download RSS feed at" << url;
|
||||||
|
qWarning() << "Reason:" << error;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::handleParsingFinished(const RSS::Private::ParsingResult &result)
|
||||||
|
{
|
||||||
|
if (!result.error.isEmpty()) {
|
||||||
|
qWarning() << "Failed to parse RSS feed at" << m_url;
|
||||||
|
qWarning() << "Reason:" << result.error;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (title() != result.title) {
|
||||||
|
m_title = result.title;
|
||||||
|
emit titleChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lastBuildDate = result.lastBuildDate;
|
||||||
|
|
||||||
|
foreach (const QVariantHash &varHash, result.articles) {
|
||||||
|
auto article = Article::fromVariantHash(this, varHash);
|
||||||
|
if (article) {
|
||||||
|
if (!addArticle(article))
|
||||||
|
delete article;
|
||||||
|
else
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_isLoading = false;
|
||||||
|
m_hasError = false;
|
||||||
|
emit stateChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::load()
|
||||||
|
{
|
||||||
|
QFile file(m_session->dataFileStorage()->storageDir().absoluteFilePath(m_dataFileName));
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
loadArticlesLegacy();
|
||||||
|
m_dirty = true;
|
||||||
|
store(); // convert to new format
|
||||||
|
}
|
||||||
|
else if (file.open(QFile::ReadOnly)) {
|
||||||
|
loadArticles(file.readAll());
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't read RSS AutoDownloader rules from %1. Error: %2")
|
||||||
|
.arg(m_dataFileName).arg(file.errorString()), Log::WARNING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::loadArticles(const QByteArray &data)
|
||||||
|
{
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
|
||||||
|
if (jsonError.error != QJsonParseError::NoError) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't parse RSS Session data. Error: %1")
|
||||||
|
.arg(jsonError.errorString()), Log::WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jsonDoc.isArray()) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't load RSS Session data. Invalid data format."), Log::WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray jsonArr = jsonDoc.array();
|
||||||
|
int i = -1;
|
||||||
|
foreach (const QJsonValue &jsonVal, jsonArr) {
|
||||||
|
++i;
|
||||||
|
if (!jsonVal.isObject()) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't load RSS article '%1#%2'. Invalid data format.").arg(m_url).arg(i)
|
||||||
|
, Log::WARNING);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto article = Article::fromJsonObject(this, jsonVal.toObject());
|
||||||
|
if (article && !addArticle(article))
|
||||||
|
delete article;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::loadArticlesLegacy()
|
||||||
|
{
|
||||||
|
SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QStringLiteral("qBittorrent-rss-feeds"));
|
||||||
|
QVariantHash allOldItems = qBTRSSFeeds->value("old_items").toHash();
|
||||||
|
|
||||||
|
foreach (const QVariant &var, allOldItems.value(m_url).toList()) {
|
||||||
|
auto article = Article::fromVariantHash(this, var.toHash());
|
||||||
|
if (article && !addArticle(article))
|
||||||
|
delete article;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::store()
|
||||||
|
{
|
||||||
|
if (!m_dirty) return;
|
||||||
|
|
||||||
|
m_dirty = false;
|
||||||
|
m_savingTimer.stop();
|
||||||
|
|
||||||
|
QJsonArray jsonArr;
|
||||||
|
foreach (Article *article, m_articles)
|
||||||
|
jsonArr << article->toJsonObject();
|
||||||
|
|
||||||
|
m_session->dataFileStorage()->store(m_dataFileName, QJsonDocument(jsonArr).toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::storeDeferred()
|
||||||
|
{
|
||||||
|
if (!m_savingTimer.isActive())
|
||||||
|
m_savingTimer.start(5 * 1000, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Feed::addArticle(Article *article)
|
||||||
|
{
|
||||||
|
Q_ASSERT(article);
|
||||||
|
|
||||||
|
if (m_articles.contains(article->guid()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Insertion sort
|
||||||
|
const int maxArticles = m_session->maxArticlesPerFeed();
|
||||||
|
auto lowerBound = std::lower_bound(m_articlesByDate.begin(), m_articlesByDate.end()
|
||||||
|
, article->date(), Article::articleDateRecentThan);
|
||||||
|
if ((lowerBound - m_articlesByDate.begin()) >= maxArticles)
|
||||||
|
return false; // we reach max articles
|
||||||
|
|
||||||
|
m_articles[article->guid()] = article;
|
||||||
|
m_articlesByDate.insert(lowerBound, article);
|
||||||
|
if (!article->isRead()) {
|
||||||
|
increaseUnreadCount();
|
||||||
|
connect(article, &Article::read, this, &Feed::handleArticleRead);
|
||||||
|
}
|
||||||
|
emit newArticle(article);
|
||||||
|
|
||||||
|
if (m_articlesByDate.size() > maxArticles)
|
||||||
|
removeOldestArticle();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::removeOldestArticle()
|
||||||
|
{
|
||||||
|
auto oldestArticle = m_articlesByDate.takeLast();
|
||||||
|
m_articles.remove(oldestArticle->guid());
|
||||||
|
emit articleAboutToBeRemoved(oldestArticle);
|
||||||
|
bool isRead = oldestArticle->isRead();
|
||||||
|
delete oldestArticle;
|
||||||
|
|
||||||
|
if (!isRead)
|
||||||
|
decreaseUnreadCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::increaseUnreadCount()
|
||||||
|
{
|
||||||
|
++m_unreadCount;
|
||||||
|
emit unreadCountChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::decreaseUnreadCount()
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_unreadCount > 0);
|
||||||
|
|
||||||
|
--m_unreadCount;
|
||||||
|
emit unreadCountChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::downloadIcon()
|
||||||
|
{
|
||||||
|
// Download the RSS Feed icon
|
||||||
|
// XXX: This works for most sites but it is not perfect
|
||||||
|
const QUrl url(m_url);
|
||||||
|
auto iconUrl = QString("%1://%2/favicon.ico").arg(url.scheme()).arg(url.host());
|
||||||
|
Net::DownloadHandler *handler = Net::DownloadManager::instance()->downloadUrl(iconUrl, true);
|
||||||
|
connect(handler
|
||||||
|
, static_cast<void (Net::DownloadHandler::*)(const QString &, const QString &)>(&Net::DownloadHandler::downloadFinished)
|
||||||
|
, this, &Feed::handleIconDownloadFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Feed::iconPath() const
|
||||||
|
{
|
||||||
|
return m_iconPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonValue Feed::toJsonValue(bool withData) const
|
||||||
|
{
|
||||||
|
if (!withData) {
|
||||||
|
// if feed alias is empty we create "reduced" JSON
|
||||||
|
// value for it since its name is equal to its URL
|
||||||
|
return (name() == url() ? "" : url());
|
||||||
|
// if we'll need storing some more properties we should check
|
||||||
|
// for its default values and produce JSON object instead of (if it's required)
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray jsonArr;
|
||||||
|
foreach (Article *article, m_articles)
|
||||||
|
jsonArr << article->toJsonObject();
|
||||||
|
|
||||||
|
QJsonObject jsonObj;
|
||||||
|
jsonObj.insert(Str_Url, url());
|
||||||
|
jsonObj.insert(Str_Title, title());
|
||||||
|
jsonObj.insert(Str_LastBuildDate, lastBuildDate());
|
||||||
|
jsonObj.insert(Str_IsLoading, isLoading());
|
||||||
|
jsonObj.insert(Str_HasError, hasError());
|
||||||
|
jsonObj.insert(Str_Articles, jsonArr);
|
||||||
|
|
||||||
|
return jsonObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::handleSessionProcessingEnabledChanged(bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled) {
|
||||||
|
downloadIcon();
|
||||||
|
disconnect(m_session, &Session::processingStateChanged
|
||||||
|
, this, &Feed::handleSessionProcessingEnabledChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::handleArticleRead(Article *article)
|
||||||
|
{
|
||||||
|
article->disconnect(this);
|
||||||
|
decreaseUnreadCount();
|
||||||
|
emit articleRead(article);
|
||||||
|
// will be stored deferred
|
||||||
|
m_dirty = true;
|
||||||
|
storeDeferred();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::cleanup()
|
||||||
|
{
|
||||||
|
Utils::Fs::forceRemove(m_session->dataFileStorage()->storageDir().absoluteFilePath(m_dataFileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Feed::timerEvent(QTimerEvent *event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(event);
|
||||||
|
store();
|
||||||
|
}
|
121
src/base/rss/rss_feed.h
Normal file
121
src/base/rss/rss_feed.h
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2015, 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QBasicTimer>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
|
#include "rss_item.h"
|
||||||
|
|
||||||
|
class AsyncFileStorage;
|
||||||
|
|
||||||
|
namespace RSS
|
||||||
|
{
|
||||||
|
class Article;
|
||||||
|
class Session;
|
||||||
|
|
||||||
|
namespace Private
|
||||||
|
{
|
||||||
|
class Parser;
|
||||||
|
struct ParsingResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Feed final: public Item
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(Feed)
|
||||||
|
|
||||||
|
friend class Session;
|
||||||
|
|
||||||
|
Feed(const QString &url, const QString &path, Session *session);
|
||||||
|
~Feed() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
QList<Article *> articles() const override;
|
||||||
|
int unreadCount() const override;
|
||||||
|
void markAsRead() override;
|
||||||
|
void refresh() override;
|
||||||
|
|
||||||
|
QString url() const;
|
||||||
|
QString title() const;
|
||||||
|
QString lastBuildDate() const;
|
||||||
|
bool hasError() const;
|
||||||
|
bool isLoading() const;
|
||||||
|
Article *articleByGUID(const QString &guid) const;
|
||||||
|
QString iconPath() const;
|
||||||
|
|
||||||
|
QJsonValue toJsonValue(bool withData = false) const override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void iconLoaded(Feed *feed = nullptr);
|
||||||
|
void titleChanged(Feed *feed = nullptr);
|
||||||
|
void stateChanged(Feed *feed = nullptr);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleSessionProcessingEnabledChanged(bool enabled);
|
||||||
|
void handleMaxArticlesPerFeedChanged(int n);
|
||||||
|
void handleIconDownloadFinished(const QString &url, const QString &filePath);
|
||||||
|
void handleDownloadFinished(const QString &url, const QByteArray &data);
|
||||||
|
void handleDownloadFailed(const QString &url, const QString &error);
|
||||||
|
void handleParsingFinished(const Private::ParsingResult &result);
|
||||||
|
void handleArticleRead(Article *article);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void timerEvent(QTimerEvent *event) override;
|
||||||
|
void cleanup() override;
|
||||||
|
void load();
|
||||||
|
void loadArticles(const QByteArray &data);
|
||||||
|
void loadArticlesLegacy();
|
||||||
|
void store();
|
||||||
|
void storeDeferred();
|
||||||
|
bool addArticle(Article *article);
|
||||||
|
void removeOldestArticle();
|
||||||
|
void increaseUnreadCount();
|
||||||
|
void decreaseUnreadCount();
|
||||||
|
void downloadIcon();
|
||||||
|
|
||||||
|
Session *m_session;
|
||||||
|
Private::Parser *m_parser;
|
||||||
|
const QString m_url;
|
||||||
|
QString m_title;
|
||||||
|
QString m_lastBuildDate;
|
||||||
|
bool m_hasError = false;
|
||||||
|
bool m_isLoading = false;
|
||||||
|
QHash<QString, Article *> m_articles;
|
||||||
|
QList<Article *> m_articlesByDate;
|
||||||
|
int m_unreadCount = 0;
|
||||||
|
QString m_iconPath;
|
||||||
|
QString m_dataFileName;
|
||||||
|
QBasicTimer m_savingTimer;
|
||||||
|
bool m_dirty = false;
|
||||||
|
};
|
||||||
|
}
|
140
src/base/rss/rss_folder.cpp
Normal file
140
src/base/rss/rss_folder.cpp
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rss_folder.h"
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
|
||||||
|
#include "rss_article.h"
|
||||||
|
|
||||||
|
using namespace RSS;
|
||||||
|
|
||||||
|
Folder::Folder(const QString &path)
|
||||||
|
: Item(path)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Folder::~Folder()
|
||||||
|
{
|
||||||
|
emit aboutToBeDestroyed(this);
|
||||||
|
|
||||||
|
foreach (auto item, items())
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Article *> Folder::articles() const
|
||||||
|
{
|
||||||
|
QList<Article *> news;
|
||||||
|
|
||||||
|
foreach (Item *item, items()) {
|
||||||
|
int n = news.size();
|
||||||
|
news << item->articles();
|
||||||
|
std::inplace_merge(news.begin(), news.begin() + n, news.end()
|
||||||
|
, [](Article *a1, Article *a2)
|
||||||
|
{
|
||||||
|
return Article::articleDateRecentThan(a1, a2->date());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return news;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Folder::unreadCount() const
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
foreach (Item *item, items())
|
||||||
|
count += item->unreadCount();
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Folder::markAsRead()
|
||||||
|
{
|
||||||
|
foreach (Item *item, items())
|
||||||
|
item->markAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Folder::refresh()
|
||||||
|
{
|
||||||
|
foreach (Item *item, items())
|
||||||
|
item->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Item *> Folder::items() const
|
||||||
|
{
|
||||||
|
return m_items;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonValue Folder::toJsonValue(bool withData) const
|
||||||
|
{
|
||||||
|
QJsonObject jsonObj;
|
||||||
|
foreach (Item *item, items())
|
||||||
|
jsonObj.insert(item->name(), item->toJsonValue(withData));
|
||||||
|
|
||||||
|
return jsonObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Folder::handleItemUnreadCountChanged()
|
||||||
|
{
|
||||||
|
emit unreadCountChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Folder::handleItemAboutToBeDestroyed(Item *item)
|
||||||
|
{
|
||||||
|
if (item->unreadCount() > 0)
|
||||||
|
emit unreadCountChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Folder::cleanup()
|
||||||
|
{
|
||||||
|
foreach (Item *item, items())
|
||||||
|
item->cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Folder::addItem(Item *item)
|
||||||
|
{
|
||||||
|
Q_ASSERT(item);
|
||||||
|
Q_ASSERT(!m_items.contains(item));
|
||||||
|
|
||||||
|
m_items.append(item);
|
||||||
|
connect(item, &Item::newArticle, this, &Item::newArticle);
|
||||||
|
connect(item, &Item::articleRead, this, &Item::articleRead);
|
||||||
|
connect(item, &Item::articleAboutToBeRemoved, this, &Item::articleAboutToBeRemoved);
|
||||||
|
connect(item, &Item::unreadCountChanged, this, &Folder::handleItemUnreadCountChanged);
|
||||||
|
connect(item, &Item::aboutToBeDestroyed, this, &Folder::handleItemAboutToBeDestroyed);
|
||||||
|
emit unreadCountChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Folder::removeItem(Item *item)
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_items.contains(item));
|
||||||
|
item->disconnect(this);
|
||||||
|
m_items.removeOne(item);
|
||||||
|
emit unreadCountChanged(this);
|
||||||
|
}
|
71
src/base/rss/rss_folder.h
Normal file
71
src/base/rss/rss_folder.h
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include "rss_item.h"
|
||||||
|
|
||||||
|
namespace RSS
|
||||||
|
{
|
||||||
|
class Session;
|
||||||
|
|
||||||
|
class Folder final: public Item
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(Folder)
|
||||||
|
|
||||||
|
friend class Session;
|
||||||
|
|
||||||
|
explicit Folder(const QString &path = "");
|
||||||
|
~Folder() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
QList<Article *> articles() const override;
|
||||||
|
int unreadCount() const override;
|
||||||
|
void markAsRead() override;
|
||||||
|
void refresh() override;
|
||||||
|
|
||||||
|
QList<Item *> items() const;
|
||||||
|
|
||||||
|
QJsonValue toJsonValue(bool withData = false) const override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleItemUnreadCountChanged();
|
||||||
|
void handleItemAboutToBeDestroyed(Item *item);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void cleanup() override;
|
||||||
|
void addItem(Item *item);
|
||||||
|
void removeItem(Item *item);
|
||||||
|
|
||||||
|
QList<Item *> m_items;
|
||||||
|
};
|
||||||
|
}
|
115
src/base/rss/rss_item.cpp
Normal file
115
src/base/rss/rss_item.cpp
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rss_item.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
using namespace RSS;
|
||||||
|
|
||||||
|
const QString Item::PathSeparator("\\");
|
||||||
|
|
||||||
|
Item::Item(const QString &path)
|
||||||
|
: m_path(path)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Item::~Item() {}
|
||||||
|
|
||||||
|
void Item::setPath(const QString &path)
|
||||||
|
{
|
||||||
|
if (path != m_path) {
|
||||||
|
m_path = path;
|
||||||
|
emit pathChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Item::path() const
|
||||||
|
{
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Item::name() const
|
||||||
|
{
|
||||||
|
return relativeName(path());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Item::isValidPath(const QString &path)
|
||||||
|
{
|
||||||
|
static const QRegularExpression re(
|
||||||
|
QString(R"(\A[^\%1]+(\%1[^\%1]+)*\z)").arg(Item::PathSeparator)
|
||||||
|
, QRegularExpression::DontCaptureOption | QRegularExpression::OptimizeOnFirstUsageOption);
|
||||||
|
|
||||||
|
if (path.isEmpty() || !re.match(path).hasMatch()) {
|
||||||
|
qDebug() << "Incorrect RSS Item path:" << path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Item::joinPath(const QString &path1, const QString &path2)
|
||||||
|
{
|
||||||
|
if (path1.isEmpty())
|
||||||
|
return path2;
|
||||||
|
else
|
||||||
|
return path1 + Item::PathSeparator + path2;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Item::expandPath(const QString &path)
|
||||||
|
{
|
||||||
|
QStringList result;
|
||||||
|
if (path.isEmpty()) return result;
|
||||||
|
// if (!isValidRSSFolderName(folder))
|
||||||
|
// return result;
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while ((index = path.indexOf(Item::PathSeparator, index)) >= 0) {
|
||||||
|
result << path.left(index);
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
result << path;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Item::parentPath(const QString &path)
|
||||||
|
{
|
||||||
|
int pos;
|
||||||
|
return ((pos = path.lastIndexOf(Item::PathSeparator)) >= 0 ? path.left(pos) : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Item::relativeName(const QString &path)
|
||||||
|
{
|
||||||
|
int pos;
|
||||||
|
return ((pos = path.lastIndexOf(Item::PathSeparator)) >= 0 ? path.right(path.size() - (pos + 1)) : path);
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
*
|
*
|
||||||
|
@ -25,58 +26,63 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef RSSFILE_H
|
#pragma once
|
||||||
#define RSSFILE_H
|
|
||||||
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QStringList>
|
#include <QObject>
|
||||||
#include <QSharedPointer>
|
|
||||||
|
|
||||||
namespace Rss
|
namespace RSS
|
||||||
{
|
{
|
||||||
class Folder;
|
|
||||||
class File;
|
|
||||||
class Article;
|
class Article;
|
||||||
|
class Folder;
|
||||||
|
class Session;
|
||||||
|
|
||||||
typedef QSharedPointer<File> FilePtr;
|
class Item: public QObject
|
||||||
typedef QSharedPointer<Article> ArticlePtr;
|
|
||||||
typedef QList<ArticlePtr> ArticleList;
|
|
||||||
typedef QList<FilePtr> FileList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parent interface for Rss::Folder and Rss::Feed.
|
|
||||||
*/
|
|
||||||
class File
|
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(Item)
|
||||||
|
|
||||||
|
friend class Folder;
|
||||||
|
friend class Session;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual ~File();
|
virtual QList<Article *> articles() const = 0;
|
||||||
|
virtual int unreadCount() const = 0;
|
||||||
virtual QString id() const = 0;
|
|
||||||
virtual QString displayName() const = 0;
|
|
||||||
virtual uint unreadCount() const = 0;
|
|
||||||
virtual QString iconPath() const = 0;
|
|
||||||
virtual ArticleList articleListByDateDesc() const = 0;
|
|
||||||
virtual ArticleList unreadArticleListByDateDesc() const = 0;
|
|
||||||
|
|
||||||
virtual void rename(const QString &newName) = 0;
|
|
||||||
virtual void markAsRead() = 0;
|
virtual void markAsRead() = 0;
|
||||||
virtual bool refresh() = 0;
|
virtual void refresh() = 0;
|
||||||
virtual void removeAllSettings() = 0;
|
|
||||||
virtual void saveItemsToDisk() = 0;
|
|
||||||
virtual void recheckRssItemsForDownload() = 0;
|
|
||||||
|
|
||||||
Folder *parentFolder() const;
|
QString path() const;
|
||||||
QStringList pathHierarchy() const;
|
QString name() const;
|
||||||
|
|
||||||
|
virtual QJsonValue toJsonValue(bool withData = false) const = 0;
|
||||||
|
|
||||||
|
static const QString PathSeparator;
|
||||||
|
|
||||||
|
static bool isValidPath(const QString &path);
|
||||||
|
static QString joinPath(const QString &path1, const QString &path2);
|
||||||
|
static QStringList expandPath(const QString &path);
|
||||||
|
static QString parentPath(const QString &path);
|
||||||
|
static QString relativeName(const QString &path);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void pathChanged(Item *item = nullptr);
|
||||||
|
void unreadCountChanged(Item *item = nullptr);
|
||||||
|
void aboutToBeDestroyed(Item *item = nullptr);
|
||||||
|
void newArticle(Article *article);
|
||||||
|
void articleRead(Article *article);
|
||||||
|
void articleAboutToBeRemoved(Article *article);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend class Folder;
|
explicit Item(const QString &path);
|
||||||
|
~Item() override;
|
||||||
|
|
||||||
Folder *m_parent = nullptr;
|
virtual void cleanup() = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setPath(const QString &path);
|
||||||
|
|
||||||
|
QString m_path;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // RSSFILE_H
|
|
485
src/base/rss/rss_session.cpp
Normal file
485
src/base/rss/rss_session.cpp
Normal file
|
@ -0,0 +1,485 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rss_session.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QString>
|
||||||
|
#include <QThread>
|
||||||
|
#include <QVariantHash>
|
||||||
|
|
||||||
|
#include "../asyncfilestorage.h"
|
||||||
|
#include "../logger.h"
|
||||||
|
#include "../profile.h"
|
||||||
|
#include "../settingsstorage.h"
|
||||||
|
#include "../utils/fs.h"
|
||||||
|
#include "rss_article.h"
|
||||||
|
#include "rss_feed.h"
|
||||||
|
#include "rss_item.h"
|
||||||
|
#include "rss_folder.h"
|
||||||
|
|
||||||
|
const int MsecsPerMin = 60000;
|
||||||
|
const QString ConfFolderName(QStringLiteral("rss"));
|
||||||
|
const QString DataFolderName(QStringLiteral("rss/articles"));
|
||||||
|
const QString FeedsFileName(QStringLiteral("feeds.json"));
|
||||||
|
|
||||||
|
const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/Session/EnableProcessing"));
|
||||||
|
const QString SettingsKey_RefreshInterval(QStringLiteral("RSS/Session/RefreshInterval"));
|
||||||
|
const QString SettingsKey_MaxArticlesPerFeed(QStringLiteral("RSS/Session/MaxArticlesPerFeed"));
|
||||||
|
|
||||||
|
using namespace RSS;
|
||||||
|
|
||||||
|
QPointer<Session> Session::m_instance = nullptr;
|
||||||
|
|
||||||
|
Session::Session()
|
||||||
|
: m_workingThread(new QThread(this))
|
||||||
|
, m_processingEnabled(SettingsStorage::instance()->loadValue(SettingsKey_ProcessingEnabled, false).toBool())
|
||||||
|
, m_refreshInterval(SettingsStorage::instance()->loadValue(SettingsKey_RefreshInterval, 30).toUInt())
|
||||||
|
, m_maxArticlesPerFeed(SettingsStorage::instance()->loadValue(SettingsKey_MaxArticlesPerFeed, 50).toInt())
|
||||||
|
{
|
||||||
|
Q_ASSERT(!m_instance); // only one instance is allowed
|
||||||
|
m_instance = this;
|
||||||
|
|
||||||
|
m_confFileStorage = new AsyncFileStorage(
|
||||||
|
Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Config) + ConfFolderName));
|
||||||
|
m_confFileStorage->moveToThread(m_workingThread);
|
||||||
|
connect(m_workingThread, &QThread::finished, m_confFileStorage, &AsyncFileStorage::deleteLater);
|
||||||
|
connect(m_confFileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
|
||||||
|
{
|
||||||
|
Logger::instance()->addMessage(QString("Couldn't save RSS Session configuration in %1. Error: %2")
|
||||||
|
.arg(fileName).arg(errorString), Log::WARNING);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_dataFileStorage = new AsyncFileStorage(
|
||||||
|
Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Data) + DataFolderName));
|
||||||
|
m_dataFileStorage->moveToThread(m_workingThread);
|
||||||
|
connect(m_workingThread, &QThread::finished, m_dataFileStorage, &AsyncFileStorage::deleteLater);
|
||||||
|
connect(m_dataFileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
|
||||||
|
{
|
||||||
|
Logger::instance()->addMessage(QString("Couldn't save RSS Session data in %1. Error: %2")
|
||||||
|
.arg(fileName).arg(errorString), Log::WARNING);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_itemsByPath.insert("", new Folder); // root folder
|
||||||
|
|
||||||
|
m_workingThread->start();
|
||||||
|
load();
|
||||||
|
|
||||||
|
connect(&m_refreshTimer, &QTimer::timeout, this, &Session::refresh);
|
||||||
|
if (m_processingEnabled) {
|
||||||
|
m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::~Session()
|
||||||
|
{
|
||||||
|
qDebug() << "Deleting RSS Session...";
|
||||||
|
|
||||||
|
m_workingThread->quit();
|
||||||
|
m_workingThread->wait();
|
||||||
|
|
||||||
|
//store();
|
||||||
|
delete m_itemsByPath[""]; // deleting root folder
|
||||||
|
|
||||||
|
qDebug() << "RSS Session deleted.";
|
||||||
|
}
|
||||||
|
|
||||||
|
Session *Session::instance()
|
||||||
|
{
|
||||||
|
return m_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::addFolder(const QString &path, QString *error)
|
||||||
|
{
|
||||||
|
Folder *destFolder = prepareItemDest(path, error);
|
||||||
|
if (!destFolder)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
addItem(new Folder(path), destFolder);
|
||||||
|
store();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::addFeed(const QString &url, const QString &path, QString *error)
|
||||||
|
{
|
||||||
|
if (m_feedsByURL.contains(url)) {
|
||||||
|
if (error)
|
||||||
|
*error = tr("RSS feed with given URL already exists: %1.").arg(url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Folder *destFolder = prepareItemDest(path, error);
|
||||||
|
if (!destFolder)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
addItem(new Feed(url, path, this), destFolder);
|
||||||
|
store();
|
||||||
|
if (m_processingEnabled)
|
||||||
|
feedByURL(url)->refresh();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::moveItem(const QString &itemPath, const QString &destPath, QString *error)
|
||||||
|
{
|
||||||
|
if (itemPath.isEmpty()) {
|
||||||
|
if (error)
|
||||||
|
*error = tr("Cannot move root folder.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto item = m_itemsByPath.value(itemPath);
|
||||||
|
if (!item) {
|
||||||
|
if (error)
|
||||||
|
*error = tr("Item doesn't exists: %1.").arg(itemPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return moveItem(item, destPath, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::moveItem(Item *item, const QString &destPath, QString *error)
|
||||||
|
{
|
||||||
|
Q_ASSERT(item);
|
||||||
|
Q_ASSERT(item != rootFolder());
|
||||||
|
|
||||||
|
Folder *destFolder = prepareItemDest(destPath, error);
|
||||||
|
if (!destFolder)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto srcFolder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
|
||||||
|
if (srcFolder != destFolder) {
|
||||||
|
srcFolder->removeItem(item);
|
||||||
|
destFolder->addItem(item);
|
||||||
|
}
|
||||||
|
m_itemsByPath.insert(destPath, m_itemsByPath.take(item->path()));
|
||||||
|
item->setPath(destPath);
|
||||||
|
store();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::removeItem(const QString &itemPath, QString *error)
|
||||||
|
{
|
||||||
|
if (itemPath.isEmpty()) {
|
||||||
|
if (error)
|
||||||
|
*error = tr("Cannot delete root folder.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto item = m_itemsByPath.value(itemPath);
|
||||||
|
if (!item) {
|
||||||
|
if (error)
|
||||||
|
*error = tr("Item doesn't exists: %1.").arg(itemPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit itemAboutToBeRemoved(item);
|
||||||
|
item->cleanup();
|
||||||
|
|
||||||
|
auto folder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
|
||||||
|
folder->removeItem(item);
|
||||||
|
delete item;
|
||||||
|
store();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Item *> Session::items() const
|
||||||
|
{
|
||||||
|
return m_itemsByPath.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
Item *Session::itemByPath(const QString &path) const
|
||||||
|
{
|
||||||
|
return m_itemsByPath.value(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::load()
|
||||||
|
{
|
||||||
|
QFile itemsFile(m_confFileStorage->storageDir().absoluteFilePath(FeedsFileName));
|
||||||
|
if (!itemsFile.exists()) {
|
||||||
|
loadLegacy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemsFile.open(QFile::ReadOnly)) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't read RSS Session data from %1. Error: %2")
|
||||||
|
.arg(itemsFile.fileName()).arg(itemsFile.errorString()), Log::WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(itemsFile.readAll(), &jsonError);
|
||||||
|
if (jsonError.error != QJsonParseError::NoError) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't parse RSS Session data from %1. Error: %2")
|
||||||
|
.arg(itemsFile.fileName()).arg(jsonError.errorString()), Log::WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jsonDoc.isObject()) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't load RSS Session data from %1. Invalid data format.")
|
||||||
|
.arg(itemsFile.fileName()), Log::WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFolder(jsonDoc.object(), rootFolder());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
|
||||||
|
{
|
||||||
|
foreach (const QString &key, jsonObj.keys()) {
|
||||||
|
QJsonValue val = jsonObj[key];
|
||||||
|
if (val.isString()) {
|
||||||
|
QString url = val.toString();
|
||||||
|
if (url.isEmpty())
|
||||||
|
url = key;
|
||||||
|
addFeedToFolder(url, key, folder);
|
||||||
|
}
|
||||||
|
else if (!val.isObject()) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't load RSS Item '%1'. Invalid data format.")
|
||||||
|
.arg(QString("%1\\%2").arg(folder->path()).arg(key)), Log::WARNING);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QJsonObject valObj = val.toObject();
|
||||||
|
if (valObj.contains("url")) {
|
||||||
|
if (!valObj["url"].isString()) {
|
||||||
|
Logger::instance()->addMessage(
|
||||||
|
QString("Couldn't load RSS Feed '%1'. URL is required.")
|
||||||
|
.arg(QString("%1\\%2").arg(folder->path()).arg(key)), Log::WARNING);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFeedToFolder(valObj["url"].toString(), key, folder);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
loadFolder(valObj, addSubfolder(key, folder));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::loadLegacy()
|
||||||
|
{
|
||||||
|
const QStringList legacyFeedPaths = SettingsStorage::instance()->loadValue("Rss/streamList").toStringList();
|
||||||
|
const QStringList feedAliases = SettingsStorage::instance()->loadValue("Rss/streamAlias").toStringList();
|
||||||
|
if (legacyFeedPaths.size() != feedAliases.size()) {
|
||||||
|
Logger::instance()->addMessage("Corrupted RSS list, not loading it.", Log::WARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint i = 0;
|
||||||
|
foreach (QString legacyPath, legacyFeedPaths) {
|
||||||
|
if (Item::PathSeparator == QString(legacyPath[0]))
|
||||||
|
legacyPath.remove(0, 1);
|
||||||
|
const QString parentFolderPath = Item::parentPath(legacyPath);
|
||||||
|
const QString feedUrl = Item::relativeName(legacyPath);
|
||||||
|
|
||||||
|
foreach (const QString &folderPath, Item::expandPath(parentFolderPath))
|
||||||
|
addFolder(folderPath);
|
||||||
|
|
||||||
|
const QString feedPath = feedAliases[i].isEmpty()
|
||||||
|
? legacyPath
|
||||||
|
: Item::joinPath(parentFolderPath, feedAliases[i]);
|
||||||
|
addFeed(feedUrl, feedPath);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
store(); // convert to new format
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::store()
|
||||||
|
{
|
||||||
|
m_confFileStorage->store(FeedsFileName, QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
Folder *Session::prepareItemDest(const QString &path, QString *error)
|
||||||
|
{
|
||||||
|
if (!Item::isValidPath(path)) {
|
||||||
|
if (error)
|
||||||
|
*error = tr("Incorrect RSS Item path: %1.").arg(path);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_itemsByPath.contains(path)) {
|
||||||
|
if (error)
|
||||||
|
*error = tr("RSS item with given path already exists: %1.").arg(path);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString destFolderPath = Item::parentPath(path);
|
||||||
|
auto destFolder = qobject_cast<Folder *>(m_itemsByPath.value(destFolderPath));
|
||||||
|
if (!destFolder) {
|
||||||
|
if (error)
|
||||||
|
*error = tr("Parent folder doesn't exist: %1.").arg(destFolderPath);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return destFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
Folder *Session::addSubfolder(const QString &name, Folder *parentFolder)
|
||||||
|
{
|
||||||
|
auto folder = new Folder(Item::joinPath(parentFolder->path(), name));
|
||||||
|
addItem(folder, parentFolder);
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
Feed *Session::addFeedToFolder(const QString &url, const QString &name, Folder *parentFolder)
|
||||||
|
{
|
||||||
|
auto feed = new Feed(url, Item::joinPath(parentFolder->path(), name), this);
|
||||||
|
addItem(feed, parentFolder);
|
||||||
|
return feed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::addItem(Item *item, Folder *destFolder)
|
||||||
|
{
|
||||||
|
if (auto feed = qobject_cast<Feed *>(item)) {
|
||||||
|
connect(feed, &Feed::titleChanged, this, &Session::handleFeedTitleChanged);
|
||||||
|
connect(feed, &Feed::iconLoaded, this, &Session::feedIconLoaded);
|
||||||
|
connect(feed, &Feed::stateChanged, this, &Session::feedStateChanged);
|
||||||
|
m_feedsByURL[feed->url()] = feed;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(item, &Item::pathChanged, this, &Session::itemPathChanged);
|
||||||
|
connect(item, &Item::aboutToBeDestroyed, this, &Session::handleItemAboutToBeDestroyed);
|
||||||
|
m_itemsByPath[item->path()] = item;
|
||||||
|
destFolder->addItem(item);
|
||||||
|
emit itemAdded(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::isProcessingEnabled() const
|
||||||
|
{
|
||||||
|
return m_processingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::setProcessingEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (m_processingEnabled != enabled) {
|
||||||
|
m_processingEnabled = enabled;
|
||||||
|
SettingsStorage::instance()->storeValue(SettingsKey_ProcessingEnabled, m_processingEnabled);
|
||||||
|
if (m_processingEnabled) {
|
||||||
|
m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_refreshTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
emit processingStateChanged(m_processingEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFileStorage *Session::confFileStorage() const
|
||||||
|
{
|
||||||
|
return m_confFileStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFileStorage *Session::dataFileStorage() const
|
||||||
|
{
|
||||||
|
return m_dataFileStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Folder *Session::rootFolder() const
|
||||||
|
{
|
||||||
|
return static_cast<Folder *>(m_itemsByPath.value(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Feed *> Session::feeds() const
|
||||||
|
{
|
||||||
|
return m_feedsByURL.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
Feed *Session::feedByURL(const QString &url) const
|
||||||
|
{
|
||||||
|
return m_feedsByURL.value(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint Session::refreshInterval() const
|
||||||
|
{
|
||||||
|
return m_refreshInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::setRefreshInterval(uint refreshInterval)
|
||||||
|
{
|
||||||
|
if (m_refreshInterval != refreshInterval) {
|
||||||
|
SettingsStorage::instance()->storeValue(SettingsKey_RefreshInterval, refreshInterval);
|
||||||
|
m_refreshInterval = refreshInterval;
|
||||||
|
m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QThread *Session::workingThread() const
|
||||||
|
{
|
||||||
|
return m_workingThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::handleItemAboutToBeDestroyed(Item *item)
|
||||||
|
{
|
||||||
|
m_itemsByPath.remove(item->path());
|
||||||
|
auto feed = qobject_cast<Feed *>(item);
|
||||||
|
if (feed)
|
||||||
|
m_feedsByURL.remove(feed->url());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::handleFeedTitleChanged(Feed *feed)
|
||||||
|
{
|
||||||
|
if (feed->name() == feed->url())
|
||||||
|
// Now we have something better than a URL.
|
||||||
|
// Trying to rename feed...
|
||||||
|
moveItem(feed, Item::joinPath(Item::parentPath(feed->path()), feed->title()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int Session::maxArticlesPerFeed() const
|
||||||
|
{
|
||||||
|
return m_maxArticlesPerFeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::setMaxArticlesPerFeed(int n)
|
||||||
|
{
|
||||||
|
if (m_maxArticlesPerFeed != n) {
|
||||||
|
m_maxArticlesPerFeed = n;
|
||||||
|
SettingsStorage::instance()->storeValue(SettingsKey_MaxArticlesPerFeed, n);
|
||||||
|
emit maxArticlesPerFeedChanged(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::refresh()
|
||||||
|
{
|
||||||
|
// NOTE: Should we allow manually refreshing for disabled session?
|
||||||
|
rootFolder()->refresh();
|
||||||
|
}
|
154
src/base/rss/rss_session.h
Normal file
154
src/base/rss/rss_session.h
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RSS Session configuration file format (JSON):
|
||||||
|
*
|
||||||
|
* =============== BEGIN ===============
|
||||||
|
* {
|
||||||
|
* "folder1": {
|
||||||
|
* "subfolder1": {
|
||||||
|
* "Feed name (Alias)": "http://some-feed-url1",
|
||||||
|
* "http://some-feed-url2": ""
|
||||||
|
* },
|
||||||
|
* "subfolder2": {},
|
||||||
|
* "http://some-feed-url3": "",
|
||||||
|
* "Feed name (Alias)": {
|
||||||
|
* "url": "http://some-feed-url4",
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* "folder2": {},
|
||||||
|
* "folder3": {}
|
||||||
|
* }
|
||||||
|
* ================ END ================
|
||||||
|
*
|
||||||
|
* 1. Document is JSON object (the same as Folder)
|
||||||
|
* 2. Folder is JSON object (keys are Item names, values are Items)
|
||||||
|
* 3.1. Feed is JSON object (keys are property names, values are property values; 'url' is required)
|
||||||
|
* 3.2. (Reduced format) Feed is JSON string (string is URL unless it's empty, otherwise we take Feed URL from name)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
class QThread;
|
||||||
|
class Application;
|
||||||
|
class AsyncFileStorage;
|
||||||
|
|
||||||
|
namespace RSS
|
||||||
|
{
|
||||||
|
class Item;
|
||||||
|
class Feed;
|
||||||
|
class Folder;
|
||||||
|
|
||||||
|
class Session: public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(Session)
|
||||||
|
|
||||||
|
friend class ::Application;
|
||||||
|
|
||||||
|
Session();
|
||||||
|
~Session() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static Session *instance();
|
||||||
|
|
||||||
|
bool isProcessingEnabled() const;
|
||||||
|
void setProcessingEnabled(bool enabled);
|
||||||
|
|
||||||
|
QThread *workingThread() const;
|
||||||
|
AsyncFileStorage *confFileStorage() const;
|
||||||
|
AsyncFileStorage *dataFileStorage() const;
|
||||||
|
|
||||||
|
int maxArticlesPerFeed() const;
|
||||||
|
void setMaxArticlesPerFeed(int n);
|
||||||
|
|
||||||
|
uint refreshInterval() const;
|
||||||
|
void setRefreshInterval(uint refreshInterval);
|
||||||
|
|
||||||
|
bool addFolder(const QString &path, QString *error = nullptr);
|
||||||
|
bool addFeed(const QString &url, const QString &path, QString *error = nullptr);
|
||||||
|
bool moveItem(const QString &itemPath, const QString &destPath
|
||||||
|
, QString *error = nullptr);
|
||||||
|
bool moveItem(Item *item, const QString &destPath, QString *error = nullptr);
|
||||||
|
bool removeItem(const QString &itemPath, QString *error = nullptr);
|
||||||
|
|
||||||
|
QList<Item *> items() const;
|
||||||
|
Item *itemByPath(const QString &path) const;
|
||||||
|
QList<Feed *> feeds() const;
|
||||||
|
Feed *feedByURL(const QString &url) const;
|
||||||
|
|
||||||
|
Folder *rootFolder() const;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void refresh();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void processingStateChanged(bool enabled);
|
||||||
|
void maxArticlesPerFeedChanged(int n);
|
||||||
|
void itemAdded(Item *item);
|
||||||
|
void itemPathChanged(Item *item);
|
||||||
|
void itemAboutToBeRemoved(Item *item);
|
||||||
|
void feedIconLoaded(Feed *feed);
|
||||||
|
void feedStateChanged(Feed *feed);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleItemAboutToBeDestroyed(Item *item);
|
||||||
|
void handleFeedTitleChanged(Feed *feed);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void load();
|
||||||
|
void loadFolder(const QJsonObject &jsonObj, Folder *folder);
|
||||||
|
void loadLegacy();
|
||||||
|
void store();
|
||||||
|
Folder *prepareItemDest(const QString &path, QString *error);
|
||||||
|
Folder *addSubfolder(const QString &name, Folder *parentFolder);
|
||||||
|
Feed *addFeedToFolder(const QString &url, const QString &name, Folder *parentFolder);
|
||||||
|
void addItem(Item *item, Folder *destFolder);
|
||||||
|
|
||||||
|
static QPointer<Session> m_instance;
|
||||||
|
|
||||||
|
bool m_processingEnabled;
|
||||||
|
QThread *m_workingThread;
|
||||||
|
AsyncFileStorage *m_confFileStorage;
|
||||||
|
AsyncFileStorage *m_dataFileStorage;
|
||||||
|
QTimer m_refreshTimer;
|
||||||
|
uint m_refreshInterval;
|
||||||
|
int m_maxArticlesPerFeed;
|
||||||
|
QHash<QString, Item *> m_itemsByPath;
|
||||||
|
QHash<QString, Feed *> m_feedsByURL;
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,143 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
|
||||||
*
|
|
||||||
* 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, arnaud@qbittorrent.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "rssfeed.h"
|
|
||||||
#include "rssarticle.h"
|
|
||||||
|
|
||||||
using namespace Rss;
|
|
||||||
|
|
||||||
// public constructor
|
|
||||||
Article::Article(Feed *parent, const QString &guid)
|
|
||||||
: m_parent(parent)
|
|
||||||
, m_guid(guid)
|
|
||||||
, m_read(false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Article::hasAttachment() const
|
|
||||||
{
|
|
||||||
return !m_torrentUrl.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantHash Article::toHash() const
|
|
||||||
{
|
|
||||||
QVariantHash item;
|
|
||||||
item["title"] = m_title;
|
|
||||||
item["id"] = m_guid;
|
|
||||||
item["torrent_url"] = m_torrentUrl;
|
|
||||||
item["news_link"] = m_link;
|
|
||||||
item["description"] = m_description;
|
|
||||||
item["date"] = m_date;
|
|
||||||
item["author"] = m_author;
|
|
||||||
item["read"] = m_read;
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticlePtr Article::fromHash(Feed *parent, const QVariantHash &h)
|
|
||||||
{
|
|
||||||
const QString guid = h.value("id").toString();
|
|
||||||
if (guid.isEmpty())
|
|
||||||
return ArticlePtr();
|
|
||||||
|
|
||||||
ArticlePtr art(new Article(parent, guid));
|
|
||||||
art->m_title = h.value("title", "").toString();
|
|
||||||
art->m_torrentUrl = h.value("torrent_url", "").toString();
|
|
||||||
art->m_link = h.value("news_link", "").toString();
|
|
||||||
art->m_description = h.value("description").toString();
|
|
||||||
art->m_date = h.value("date").toDateTime();
|
|
||||||
art->m_author = h.value("author").toString();
|
|
||||||
art->m_read = h.value("read", false).toBool();
|
|
||||||
|
|
||||||
return art;
|
|
||||||
}
|
|
||||||
|
|
||||||
Feed *Article::parent() const
|
|
||||||
{
|
|
||||||
return m_parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString &Article::author() const
|
|
||||||
{
|
|
||||||
return m_author;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString &Article::torrentUrl() const
|
|
||||||
{
|
|
||||||
return m_torrentUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString &Article::link() const
|
|
||||||
{
|
|
||||||
return m_link;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Article::description() const
|
|
||||||
{
|
|
||||||
return m_description.isNull() ? "" : m_description;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QDateTime &Article::date() const
|
|
||||||
{
|
|
||||||
return m_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Article::isRead() const
|
|
||||||
{
|
|
||||||
return m_read;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Article::markAsRead()
|
|
||||||
{
|
|
||||||
if (!m_read) {
|
|
||||||
m_read = true;
|
|
||||||
emit articleWasRead();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString &Article::guid() const
|
|
||||||
{
|
|
||||||
return m_guid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString &Article::title() const
|
|
||||||
{
|
|
||||||
return m_title;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Article::handleTorrentDownloadSuccess(const QString &url)
|
|
||||||
{
|
|
||||||
if (url == m_torrentUrl)
|
|
||||||
markAsRead();
|
|
||||||
}
|
|
|
@ -1,440 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2010 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 <QDebug>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QRegExp>
|
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
#include "base/preferences.h"
|
|
||||||
#include "base/utils/fs.h"
|
|
||||||
#include "base/utils/string.h"
|
|
||||||
#include "rssfeed.h"
|
|
||||||
#include "rssarticle.h"
|
|
||||||
#include "rssdownloadrule.h"
|
|
||||||
|
|
||||||
using namespace Rss;
|
|
||||||
|
|
||||||
DownloadRule::DownloadRule()
|
|
||||||
: m_enabled(false)
|
|
||||||
, m_useRegex(false)
|
|
||||||
, m_apstate(USE_GLOBAL)
|
|
||||||
, m_ignoreDays(0)
|
|
||||||
, m_cachedRegexes(new QHash<QString, QRegularExpression>)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadRule::~DownloadRule()
|
|
||||||
{
|
|
||||||
delete m_cachedRegexes;
|
|
||||||
}
|
|
||||||
|
|
||||||
QRegularExpression DownloadRule::cachedRegex(const QString &expression, bool isRegex) const
|
|
||||||
{
|
|
||||||
// Use a cache of regexes so we don't have to continually recompile - big performance increase.
|
|
||||||
// The cache is cleared whenever the regex/wildcard, must or must not contain fields or
|
|
||||||
// episode filter are modified.
|
|
||||||
Q_ASSERT(!expression.isEmpty());
|
|
||||||
QRegularExpression regex((*m_cachedRegexes)[expression]);
|
|
||||||
|
|
||||||
if (!regex.pattern().isEmpty())
|
|
||||||
return regex;
|
|
||||||
|
|
||||||
return (*m_cachedRegexes)[expression] = QRegularExpression(isRegex ? expression : Utils::String::wildcardToRegex(expression), QRegularExpression::CaseInsensitiveOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DownloadRule::matches(const QString &articleTitle, const QString &expression) const
|
|
||||||
{
|
|
||||||
static QRegularExpression whitespace("\\s+");
|
|
||||||
|
|
||||||
if (expression.isEmpty()) {
|
|
||||||
// A regex of the form "expr|" will always match, so do the same for wildcards
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (m_useRegex) {
|
|
||||||
QRegularExpression reg(cachedRegex(expression));
|
|
||||||
return reg.match(articleTitle).hasMatch();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Only match if every wildcard token (separated by spaces) is present in the article name.
|
|
||||||
// Order of wildcard tokens is unimportant (if order is important, they should have used *).
|
|
||||||
foreach (const QString &wildcard, expression.split(whitespace, QString::SplitBehavior::SkipEmptyParts)) {
|
|
||||||
QRegularExpression reg(cachedRegex(wildcard, false));
|
|
||||||
|
|
||||||
if (!reg.match(articleTitle).hasMatch())
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DownloadRule::matches(const QString &articleTitle) const
|
|
||||||
{
|
|
||||||
if (!m_mustContain.empty()) {
|
|
||||||
bool logged = false;
|
|
||||||
bool foundMustContain = false;
|
|
||||||
|
|
||||||
// Each expression is either a regex, or a set of wildcards separated by whitespace.
|
|
||||||
// Accept if any complete expression matches.
|
|
||||||
foreach (const QString &expression, m_mustContain) {
|
|
||||||
if (!logged) {
|
|
||||||
qDebug() << "Checking matching" << (m_useRegex ? "regex:" : "wildcard expressions:") << m_mustContain.join("|");
|
|
||||||
logged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A regex of the form "expr|" will always match, so do the same for wildcards
|
|
||||||
foundMustContain = matches(articleTitle, expression);
|
|
||||||
|
|
||||||
if (foundMustContain) {
|
|
||||||
qDebug() << "Found matching" << (m_useRegex ? "regex:" : "wildcard expression:") << expression;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundMustContain)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_mustNotContain.empty()) {
|
|
||||||
bool logged = false;
|
|
||||||
|
|
||||||
// Each expression is either a regex, or a set of wildcards separated by whitespace.
|
|
||||||
// Reject if any complete expression matches.
|
|
||||||
foreach (const QString &expression, m_mustNotContain) {
|
|
||||||
if (!logged) {
|
|
||||||
qDebug() << "Checking not matching" << (m_useRegex ? "regex:" : "wildcard expressions:") << m_mustNotContain.join("|");
|
|
||||||
logged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A regex of the form "expr|" will always match, so do the same for wildcards
|
|
||||||
if (matches(articleTitle, expression)) {
|
|
||||||
qDebug() << "Found not matching" << (m_useRegex ? "regex:" : "wildcard expression:") << expression;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_episodeFilter.isEmpty()) {
|
|
||||||
qDebug() << "Checking episode filter:" << m_episodeFilter;
|
|
||||||
QRegularExpression f(cachedRegex("(^\\d{1,4})x(.*;$)"));
|
|
||||||
QRegularExpressionMatch matcher = f.match(m_episodeFilter);
|
|
||||||
bool matched = matcher.hasMatch();
|
|
||||||
|
|
||||||
if (!matched)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
QString s = matcher.captured(1);
|
|
||||||
QStringList eps = matcher.captured(2).split(";");
|
|
||||||
int sOurs = s.toInt();
|
|
||||||
|
|
||||||
foreach (QString ep, eps) {
|
|
||||||
if (ep.isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// We need to trim leading zeroes, but if it's all zeros then we want episode zero.
|
|
||||||
while (ep.size() > 1 && ep.startsWith("0"))
|
|
||||||
ep = ep.right(ep.size() - 1);
|
|
||||||
|
|
||||||
if (ep.indexOf('-') != -1) { // Range detected
|
|
||||||
QString partialPattern1 = "\\bs0?(\\d{1,4})[ -_\\.]?e(0?\\d{1,4})(?:\\D|\\b)";
|
|
||||||
QString partialPattern2 = "\\b(\\d{1,4})x(0?\\d{1,4})(?:\\D|\\b)";
|
|
||||||
QRegularExpression reg(cachedRegex(partialPattern1));
|
|
||||||
|
|
||||||
if (ep.endsWith('-')) { // Infinite range
|
|
||||||
int epOurs = ep.left(ep.size() - 1).toInt();
|
|
||||||
|
|
||||||
// Extract partial match from article and compare as digits
|
|
||||||
matcher = reg.match(articleTitle);
|
|
||||||
matched = matcher.hasMatch();
|
|
||||||
|
|
||||||
if (!matched) {
|
|
||||||
reg = QRegularExpression(cachedRegex(partialPattern2));
|
|
||||||
matcher = reg.match(articleTitle);
|
|
||||||
matched = matcher.hasMatch();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matched) {
|
|
||||||
int sTheirs = matcher.captured(1).toInt();
|
|
||||||
int epTheirs = matcher.captured(2).toInt();
|
|
||||||
if (((sTheirs == sOurs) && (epTheirs >= epOurs)) || (sTheirs > sOurs)) {
|
|
||||||
qDebug() << "Matched episode:" << ep;
|
|
||||||
qDebug() << "Matched article:" << articleTitle;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else { // Normal range
|
|
||||||
QStringList range = ep.split('-');
|
|
||||||
Q_ASSERT(range.size() == 2);
|
|
||||||
if (range.first().toInt() > range.last().toInt())
|
|
||||||
continue; // Ignore this subrule completely
|
|
||||||
|
|
||||||
int epOursFirst = range.first().toInt();
|
|
||||||
int epOursLast = range.last().toInt();
|
|
||||||
|
|
||||||
// Extract partial match from article and compare as digits
|
|
||||||
matcher = reg.match(articleTitle);
|
|
||||||
matched = matcher.hasMatch();
|
|
||||||
|
|
||||||
if (!matched) {
|
|
||||||
reg = QRegularExpression(cachedRegex(partialPattern2));
|
|
||||||
matcher = reg.match(articleTitle);
|
|
||||||
matched = matcher.hasMatch();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matched) {
|
|
||||||
int sTheirs = matcher.captured(1).toInt();
|
|
||||||
int epTheirs = matcher.captured(2).toInt();
|
|
||||||
if ((sTheirs == sOurs) && ((epOursFirst <= epTheirs) && (epOursLast >= epTheirs))) {
|
|
||||||
qDebug() << "Matched episode:" << ep;
|
|
||||||
qDebug() << "Matched article:" << articleTitle;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else { // Single number
|
|
||||||
QString expStr("\\b(?:s0?" + s + "[ -_\\.]?" + "e0?" + ep + "|" + s + "x" + "0?" + ep + ")(?:\\D|\\b)");
|
|
||||||
QRegularExpression reg(cachedRegex(expStr));
|
|
||||||
if (reg.match(articleTitle).hasMatch()) {
|
|
||||||
qDebug() << "Matched episode:" << ep;
|
|
||||||
qDebug() << "Matched article:" << articleTitle;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug() << "Matched article:" << articleTitle;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setMustContain(const QString &tokens)
|
|
||||||
{
|
|
||||||
m_cachedRegexes->clear();
|
|
||||||
|
|
||||||
if (m_useRegex)
|
|
||||||
m_mustContain = QStringList() << tokens;
|
|
||||||
else
|
|
||||||
m_mustContain = tokens.split("|");
|
|
||||||
|
|
||||||
// Check for single empty string - if so, no condition
|
|
||||||
if ((m_mustContain.size() == 1) && m_mustContain[0].isEmpty())
|
|
||||||
m_mustContain.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setMustNotContain(const QString &tokens)
|
|
||||||
{
|
|
||||||
m_cachedRegexes->clear();
|
|
||||||
|
|
||||||
if (m_useRegex)
|
|
||||||
m_mustNotContain = QStringList() << tokens;
|
|
||||||
else
|
|
||||||
m_mustNotContain = tokens.split("|");
|
|
||||||
|
|
||||||
// Check for single empty string - if so, no condition
|
|
||||||
if ((m_mustNotContain.size() == 1) && m_mustNotContain[0].isEmpty())
|
|
||||||
m_mustNotContain.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList DownloadRule::rssFeeds() const
|
|
||||||
{
|
|
||||||
return m_rssFeeds;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setRssFeeds(const QStringList &rssFeeds)
|
|
||||||
{
|
|
||||||
m_rssFeeds = rssFeeds;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DownloadRule::name() const
|
|
||||||
{
|
|
||||||
return m_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setName(const QString &name)
|
|
||||||
{
|
|
||||||
m_name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DownloadRule::savePath() const
|
|
||||||
{
|
|
||||||
return m_savePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadRulePtr DownloadRule::fromVariantHash(const QVariantHash &ruleHash)
|
|
||||||
{
|
|
||||||
DownloadRulePtr rule(new DownloadRule);
|
|
||||||
rule->setName(ruleHash.value("name").toString());
|
|
||||||
rule->setUseRegex(ruleHash.value("use_regex", false).toBool());
|
|
||||||
rule->setMustContain(ruleHash.value("must_contain").toString());
|
|
||||||
rule->setMustNotContain(ruleHash.value("must_not_contain").toString());
|
|
||||||
rule->setEpisodeFilter(ruleHash.value("episode_filter").toString());
|
|
||||||
rule->setRssFeeds(ruleHash.value("affected_feeds").toStringList());
|
|
||||||
rule->setEnabled(ruleHash.value("enabled", false).toBool());
|
|
||||||
rule->setSavePath(ruleHash.value("save_path").toString());
|
|
||||||
rule->setCategory(ruleHash.value("category_assigned").toString());
|
|
||||||
rule->setAddPaused(AddPausedState(ruleHash.value("add_paused").toUInt()));
|
|
||||||
rule->setLastMatch(ruleHash.value("last_match").toDateTime());
|
|
||||||
rule->setIgnoreDays(ruleHash.value("ignore_days").toInt());
|
|
||||||
return rule;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantHash DownloadRule::toVariantHash() const
|
|
||||||
{
|
|
||||||
QVariantHash hash;
|
|
||||||
hash["name"] = m_name;
|
|
||||||
hash["must_contain"] = m_mustContain.join("|");
|
|
||||||
hash["must_not_contain"] = m_mustNotContain.join("|");
|
|
||||||
hash["save_path"] = m_savePath;
|
|
||||||
hash["affected_feeds"] = m_rssFeeds;
|
|
||||||
hash["enabled"] = m_enabled;
|
|
||||||
hash["category_assigned"] = m_category;
|
|
||||||
hash["use_regex"] = m_useRegex;
|
|
||||||
hash["add_paused"] = m_apstate;
|
|
||||||
hash["episode_filter"] = m_episodeFilter;
|
|
||||||
hash["last_match"] = m_lastMatch;
|
|
||||||
hash["ignore_days"] = m_ignoreDays;
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DownloadRule::operator==(const DownloadRule &other) const
|
|
||||||
{
|
|
||||||
return m_name == other.name();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setSavePath(const QString &savePath)
|
|
||||||
{
|
|
||||||
m_savePath = Utils::Fs::fromNativePath(savePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadRule::AddPausedState DownloadRule::addPaused() const
|
|
||||||
{
|
|
||||||
return m_apstate;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setAddPaused(const DownloadRule::AddPausedState &aps)
|
|
||||||
{
|
|
||||||
m_apstate = aps;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DownloadRule::category() const
|
|
||||||
{
|
|
||||||
return m_category;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setCategory(const QString &category)
|
|
||||||
{
|
|
||||||
m_category = category;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DownloadRule::isEnabled() const
|
|
||||||
{
|
|
||||||
return m_enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setEnabled(bool enable)
|
|
||||||
{
|
|
||||||
m_enabled = enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setLastMatch(const QDateTime &d)
|
|
||||||
{
|
|
||||||
m_lastMatch = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDateTime DownloadRule::lastMatch() const
|
|
||||||
{
|
|
||||||
return m_lastMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setIgnoreDays(int d)
|
|
||||||
{
|
|
||||||
m_ignoreDays = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
int DownloadRule::ignoreDays() const
|
|
||||||
{
|
|
||||||
return m_ignoreDays;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DownloadRule::mustContain() const
|
|
||||||
{
|
|
||||||
return m_mustContain.join("|");
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DownloadRule::mustNotContain() const
|
|
||||||
{
|
|
||||||
return m_mustNotContain.join("|");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DownloadRule::useRegex() const
|
|
||||||
{
|
|
||||||
return m_useRegex;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setUseRegex(bool enabled)
|
|
||||||
{
|
|
||||||
m_useRegex = enabled;
|
|
||||||
m_cachedRegexes->clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString DownloadRule::episodeFilter() const
|
|
||||||
{
|
|
||||||
return m_episodeFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRule::setEpisodeFilter(const QString &e)
|
|
||||||
{
|
|
||||||
m_episodeFilter = e;
|
|
||||||
m_cachedRegexes->clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList DownloadRule::findMatchingArticles(const FeedPtr &feed) const
|
|
||||||
{
|
|
||||||
QStringList ret;
|
|
||||||
const ArticleHash &feedArticles = feed->articleHash();
|
|
||||||
|
|
||||||
ArticleHash::ConstIterator artIt = feedArticles.begin();
|
|
||||||
ArticleHash::ConstIterator artItend = feedArticles.end();
|
|
||||||
for (; artIt != artItend; ++artIt) {
|
|
||||||
const QString title = artIt.value()->title();
|
|
||||||
qDebug() << "Matching article:" << title;
|
|
||||||
if (matches(title))
|
|
||||||
ret << title;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
|
@ -1,187 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2010 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 "rssdownloadrulelist.h"
|
|
||||||
|
|
||||||
#include <QFile>
|
|
||||||
#include <QDataStream>
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include "base/preferences.h"
|
|
||||||
#include "base/profile.h"
|
|
||||||
|
|
||||||
using namespace Rss;
|
|
||||||
|
|
||||||
DownloadRuleList::DownloadRuleList()
|
|
||||||
{
|
|
||||||
loadRulesFromStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadRulePtr DownloadRuleList::findMatchingRule(const QString &feedUrl, const QString &articleTitle) const
|
|
||||||
{
|
|
||||||
Q_ASSERT(Preferences::instance()->isRssDownloadingEnabled());
|
|
||||||
qDebug() << "Matching article:" << articleTitle;
|
|
||||||
QStringList ruleNames = m_feedRules.value(feedUrl);
|
|
||||||
foreach (const QString &rule_name, ruleNames) {
|
|
||||||
DownloadRulePtr rule = m_rules[rule_name];
|
|
||||||
if (rule->isEnabled() && rule->matches(articleTitle)) return rule;
|
|
||||||
}
|
|
||||||
return DownloadRulePtr();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRuleList::replace(DownloadRuleList *other)
|
|
||||||
{
|
|
||||||
m_rules.clear();
|
|
||||||
m_feedRules.clear();
|
|
||||||
foreach (const QString &name, other->ruleNames()) {
|
|
||||||
saveRule(other->getRule(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRuleList::saveRulesToStorage()
|
|
||||||
{
|
|
||||||
SettingsPtr qBTRSS = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss"));
|
|
||||||
qBTRSS->setValue("download_rules", toVariantHash());
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRuleList::loadRulesFromStorage()
|
|
||||||
{
|
|
||||||
SettingsPtr qBTRSS = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss"));
|
|
||||||
loadRulesFromVariantHash(qBTRSS->value("download_rules").toHash());
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantHash DownloadRuleList::toVariantHash() const
|
|
||||||
{
|
|
||||||
QVariantHash ret;
|
|
||||||
foreach (const DownloadRulePtr &rule, m_rules.values()) {
|
|
||||||
ret.insert(rule->name(), rule->toVariantHash());
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRuleList::loadRulesFromVariantHash(const QVariantHash &h)
|
|
||||||
{
|
|
||||||
QVariantHash::ConstIterator it = h.begin();
|
|
||||||
QVariantHash::ConstIterator itend = h.end();
|
|
||||||
for ( ; it != itend; ++it) {
|
|
||||||
DownloadRulePtr rule = DownloadRule::fromVariantHash(it.value().toHash());
|
|
||||||
if (rule && !rule->name().isEmpty())
|
|
||||||
saveRule(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRuleList::saveRule(const DownloadRulePtr &rule)
|
|
||||||
{
|
|
||||||
qDebug() << Q_FUNC_INFO << rule->name();
|
|
||||||
Q_ASSERT(rule);
|
|
||||||
if (m_rules.contains(rule->name())) {
|
|
||||||
qDebug("This is an update, removing old rule first");
|
|
||||||
removeRule(rule->name());
|
|
||||||
}
|
|
||||||
m_rules.insert(rule->name(), rule);
|
|
||||||
// Update feedRules hashtable
|
|
||||||
foreach (const QString &feedUrl, rule->rssFeeds()) {
|
|
||||||
m_feedRules[feedUrl].append(rule->name());
|
|
||||||
}
|
|
||||||
qDebug() << Q_FUNC_INFO << "EXIT";
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRuleList::removeRule(const QString &name)
|
|
||||||
{
|
|
||||||
qDebug() << Q_FUNC_INFO << name;
|
|
||||||
if (!m_rules.contains(name)) return;
|
|
||||||
DownloadRulePtr rule = m_rules.take(name);
|
|
||||||
// Update feedRules hashtable
|
|
||||||
foreach (const QString &feedUrl, rule->rssFeeds()) {
|
|
||||||
m_feedRules[feedUrl].removeOne(rule->name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadRuleList::renameRule(const QString &oldName, const QString &newName)
|
|
||||||
{
|
|
||||||
if (!m_rules.contains(oldName)) return;
|
|
||||||
|
|
||||||
DownloadRulePtr rule = m_rules.take(oldName);
|
|
||||||
rule->setName(newName);
|
|
||||||
m_rules.insert(newName, rule);
|
|
||||||
// Update feedRules hashtable
|
|
||||||
foreach (const QString &feedUrl, rule->rssFeeds()) {
|
|
||||||
m_feedRules[feedUrl].replace(m_feedRules[feedUrl].indexOf(oldName), newName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadRulePtr DownloadRuleList::getRule(const QString &name) const
|
|
||||||
{
|
|
||||||
return m_rules.value(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList DownloadRuleList::ruleNames() const
|
|
||||||
{
|
|
||||||
return m_rules.keys();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DownloadRuleList::isEmpty() const
|
|
||||||
{
|
|
||||||
return m_rules.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DownloadRuleList::serialize(const QString &path)
|
|
||||||
{
|
|
||||||
QFile f(path);
|
|
||||||
if (f.open(QIODevice::WriteOnly)) {
|
|
||||||
QDataStream out(&f);
|
|
||||||
out.setVersion(QDataStream::Qt_4_5);
|
|
||||||
out << toVariantHash();
|
|
||||||
f.close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DownloadRuleList::unserialize(const QString &path)
|
|
||||||
{
|
|
||||||
QFile f(path);
|
|
||||||
if (f.open(QIODevice::ReadOnly)) {
|
|
||||||
QDataStream in(&f);
|
|
||||||
in.setVersion(QDataStream::Qt_4_5);
|
|
||||||
QVariantHash tmp;
|
|
||||||
in >> tmp;
|
|
||||||
f.close();
|
|
||||||
if (tmp.isEmpty())
|
|
||||||
return false;
|
|
||||||
qDebug("Processing was successful!");
|
|
||||||
loadRulesFromVariantHash(tmp);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
qDebug("Error: could not open file at %s", qPrintable(path));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2010 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 RSSDOWNLOADRULELIST_H
|
|
||||||
#define RSSDOWNLOADRULELIST_H
|
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QVariantHash>
|
|
||||||
|
|
||||||
#include "rssdownloadrule.h"
|
|
||||||
|
|
||||||
namespace Rss
|
|
||||||
{
|
|
||||||
class DownloadRuleList
|
|
||||||
{
|
|
||||||
Q_DISABLE_COPY(DownloadRuleList)
|
|
||||||
|
|
||||||
public:
|
|
||||||
DownloadRuleList();
|
|
||||||
|
|
||||||
DownloadRulePtr findMatchingRule(const QString &feedUrl, const QString &articleTitle) const;
|
|
||||||
// Operators
|
|
||||||
void saveRule(const DownloadRulePtr &rule);
|
|
||||||
void removeRule(const QString &name);
|
|
||||||
void renameRule(const QString &oldName, const QString &newName);
|
|
||||||
DownloadRulePtr getRule(const QString &name) const;
|
|
||||||
QStringList ruleNames() const;
|
|
||||||
bool isEmpty() const;
|
|
||||||
void saveRulesToStorage();
|
|
||||||
bool serialize(const QString &path);
|
|
||||||
bool unserialize(const QString &path);
|
|
||||||
void replace(DownloadRuleList *other);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void loadRulesFromStorage();
|
|
||||||
void loadRulesFromVariantHash(const QVariantHash &l);
|
|
||||||
QVariantHash toVariantHash() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QHash<QString, DownloadRulePtr> m_rules;
|
|
||||||
QHash<QString, QStringList> m_feedRules;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // RSSDOWNLOADFILTERLIST_H
|
|
|
@ -1,458 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
|
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
|
||||||
*
|
|
||||||
* 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, arnaud@qbittorrent.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rssfeed.h"
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include "base/preferences.h"
|
|
||||||
#include "base/logger.h"
|
|
||||||
#include "base/profile.h"
|
|
||||||
#include "base/bittorrent/session.h"
|
|
||||||
#include "base/bittorrent/magneturi.h"
|
|
||||||
#include "base/utils/misc.h"
|
|
||||||
#include "base/utils/fs.h"
|
|
||||||
#include "base/net/downloadmanager.h"
|
|
||||||
#include "base/net/downloadhandler.h"
|
|
||||||
#include "private/rssparser.h"
|
|
||||||
#include "rssdownloadrulelist.h"
|
|
||||||
#include "rssarticle.h"
|
|
||||||
#include "rssfolder.h"
|
|
||||||
#include "rssmanager.h"
|
|
||||||
|
|
||||||
namespace Rss
|
|
||||||
{
|
|
||||||
bool articleDateRecentThan(const ArticlePtr &left, const ArticlePtr &right)
|
|
||||||
{
|
|
||||||
return left->date() > right->date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using namespace Rss;
|
|
||||||
|
|
||||||
Feed::Feed(const QString &url, Manager *manager)
|
|
||||||
: m_manager(manager)
|
|
||||||
, m_url (QUrl::fromEncoded(url.toUtf8()).toString())
|
|
||||||
, m_icon(":/icons/qbt-theme/application-rss+xml.png")
|
|
||||||
, m_unreadCount(0)
|
|
||||||
, m_dirty(false)
|
|
||||||
, m_inErrorState(false)
|
|
||||||
, m_loading(false)
|
|
||||||
{
|
|
||||||
qDebug() << Q_FUNC_INFO << m_url;
|
|
||||||
m_parser = new Private::Parser;
|
|
||||||
m_parser->moveToThread(m_manager->workingThread());
|
|
||||||
connect(this, SIGNAL(destroyed()), m_parser, SLOT(deleteLater()));
|
|
||||||
// Listen for new RSS downloads
|
|
||||||
connect(m_parser, SIGNAL(feedTitle(QString)), SLOT(handleFeedTitle(QString)));
|
|
||||||
connect(m_parser, SIGNAL(newArticle(QVariantHash)), SLOT(handleNewArticle(QVariantHash)));
|
|
||||||
connect(m_parser, SIGNAL(finished(QString)), SLOT(handleParsingFinished(QString)));
|
|
||||||
|
|
||||||
// Download the RSS Feed icon
|
|
||||||
Net::DownloadHandler *handler = Net::DownloadManager::instance()->downloadUrl(iconUrl(), true);
|
|
||||||
connect(handler, SIGNAL(downloadFinished(QString,QString)), this, SLOT(handleIconDownloadFinished(QString,QString)));
|
|
||||||
|
|
||||||
// Load old RSS articles
|
|
||||||
loadItemsFromDisk();
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
Feed::~Feed()
|
|
||||||
{
|
|
||||||
if (!m_icon.startsWith(":/") && QFile::exists(m_icon))
|
|
||||||
Utils::Fs::forceRemove(m_icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::saveItemsToDisk()
|
|
||||||
{
|
|
||||||
qDebug() << Q_FUNC_INFO << m_url;
|
|
||||||
if (!m_dirty) return;
|
|
||||||
|
|
||||||
m_dirty = false;
|
|
||||||
|
|
||||||
SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss-feeds"));
|
|
||||||
QVariantList oldItems;
|
|
||||||
|
|
||||||
ArticleHash::ConstIterator it = m_articles.begin();
|
|
||||||
ArticleHash::ConstIterator itend = m_articles.end();
|
|
||||||
for (; it != itend; ++it)
|
|
||||||
oldItems << it.value()->toHash();
|
|
||||||
qDebug("Saving %d old items for feed %s", oldItems.size(), qPrintable(displayName()));
|
|
||||||
QHash<QString, QVariant> allOldItems = qBTRSSFeeds->value("old_items", QHash<QString, QVariant>()).toHash();
|
|
||||||
allOldItems[m_url] = oldItems;
|
|
||||||
qBTRSSFeeds->setValue("old_items", allOldItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::loadItemsFromDisk()
|
|
||||||
{
|
|
||||||
SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss-feeds"));
|
|
||||||
QHash<QString, QVariant> allOldItems = qBTRSSFeeds->value("old_items", QHash<QString, QVariant>()).toHash();
|
|
||||||
const QVariantList oldItems = allOldItems.value(m_url, QVariantList()).toList();
|
|
||||||
qDebug("Loading %d old items for feed %s", oldItems.size(), qPrintable(displayName()));
|
|
||||||
|
|
||||||
foreach (const QVariant &var_it, oldItems) {
|
|
||||||
QVariantHash item = var_it.toHash();
|
|
||||||
ArticlePtr rssItem = Article::fromHash(this, item);
|
|
||||||
if (rssItem)
|
|
||||||
addArticle(rssItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::addArticle(const ArticlePtr &article)
|
|
||||||
{
|
|
||||||
int maxArticles = Preferences::instance()->getRSSMaxArticlesPerFeed();
|
|
||||||
|
|
||||||
if (!m_articles.contains(article->guid())) {
|
|
||||||
m_dirty = true;
|
|
||||||
|
|
||||||
// Update unreadCount
|
|
||||||
if (!article->isRead())
|
|
||||||
++m_unreadCount;
|
|
||||||
// Insert in hash table
|
|
||||||
m_articles[article->guid()] = article;
|
|
||||||
if (!article->isRead()) // Optimization
|
|
||||||
connect(article.data(), SIGNAL(articleWasRead()), SLOT(handleArticleRead()), Qt::UniqueConnection);
|
|
||||||
// Insertion sort
|
|
||||||
ArticleList::Iterator lowerBound = qLowerBound(m_articlesByDate.begin(), m_articlesByDate.end(), article, articleDateRecentThan);
|
|
||||||
m_articlesByDate.insert(lowerBound, article);
|
|
||||||
int lbIndex = m_articlesByDate.indexOf(article);
|
|
||||||
if (m_articlesByDate.size() > maxArticles) {
|
|
||||||
ArticlePtr oldestArticle = m_articlesByDate.takeLast();
|
|
||||||
m_articles.remove(oldestArticle->guid());
|
|
||||||
// Update unreadCount
|
|
||||||
if (!oldestArticle->isRead())
|
|
||||||
--m_unreadCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if article was inserted at the end of the list and will break max_articles limit
|
|
||||||
if (Preferences::instance()->isRssDownloadingEnabled())
|
|
||||||
if ((lbIndex < maxArticles) && !article->isRead())
|
|
||||||
downloadArticleTorrentIfMatching(article);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// m_articles.contains(article->guid())
|
|
||||||
// Try to download skipped articles
|
|
||||||
if (Preferences::instance()->isRssDownloadingEnabled()) {
|
|
||||||
ArticlePtr skipped = m_articles.value(article->guid(), ArticlePtr());
|
|
||||||
if (skipped)
|
|
||||||
if (!skipped->isRead())
|
|
||||||
downloadArticleTorrentIfMatching(skipped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Feed::refresh()
|
|
||||||
{
|
|
||||||
if (m_loading) {
|
|
||||||
qWarning() << Q_FUNC_INFO << "Feed" << displayName() << "is already being refreshed, ignoring request";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
m_loading = true;
|
|
||||||
// Download the RSS again
|
|
||||||
Net::DownloadHandler *handler = Net::DownloadManager::instance()->downloadUrl(m_url);
|
|
||||||
connect(handler, SIGNAL(downloadFinished(QString,QByteArray)), this, SLOT(handleRssDownloadFinished(QString,QByteArray)));
|
|
||||||
connect(handler, SIGNAL(downloadFailed(QString,QString)), this, SLOT(handleRssDownloadFailed(QString,QString)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Feed::id() const
|
|
||||||
{
|
|
||||||
return m_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::removeAllSettings()
|
|
||||||
{
|
|
||||||
qDebug() << "Removing all settings / history for feed: " << m_url;
|
|
||||||
SettingsPtr qBTRSS = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss"));
|
|
||||||
QVariantHash feedsWDownloader = qBTRSS->value("downloader_on", QVariantHash()).toHash();
|
|
||||||
if (feedsWDownloader.contains(m_url)) {
|
|
||||||
feedsWDownloader.remove(m_url);
|
|
||||||
qBTRSS->setValue("downloader_on", feedsWDownloader);
|
|
||||||
}
|
|
||||||
QVariantHash allFeedsFilters = qBTRSS->value("feed_filters", QVariantHash()).toHash();
|
|
||||||
if (allFeedsFilters.contains(m_url)) {
|
|
||||||
allFeedsFilters.remove(m_url);
|
|
||||||
qBTRSS->setValue("feed_filters", allFeedsFilters);
|
|
||||||
}
|
|
||||||
SettingsPtr qBTRSSFeeds = Profile::instance().applicationSettings(QLatin1String("qBittorrent-rss-feeds"));
|
|
||||||
QVariantHash allOldItems = qBTRSSFeeds->value("old_items", QVariantHash()).toHash();
|
|
||||||
if (allOldItems.contains(m_url)) {
|
|
||||||
allOldItems.remove(m_url);
|
|
||||||
qBTRSSFeeds->setValue("old_items", allOldItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Feed::isLoading() const
|
|
||||||
{
|
|
||||||
return m_loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Feed::title() const
|
|
||||||
{
|
|
||||||
return m_title;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::rename(const QString &newName)
|
|
||||||
{
|
|
||||||
qDebug() << "Renaming stream to" << newName;
|
|
||||||
m_alias = newName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the alias if the stream has one, the url if it has no alias
|
|
||||||
QString Feed::displayName() const
|
|
||||||
{
|
|
||||||
if (!m_alias.isEmpty())
|
|
||||||
return m_alias;
|
|
||||||
if (!m_title.isEmpty())
|
|
||||||
return m_title;
|
|
||||||
return m_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Feed::url() const
|
|
||||||
{
|
|
||||||
return m_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Feed::iconPath() const
|
|
||||||
{
|
|
||||||
if (m_inErrorState)
|
|
||||||
return QLatin1String(":/icons/qbt-theme/unavailable.png");
|
|
||||||
|
|
||||||
return m_icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Feed::hasCustomIcon() const
|
|
||||||
{
|
|
||||||
return !m_icon.startsWith(":/");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::setIconPath(const QString &path)
|
|
||||||
{
|
|
||||||
QString nativePath = Utils::Fs::fromNativePath(path);
|
|
||||||
if ((nativePath == m_icon) || nativePath.isEmpty() || !QFile::exists(nativePath)) return;
|
|
||||||
|
|
||||||
if (!m_icon.startsWith(":/") && QFile::exists(m_icon))
|
|
||||||
Utils::Fs::forceRemove(m_icon);
|
|
||||||
|
|
||||||
m_icon = nativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticlePtr Feed::getItem(const QString &guid) const
|
|
||||||
{
|
|
||||||
return m_articles.value(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint Feed::count() const
|
|
||||||
{
|
|
||||||
return m_articles.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::markAsRead()
|
|
||||||
{
|
|
||||||
ArticleHash::ConstIterator it = m_articles.begin();
|
|
||||||
ArticleHash::ConstIterator itend = m_articles.end();
|
|
||||||
for (; it != itend; ++it)
|
|
||||||
it.value()->markAsRead();
|
|
||||||
m_unreadCount = 0;
|
|
||||||
m_manager->forwardFeedInfosChanged(m_url, displayName(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint Feed::unreadCount() const
|
|
||||||
{
|
|
||||||
return m_unreadCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticleList Feed::articleListByDateDesc() const
|
|
||||||
{
|
|
||||||
return m_articlesByDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ArticleHash &Feed::articleHash() const
|
|
||||||
{
|
|
||||||
return m_articles;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticleList Feed::unreadArticleListByDateDesc() const
|
|
||||||
{
|
|
||||||
ArticleList unreadNews;
|
|
||||||
|
|
||||||
ArticleList::ConstIterator it = m_articlesByDate.begin();
|
|
||||||
ArticleList::ConstIterator itend = m_articlesByDate.end();
|
|
||||||
for (; it != itend; ++it)
|
|
||||||
if (!(*it)->isRead())
|
|
||||||
unreadNews << *it;
|
|
||||||
return unreadNews;
|
|
||||||
}
|
|
||||||
|
|
||||||
// download the icon from the address
|
|
||||||
QString Feed::iconUrl() const
|
|
||||||
{
|
|
||||||
// XXX: This works for most sites but it is not perfect
|
|
||||||
return QString("http://%1/favicon.ico").arg(QUrl(m_url).host());
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::handleIconDownloadFinished(const QString &url, const QString &filePath)
|
|
||||||
{
|
|
||||||
Q_UNUSED(url);
|
|
||||||
setIconPath(filePath);
|
|
||||||
qDebug() << Q_FUNC_INFO << "icon path:" << m_icon;
|
|
||||||
m_manager->forwardFeedIconChanged(m_url, m_icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::handleRssDownloadFinished(const QString &url, const QByteArray &data)
|
|
||||||
{
|
|
||||||
Q_UNUSED(url);
|
|
||||||
qDebug() << Q_FUNC_INFO << "Successfully downloaded RSS feed at" << m_url;
|
|
||||||
// Parse the download RSS
|
|
||||||
QMetaObject::invokeMethod(m_parser, "parse", Qt::QueuedConnection, Q_ARG(QByteArray, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::handleRssDownloadFailed(const QString &url, const QString &error)
|
|
||||||
{
|
|
||||||
Q_UNUSED(url);
|
|
||||||
m_inErrorState = true;
|
|
||||||
m_loading = false;
|
|
||||||
m_manager->forwardFeedInfosChanged(m_url, displayName(), m_unreadCount);
|
|
||||||
qWarning() << "Failed to download RSS feed at" << m_url;
|
|
||||||
qWarning() << "Reason:" << error;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::handleFeedTitle(const QString &title)
|
|
||||||
{
|
|
||||||
if (m_title == title) return;
|
|
||||||
|
|
||||||
m_title = title;
|
|
||||||
|
|
||||||
// Notify that we now have something better than a URL to display
|
|
||||||
if (m_alias.isEmpty())
|
|
||||||
m_manager->forwardFeedInfosChanged(m_url, title, m_unreadCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::downloadArticleTorrentIfMatching(const ArticlePtr &article)
|
|
||||||
{
|
|
||||||
Q_ASSERT(Preferences::instance()->isRssDownloadingEnabled());
|
|
||||||
qDebug().nospace() << Q_FUNC_INFO << " Deferring matching of " << article->title() << " from " << displayName() << " RSS feed";
|
|
||||||
m_manager->downloadArticleTorrentIfMatching(m_url, article);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::deferredDownloadArticleTorrentIfMatching(const ArticlePtr &article)
|
|
||||||
{
|
|
||||||
qDebug().nospace() << Q_FUNC_INFO << " Matching of " << article->title() << " from " << displayName() << " RSS feed";
|
|
||||||
|
|
||||||
DownloadRuleList *rules = m_manager->downloadRules();
|
|
||||||
DownloadRulePtr matchingRule = rules->findMatchingRule(m_url, article->title());
|
|
||||||
if (!matchingRule) return;
|
|
||||||
|
|
||||||
if (matchingRule->ignoreDays() > 0) {
|
|
||||||
QDateTime lastMatch = matchingRule->lastMatch();
|
|
||||||
if (lastMatch.isValid()) {
|
|
||||||
if (QDateTime::currentDateTime() < lastMatch.addDays(matchingRule->ignoreDays())) {
|
|
||||||
article->markAsRead();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matchingRule->setLastMatch(QDateTime::currentDateTime());
|
|
||||||
rules->saveRulesToStorage();
|
|
||||||
// Download the torrent
|
|
||||||
const QString &torrentUrl = article->torrentUrl();
|
|
||||||
if (torrentUrl.isEmpty()) {
|
|
||||||
Logger::instance()->addMessage(tr("Automatic download of '%1' from '%2' RSS feed failed because it doesn't contain a torrent or a magnet link...").arg(article->title()).arg(displayName()), Log::WARNING);
|
|
||||||
article->markAsRead();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger::instance()->addMessage(tr("Automatically downloading '%1' torrent from '%2' RSS feed...").arg(article->title()).arg(displayName()));
|
|
||||||
if (BitTorrent::MagnetUri(torrentUrl).isValid())
|
|
||||||
article->markAsRead();
|
|
||||||
else
|
|
||||||
connect(BitTorrent::Session::instance(), SIGNAL(downloadFromUrlFinished(QString)), article.data(), SLOT(handleTorrentDownloadSuccess(const QString&)), Qt::UniqueConnection);
|
|
||||||
|
|
||||||
BitTorrent::AddTorrentParams params;
|
|
||||||
params.savePath = matchingRule->savePath();
|
|
||||||
params.category = matchingRule->category();
|
|
||||||
if (matchingRule->addPaused() == DownloadRule::ALWAYS_PAUSED)
|
|
||||||
params.addPaused = TriStateBool::True;
|
|
||||||
else if (matchingRule->addPaused() == DownloadRule::NEVER_PAUSED)
|
|
||||||
params.addPaused = TriStateBool::False;
|
|
||||||
BitTorrent::Session::instance()->addTorrent(torrentUrl, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::recheckRssItemsForDownload()
|
|
||||||
{
|
|
||||||
Q_ASSERT(Preferences::instance()->isRssDownloadingEnabled());
|
|
||||||
foreach (const ArticlePtr &article, m_articlesByDate)
|
|
||||||
if (!article->isRead())
|
|
||||||
downloadArticleTorrentIfMatching(article);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::handleNewArticle(const QVariantHash &articleData)
|
|
||||||
{
|
|
||||||
ArticlePtr article = Article::fromHash(this, articleData);
|
|
||||||
if (article.isNull()) {
|
|
||||||
qDebug() << "Article hash corrupted or guid is uncomputable; feed url: " << m_url;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Q_ASSERT(article);
|
|
||||||
addArticle(article);
|
|
||||||
|
|
||||||
m_manager->forwardFeedInfosChanged(m_url, displayName(), m_unreadCount);
|
|
||||||
// FIXME: We should forward the information here but this would seriously decrease
|
|
||||||
// performance with current design.
|
|
||||||
// m_manager->forwardFeedContentChanged(m_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::handleParsingFinished(const QString &error)
|
|
||||||
{
|
|
||||||
if (!error.isEmpty()) {
|
|
||||||
qWarning() << "Failed to parse RSS feed at" << m_url;
|
|
||||||
qWarning() << "Reason:" << error;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_loading = false;
|
|
||||||
m_inErrorState = !error.isEmpty();
|
|
||||||
|
|
||||||
m_manager->forwardFeedInfosChanged(m_url, displayName(), m_unreadCount);
|
|
||||||
// XXX: Would not be needed if we did this in handleNewArticle() instead
|
|
||||||
m_manager->forwardFeedContentChanged(m_url);
|
|
||||||
|
|
||||||
saveItemsToDisk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Feed::handleArticleRead()
|
|
||||||
{
|
|
||||||
--m_unreadCount;
|
|
||||||
m_dirty = true;
|
|
||||||
m_manager->forwardFeedInfosChanged(m_url, displayName(), m_unreadCount);
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
|
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
|
||||||
*
|
|
||||||
* 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, arnaud@qbittorrent.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef RSSFEED_H
|
|
||||||
#define RSSFEED_H
|
|
||||||
|
|
||||||
#include <QHash>
|
|
||||||
#include <QSharedPointer>
|
|
||||||
#include <QVariantHash>
|
|
||||||
#include <QXmlStreamReader>
|
|
||||||
#include <QNetworkCookie>
|
|
||||||
|
|
||||||
#include "rssfile.h"
|
|
||||||
|
|
||||||
namespace Rss
|
|
||||||
{
|
|
||||||
class Folder;
|
|
||||||
class Feed;
|
|
||||||
class Manager;
|
|
||||||
class DownloadRuleList;
|
|
||||||
|
|
||||||
typedef QHash<QString, ArticlePtr> ArticleHash;
|
|
||||||
typedef QSharedPointer<Feed> FeedPtr;
|
|
||||||
typedef QList<FeedPtr> FeedList;
|
|
||||||
|
|
||||||
namespace Private
|
|
||||||
{
|
|
||||||
class Parser;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool articleDateRecentThan(const ArticlePtr &left, const ArticlePtr &right);
|
|
||||||
|
|
||||||
class Feed: public QObject, public File
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
Feed(const QString &url, Manager * manager);
|
|
||||||
~Feed();
|
|
||||||
|
|
||||||
bool refresh();
|
|
||||||
QString id() const;
|
|
||||||
void removeAllSettings();
|
|
||||||
void saveItemsToDisk();
|
|
||||||
bool isLoading() const;
|
|
||||||
QString title() const;
|
|
||||||
void rename(const QString &newName);
|
|
||||||
QString displayName() const;
|
|
||||||
QString url() const;
|
|
||||||
QString iconPath() const;
|
|
||||||
bool hasCustomIcon() const;
|
|
||||||
void setIconPath(const QString &pathHierarchy);
|
|
||||||
ArticlePtr getItem(const QString &guid) const;
|
|
||||||
uint count() const;
|
|
||||||
void markAsRead();
|
|
||||||
uint unreadCount() const;
|
|
||||||
ArticleList articleListByDateDesc() const;
|
|
||||||
const ArticleHash &articleHash() const;
|
|
||||||
ArticleList unreadArticleListByDateDesc() const;
|
|
||||||
void recheckRssItemsForDownload();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handleIconDownloadFinished(const QString &url, const QString &filePath);
|
|
||||||
void handleRssDownloadFinished(const QString &url, const QByteArray &data);
|
|
||||||
void handleRssDownloadFailed(const QString &url, const QString &error);
|
|
||||||
void handleFeedTitle(const QString &title);
|
|
||||||
void handleNewArticle(const QVariantHash &article);
|
|
||||||
void handleParsingFinished(const QString &error);
|
|
||||||
void handleArticleRead();
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class Manager;
|
|
||||||
|
|
||||||
QString iconUrl() const;
|
|
||||||
void loadItemsFromDisk();
|
|
||||||
void addArticle(const ArticlePtr &article);
|
|
||||||
void downloadArticleTorrentIfMatching(const ArticlePtr &article);
|
|
||||||
void deferredDownloadArticleTorrentIfMatching(const ArticlePtr &article);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Manager *m_manager;
|
|
||||||
Private::Parser *m_parser;
|
|
||||||
ArticleHash m_articles;
|
|
||||||
ArticleList m_articlesByDate; // Articles sorted by date (more recent first)
|
|
||||||
QString m_title;
|
|
||||||
QString m_url;
|
|
||||||
QString m_alias;
|
|
||||||
QString m_icon;
|
|
||||||
uint m_unreadCount;
|
|
||||||
bool m_dirty;
|
|
||||||
bool m_inErrorState;
|
|
||||||
bool m_loading;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // RSSFEED_H
|
|
|
@ -1,253 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
|
||||||
*
|
|
||||||
* 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, arnaud@qbittorrent.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include "base/iconprovider.h"
|
|
||||||
#include "base/bittorrent/session.h"
|
|
||||||
#include "rssmanager.h"
|
|
||||||
#include "rssfeed.h"
|
|
||||||
#include "rssarticle.h"
|
|
||||||
#include "rssfolder.h"
|
|
||||||
|
|
||||||
using namespace Rss;
|
|
||||||
|
|
||||||
Folder::Folder(const QString &name)
|
|
||||||
: m_name(name)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
uint Folder::unreadCount() const
|
|
||||||
{
|
|
||||||
uint nbUnread = 0;
|
|
||||||
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it)
|
|
||||||
nbUnread += it.value()->unreadCount();
|
|
||||||
|
|
||||||
return nbUnread;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Folder::removeChild(const QString &childId)
|
|
||||||
{
|
|
||||||
if (m_children.contains(childId)) {
|
|
||||||
FilePtr child = m_children.take(childId);
|
|
||||||
child->removeAllSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh All Children
|
|
||||||
bool Folder::refresh()
|
|
||||||
{
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
bool refreshed = false;
|
|
||||||
for ( ; it != itend; ++it) {
|
|
||||||
if (it.value()->refresh())
|
|
||||||
refreshed = true;
|
|
||||||
}
|
|
||||||
return refreshed;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticleList Folder::articleListByDateDesc() const
|
|
||||||
{
|
|
||||||
ArticleList news;
|
|
||||||
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it) {
|
|
||||||
int n = news.size();
|
|
||||||
news << it.value()->articleListByDateDesc();
|
|
||||||
std::inplace_merge(news.begin(), news.begin() + n, news.end(), articleDateRecentThan);
|
|
||||||
}
|
|
||||||
return news;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticleList Folder::unreadArticleListByDateDesc() const
|
|
||||||
{
|
|
||||||
ArticleList unreadNews;
|
|
||||||
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it) {
|
|
||||||
int n = unreadNews.size();
|
|
||||||
unreadNews << it.value()->unreadArticleListByDateDesc();
|
|
||||||
std::inplace_merge(unreadNews.begin(), unreadNews.begin() + n, unreadNews.end(), articleDateRecentThan);
|
|
||||||
}
|
|
||||||
return unreadNews;
|
|
||||||
}
|
|
||||||
|
|
||||||
FileList Folder::getContent() const
|
|
||||||
{
|
|
||||||
return m_children.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint Folder::getNbFeeds() const
|
|
||||||
{
|
|
||||||
uint nbFeeds = 0;
|
|
||||||
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it) {
|
|
||||||
if (FolderPtr folder = qSharedPointerDynamicCast<Folder>(it.value()))
|
|
||||||
nbFeeds += folder->getNbFeeds();
|
|
||||||
else
|
|
||||||
++nbFeeds; // Feed
|
|
||||||
}
|
|
||||||
return nbFeeds;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Folder::displayName() const
|
|
||||||
{
|
|
||||||
return m_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Folder::rename(const QString &newName)
|
|
||||||
{
|
|
||||||
if (m_name == newName) return;
|
|
||||||
|
|
||||||
Q_ASSERT(!m_parent->hasChild(newName));
|
|
||||||
if (!m_parent->hasChild(newName)) {
|
|
||||||
// Update parent
|
|
||||||
FilePtr folder = m_parent->m_children.take(m_name);
|
|
||||||
m_parent->m_children[newName] = folder;
|
|
||||||
// Actually rename
|
|
||||||
m_name = newName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Folder::markAsRead()
|
|
||||||
{
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it) {
|
|
||||||
it.value()->markAsRead();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FeedList Folder::getAllFeeds() const
|
|
||||||
{
|
|
||||||
FeedList streams;
|
|
||||||
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it) {
|
|
||||||
if (FeedPtr feed = qSharedPointerDynamicCast<Feed>(it.value()))
|
|
||||||
streams << feed;
|
|
||||||
else if (FolderPtr folder = qSharedPointerDynamicCast<Folder>(it.value()))
|
|
||||||
streams << folder->getAllFeeds();
|
|
||||||
}
|
|
||||||
return streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<QString, FeedPtr> Folder::getAllFeedsAsHash() const
|
|
||||||
{
|
|
||||||
QHash<QString, FeedPtr> ret;
|
|
||||||
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it) {
|
|
||||||
if (FeedPtr feed = qSharedPointerDynamicCast<Feed>(it.value())) {
|
|
||||||
qDebug() << Q_FUNC_INFO << feed->url();
|
|
||||||
ret[feed->url()] = feed;
|
|
||||||
}
|
|
||||||
else if (FolderPtr folder = qSharedPointerDynamicCast<Folder>(it.value())) {
|
|
||||||
ret.unite(folder->getAllFeedsAsHash());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Folder::addFile(const FilePtr &item)
|
|
||||||
{
|
|
||||||
Q_ASSERT(!m_children.contains(item->id()));
|
|
||||||
if (!m_children.contains(item->id())) {
|
|
||||||
m_children[item->id()] = item;
|
|
||||||
// Update parent
|
|
||||||
item->m_parent = this;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Folder::removeAllItems()
|
|
||||||
{
|
|
||||||
m_children.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
FilePtr Folder::child(const QString &childId)
|
|
||||||
{
|
|
||||||
return m_children.value(childId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Folder::removeAllSettings()
|
|
||||||
{
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it)
|
|
||||||
it.value()->removeAllSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Folder::saveItemsToDisk()
|
|
||||||
{
|
|
||||||
foreach (const FilePtr &child, m_children.values())
|
|
||||||
child->saveItemsToDisk();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Folder::id() const
|
|
||||||
{
|
|
||||||
return m_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Folder::iconPath() const
|
|
||||||
{
|
|
||||||
return IconProvider::instance()->getIconPath("inode-directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Folder::hasChild(const QString &childId)
|
|
||||||
{
|
|
||||||
return m_children.contains(childId);
|
|
||||||
}
|
|
||||||
|
|
||||||
FilePtr Folder::takeChild(const QString &childId)
|
|
||||||
{
|
|
||||||
return m_children.take(childId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Folder::recheckRssItemsForDownload()
|
|
||||||
{
|
|
||||||
FileHash::ConstIterator it = m_children.begin();
|
|
||||||
FileHash::ConstIterator itend = m_children.end();
|
|
||||||
for ( ; it != itend; ++it)
|
|
||||||
it.value()->recheckRssItemsForDownload();
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
|
||||||
*
|
|
||||||
* 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, arnaud@qbittorrent.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef RSSFOLDER_H
|
|
||||||
#define RSSFOLDER_H
|
|
||||||
|
|
||||||
#include <QHash>
|
|
||||||
#include <QSharedPointer>
|
|
||||||
|
|
||||||
#include "rssfile.h"
|
|
||||||
|
|
||||||
namespace Rss
|
|
||||||
{
|
|
||||||
class Folder;
|
|
||||||
class Feed;
|
|
||||||
class Manager;
|
|
||||||
|
|
||||||
typedef QHash<QString, FilePtr> FileHash;
|
|
||||||
typedef QSharedPointer<Feed> FeedPtr;
|
|
||||||
typedef QSharedPointer<Folder> FolderPtr;
|
|
||||||
typedef QList<FeedPtr> FeedList;
|
|
||||||
|
|
||||||
class Folder: public File
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit Folder(const QString &name = QString());
|
|
||||||
|
|
||||||
uint unreadCount() const;
|
|
||||||
uint getNbFeeds() const;
|
|
||||||
FileList getContent() const;
|
|
||||||
FeedList getAllFeeds() const;
|
|
||||||
QHash<QString, FeedPtr> getAllFeedsAsHash() const;
|
|
||||||
QString displayName() const;
|
|
||||||
QString id() const;
|
|
||||||
QString iconPath() const;
|
|
||||||
bool hasChild(const QString &childId);
|
|
||||||
ArticleList articleListByDateDesc() const;
|
|
||||||
ArticleList unreadArticleListByDateDesc() const;
|
|
||||||
|
|
||||||
void rename(const QString &newName);
|
|
||||||
void markAsRead();
|
|
||||||
bool refresh();
|
|
||||||
void removeAllSettings();
|
|
||||||
void saveItemsToDisk();
|
|
||||||
void recheckRssItemsForDownload();
|
|
||||||
void removeAllItems();
|
|
||||||
FilePtr child(const QString &childId);
|
|
||||||
FilePtr takeChild(const QString &childId);
|
|
||||||
bool addFile(const FilePtr &item);
|
|
||||||
void removeChild(const QString &childId);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_name;
|
|
||||||
FileHash m_children;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // RSSFOLDER_H
|
|
|
@ -1,218 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
|
||||||
*
|
|
||||||
* 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, arnaud@qbittorrent.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include "base/logger.h"
|
|
||||||
#include "base/preferences.h"
|
|
||||||
#include "rssfolder.h"
|
|
||||||
#include "rssfeed.h"
|
|
||||||
#include "rssarticle.h"
|
|
||||||
#include "rssdownloadrulelist.h"
|
|
||||||
#include "rssmanager.h"
|
|
||||||
|
|
||||||
static const int MSECS_PER_MIN = 60000;
|
|
||||||
|
|
||||||
using namespace Rss;
|
|
||||||
using namespace Rss::Private;
|
|
||||||
|
|
||||||
Manager::Manager(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_downloadRules(new DownloadRuleList)
|
|
||||||
, m_rootFolder(new Folder)
|
|
||||||
, m_workingThread(new QThread(this))
|
|
||||||
{
|
|
||||||
m_workingThread->start();
|
|
||||||
connect(&m_refreshTimer, SIGNAL(timeout()), SLOT(refresh()));
|
|
||||||
m_refreshInterval = Preferences::instance()->getRSSRefreshInterval();
|
|
||||||
m_refreshTimer.start(m_refreshInterval * MSECS_PER_MIN);
|
|
||||||
|
|
||||||
m_deferredDownloadTimer.setInterval(1);
|
|
||||||
m_deferredDownloadTimer.setSingleShot(true);
|
|
||||||
connect(&m_deferredDownloadTimer, SIGNAL(timeout()), SLOT(downloadNextArticleTorrentIfMatching()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Manager::~Manager()
|
|
||||||
{
|
|
||||||
qDebug("Deleting RSSManager...");
|
|
||||||
m_workingThread->quit();
|
|
||||||
m_workingThread->wait();
|
|
||||||
delete m_downloadRules;
|
|
||||||
m_rootFolder->saveItemsToDisk();
|
|
||||||
saveStreamList();
|
|
||||||
m_rootFolder.clear();
|
|
||||||
qDebug("RSSManager deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::updateRefreshInterval(uint val)
|
|
||||||
{
|
|
||||||
if (m_refreshInterval != val) {
|
|
||||||
m_refreshInterval = val;
|
|
||||||
m_refreshTimer.start(m_refreshInterval * 60000);
|
|
||||||
qDebug("New RSS refresh interval is now every %dmin", m_refreshInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::loadStreamList()
|
|
||||||
{
|
|
||||||
const Preferences *const pref = Preferences::instance();
|
|
||||||
const QStringList streamsUrl = pref->getRssFeedsUrls();
|
|
||||||
const QStringList aliases = pref->getRssFeedsAliases();
|
|
||||||
if (streamsUrl.size() != aliases.size()) {
|
|
||||||
Logger::instance()->addMessage("Corrupted RSS list, not loading it.", Log::WARNING);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint i = 0;
|
|
||||||
qDebug() << Q_FUNC_INFO << streamsUrl;
|
|
||||||
foreach (QString s, streamsUrl) {
|
|
||||||
QStringList path = s.split("\\", QString::SkipEmptyParts);
|
|
||||||
if (path.empty()) continue;
|
|
||||||
|
|
||||||
const QString feedUrl = path.takeLast();
|
|
||||||
qDebug() << "Feed URL:" << feedUrl;
|
|
||||||
// Create feed path (if it does not exists)
|
|
||||||
FolderPtr feedParent = m_rootFolder;
|
|
||||||
foreach (const QString &folderName, path) {
|
|
||||||
if (!feedParent->hasChild(folderName)) {
|
|
||||||
qDebug() << "Adding parent folder:" << folderName;
|
|
||||||
FolderPtr folder(new Folder(folderName));
|
|
||||||
feedParent->addFile(folder);
|
|
||||||
feedParent = folder;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
feedParent = qSharedPointerDynamicCast<Folder>(feedParent->child(folderName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Create feed
|
|
||||||
qDebug() << "Adding feed to parent folder";
|
|
||||||
FeedPtr stream(new Feed(feedUrl, this));
|
|
||||||
feedParent->addFile(stream);
|
|
||||||
const QString &alias = aliases[i];
|
|
||||||
if (!alias.isEmpty())
|
|
||||||
stream->rename(alias);
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
qDebug("NB RSS streams loaded: %d", streamsUrl.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::forwardFeedContentChanged(const QString &url)
|
|
||||||
{
|
|
||||||
emit feedContentChanged(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::forwardFeedInfosChanged(const QString &url, const QString &displayName, uint unreadCount)
|
|
||||||
{
|
|
||||||
emit feedInfosChanged(url, displayName, unreadCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::forwardFeedIconChanged(const QString &url, const QString &iconPath)
|
|
||||||
{
|
|
||||||
emit feedIconChanged(url, iconPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::moveFile(const FilePtr &file, const FolderPtr &destinationFolder)
|
|
||||||
{
|
|
||||||
Folder *srcFolder = file->parentFolder();
|
|
||||||
if (destinationFolder != srcFolder) {
|
|
||||||
// Remove reference in old folder
|
|
||||||
srcFolder->takeChild(file->id());
|
|
||||||
// add to new Folder
|
|
||||||
destinationFolder->addFile(file);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qDebug("Nothing to move, same destination folder");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::saveStreamList() const
|
|
||||||
{
|
|
||||||
QStringList streamsUrl;
|
|
||||||
QStringList aliases;
|
|
||||||
FeedList streams = m_rootFolder->getAllFeeds();
|
|
||||||
foreach (const FeedPtr &stream, streams) {
|
|
||||||
// This backslash has nothing to do with path handling
|
|
||||||
QString streamPath = stream->pathHierarchy().join("\\");
|
|
||||||
if (streamPath.isNull())
|
|
||||||
streamPath = "";
|
|
||||||
qDebug("Saving stream path: %s", qPrintable(streamPath));
|
|
||||||
streamsUrl << streamPath;
|
|
||||||
aliases << stream->displayName();
|
|
||||||
}
|
|
||||||
Preferences *const pref = Preferences::instance();
|
|
||||||
pref->setRssFeedsUrls(streamsUrl);
|
|
||||||
pref->setRssFeedsAliases(aliases);
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadRuleList *Manager::downloadRules() const
|
|
||||||
{
|
|
||||||
Q_ASSERT(m_downloadRules);
|
|
||||||
return m_downloadRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderPtr Manager::rootFolder() const
|
|
||||||
{
|
|
||||||
return m_rootFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
QThread *Manager::workingThread() const
|
|
||||||
{
|
|
||||||
return m_workingThread;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::refresh()
|
|
||||||
{
|
|
||||||
m_rootFolder->refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::downloadArticleTorrentIfMatching(const QString &url, const ArticlePtr &article)
|
|
||||||
{
|
|
||||||
m_deferredDownloads.append(qMakePair(url, article));
|
|
||||||
m_deferredDownloadTimer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Manager::downloadNextArticleTorrentIfMatching()
|
|
||||||
{
|
|
||||||
if (m_deferredDownloads.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Schedule to process the next article (if any)
|
|
||||||
m_deferredDownloadTimer.start();
|
|
||||||
|
|
||||||
QPair<QString, ArticlePtr> urlArticle(m_deferredDownloads.takeFirst());
|
|
||||||
FeedList streams = m_rootFolder->getAllFeeds();
|
|
||||||
foreach (const FeedPtr &stream, streams) {
|
|
||||||
if (stream->url() == urlArticle.first) {
|
|
||||||
stream->deferredDownloadArticleTorrentIfMatching(urlArticle.second);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
|
||||||
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
|
||||||
* Copyright (C) 2010 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
|
||||||
*
|
|
||||||
* 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, arnaud@qbittorrent.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef RSSMANAGER_H
|
|
||||||
#define RSSMANAGER_H
|
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QPair>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QSharedPointer>
|
|
||||||
#include <QThread>
|
|
||||||
|
|
||||||
namespace Rss
|
|
||||||
{
|
|
||||||
class Article;
|
|
||||||
class DownloadRuleList;
|
|
||||||
class File;
|
|
||||||
class Folder;
|
|
||||||
class Feed;
|
|
||||||
class Manager;
|
|
||||||
|
|
||||||
typedef QSharedPointer<Article> ArticlePtr;
|
|
||||||
typedef QSharedPointer<File> FilePtr;
|
|
||||||
typedef QSharedPointer<Folder> FolderPtr;
|
|
||||||
typedef QSharedPointer<Feed> FeedPtr;
|
|
||||||
|
|
||||||
typedef QSharedPointer<Manager> ManagerPtr;
|
|
||||||
|
|
||||||
class Manager: public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit Manager(QObject *parent = 0);
|
|
||||||
~Manager();
|
|
||||||
|
|
||||||
DownloadRuleList *downloadRules() const;
|
|
||||||
FolderPtr rootFolder() const;
|
|
||||||
QThread *workingThread() const;
|
|
||||||
void downloadArticleTorrentIfMatching(const QString &url, const ArticlePtr &article);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void refresh();
|
|
||||||
void loadStreamList();
|
|
||||||
void saveStreamList() const;
|
|
||||||
void forwardFeedContentChanged(const QString &url);
|
|
||||||
void forwardFeedInfosChanged(const QString &url, const QString &displayName, uint unreadCount);
|
|
||||||
void forwardFeedIconChanged(const QString &url, const QString &iconPath);
|
|
||||||
void moveFile(const FilePtr &file, const FolderPtr &destinationFolder);
|
|
||||||
void updateRefreshInterval(uint val);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void feedContentChanged(const QString &url);
|
|
||||||
void feedInfosChanged(const QString &url, const QString &displayName, uint unreadCount);
|
|
||||||
void feedIconChanged(const QString &url, const QString &iconPath);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void downloadNextArticleTorrentIfMatching();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QTimer m_refreshTimer;
|
|
||||||
uint m_refreshInterval;
|
|
||||||
DownloadRuleList *m_downloadRules;
|
|
||||||
FolderPtr m_rootFolder;
|
|
||||||
QThread *m_workingThread;
|
|
||||||
QTimer m_deferredDownloadTimer;
|
|
||||||
QList<QPair<QString, ArticlePtr>> m_deferredDownloads;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // RSSMANAGER_H
|
|
|
@ -28,33 +28,31 @@
|
||||||
|
|
||||||
#include "tristatebool.h"
|
#include "tristatebool.h"
|
||||||
|
|
||||||
TriStateBool::TriStateBool()
|
const TriStateBool TriStateBool::Undefined(-1);
|
||||||
: m_value(Undefined)
|
const TriStateBool TriStateBool::False(0);
|
||||||
{
|
const TriStateBool TriStateBool::True(1);
|
||||||
}
|
|
||||||
|
|
||||||
TriStateBool::TriStateBool(bool b)
|
TriStateBool::operator int() const
|
||||||
{
|
|
||||||
m_value = (b ? True : False);
|
|
||||||
}
|
|
||||||
|
|
||||||
TriStateBool::TriStateBool(TriStateBool::ValueType value)
|
|
||||||
: m_value(Undefined)
|
|
||||||
{
|
|
||||||
switch (value) {
|
|
||||||
case Undefined:
|
|
||||||
case True:
|
|
||||||
case False:
|
|
||||||
m_value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TriStateBool::operator bool() const
|
|
||||||
{
|
|
||||||
return (m_value == True);
|
|
||||||
}
|
|
||||||
|
|
||||||
TriStateBool::operator ValueType() const
|
|
||||||
{
|
{
|
||||||
return m_value;
|
return m_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TriStateBool::TriStateBool(int value)
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
m_value = -1;
|
||||||
|
else if (value > 0)
|
||||||
|
m_value = 1;
|
||||||
|
else
|
||||||
|
m_value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TriStateBool::operator==(const TriStateBool &other) const
|
||||||
|
{
|
||||||
|
return (m_value == other.m_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TriStateBool::operator!=(const TriStateBool &other) const
|
||||||
|
{
|
||||||
|
return !operator==(other);
|
||||||
|
}
|
||||||
|
|
|
@ -32,22 +32,22 @@
|
||||||
class TriStateBool
|
class TriStateBool
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum ValueType
|
static const TriStateBool Undefined;
|
||||||
{
|
static const TriStateBool False;
|
||||||
Undefined = -1,
|
static const TriStateBool True;
|
||||||
False = 0,
|
|
||||||
True = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
TriStateBool();
|
TriStateBool() = default;
|
||||||
TriStateBool(bool b);
|
TriStateBool(const TriStateBool &other) = default;
|
||||||
TriStateBool(ValueType value);
|
|
||||||
|
|
||||||
operator ValueType() const;
|
explicit TriStateBool(int value);
|
||||||
operator bool() const;
|
explicit operator int() const;
|
||||||
|
|
||||||
|
TriStateBool &operator=(const TriStateBool &other) = default;
|
||||||
|
bool operator==(const TriStateBool &other) const;
|
||||||
|
bool operator!=(const TriStateBool &other) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ValueType m_value;
|
int m_value = -1; // Undefined by default
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // TRISTATEBOOL_H
|
#endif // TRISTATEBOOL_H
|
||||||
|
|
|
@ -214,12 +214,12 @@ bool Utils::Fs::sameFiles(const QString& path1, const QString& path2)
|
||||||
return same;
|
return same;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Utils::Fs::toValidFileSystemName(const QString &name, bool allowSeparators)
|
QString Utils::Fs::toValidFileSystemName(const QString &name, bool allowSeparators, const QString &pad)
|
||||||
{
|
{
|
||||||
QRegExp regex(allowSeparators ? "[:?\"*<>|]+" : "[\\\\/:?\"*<>|]+");
|
QRegExp regex(allowSeparators ? "[:?\"*<>|]+" : "[\\\\/:?\"*<>|]+");
|
||||||
|
|
||||||
QString validName = name.trimmed();
|
QString validName = name.trimmed();
|
||||||
validName.replace(regex, " ");
|
validName.replace(regex, pad);
|
||||||
qDebug() << "toValidFileSystemName:" << name << "=>" << validName;
|
qDebug() << "toValidFileSystemName:" << name << "=>" << validName;
|
||||||
|
|
||||||
return validName;
|
return validName;
|
||||||
|
|
|
@ -48,7 +48,8 @@ namespace Utils
|
||||||
QString folderName(const QString& file_path);
|
QString folderName(const QString& file_path);
|
||||||
qint64 computePathSize(const QString& path);
|
qint64 computePathSize(const QString& path);
|
||||||
bool sameFiles(const QString& path1, const QString& path2);
|
bool sameFiles(const QString& path1, const QString& path2);
|
||||||
QString toValidFileSystemName(const QString &name, bool allowSeparators = false);
|
QString toValidFileSystemName(const QString &name, bool allowSeparators = false
|
||||||
|
, const QString &pad = QLatin1String(" "));
|
||||||
bool isValidFileSystemName(const QString& name, bool allowSeparators = false);
|
bool isValidFileSystemName(const QString& name, bool allowSeparators = false);
|
||||||
qulonglong freeDiskSpaceOnPath(const QString &path);
|
qulonglong freeDiskSpaceOnPath(const QString &path);
|
||||||
QString branchPath(const QString& file_path, QString* removed = 0);
|
QString branchPath(const QString& file_path, QString* removed = 0);
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QSysInfo>
|
#include <QSysInfo>
|
||||||
#include <boost/version.hpp>
|
#include <boost/version.hpp>
|
||||||
|
@ -503,9 +504,10 @@ QList<bool> Utils::Misc::boolListfromStringList(const QStringList &l)
|
||||||
|
|
||||||
bool Utils::Misc::isUrl(const QString &s)
|
bool Utils::Misc::isUrl(const QString &s)
|
||||||
{
|
{
|
||||||
const QString scheme = QUrl(s).scheme();
|
static const QRegularExpression reURLScheme(
|
||||||
QRegExp is_url("http[s]?|ftp", Qt::CaseInsensitive);
|
"http[s]?|ftp", QRegularExpression::CaseInsensitiveOption);
|
||||||
return is_url.exactMatch(scheme);
|
|
||||||
|
return reURLScheme.match(QUrl(s).scheme()).hasMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Utils::Misc::parseHtmlLinks(const QString &raw_text)
|
QString Utils::Misc::parseHtmlLinks(const QString &raw_text)
|
||||||
|
|
|
@ -641,8 +641,8 @@ void AddNewTorrentDialog::accept()
|
||||||
if (m_contentModel)
|
if (m_contentModel)
|
||||||
params.filePriorities = m_contentModel->model()->getFilePriorities();
|
params.filePriorities = m_contentModel->model()->getFilePriorities();
|
||||||
|
|
||||||
params.addPaused = !ui->startTorrentCheckBox->isChecked();
|
params.addPaused = TriStateBool(!ui->startTorrentCheckBox->isChecked());
|
||||||
params.createSubfolder = ui->createSubfolderCheckBox->isChecked();
|
params.createSubfolder = !ui->startTorrentCheckBox->isChecked();
|
||||||
|
|
||||||
QString savePath = ui->savePathComboBox->itemData(ui->savePathComboBox->currentIndex()).toString();
|
QString savePath = ui->savePathComboBox->itemData(ui->savePathComboBox->currentIndex()).toString();
|
||||||
if (ui->comboTTM->currentIndex() != 1) { // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode.
|
if (ui->comboTTM->currentIndex() != 1) { // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode.
|
||||||
|
|
|
@ -2,7 +2,6 @@ INCLUDEPATH += $$PWD
|
||||||
|
|
||||||
include(lineedit/lineedit.pri)
|
include(lineedit/lineedit.pri)
|
||||||
include(properties/properties.pri)
|
include(properties/properties.pri)
|
||||||
include(rss/rss.pri)
|
|
||||||
include(powermanagement/powermanagement.pri)
|
include(powermanagement/powermanagement.pri)
|
||||||
unix:!macx:dbus: include(qtnotify/qtnotify.pri)
|
unix:!macx:dbus: include(qtnotify/qtnotify.pri)
|
||||||
|
|
||||||
|
@ -52,7 +51,12 @@ HEADERS += \
|
||||||
$$PWD/cookiesdialog.h \
|
$$PWD/cookiesdialog.h \
|
||||||
$$PWD/categoryfiltermodel.h \
|
$$PWD/categoryfiltermodel.h \
|
||||||
$$PWD/categoryfilterwidget.h \
|
$$PWD/categoryfilterwidget.h \
|
||||||
$$PWD/banlistoptions.h
|
$$PWD/banlistoptions.h \
|
||||||
|
$$PWD/rss/rsswidget.h \
|
||||||
|
$$PWD/rss/articlelistwidget.h \
|
||||||
|
$$PWD/rss/feedlistwidget.h \
|
||||||
|
$$PWD/rss/automatedrssdownloader.h \
|
||||||
|
$$PWD/rss/htmlbrowser.h
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
$$PWD/mainwindow.cpp \
|
$$PWD/mainwindow.cpp \
|
||||||
|
@ -95,7 +99,12 @@ SOURCES += \
|
||||||
$$PWD/cookiesdialog.cpp \
|
$$PWD/cookiesdialog.cpp \
|
||||||
$$PWD/categoryfiltermodel.cpp \
|
$$PWD/categoryfiltermodel.cpp \
|
||||||
$$PWD/categoryfilterwidget.cpp \
|
$$PWD/categoryfilterwidget.cpp \
|
||||||
$$PWD/banlistoptions.cpp
|
$$PWD/banlistoptions.cpp \
|
||||||
|
$$PWD/rss/rsswidget.cpp \
|
||||||
|
$$PWD/rss/articlelistwidget.cpp \
|
||||||
|
$$PWD/rss/feedlistwidget.cpp \
|
||||||
|
$$PWD/rss/automatedrssdownloader.cpp \
|
||||||
|
$$PWD/rss/htmlbrowser.cpp
|
||||||
|
|
||||||
win32|macx {
|
win32|macx {
|
||||||
HEADERS += $$PWD/programupdater.h
|
HEADERS += $$PWD/programupdater.h
|
||||||
|
@ -123,6 +132,8 @@ FORMS += \
|
||||||
$$PWD/search/pluginsourcedlg.ui \
|
$$PWD/search/pluginsourcedlg.ui \
|
||||||
$$PWD/search/searchtab.ui \
|
$$PWD/search/searchtab.ui \
|
||||||
$$PWD/cookiesdialog.ui \
|
$$PWD/cookiesdialog.ui \
|
||||||
$$PWD/banlistoptions.ui
|
$$PWD/banlistoptions.ui \
|
||||||
|
$$PWD/rss/rsswidget.ui \
|
||||||
|
$$PWD/rss/automatedrssdownloader.ui
|
||||||
|
|
||||||
RESOURCES += $$PWD/about.qrc
|
RESOURCES += $$PWD/about.qrc
|
||||||
|
|
|
@ -64,6 +64,8 @@
|
||||||
#include "base/bittorrent/session.h"
|
#include "base/bittorrent/session.h"
|
||||||
#include "base/bittorrent/sessionstatus.h"
|
#include "base/bittorrent/sessionstatus.h"
|
||||||
#include "base/bittorrent/torrenthandle.h"
|
#include "base/bittorrent/torrenthandle.h"
|
||||||
|
#include "base/rss/rss_folder.h"
|
||||||
|
#include "base/rss/rss_session.h"
|
||||||
|
|
||||||
#include "application.h"
|
#include "application.h"
|
||||||
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
||||||
|
@ -86,7 +88,7 @@
|
||||||
#include "transferlistfilterswidget.h"
|
#include "transferlistfilterswidget.h"
|
||||||
#include "propertieswidget.h"
|
#include "propertieswidget.h"
|
||||||
#include "statusbar.h"
|
#include "statusbar.h"
|
||||||
#include "rss_imp.h"
|
#include "rss/rsswidget.h"
|
||||||
#include "about_imp.h"
|
#include "about_imp.h"
|
||||||
#include "optionsdlg.h"
|
#include "optionsdlg.h"
|
||||||
#include "trackerlogin.h"
|
#include "trackerlogin.h"
|
||||||
|
@ -296,7 +298,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
// View settings
|
// View settings
|
||||||
m_ui->actionTopToolBar->setChecked(pref->isToolbarDisplayed());
|
m_ui->actionTopToolBar->setChecked(pref->isToolbarDisplayed());
|
||||||
m_ui->actionSpeedInTitleBar->setChecked(pref->speedInTitleBar());
|
m_ui->actionSpeedInTitleBar->setChecked(pref->speedInTitleBar());
|
||||||
m_ui->actionRSSReader->setChecked(pref->isRSSEnabled());
|
m_ui->actionRSSReader->setChecked(pref->isRSSWidgetEnabled());
|
||||||
m_ui->actionSearchWidget->setChecked(pref->isSearchEnabled());
|
m_ui->actionSearchWidget->setChecked(pref->isSearchEnabled());
|
||||||
m_ui->actionExecutionLogs->setChecked(isExecutionLogEnabled());
|
m_ui->actionExecutionLogs->setChecked(isExecutionLogEnabled());
|
||||||
|
|
||||||
|
@ -600,14 +602,19 @@ void MainWindow::on_actionLock_triggered()
|
||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::handleRSSUnreadCountUpdated(int count)
|
||||||
|
{
|
||||||
|
m_tabs->setTabText(m_tabs->indexOf(m_rssWidget), tr("RSS (%1)").arg(count));
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::displayRSSTab(bool enable)
|
void MainWindow::displayRSSTab(bool enable)
|
||||||
{
|
{
|
||||||
if (enable) {
|
if (enable) {
|
||||||
// RSS tab
|
// RSS tab
|
||||||
if (!m_rssWidget) {
|
if (!m_rssWidget) {
|
||||||
m_rssWidget = new RSSImp(m_tabs);
|
m_rssWidget = new RSSWidget(m_tabs);
|
||||||
connect(m_rssWidget, SIGNAL(updateRSSCount(int)), this, SLOT(updateRSSTabLabel(int)));
|
connect(m_rssWidget.data(), &RSSWidget::unreadCountUpdated, this, &MainWindow::handleRSSUnreadCountUpdated);
|
||||||
int indexTab = m_tabs->addTab(m_rssWidget, tr("RSS (%1)").arg(0));
|
int indexTab = m_tabs->addTab(m_rssWidget, tr("RSS (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount()));
|
||||||
m_tabs->setTabIcon(indexTab, GuiIconProvider::instance()->getIcon("application-rss+xml"));
|
m_tabs->setTabIcon(indexTab, GuiIconProvider::instance()->getIcon("application-rss+xml"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -616,11 +623,6 @@ void MainWindow::displayRSSTab(bool enable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::updateRSSTabLabel(int count)
|
|
||||||
{
|
|
||||||
m_tabs->setTabText(m_tabs->indexOf(m_rssWidget), tr("RSS (%1)").arg(count));
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::displaySearchTab(bool enable)
|
void MainWindow::displaySearchTab(bool enable)
|
||||||
{
|
{
|
||||||
Preferences::instance()->setSearchEnabled(enable);
|
Preferences::instance()->setSearchEnabled(enable);
|
||||||
|
@ -685,6 +687,10 @@ void MainWindow::cleanup()
|
||||||
{
|
{
|
||||||
writeSettings();
|
writeSettings();
|
||||||
|
|
||||||
|
// delete RSSWidget explicitly to avoid crash in
|
||||||
|
// handleRSSUnreadCountUpdated() at application shutdown
|
||||||
|
delete m_rssWidget;
|
||||||
|
|
||||||
delete m_executableWatcher;
|
delete m_executableWatcher;
|
||||||
if (m_systrayCreator)
|
if (m_systrayCreator)
|
||||||
m_systrayCreator->stop();
|
m_systrayCreator->stop();
|
||||||
|
@ -1502,7 +1508,7 @@ void MainWindow::on_actionSpeedInTitleBar_triggered()
|
||||||
|
|
||||||
void MainWindow::on_actionRSSReader_triggered()
|
void MainWindow::on_actionRSSReader_triggered()
|
||||||
{
|
{
|
||||||
Preferences::instance()->setRSSEnabled(m_ui->actionRSSReader->isChecked());
|
Preferences::instance()->setRSSWidgetVisible(m_ui->actionRSSReader->isChecked());
|
||||||
displayRSSTab(m_ui->actionRSSReader->isChecked());
|
displayRSSTab(m_ui->actionRSSReader->isChecked());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class QTimer;
|
||||||
|
|
||||||
class downloadFromURL;
|
class downloadFromURL;
|
||||||
class SearchWidget;
|
class SearchWidget;
|
||||||
class RSSImp;
|
class RSSWidget;
|
||||||
class about;
|
class about;
|
||||||
class OptionsDialog;
|
class OptionsDialog;
|
||||||
class TransferListWidget;
|
class TransferListWidget;
|
||||||
|
@ -138,7 +138,6 @@ private slots:
|
||||||
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
|
||||||
void handleUpdateCheckFinished(bool updateAvailable, QString newVersion, bool invokedByUser);
|
void handleUpdateCheckFinished(bool updateAvailable, QString newVersion, bool invokedByUser);
|
||||||
#endif
|
#endif
|
||||||
void updateRSSTabLabel(int count);
|
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
void pythonDownloadSuccess(const QString &url, const QString &filePath);
|
void pythonDownloadSuccess(const QString &url, const QString &filePath);
|
||||||
|
@ -151,6 +150,7 @@ private slots:
|
||||||
void downloadFromURLList(const QStringList &urlList);
|
void downloadFromURLList(const QStringList &urlList);
|
||||||
void updateAltSpeedsBtn(bool alternative);
|
void updateAltSpeedsBtn(bool alternative);
|
||||||
void updateNbTorrents();
|
void updateNbTorrents();
|
||||||
|
void handleRSSUnreadCountUpdated(int count);
|
||||||
|
|
||||||
void on_actionSearchWidget_triggered();
|
void on_actionSearchWidget_triggered();
|
||||||
void on_actionRSSReader_triggered();
|
void on_actionRSSReader_triggered();
|
||||||
|
@ -211,7 +211,7 @@ private:
|
||||||
QList<QPair<BitTorrent::TorrentHandle *, QString >> m_unauthenticatedTrackers; // Still needed?
|
QList<QPair<BitTorrent::TorrentHandle *, QString >> m_unauthenticatedTrackers; // Still needed?
|
||||||
// GUI related
|
// GUI related
|
||||||
bool m_posInitialized;
|
bool m_posInitialized;
|
||||||
QTabWidget *m_tabs;
|
QPointer<QTabWidget> m_tabs;
|
||||||
StatusBar *m_statusBar;
|
StatusBar *m_statusBar;
|
||||||
QPointer<OptionsDialog> m_options;
|
QPointer<OptionsDialog> m_options;
|
||||||
QPointer<about> m_aboutDlg;
|
QPointer<about> m_aboutDlg;
|
||||||
|
@ -235,7 +235,7 @@ private:
|
||||||
QAction *m_prioSeparatorMenu;
|
QAction *m_prioSeparatorMenu;
|
||||||
QSplitter *m_splitter;
|
QSplitter *m_splitter;
|
||||||
QPointer<SearchWidget> m_searchWidget;
|
QPointer<SearchWidget> m_searchWidget;
|
||||||
QPointer<RSSImp> m_rssWidget;
|
QPointer<RSSWidget> m_rssWidget;
|
||||||
QPointer<ExecutionLog> m_executionLog;
|
QPointer<ExecutionLog> m_executionLog;
|
||||||
// Power Management
|
// Power Management
|
||||||
PowerManagement *m_pwr;
|
PowerManagement *m_pwr;
|
||||||
|
|
|
@ -54,6 +54,8 @@
|
||||||
#include "base/net/portforwarder.h"
|
#include "base/net/portforwarder.h"
|
||||||
#include "base/net/proxyconfigurationmanager.h"
|
#include "base/net/proxyconfigurationmanager.h"
|
||||||
#include "base/preferences.h"
|
#include "base/preferences.h"
|
||||||
|
#include "base/rss/rss_autodownloader.h"
|
||||||
|
#include "base/rss/rss_session.h"
|
||||||
#include "base/scanfoldersmodel.h"
|
#include "base/scanfoldersmodel.h"
|
||||||
#include "base/torrentfileguard.h"
|
#include "base/torrentfileguard.h"
|
||||||
#include "base/unicodestrings.h"
|
#include "base/unicodestrings.h"
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
#include "base/utils/random.h"
|
#include "base/utils/random.h"
|
||||||
#include "addnewtorrentdialog.h"
|
#include "addnewtorrentdialog.h"
|
||||||
#include "advancedsettings.h"
|
#include "advancedsettings.h"
|
||||||
|
#include "rss/automatedrssdownloader.h"
|
||||||
#include "banlistoptions.h"
|
#include "banlistoptions.h"
|
||||||
#include "guiiconprovider.h"
|
#include "guiiconprovider.h"
|
||||||
#include "scanfoldersdelegate.h"
|
#include "scanfoldersdelegate.h"
|
||||||
|
@ -84,6 +87,7 @@ OptionsDialog::OptionsDialog(QWidget *parent)
|
||||||
m_ui->tabSelection->item(TAB_CONNECTION)->setIcon(GuiIconProvider::instance()->getIcon("network-wired"));
|
m_ui->tabSelection->item(TAB_CONNECTION)->setIcon(GuiIconProvider::instance()->getIcon("network-wired"));
|
||||||
m_ui->tabSelection->item(TAB_DOWNLOADS)->setIcon(GuiIconProvider::instance()->getIcon("folder-download"));
|
m_ui->tabSelection->item(TAB_DOWNLOADS)->setIcon(GuiIconProvider::instance()->getIcon("folder-download"));
|
||||||
m_ui->tabSelection->item(TAB_SPEED)->setIcon(GuiIconProvider::instance()->getIcon("speedometer", "chronometer"));
|
m_ui->tabSelection->item(TAB_SPEED)->setIcon(GuiIconProvider::instance()->getIcon("speedometer", "chronometer"));
|
||||||
|
m_ui->tabSelection->item(TAB_RSS)->setIcon(GuiIconProvider::instance()->getIcon("rss-config", "application-rss+xml"));
|
||||||
#ifndef DISABLE_WEBUI
|
#ifndef DISABLE_WEBUI
|
||||||
m_ui->tabSelection->item(TAB_WEBUI)->setIcon(GuiIconProvider::instance()->getIcon("network-server"));
|
m_ui->tabSelection->item(TAB_WEBUI)->setIcon(GuiIconProvider::instance()->getIcon("network-server"));
|
||||||
#else
|
#else
|
||||||
|
@ -335,6 +339,15 @@ OptionsDialog::OptionsDialog(QWidget *parent)
|
||||||
connect(m_ui->DNSUsernameTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton()));
|
connect(m_ui->DNSUsernameTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton()));
|
||||||
connect(m_ui->DNSPasswordTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton()));
|
connect(m_ui->DNSPasswordTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton()));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// RSS tab
|
||||||
|
connect(m_ui->checkRSSEnable, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton);
|
||||||
|
connect(m_ui->checkRSSAutoDownloaderEnable, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton);
|
||||||
|
connect(m_ui->spinRSSRefreshInterval, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged)
|
||||||
|
, this, &OptionsDialog::enableApplyButton);
|
||||||
|
connect(m_ui->spinRSSMaxArticlesPerFeed, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &OptionsDialog::enableApplyButton);
|
||||||
|
connect(m_ui->btnEditRules, &QPushButton::clicked, [this]() { AutomatedRssDownloader(this).exec(); });
|
||||||
|
|
||||||
// Disable apply Button
|
// Disable apply Button
|
||||||
applyButton->setEnabled(false);
|
applyButton->setEnabled(false);
|
||||||
// Tab selection mechanism
|
// Tab selection mechanism
|
||||||
|
@ -500,6 +513,11 @@ void OptionsDialog::saveOptions()
|
||||||
app->setFileLoggerEnabled(m_ui->checkFileLog->isChecked());
|
app->setFileLoggerEnabled(m_ui->checkFileLog->isChecked());
|
||||||
// End General preferences
|
// End General preferences
|
||||||
|
|
||||||
|
RSS::Session::instance()->setRefreshInterval(m_ui->spinRSSRefreshInterval->value());
|
||||||
|
RSS::Session::instance()->setMaxArticlesPerFeed(m_ui->spinRSSMaxArticlesPerFeed->value());
|
||||||
|
RSS::Session::instance()->setProcessingEnabled(m_ui->checkRSSEnable->isChecked());
|
||||||
|
RSS::AutoDownloader::instance()->setProcessingEnabled(m_ui->checkRSSAutoDownloaderEnable->isChecked());
|
||||||
|
|
||||||
auto session = BitTorrent::Session::instance();
|
auto session = BitTorrent::Session::instance();
|
||||||
|
|
||||||
// Downloads preferences
|
// Downloads preferences
|
||||||
|
@ -712,6 +730,11 @@ void OptionsDialog::loadOptions()
|
||||||
m_ui->comboFileLogAgeType->setCurrentIndex(app->fileLoggerAgeType());
|
m_ui->comboFileLogAgeType->setCurrentIndex(app->fileLoggerAgeType());
|
||||||
// End General preferences
|
// End General preferences
|
||||||
|
|
||||||
|
m_ui->checkRSSEnable->setChecked(RSS::Session::instance()->isProcessingEnabled());
|
||||||
|
m_ui->checkRSSAutoDownloaderEnable->setChecked(RSS::AutoDownloader::instance()->isProcessingEnabled());
|
||||||
|
m_ui->spinRSSRefreshInterval->setValue(RSS::Session::instance()->refreshInterval());
|
||||||
|
m_ui->spinRSSMaxArticlesPerFeed->setValue(RSS::Session::instance()->maxArticlesPerFeed());
|
||||||
|
|
||||||
auto session = BitTorrent::Session::instance();
|
auto session = BitTorrent::Session::instance();
|
||||||
|
|
||||||
// Downloads preferences
|
// Downloads preferences
|
||||||
|
|
|
@ -68,6 +68,7 @@ private:
|
||||||
TAB_CONNECTION,
|
TAB_CONNECTION,
|
||||||
TAB_SPEED,
|
TAB_SPEED,
|
||||||
TAB_BITTORRENT,
|
TAB_BITTORRENT,
|
||||||
|
TAB_RSS,
|
||||||
TAB_WEBUI,
|
TAB_WEBUI,
|
||||||
TAB_ADVANCED
|
TAB_ADVANCED
|
||||||
};
|
};
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
<enum>QListView::IconMode</enum>
|
<enum>QListView::IconMode</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="currentRow">
|
<property name="currentRow">
|
||||||
<number>0</number>
|
<number>-1</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -72,6 +72,11 @@
|
||||||
<string>BitTorrent</string>
|
<string>BitTorrent</string>
|
||||||
</property>
|
</property>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>RSS</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Web UI</string>
|
<string>Web UI</string>
|
||||||
|
@ -117,8 +122,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>497</width>
|
<width>470</width>
|
||||||
<height>880</height>
|
<height>783</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||||
|
@ -673,8 +678,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>586</width>
|
<width>470</width>
|
||||||
<height>1118</height>
|
<height>994</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
@ -1358,8 +1363,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>457</width>
|
<width>470</width>
|
||||||
<height>672</height>
|
<height>619</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_20">
|
<layout class="QVBoxLayout" name="verticalLayout_20">
|
||||||
|
@ -1838,8 +1843,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>388</width>
|
<width>487</width>
|
||||||
<height>452</height>
|
<height>542</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
|
@ -2225,8 +2230,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>574</width>
|
<width>487</width>
|
||||||
<height>534</height>
|
<height>542</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||||
|
@ -2598,6 +2603,125 @@
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QWidget" name="tabRSSPage">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_25">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupRSSReader">
|
||||||
|
<property name="title">
|
||||||
|
<string>RSS Reader</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_26">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkRSSEnable">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable fetching RSS feeds</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_5">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_11">
|
||||||
|
<property name="text">
|
||||||
|
<string>Feeds refresh interval:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_12">
|
||||||
|
<property name="text">
|
||||||
|
<string>Maximum number of articles per feed:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QSpinBox" name="spinRSSRefreshInterval">
|
||||||
|
<property name="suffix">
|
||||||
|
<string> min</string>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>999999</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QSpinBox" name="spinRSSMaxArticlesPerFeed">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>9999</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<spacer name="horizontalSpacer_6">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupRSSAutoDownloader">
|
||||||
|
<property name="title">
|
||||||
|
<string>RSS Torrent Auto Downloader</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_21">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkRSSAutoDownloaderEnable">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable auto downloading of RSS torrents</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="btnEditRules">
|
||||||
|
<property name="text">
|
||||||
|
<string>Edit auto downloading rules...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_5">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
<widget class="QWidget" name="tabWebuiPage">
|
<widget class="QWidget" name="tabWebuiPage">
|
||||||
<layout class="QVBoxLayout" name="tabWebuiPageLayout">
|
<layout class="QVBoxLayout" name="tabWebuiPageLayout">
|
||||||
<property name="leftMargin">
|
<property name="leftMargin">
|
||||||
|
@ -2622,8 +2746,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>438</width>
|
<width>487</width>
|
||||||
<height>543</height>
|
<height>542</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_23">
|
<layout class="QVBoxLayout" name="verticalLayout_23">
|
||||||
|
|
117
src/gui/rss/articlelistwidget.cpp
Normal file
117
src/gui/rss/articlelistwidget.cpp
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "articlelistwidget.h"
|
||||||
|
|
||||||
|
#include <QListWidgetItem>
|
||||||
|
|
||||||
|
#include "base/rss/rss_article.h"
|
||||||
|
#include "base/rss/rss_item.h"
|
||||||
|
|
||||||
|
ArticleListWidget::ArticleListWidget(QWidget *parent)
|
||||||
|
: QListWidget(parent)
|
||||||
|
{
|
||||||
|
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||||
|
}
|
||||||
|
|
||||||
|
RSS::Article *ArticleListWidget::getRSSArticle(QListWidgetItem *item) const
|
||||||
|
{
|
||||||
|
Q_ASSERT(item);
|
||||||
|
return reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
|
||||||
|
}
|
||||||
|
|
||||||
|
QListWidgetItem *ArticleListWidget::mapRSSArticle(RSS::Article *rssArticle) const
|
||||||
|
{
|
||||||
|
return m_rssArticleToListItemMapping.value(rssArticle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArticleListWidget::setRSSItem(RSS::Item *rssItem, bool unreadOnly)
|
||||||
|
{
|
||||||
|
// Clear the list first
|
||||||
|
clear();
|
||||||
|
if (m_rssItem)
|
||||||
|
m_rssItem->disconnect(this);
|
||||||
|
|
||||||
|
m_unreadOnly = unreadOnly;
|
||||||
|
m_rssItem = rssItem;
|
||||||
|
if (!m_rssItem) return;
|
||||||
|
|
||||||
|
m_rssItem = rssItem;
|
||||||
|
connect(m_rssItem, &RSS::Item::newArticle, this, &ArticleListWidget::handleArticleAdded);
|
||||||
|
connect(m_rssItem, &RSS::Item::articleRead, this, &ArticleListWidget::handleArticleRead);
|
||||||
|
connect(m_rssItem, &RSS::Item::articleAboutToBeRemoved, this, &ArticleListWidget::handleArticleAboutToBeRemoved);
|
||||||
|
|
||||||
|
foreach (auto article, rssItem->articles()) {
|
||||||
|
if (!(m_unreadOnly && article->isRead()))
|
||||||
|
addItem(createItem(article));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArticleListWidget::handleArticleAdded(RSS::Article *rssArticle)
|
||||||
|
{
|
||||||
|
if (!(m_unreadOnly && rssArticle->isRead()))
|
||||||
|
addItem(createItem(rssArticle));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArticleListWidget::handleArticleRead(RSS::Article *rssArticle)
|
||||||
|
{
|
||||||
|
if (m_unreadOnly) {
|
||||||
|
delete m_rssArticleToListItemMapping.take(rssArticle);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
auto item = mapRSSArticle(rssArticle);
|
||||||
|
item->setData(Qt::ForegroundRole, QColor("grey"));
|
||||||
|
item->setData(Qt::DecorationRole, QIcon(":/icons/sphere.png"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArticleListWidget::handleArticleAboutToBeRemoved(RSS::Article *rssArticle)
|
||||||
|
{
|
||||||
|
delete m_rssArticleToListItemMapping.take(rssArticle);
|
||||||
|
}
|
||||||
|
|
||||||
|
QListWidgetItem *ArticleListWidget::createItem(RSS::Article *article)
|
||||||
|
{
|
||||||
|
Q_ASSERT(article);
|
||||||
|
QListWidgetItem *item = new QListWidgetItem;
|
||||||
|
|
||||||
|
item->setData(Qt::DisplayRole, article->title());
|
||||||
|
item->setData(Qt::UserRole, reinterpret_cast<quintptr>(article));
|
||||||
|
if (article->isRead()) {
|
||||||
|
item->setData(Qt::ForegroundRole, QColor("grey"));
|
||||||
|
item->setData(Qt::DecorationRole, QIcon(":/icons/sphere.png"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item->setData(Qt::ForegroundRole, QColor("blue"));
|
||||||
|
item->setData(Qt::DecorationRole, QIcon(":/icons/sphere2.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_rssArticleToListItemMapping.insert(article, item);
|
||||||
|
return item;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt4 and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2010 Christophe Dumez
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -24,35 +24,43 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact : chris@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef RSSSETTINGSDLG_H
|
#ifndef ARTICLELISTWIDGET_H
|
||||||
#define RSSSETTINGSDLG_H
|
#define ARTICLELISTWIDGET_H
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QHash>
|
||||||
|
#include <QListWidget>
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
namespace RSS
|
||||||
namespace Ui {
|
{
|
||||||
class RssSettingsDlg;
|
class Article;
|
||||||
|
class Item;
|
||||||
}
|
}
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
class RssSettingsDlg : public QDialog
|
class ArticleListWidget: public QListWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit RssSettingsDlg(QWidget *parent = 0);
|
explicit ArticleListWidget(QWidget *parent);
|
||||||
~RssSettingsDlg();
|
|
||||||
|
|
||||||
protected slots:
|
RSS::Article *getRSSArticle(QListWidgetItem *item) const;
|
||||||
void on_buttonBox_accepted();
|
QListWidgetItem *mapRSSArticle(RSS::Article *rssArticle) const;
|
||||||
|
|
||||||
|
void setRSSItem(RSS::Item *rssItem, bool unreadOnly = false);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleArticleAdded(RSS::Article *rssArticle);
|
||||||
|
void handleArticleRead(RSS::Article *rssArticle);
|
||||||
|
void handleArticleAboutToBeRemoved(RSS::Article *rssArticle);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::RssSettingsDlg *ui;
|
QListWidgetItem *createItem(RSS::Article *article);
|
||||||
|
|
||||||
|
RSS::Item *m_rssItem = nullptr;
|
||||||
|
bool m_unreadOnly = false;
|
||||||
|
QHash<RSS::Article *, QListWidgetItem *> m_rssArticleToListItemMapping;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // RSSSETTINGS_H
|
#endif // ARTICLELISTWIDGET_H
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt4 and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2010 Christophe Dumez
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -24,99 +25,85 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact : chris@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef AUTOMATEDRSSDOWNLOADER_H
|
#ifndef AUTOMATEDRSSDOWNLOADER_H
|
||||||
#define AUTOMATEDRSSDOWNLOADER_H
|
#define AUTOMATEDRSSDOWNLOADER_H
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QHideEvent>
|
#include <QHash>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QShortcut>
|
|
||||||
#include <QShowEvent>
|
|
||||||
#include <QString>
|
|
||||||
#include <QWeakPointer>
|
|
||||||
|
|
||||||
#include "base/rss/rssdownloadrule.h"
|
#include "base/rss/rss_autodownloadrule.h"
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
class AutomatedRssDownloader;
|
class AutomatedRssDownloader;
|
||||||
}
|
}
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
namespace Rss
|
|
||||||
{
|
|
||||||
class DownloadRuleList;
|
|
||||||
class Manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
class QListWidgetItem;
|
class QListWidgetItem;
|
||||||
class QRegularExpression;
|
class QRegularExpression;
|
||||||
QT_END_NAMESPACE
|
class QShortcut;
|
||||||
|
|
||||||
|
namespace RSS
|
||||||
|
{
|
||||||
|
class Feed;
|
||||||
|
}
|
||||||
|
|
||||||
class AutomatedRssDownloader: public QDialog
|
class AutomatedRssDownloader: public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit AutomatedRssDownloader(const QWeakPointer<Rss::Manager> &manager, QWidget *parent = 0);
|
explicit AutomatedRssDownloader(QWidget *parent = nullptr);
|
||||||
~AutomatedRssDownloader();
|
~AutomatedRssDownloader() override;
|
||||||
bool isRssDownloaderEnabled() const;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void showEvent(QShowEvent *event) override;
|
|
||||||
virtual void hideEvent(QHideEvent *event) override;
|
|
||||||
|
|
||||||
protected slots:
|
|
||||||
void loadSettings();
|
|
||||||
void saveSettings();
|
|
||||||
void loadRulesList();
|
|
||||||
void handleRuleCheckStateChange(QListWidgetItem *rule_item);
|
|
||||||
void handleFeedCheckStateChange(QListWidgetItem *feed_item);
|
|
||||||
void updateRuleDefinitionBox(QListWidgetItem *selected = 0);
|
|
||||||
void clearRuleDefinitionBox();
|
|
||||||
void saveEditedRule();
|
|
||||||
void loadFeedList();
|
|
||||||
void updateFeedList(QListWidgetItem *selected = 0);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void displayRulesListMenu(const QPoint &pos);
|
|
||||||
void on_addRuleBtn_clicked();
|
void on_addRuleBtn_clicked();
|
||||||
void on_removeRuleBtn_clicked();
|
void on_removeRuleBtn_clicked();
|
||||||
void on_browseSP_clicked();
|
void on_browseSP_clicked();
|
||||||
void on_exportBtn_clicked();
|
void on_exportBtn_clicked();
|
||||||
void on_importBtn_clicked();
|
void on_importBtn_clicked();
|
||||||
|
|
||||||
|
void handleRuleCheckStateChange(QListWidgetItem *ruleItem);
|
||||||
|
void handleFeedCheckStateChange(QListWidgetItem *feedItem);
|
||||||
|
void displayRulesListMenu();
|
||||||
void renameSelectedRule();
|
void renameSelectedRule();
|
||||||
void updateMatchingArticles();
|
void updateRuleDefinitionBox();
|
||||||
void updateFieldsToolTips(bool regex);
|
void updateFieldsToolTips(bool regex);
|
||||||
void updateMustLineValidity();
|
void updateMustLineValidity();
|
||||||
void updateMustNotLineValidity();
|
void updateMustNotLineValidity();
|
||||||
void updateEpisodeFilterValidity();
|
void updateEpisodeFilterValidity();
|
||||||
void onFinished(int result);
|
void handleRuleDefinitionChanged();
|
||||||
|
void handleRuleAdded(const QString &ruleName);
|
||||||
|
void handleRuleRenamed(const QString &ruleName, const QString &oldRuleName);
|
||||||
|
void handleRuleChanged(const QString &ruleName);
|
||||||
|
void handleRuleAboutToBeRemoved(const QString &ruleName);
|
||||||
|
|
||||||
|
void handleProcessingStateChanged(bool enabled);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Rss::DownloadRulePtr getCurrentRule() const;
|
void loadSettings();
|
||||||
|
void saveSettings();
|
||||||
|
void createRuleItem(const RSS::AutoDownloadRule &rule);
|
||||||
void initCategoryCombobox();
|
void initCategoryCombobox();
|
||||||
void addFeedArticlesToTree(const Rss::FeedPtr &feed, const QStringList &articles);
|
void clearRuleDefinitionBox();
|
||||||
void disconnectRuleFeedSlots();
|
void updateEditedRule();
|
||||||
void connectRuleFeedSlots();
|
void updateMatchingArticles();
|
||||||
|
void saveEditedRule();
|
||||||
|
void loadFeedList();
|
||||||
|
void updateFeedList();
|
||||||
|
void addFeedArticlesToTree(RSS::Feed *feed, const QStringList &articles);
|
||||||
|
|
||||||
private:
|
Ui::AutomatedRssDownloader *m_ui;
|
||||||
Ui::AutomatedRssDownloader *ui;
|
QListWidgetItem *m_currentRuleItem;
|
||||||
QWeakPointer<Rss::Manager> m_manager;
|
QShortcut *m_editHotkey;
|
||||||
QListWidgetItem *m_editedRule;
|
QShortcut *m_deleteHotkey;
|
||||||
Rss::DownloadRuleList *m_ruleList;
|
QSet<QPair<QString, QString>> m_treeListEntries;
|
||||||
Rss::DownloadRuleList *m_editableRuleList;
|
RSS::AutoDownloadRule m_currentRule;
|
||||||
|
QHash<QString, QListWidgetItem *> m_itemsByRuleName;
|
||||||
QRegularExpression *m_episodeRegex;
|
QRegularExpression *m_episodeRegex;
|
||||||
QShortcut *editHotkey;
|
|
||||||
QShortcut *deleteHotkey;
|
|
||||||
QSet<QPair<QString, QString >> m_treeListEntries;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // AUTOMATEDRSSDOWNLOADER_H
|
#endif // AUTOMATEDRSSDOWNLOADER_H
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>816</width>
|
<width>816</width>
|
||||||
<height>537</height>
|
<height>523</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
|
@ -15,20 +15,31 @@
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="checkEnableDownloader">
|
<widget class="QLabel" name="labelWarn">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<weight>75</weight>
|
<italic>true</italic>
|
||||||
<bold>true</bold>
|
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">color: red;</string>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable Automated RSS Downloader</string>
|
<string>Auto downloading of RSS torrents is disabled now! You can enable it in application settings.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QSplitter" name="hsplitter">
|
<widget class="QSplitter" name="hsplitter">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
|
@ -249,12 +260,12 @@
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QSpinBox" name="spinIgnorePeriod">
|
<widget class="QSpinBox" name="spinIgnorePeriod">
|
||||||
<property name="specialValueText">
|
|
||||||
<string>Disabled</string>
|
|
||||||
</property>
|
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="specialValueText">
|
||||||
|
<string>Disabled</string>
|
||||||
|
</property>
|
||||||
<property name="suffix">
|
<property name="suffix">
|
||||||
<string> days</string>
|
<string> days</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -374,6 +385,9 @@
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="importBtn">
|
<widget class="QPushButton" name="importBtn">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Import...</string>
|
<string>&Import...</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -381,6 +395,9 @@
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="exportBtn">
|
<widget class="QPushButton" name="exportBtn">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Export...</string>
|
<string>&Export...</string>
|
||||||
</property>
|
</property>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt4 and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2010 Christophe Dumez
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -24,213 +25,241 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "base/rss/rssmanager.h"
|
|
||||||
#include "base/rss/rssfolder.h"
|
|
||||||
#include "base/rss/rssfeed.h"
|
|
||||||
#include "guiiconprovider.h"
|
|
||||||
#include "feedlistwidget.h"
|
#include "feedlistwidget.h"
|
||||||
|
|
||||||
FeedListWidget::FeedListWidget(QWidget *parent, const Rss::ManagerPtr& rssmanager)
|
#include <QDragMoveEvent>
|
||||||
|
#include <QDropEvent>
|
||||||
|
#include <QTreeWidgetItem>
|
||||||
|
|
||||||
|
#include "base/rss/rss_article.h"
|
||||||
|
#include "base/rss/rss_feed.h"
|
||||||
|
#include "base/rss/rss_folder.h"
|
||||||
|
#include "base/rss/rss_session.h"
|
||||||
|
#include "guiiconprovider.h"
|
||||||
|
|
||||||
|
FeedListWidget::FeedListWidget(QWidget *parent)
|
||||||
: QTreeWidget(parent)
|
: QTreeWidget(parent)
|
||||||
, m_rssManager(rssmanager)
|
|
||||||
, m_currentFeed(nullptr)
|
|
||||||
{
|
{
|
||||||
setContextMenuPolicy(Qt::CustomContextMenu);
|
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
setDragDropMode(QAbstractItemView::InternalMove);
|
setDragDropMode(QAbstractItemView::InternalMove);
|
||||||
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||||
setColumnCount(1);
|
setColumnCount(1);
|
||||||
headerItem()->setText(0, tr("RSS feeds"));
|
headerItem()->setText(0, tr("RSS feeds"));
|
||||||
|
|
||||||
|
connect(RSS::Session::instance(), &RSS::Session::itemAdded, this, &FeedListWidget::handleItemAdded);
|
||||||
|
connect(RSS::Session::instance(), &RSS::Session::feedStateChanged, this, &FeedListWidget::handleFeedStateChanged);
|
||||||
|
connect(RSS::Session::instance(), &RSS::Session::feedIconLoaded, this, &FeedListWidget::handleFeedIconLoaded);
|
||||||
|
connect(RSS::Session::instance(), &RSS::Session::itemPathChanged, this, &FeedListWidget::handleItemPathChanged);
|
||||||
|
connect(RSS::Session::instance(), &RSS::Session::itemAboutToBeRemoved, this, &FeedListWidget::handleItemAboutToBeRemoved);
|
||||||
|
|
||||||
|
m_rssToTreeItemMapping[RSS::Session::instance()->rootFolder()] = invisibleRootItem();
|
||||||
|
|
||||||
m_unreadStickyItem = new QTreeWidgetItem(this);
|
m_unreadStickyItem = new QTreeWidgetItem(this);
|
||||||
m_unreadStickyItem->setText(0, tr("Unread") + QString::fromUtf8(" (") + QString::number(rssmanager->rootFolder()->unreadCount()) + QString(")"));
|
m_unreadStickyItem->setData(0, Qt::UserRole, reinterpret_cast<quintptr>(RSS::Session::instance()->rootFolder()));
|
||||||
m_unreadStickyItem->setData(0,Qt::DecorationRole, GuiIconProvider::instance()->getIcon("mail-folder-inbox"));
|
m_unreadStickyItem->setText(0, tr("Unread (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount()));
|
||||||
itemAdded(m_unreadStickyItem, rssmanager->rootFolder());
|
m_unreadStickyItem->setData(0, Qt::DecorationRole, GuiIconProvider::instance()->getIcon("mail-folder-inbox"));
|
||||||
connect(this, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), SLOT(updateCurrentFeed(QTreeWidgetItem*)));
|
|
||||||
setCurrentItem(m_unreadStickyItem);
|
connect(RSS::Session::instance()->rootFolder(), &RSS::Item::unreadCountChanged, this, &FeedListWidget::handleItemUnreadCountChanged);
|
||||||
|
|
||||||
|
setSortingEnabled(false);
|
||||||
|
fill(nullptr, RSS::Session::instance()->rootFolder());
|
||||||
|
setSortingEnabled(true);
|
||||||
|
|
||||||
|
// setCurrentItem(m_unreadStickyItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
FeedListWidget::~FeedListWidget() {
|
FeedListWidget::~FeedListWidget()
|
||||||
|
{
|
||||||
delete m_unreadStickyItem;
|
delete m_unreadStickyItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FeedListWidget::itemAdded(QTreeWidgetItem *item, const Rss::FilePtr& file) {
|
void FeedListWidget::handleItemAdded(RSS::Item *rssItem)
|
||||||
m_rssMapping[item] = file;
|
{
|
||||||
if (Rss::FeedPtr feed = qSharedPointerDynamicCast<Rss::Feed>(file)) {
|
auto parentItem = m_rssToTreeItemMapping.value(
|
||||||
m_feedsItems[feed->id()] = item;
|
RSS::Session::instance()->itemByPath(RSS::Item::parentPath(rssItem->path())));
|
||||||
|
createItem(rssItem, parentItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedListWidget::handleFeedStateChanged(RSS::Feed *feed)
|
||||||
|
{
|
||||||
|
QTreeWidgetItem *item = m_rssToTreeItemMapping.value(feed);
|
||||||
|
Q_ASSERT(item);
|
||||||
|
|
||||||
|
QIcon icon;
|
||||||
|
if (feed->isLoading())
|
||||||
|
icon = QIcon(QStringLiteral(":/icons/loading.png"));
|
||||||
|
else if (feed->hasError())
|
||||||
|
icon = GuiIconProvider::instance()->getIcon(QStringLiteral("unavailable"));
|
||||||
|
else if (!feed->iconPath().isEmpty())
|
||||||
|
icon = QIcon(feed->iconPath());
|
||||||
|
else
|
||||||
|
icon = GuiIconProvider::instance()->getIcon(QStringLiteral("application-rss+xml"));
|
||||||
|
item->setData(0, Qt::DecorationRole, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedListWidget::handleFeedIconLoaded(RSS::Feed *feed)
|
||||||
|
{
|
||||||
|
if (!feed->isLoading() && !feed->hasError()) {
|
||||||
|
QTreeWidgetItem *item = m_rssToTreeItemMapping.value(feed);
|
||||||
|
Q_ASSERT(item);
|
||||||
|
|
||||||
|
item->setData(0, Qt::DecorationRole, QIcon(feed->iconPath()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FeedListWidget::itemAboutToBeRemoved(QTreeWidgetItem *item) {
|
void FeedListWidget::handleItemUnreadCountChanged(RSS::Item *rssItem)
|
||||||
Rss::FilePtr file = m_rssMapping.take(item);
|
{
|
||||||
if (Rss::FeedPtr feed = qSharedPointerDynamicCast<Rss::Feed>(file)) {
|
if (rssItem == RSS::Session::instance()->rootFolder()) {
|
||||||
m_feedsItems.remove(feed->id());
|
m_unreadStickyItem->setText(0, tr("Unread (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount()));
|
||||||
} else if (Rss::FolderPtr folder = qSharedPointerDynamicCast<Rss::Folder>(file)) {
|
|
||||||
Rss::FeedList feeds = folder->getAllFeeds();
|
|
||||||
foreach (const Rss::FeedPtr& feed, feeds) {
|
|
||||||
m_feedsItems.remove(feed->id());
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
QTreeWidgetItem *item = mapRSSItem(rssItem);
|
||||||
|
Q_ASSERT(item);
|
||||||
|
item->setData(0, Qt::DisplayRole, QString("%1 (%2)").arg(rssItem->name()).arg(rssItem->unreadCount()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FeedListWidget::hasFeed(const QString &url) const {
|
void FeedListWidget::handleItemPathChanged(RSS::Item *rssItem)
|
||||||
return m_feedsItems.contains(QUrl(url).toString());
|
{
|
||||||
|
QTreeWidgetItem *item = mapRSSItem(rssItem);
|
||||||
|
Q_ASSERT(item);
|
||||||
|
|
||||||
|
item->setData(0, Qt::DisplayRole, QString("%1 (%2)").arg(rssItem->name()).arg(rssItem->unreadCount()));
|
||||||
|
|
||||||
|
RSS::Item *parentRssItem = RSS::Session::instance()->itemByPath(RSS::Item::parentPath(rssItem->path()));
|
||||||
|
QTreeWidgetItem *parentItem = mapRSSItem(parentRssItem);
|
||||||
|
Q_ASSERT(parentItem);
|
||||||
|
|
||||||
|
parentItem->addChild(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QTreeWidgetItem*> FeedListWidget::getAllFeedItems() const {
|
void FeedListWidget::handleItemAboutToBeRemoved(RSS::Item *rssItem)
|
||||||
return m_feedsItems.values();
|
{
|
||||||
|
delete m_rssToTreeItemMapping.take(rssItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
QTreeWidgetItem* FeedListWidget::stickyUnreadItem() const {
|
QTreeWidgetItem *FeedListWidget::stickyUnreadItem() const
|
||||||
|
{
|
||||||
return m_unreadStickyItem;
|
return m_unreadStickyItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList FeedListWidget::getItemPath(QTreeWidgetItem* item) const {
|
QList<QTreeWidgetItem *> FeedListWidget::getAllOpenedFolders(QTreeWidgetItem *parent) const
|
||||||
QStringList path;
|
{
|
||||||
if (item) {
|
QList<QTreeWidgetItem *> openedFolders;
|
||||||
if (item->parent())
|
int nbChildren = (parent ? parent->childCount() : topLevelItemCount());
|
||||||
path << getItemPath(item->parent());
|
for (int i = 0; i < nbChildren; ++i) {
|
||||||
path.append(getRSSItem(item)->id());
|
QTreeWidgetItem *item (parent ? parent->child(i) : topLevelItem(i));
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<QTreeWidgetItem*> FeedListWidget::getAllOpenFolders(QTreeWidgetItem *parent) const {
|
|
||||||
QList<QTreeWidgetItem*> open_folders;
|
|
||||||
int nbChildren;
|
|
||||||
if (parent)
|
|
||||||
nbChildren = parent->childCount();
|
|
||||||
else
|
|
||||||
nbChildren = topLevelItemCount();
|
|
||||||
for (int i=0; i<nbChildren; ++i) {
|
|
||||||
QTreeWidgetItem *item;
|
|
||||||
if (parent)
|
|
||||||
item = parent->child(i);
|
|
||||||
else
|
|
||||||
item = topLevelItem(i);
|
|
||||||
if (isFolder(item) && item->isExpanded()) {
|
if (isFolder(item) && item->isExpanded()) {
|
||||||
QList<QTreeWidgetItem*> open_subfolders = getAllOpenFolders(item);
|
QList<QTreeWidgetItem *> openedSubfolders = getAllOpenedFolders(item);
|
||||||
if (!open_subfolders.empty()) {
|
if (!openedSubfolders.empty())
|
||||||
open_folders << open_subfolders;
|
openedFolders << openedSubfolders;
|
||||||
} else {
|
else
|
||||||
open_folders << item;
|
openedFolders << item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return openedFolders;
|
||||||
return open_folders;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QTreeWidgetItem*> FeedListWidget::getAllFeedItems(QTreeWidgetItem* folder) {
|
RSS::Item *FeedListWidget::getRSSItem(QTreeWidgetItem *item) const
|
||||||
QList<QTreeWidgetItem*> feeds;
|
{
|
||||||
const int nbChildren = folder->childCount();
|
return reinterpret_cast<RSS::Item *>(item->data(0, Qt::UserRole).value<quintptr>());
|
||||||
for (int i=0; i<nbChildren; ++i) {
|
|
||||||
QTreeWidgetItem *item = folder->child(i);
|
|
||||||
if (isFeed(item)) {
|
|
||||||
feeds << item;
|
|
||||||
} else {
|
|
||||||
feeds << getAllFeedItems(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return feeds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rss::FilePtr FeedListWidget::getRSSItem(QTreeWidgetItem *item) const {
|
QTreeWidgetItem *FeedListWidget::mapRSSItem(RSS::Item *rssItem) const
|
||||||
return m_rssMapping.value(item, Rss::FilePtr());
|
{
|
||||||
|
return m_rssToTreeItemMapping.value(rssItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FeedListWidget::itemPath(QTreeWidgetItem *item) const
|
||||||
|
{
|
||||||
|
return getRSSItem(item)->path();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FeedListWidget::isFeed(QTreeWidgetItem *item) const
|
bool FeedListWidget::isFeed(QTreeWidgetItem *item) const
|
||||||
{
|
{
|
||||||
return (qSharedPointerDynamicCast<Rss::Feed>(m_rssMapping.value(item)) != NULL);
|
return qobject_cast<RSS::Feed *>(getRSSItem(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FeedListWidget::isFolder(QTreeWidgetItem *item) const
|
bool FeedListWidget::isFolder(QTreeWidgetItem *item) const
|
||||||
{
|
{
|
||||||
return (qSharedPointerDynamicCast<Rss::Folder>(m_rssMapping.value(item)) != NULL);
|
return qobject_cast<RSS::Folder *>(getRSSItem(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
QString FeedListWidget::getItemID(QTreeWidgetItem *item) const {
|
void FeedListWidget::dragMoveEvent(QDragMoveEvent *event)
|
||||||
return m_rssMapping.value(item)->id();
|
{
|
||||||
}
|
|
||||||
|
|
||||||
QTreeWidgetItem* FeedListWidget::getTreeItemFromUrl(const QString &url) const {
|
|
||||||
return m_feedsItems.value(url, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Rss::FeedPtr FeedListWidget::getRSSItemFromUrl(const QString &url) const {
|
|
||||||
return qSharedPointerDynamicCast<Rss::Feed>(getRSSItem(getTreeItemFromUrl(url)));
|
|
||||||
}
|
|
||||||
|
|
||||||
QTreeWidgetItem* FeedListWidget::currentItem() const {
|
|
||||||
return m_currentFeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTreeWidgetItem* FeedListWidget::currentFeed() const {
|
|
||||||
return m_currentFeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FeedListWidget::updateCurrentFeed(QTreeWidgetItem* new_item) {
|
|
||||||
if (!new_item) return;
|
|
||||||
if (!m_rssMapping.contains(new_item)) return;
|
|
||||||
if (isFeed(new_item) || new_item == m_unreadStickyItem)
|
|
||||||
m_currentFeed = new_item;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FeedListWidget::dragMoveEvent(QDragMoveEvent * event) {
|
|
||||||
QTreeWidget::dragMoveEvent(event);
|
QTreeWidget::dragMoveEvent(event);
|
||||||
|
|
||||||
QTreeWidgetItem *item = itemAt(event->pos());
|
QTreeWidgetItem *item = itemAt(event->pos());
|
||||||
// Prohibit dropping onto global unread counter
|
// Prohibit dropping onto global unread counter
|
||||||
if (item == m_unreadStickyItem) {
|
if (item == m_unreadStickyItem)
|
||||||
event->ignore();
|
event->ignore();
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Prohibit dragging of global unread counter
|
// Prohibit dragging of global unread counter
|
||||||
if (selectedItems().contains(m_unreadStickyItem)) {
|
else if (selectedItems().contains(m_unreadStickyItem))
|
||||||
event->ignore();
|
event->ignore();
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Prohibit dropping onto feeds
|
// Prohibit dropping onto feeds
|
||||||
if (item && isFeed(item)) {
|
else if (item && isFeed(item))
|
||||||
event->ignore();
|
event->ignore();
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FeedListWidget::dropEvent(QDropEvent *event) {
|
void FeedListWidget::dropEvent(QDropEvent *event)
|
||||||
qDebug("dropEvent");
|
{
|
||||||
QList<QTreeWidgetItem*> folders_altered;
|
QTreeWidgetItem *destFolderItem = itemAt(event->pos());
|
||||||
QTreeWidgetItem *dest_folder_item = itemAt(event->pos());
|
RSS::Folder *destFolder = (destFolderItem
|
||||||
Rss::FolderPtr dest_folder;
|
? static_cast<RSS::Folder *>(getRSSItem(destFolderItem))
|
||||||
if (dest_folder_item) {
|
: RSS::Session::instance()->rootFolder());
|
||||||
dest_folder = qSharedPointerCast<Rss::Folder>(getRSSItem(dest_folder_item));
|
|
||||||
folders_altered << dest_folder_item;
|
// move as much items as possible
|
||||||
} else {
|
foreach (QTreeWidgetItem *srcItem, selectedItems()) {
|
||||||
dest_folder = m_rssManager->rootFolder();
|
auto rssItem = getRSSItem(srcItem);
|
||||||
|
RSS::Session::instance()->moveItem(rssItem, RSS::Item::joinPath(destFolder->path(), rssItem->name()));
|
||||||
}
|
}
|
||||||
QList<QTreeWidgetItem *> src_items = selectedItems();
|
|
||||||
// Check if there is not going to overwrite another file
|
|
||||||
foreach (QTreeWidgetItem *src_item, src_items) {
|
|
||||||
Rss::FilePtr file = getRSSItem(src_item);
|
|
||||||
if (dest_folder->hasChild(file->id())) {
|
|
||||||
QTreeWidget::dropEvent(event);
|
QTreeWidget::dropEvent(event);
|
||||||
return;
|
if (destFolderItem)
|
||||||
}
|
destFolderItem->setExpanded(true);
|
||||||
}
|
}
|
||||||
// Proceed with the move
|
|
||||||
foreach (QTreeWidgetItem *src_item, src_items) {
|
QTreeWidgetItem *FeedListWidget::createItem(RSS::Item *rssItem, QTreeWidgetItem *parentItem)
|
||||||
QTreeWidgetItem *parent_folder = src_item->parent();
|
{
|
||||||
if (parent_folder && !folders_altered.contains(parent_folder))
|
QTreeWidgetItem *item = new QTreeWidgetItem;
|
||||||
folders_altered << parent_folder;
|
item->setData(0, Qt::DisplayRole, QString("%1 (%2)").arg(rssItem->name()).arg(rssItem->unreadCount()));
|
||||||
// Actually move the file
|
item->setData(0, Qt::UserRole, reinterpret_cast<quintptr>(rssItem));
|
||||||
Rss::FilePtr file = getRSSItem(src_item);
|
m_rssToTreeItemMapping[rssItem] = item;
|
||||||
m_rssManager->moveFile(file, dest_folder);
|
|
||||||
}
|
QIcon icon;
|
||||||
QTreeWidget::dropEvent(event);
|
if (auto feed = qobject_cast<RSS::Feed *>(rssItem)) {
|
||||||
if (dest_folder_item)
|
if (feed->isLoading())
|
||||||
dest_folder_item->setExpanded(true);
|
icon = QIcon(QStringLiteral(":/icons/loading.png"));
|
||||||
// Emit signal for update
|
else if (feed->hasError())
|
||||||
if (!folders_altered.empty())
|
icon = GuiIconProvider::instance()->getIcon(QStringLiteral("unavailable"));
|
||||||
emit foldersAltered(folders_altered);
|
else if (!feed->iconPath().isEmpty())
|
||||||
|
icon = QIcon(feed->iconPath());
|
||||||
|
else
|
||||||
|
icon = GuiIconProvider::instance()->getIcon(QStringLiteral("application-rss+xml"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
icon = GuiIconProvider::instance()->getIcon("inode-directory");
|
||||||
|
}
|
||||||
|
item->setData(0, Qt::DecorationRole, icon);
|
||||||
|
|
||||||
|
connect(rssItem, &RSS::Item::unreadCountChanged, this, &FeedListWidget::handleItemUnreadCountChanged);
|
||||||
|
|
||||||
|
if (!parentItem || (parentItem == m_unreadStickyItem))
|
||||||
|
addTopLevelItem(item);
|
||||||
|
else
|
||||||
|
parentItem->addChild(item);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedListWidget::fill(QTreeWidgetItem *parent, RSS::Folder *rssParent)
|
||||||
|
{
|
||||||
|
foreach (auto rssItem, rssParent->items()) {
|
||||||
|
QTreeWidgetItem *item = createItem(rssItem, parent);
|
||||||
|
// Recursive call if this is a folder.
|
||||||
|
if (auto folder = qobject_cast<RSS::Folder *>(rssItem))
|
||||||
|
fill(item, folder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt4 and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2010 Christophe Dumez
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -24,67 +25,54 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef FEEDLIST_H
|
#ifndef FEEDLISTWIDGET_H
|
||||||
#define FEEDLIST_H
|
#define FEEDLISTWIDGET_H
|
||||||
|
|
||||||
#include <QTreeWidget>
|
|
||||||
#include <QTreeWidgetItem>
|
|
||||||
#include <QDropEvent>
|
|
||||||
#include <QDragMoveEvent>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QUrl>
|
#include <QTreeWidget>
|
||||||
|
|
||||||
#include "base/rss/rssfile.h"
|
namespace RSS
|
||||||
#include "base/rss/rssfeed.h"
|
{
|
||||||
#include "base/rss/rssmanager.h"
|
class Article;
|
||||||
|
class Feed;
|
||||||
|
class Folder;
|
||||||
|
class Item;
|
||||||
|
}
|
||||||
|
|
||||||
class FeedListWidget: public QTreeWidget {
|
class FeedListWidget: public QTreeWidget
|
||||||
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
FeedListWidget(QWidget *parent, const Rss::ManagerPtr& rssManager);
|
explicit FeedListWidget(QWidget *parent);
|
||||||
~FeedListWidget();
|
~FeedListWidget();
|
||||||
|
|
||||||
bool hasFeed(const QString &url) const;
|
QTreeWidgetItem *stickyUnreadItem() const;
|
||||||
QList<QTreeWidgetItem*> getAllFeedItems() const;
|
QList<QTreeWidgetItem *> getAllOpenedFolders(QTreeWidgetItem *parent = nullptr) const;
|
||||||
QTreeWidgetItem* stickyUnreadItem() const;
|
RSS::Item *getRSSItem(QTreeWidgetItem *item) const;
|
||||||
QStringList getItemPath(QTreeWidgetItem* item) const;
|
QTreeWidgetItem *mapRSSItem(RSS::Item *rssItem) const;
|
||||||
QList<QTreeWidgetItem*> getAllOpenFolders(QTreeWidgetItem *parent=0) const;
|
QString itemPath(QTreeWidgetItem *item) const;
|
||||||
QList<QTreeWidgetItem*> getAllFeedItems(QTreeWidgetItem* folder);
|
|
||||||
Rss::FilePtr getRSSItem(QTreeWidgetItem *item) const;
|
|
||||||
bool isFeed(QTreeWidgetItem *item) const;
|
bool isFeed(QTreeWidgetItem *item) const;
|
||||||
bool isFolder(QTreeWidgetItem *item) const;
|
bool isFolder(QTreeWidgetItem *item) const;
|
||||||
QString getItemID(QTreeWidgetItem *item) const;
|
|
||||||
QTreeWidgetItem* getTreeItemFromUrl(const QString &url) const;
|
|
||||||
Rss::FeedPtr getRSSItemFromUrl(const QString &url) const;
|
|
||||||
QTreeWidgetItem* currentItem() const;
|
|
||||||
QTreeWidgetItem* currentFeed() const;
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void itemAdded(QTreeWidgetItem *item, const Rss::FilePtr& file);
|
|
||||||
void itemAboutToBeRemoved(QTreeWidgetItem *item);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void foldersAltered(const QList<QTreeWidgetItem*> &folders);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void updateCurrentFeed(QTreeWidgetItem* new_item);
|
void handleItemAdded(RSS::Item *rssItem);
|
||||||
|
void handleFeedStateChanged(RSS::Feed *feed);
|
||||||
protected:
|
void handleFeedIconLoaded(RSS::Feed *feed);
|
||||||
void dragMoveEvent(QDragMoveEvent * event);
|
void handleItemUnreadCountChanged(RSS::Item *rssItem);
|
||||||
void dropEvent(QDropEvent *event);
|
void handleItemPathChanged(RSS::Item *rssItem);
|
||||||
|
void handleItemAboutToBeRemoved(RSS::Item *rssItem);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Rss::ManagerPtr m_rssManager;
|
void dragMoveEvent(QDragMoveEvent *event);
|
||||||
QHash<QTreeWidgetItem*, Rss::FilePtr> m_rssMapping;
|
void dropEvent(QDropEvent *event);
|
||||||
QHash<QString, QTreeWidgetItem*> m_feedsItems;
|
QTreeWidgetItem *createItem(RSS::Item *rssItem, QTreeWidgetItem *parentItem = nullptr);
|
||||||
QTreeWidgetItem* m_currentFeed;
|
void fill(QTreeWidgetItem *parent, RSS::Folder *rssParent);
|
||||||
|
|
||||||
|
QHash<RSS::Item *, QTreeWidgetItem *> m_rssToTreeItemMapping;
|
||||||
QTreeWidgetItem *m_unreadStickyItem;
|
QTreeWidgetItem *m_unreadStickyItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // FEEDLIST_H
|
#endif // FEEDLISTWIDGET_H
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
INCLUDEPATH += $$PWD
|
|
||||||
|
|
||||||
HEADERS += $$PWD/rss_imp.h \
|
|
||||||
$$PWD/rsssettingsdlg.h \
|
|
||||||
$$PWD/feedlistwidget.h \
|
|
||||||
$$PWD/automatedrssdownloader.h \
|
|
||||||
$$PWD/htmlbrowser.h
|
|
||||||
|
|
||||||
SOURCES += $$PWD/rss_imp.cpp \
|
|
||||||
$$PWD/rsssettingsdlg.cpp \
|
|
||||||
$$PWD/feedlistwidget.cpp \
|
|
||||||
$$PWD/automatedrssdownloader.cpp \
|
|
||||||
$$PWD/htmlbrowser.cpp
|
|
||||||
|
|
||||||
FORMS += $$PWD/rss.ui \
|
|
||||||
$$PWD/rsssettingsdlg.ui \
|
|
||||||
$$PWD/automatedrssdownloader.ui
|
|
|
@ -1,806 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt4 and libtorrent.
|
|
||||||
* Copyright (C) 2006 Christophe Dumez, Arnaud Demaiziere
|
|
||||||
*
|
|
||||||
* 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 arnaud@qbittorrent.org
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rss_imp.h"
|
|
||||||
|
|
||||||
#include <QDesktopServices>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QStandardItemModel>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QString>
|
|
||||||
#include <QClipboard>
|
|
||||||
#include <QDragMoveEvent>
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include "feedlistwidget.h"
|
|
||||||
#include "base/bittorrent/session.h"
|
|
||||||
#include "base/net/downloadmanager.h"
|
|
||||||
#include "base/preferences.h"
|
|
||||||
#include "rsssettingsdlg.h"
|
|
||||||
#include "base/rss/rssmanager.h"
|
|
||||||
#include "base/rss/rssfolder.h"
|
|
||||||
#include "base/rss/rssarticle.h"
|
|
||||||
#include "base/rss/rssfeed.h"
|
|
||||||
#include "automatedrssdownloader.h"
|
|
||||||
#include "guiiconprovider.h"
|
|
||||||
#include "autoexpandabledialog.h"
|
|
||||||
#include "addnewtorrentdialog.h"
|
|
||||||
|
|
||||||
#include "ui_rss.h"
|
|
||||||
|
|
||||||
namespace Article
|
|
||||||
{
|
|
||||||
enum ArticleRoles
|
|
||||||
{
|
|
||||||
TitleRole = Qt::DisplayRole,
|
|
||||||
IconRole = Qt::DecorationRole,
|
|
||||||
ColorRole = Qt::ForegroundRole,
|
|
||||||
IdRole = Qt::UserRole + 1,
|
|
||||||
FeedUrlRole = Qt::UserRole + 2
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// display a right-click menu
|
|
||||||
void RSSImp::displayRSSListMenu(const QPoint &pos)
|
|
||||||
{
|
|
||||||
if (!m_feedList->indexAt(pos).isValid())
|
|
||||||
// No item under the mouse, clear selection
|
|
||||||
m_feedList->clearSelection();
|
|
||||||
QMenu myRSSListMenu(this);
|
|
||||||
QList<QTreeWidgetItem * > selectedItems = m_feedList->selectedItems();
|
|
||||||
if (selectedItems.size() > 0) {
|
|
||||||
myRSSListMenu.addAction(m_ui->actionUpdate);
|
|
||||||
myRSSListMenu.addAction(m_ui->actionMark_items_read);
|
|
||||||
myRSSListMenu.addSeparator();
|
|
||||||
if (selectedItems.size() == 1) {
|
|
||||||
if (m_feedList->getRSSItem(selectedItems.first()) != m_rssManager->rootFolder()) {
|
|
||||||
myRSSListMenu.addAction(m_ui->actionRename);
|
|
||||||
myRSSListMenu.addAction(m_ui->actionDelete);
|
|
||||||
myRSSListMenu.addSeparator();
|
|
||||||
if (m_feedList->isFolder(selectedItems.first()))
|
|
||||||
myRSSListMenu.addAction(m_ui->actionNew_folder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
myRSSListMenu.addAction(m_ui->actionDelete);
|
|
||||||
myRSSListMenu.addSeparator();
|
|
||||||
}
|
|
||||||
myRSSListMenu.addAction(m_ui->actionNew_subscription);
|
|
||||||
if (m_feedList->isFeed(selectedItems.first())) {
|
|
||||||
myRSSListMenu.addSeparator();
|
|
||||||
myRSSListMenu.addAction(m_ui->actionCopy_feed_URL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
myRSSListMenu.addAction(m_ui->actionNew_subscription);
|
|
||||||
myRSSListMenu.addAction(m_ui->actionNew_folder);
|
|
||||||
myRSSListMenu.addSeparator();
|
|
||||||
myRSSListMenu.addAction(m_ui->actionUpdate_all_feeds);
|
|
||||||
}
|
|
||||||
myRSSListMenu.exec(QCursor::pos());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::displayItemsListMenu(const QPoint &)
|
|
||||||
{
|
|
||||||
QMenu myItemListMenu(this);
|
|
||||||
QList<QListWidgetItem * > selectedItems = m_ui->listArticles->selectedItems();
|
|
||||||
if (selectedItems.size() <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool hasTorrent = false;
|
|
||||||
bool hasLink = false;
|
|
||||||
foreach (const QListWidgetItem *item, selectedItems) {
|
|
||||||
if (!item) continue;
|
|
||||||
Rss::FeedPtr feed = m_feedList->getRSSItemFromUrl(item->data(Article::FeedUrlRole).toString());
|
|
||||||
if (!feed) continue;
|
|
||||||
Rss::ArticlePtr article = feed->getItem(item->data(Article::IdRole).toString());
|
|
||||||
if (!article) continue;
|
|
||||||
|
|
||||||
if (!article->torrentUrl().isEmpty())
|
|
||||||
hasTorrent = true;
|
|
||||||
if (!article->link().isEmpty())
|
|
||||||
hasLink = true;
|
|
||||||
if (hasTorrent && hasLink)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (hasTorrent)
|
|
||||||
myItemListMenu.addAction(m_ui->actionDownload_torrent);
|
|
||||||
if (hasLink)
|
|
||||||
myItemListMenu.addAction(m_ui->actionOpen_news_URL);
|
|
||||||
if (hasTorrent || hasLink)
|
|
||||||
myItemListMenu.exec(QCursor::pos());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::askNewFolder()
|
|
||||||
{
|
|
||||||
QTreeWidgetItem *parent_item = 0;
|
|
||||||
Rss::FolderPtr rss_parent;
|
|
||||||
if (m_feedList->selectedItems().size() > 0) {
|
|
||||||
parent_item = m_feedList->selectedItems().at(0);
|
|
||||||
rss_parent = qSharedPointerDynamicCast<Rss::Folder>(m_feedList->getRSSItem(parent_item));
|
|
||||||
Q_ASSERT(rss_parent);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rss_parent = m_rssManager->rootFolder();
|
|
||||||
}
|
|
||||||
bool ok;
|
|
||||||
QString new_name = AutoExpandableDialog::getText(this, tr("Please choose a folder name"), tr("Folder name:"), QLineEdit::Normal, tr("New folder"), &ok);
|
|
||||||
if (!ok || rss_parent->hasChild(new_name))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Rss::FolderPtr newFolder(new Rss::Folder(new_name));
|
|
||||||
rss_parent->addFile(newFolder);
|
|
||||||
QTreeWidgetItem *folderItem = createFolderListItem(newFolder);
|
|
||||||
if (parent_item)
|
|
||||||
parent_item->addChild(folderItem);
|
|
||||||
else
|
|
||||||
m_feedList->addTopLevelItem(folderItem);
|
|
||||||
// Notify TreeWidget
|
|
||||||
m_feedList->itemAdded(folderItem, newFolder);
|
|
||||||
// Expand parent folder to display new folder
|
|
||||||
if (parent_item)
|
|
||||||
parent_item->setExpanded(true);
|
|
||||||
m_feedList->setCurrentItem(folderItem);
|
|
||||||
m_rssManager->saveStreamList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// add a stream by a button
|
|
||||||
void RSSImp::on_newFeedButton_clicked()
|
|
||||||
{
|
|
||||||
// Determine parent folder for new feed
|
|
||||||
QTreeWidgetItem *parent_item = 0;
|
|
||||||
QList<QTreeWidgetItem * > selected_items = m_feedList->selectedItems();
|
|
||||||
if (!selected_items.empty()) {
|
|
||||||
parent_item = selected_items.first();
|
|
||||||
// Consider the case where the user clicked on Unread item
|
|
||||||
if (parent_item == m_feedList->stickyUnreadItem())
|
|
||||||
parent_item = 0;
|
|
||||||
else
|
|
||||||
if (!m_feedList->isFolder(parent_item))
|
|
||||||
parent_item = parent_item->parent();
|
|
||||||
}
|
|
||||||
Rss::FolderPtr rss_parent;
|
|
||||||
if (parent_item)
|
|
||||||
rss_parent = qSharedPointerCast<Rss::Folder>(m_feedList->getRSSItem(parent_item));
|
|
||||||
else
|
|
||||||
rss_parent = m_rssManager->rootFolder();
|
|
||||||
// Ask for feed URL
|
|
||||||
bool ok;
|
|
||||||
QString clip_txt = qApp->clipboard()->text();
|
|
||||||
QString default_url = "http://";
|
|
||||||
if (clip_txt.startsWith("http://", Qt::CaseInsensitive) || clip_txt.startsWith("https://", Qt::CaseInsensitive) || clip_txt.startsWith("ftp://", Qt::CaseInsensitive))
|
|
||||||
default_url = clip_txt;
|
|
||||||
|
|
||||||
QString newUrl = AutoExpandableDialog::getText(this, tr("Please type a RSS stream URL"), tr("Stream URL:"), QLineEdit::Normal, default_url, &ok);
|
|
||||||
if (!ok)
|
|
||||||
return;
|
|
||||||
|
|
||||||
newUrl = newUrl.trimmed();
|
|
||||||
if (newUrl.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (m_feedList->hasFeed(newUrl)) {
|
|
||||||
QMessageBox::warning(this, "qBittorrent",
|
|
||||||
tr("This RSS feed is already in the list."),
|
|
||||||
QMessageBox::Ok);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rss::FeedPtr stream(new Rss::Feed(newUrl, m_rssManager.data()));
|
|
||||||
rss_parent->addFile(stream);
|
|
||||||
// Create TreeWidget item
|
|
||||||
QTreeWidgetItem *item = createFolderListItem(stream);
|
|
||||||
if (parent_item)
|
|
||||||
parent_item->addChild(item);
|
|
||||||
else
|
|
||||||
m_feedList->addTopLevelItem(item);
|
|
||||||
// Notify TreeWidget
|
|
||||||
m_feedList->itemAdded(item, stream);
|
|
||||||
// Expand parent folder to display new feed
|
|
||||||
if (parent_item)
|
|
||||||
parent_item->setExpanded(true);
|
|
||||||
m_feedList->setCurrentItem(item);
|
|
||||||
m_rssManager->saveStreamList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete a stream by a button
|
|
||||||
void RSSImp::deleteSelectedItems()
|
|
||||||
{
|
|
||||||
QList<QTreeWidgetItem * > selectedItems = m_feedList->selectedItems();
|
|
||||||
if (selectedItems.isEmpty())
|
|
||||||
return;
|
|
||||||
if ((selectedItems.size() == 1) && (selectedItems.first() == m_feedList->stickyUnreadItem()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
QMessageBox::StandardButton answer = QMessageBox::question(this, tr("Deletion confirmation"),
|
|
||||||
tr("Are you sure you want to delete the selected RSS feeds?"),
|
|
||||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
|
||||||
if (answer == QMessageBox::No)
|
|
||||||
return;
|
|
||||||
|
|
||||||
QList<QString> deleted;
|
|
||||||
|
|
||||||
foreach (QTreeWidgetItem *item, selectedItems) {
|
|
||||||
if (item == m_feedList->stickyUnreadItem())
|
|
||||||
continue;
|
|
||||||
Rss::FilePtr rss_item = m_feedList->getRSSItem(item);
|
|
||||||
QTreeWidgetItem *parent = item->parent();
|
|
||||||
// Notify TreeWidget
|
|
||||||
m_feedList->itemAboutToBeRemoved(item);
|
|
||||||
// Actually delete the item
|
|
||||||
rss_item->parentFolder()->removeChild(rss_item->id());
|
|
||||||
deleted << rss_item->id();
|
|
||||||
delete item;
|
|
||||||
// Update parents count
|
|
||||||
while (parent && (parent != m_feedList->invisibleRootItem())) {
|
|
||||||
updateItemInfos(parent);
|
|
||||||
parent = parent->parent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_rssManager->saveStreamList();
|
|
||||||
|
|
||||||
foreach (const QString &feed_id, deleted)
|
|
||||||
m_rssManager->forwardFeedInfosChanged(feed_id, "", 0);
|
|
||||||
|
|
||||||
// Update Unread items
|
|
||||||
updateItemInfos(m_feedList->stickyUnreadItem());
|
|
||||||
if (m_feedList->currentItem() == m_feedList->stickyUnreadItem())
|
|
||||||
populateArticleList(m_feedList->stickyUnreadItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::loadFoldersOpenState()
|
|
||||||
{
|
|
||||||
QStringList open_folders = Preferences::instance()->getRssOpenFolders();
|
|
||||||
foreach (const QString &var_path, open_folders) {
|
|
||||||
QStringList path = var_path.split("\\");
|
|
||||||
QTreeWidgetItem *parent = 0;
|
|
||||||
foreach (const QString &name, path) {
|
|
||||||
int nbChildren = 0;
|
|
||||||
if (parent)
|
|
||||||
nbChildren = parent->childCount();
|
|
||||||
else
|
|
||||||
nbChildren = m_feedList->topLevelItemCount();
|
|
||||||
for (int i = 0; i < nbChildren; ++i) {
|
|
||||||
QTreeWidgetItem *child;
|
|
||||||
if (parent)
|
|
||||||
child = parent->child(i);
|
|
||||||
else
|
|
||||||
child = m_feedList->topLevelItem(i);
|
|
||||||
if (m_feedList->getRSSItem(child)->id() == name) {
|
|
||||||
parent = child;
|
|
||||||
parent->setExpanded(true);
|
|
||||||
qDebug("expanding folder %s", qPrintable(name));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::saveFoldersOpenState()
|
|
||||||
{
|
|
||||||
QStringList open_folders;
|
|
||||||
QList<QTreeWidgetItem * > items = m_feedList->getAllOpenFolders();
|
|
||||||
foreach (QTreeWidgetItem *item, items) {
|
|
||||||
QString path = m_feedList->getItemPath(item).join("\\");
|
|
||||||
qDebug("saving open folder: %s", qPrintable(path));
|
|
||||||
open_folders << path;
|
|
||||||
}
|
|
||||||
Preferences::instance()->setRssOpenFolders(open_folders);
|
|
||||||
}
|
|
||||||
|
|
||||||
// refresh all streams by a button
|
|
||||||
void RSSImp::refreshAllFeeds()
|
|
||||||
{
|
|
||||||
foreach (QTreeWidgetItem *item, m_feedList->getAllFeedItems())
|
|
||||||
item->setData(0, Qt::DecorationRole, QVariant(QIcon(":/icons/loading.png")));
|
|
||||||
m_rssManager->refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::downloadSelectedTorrents()
|
|
||||||
{
|
|
||||||
QList<QListWidgetItem * > selected_items = m_ui->listArticles->selectedItems();
|
|
||||||
if (selected_items.size() <= 0)
|
|
||||||
return;
|
|
||||||
foreach (QListWidgetItem *item, selected_items) {
|
|
||||||
if (!item) continue;
|
|
||||||
Rss::FeedPtr feed = m_feedList->getRSSItemFromUrl(item->data(Article::FeedUrlRole).toString());
|
|
||||||
if (!feed) continue;
|
|
||||||
Rss::ArticlePtr article = feed->getItem(item->data(Article::IdRole).toString());
|
|
||||||
if (!article) continue;
|
|
||||||
|
|
||||||
// Mark as read
|
|
||||||
article->markAsRead();
|
|
||||||
item->setData(Article::ColorRole, QVariant(QColor("grey")));
|
|
||||||
item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere.png")));
|
|
||||||
|
|
||||||
if (article->torrentUrl().isEmpty())
|
|
||||||
continue;
|
|
||||||
if (AddNewTorrentDialog::isEnabled())
|
|
||||||
AddNewTorrentDialog::show(article->torrentUrl());
|
|
||||||
else
|
|
||||||
BitTorrent::Session::instance()->addTorrent(article->torrentUrl());
|
|
||||||
}
|
|
||||||
// Decrement feed nb unread news
|
|
||||||
updateItemInfos(m_feedList->stickyUnreadItem());
|
|
||||||
updateItemInfos(m_feedList->getTreeItemFromUrl(selected_items.first()->data(Article::FeedUrlRole).toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// open the url of the selected RSS articles in the Web browser
|
|
||||||
void RSSImp::openSelectedArticlesUrls()
|
|
||||||
{
|
|
||||||
QList<QListWidgetItem * > selected_items = m_ui->listArticles->selectedItems();
|
|
||||||
if (selected_items.size() <= 0)
|
|
||||||
return;
|
|
||||||
foreach (QListWidgetItem *item, selected_items) {
|
|
||||||
if (!item) continue;
|
|
||||||
Rss::FeedPtr feed = m_feedList->getRSSItemFromUrl(item->data(Article::FeedUrlRole).toString());
|
|
||||||
if (!feed) continue;
|
|
||||||
Rss::ArticlePtr article = feed->getItem(item->data(Article::IdRole).toString());
|
|
||||||
if (!article) continue;
|
|
||||||
|
|
||||||
// Mark as read
|
|
||||||
article->markAsRead();
|
|
||||||
item->setData(Article::ColorRole, QVariant(QColor("grey")));
|
|
||||||
item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere.png")));
|
|
||||||
|
|
||||||
const QString link = article->link();
|
|
||||||
if (!link.isEmpty())
|
|
||||||
QDesktopServices::openUrl(QUrl(link));
|
|
||||||
}
|
|
||||||
// Decrement feed nb unread news
|
|
||||||
updateItemInfos(m_feedList->stickyUnreadItem());
|
|
||||||
updateItemInfos(m_feedList->getTreeItemFromUrl(selected_items.first()->data(Article::FeedUrlRole).toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// right-click on stream : give it an alias
|
|
||||||
void RSSImp::renameSelectedRssFile()
|
|
||||||
{
|
|
||||||
QList<QTreeWidgetItem * > selectedItems = m_feedList->selectedItems();
|
|
||||||
if (selectedItems.size() != 1)
|
|
||||||
return;
|
|
||||||
QTreeWidgetItem *item = selectedItems.first();
|
|
||||||
if (item == m_feedList->stickyUnreadItem())
|
|
||||||
return;
|
|
||||||
Rss::FilePtr rss_item = m_feedList->getRSSItem(item);
|
|
||||||
bool ok;
|
|
||||||
QString newName;
|
|
||||||
do {
|
|
||||||
newName = AutoExpandableDialog::getText(this, tr("Please choose a new name for this RSS feed"), tr("New feed name:"), QLineEdit::Normal, m_feedList->getRSSItem(item)->displayName(), &ok);
|
|
||||||
// Check if name is already taken
|
|
||||||
if (ok) {
|
|
||||||
if (rss_item->parentFolder()->hasChild(newName)) {
|
|
||||||
QMessageBox::warning(0, tr("Name already in use"), tr("This name is already used by another item, please choose another one."));
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} while (!ok);
|
|
||||||
// Rename item
|
|
||||||
rss_item->rename(newName);
|
|
||||||
m_rssManager->saveStreamList();
|
|
||||||
// Update TreeWidget
|
|
||||||
updateItemInfos(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// right-click on stream : refresh it
|
|
||||||
void RSSImp::refreshSelectedItems()
|
|
||||||
{
|
|
||||||
QList<QTreeWidgetItem * > selectedItems = m_feedList->selectedItems();
|
|
||||||
foreach (QTreeWidgetItem *item, selectedItems) {
|
|
||||||
Rss::FilePtr file = m_feedList->getRSSItem(item);
|
|
||||||
// Update icons
|
|
||||||
if (item == m_feedList->stickyUnreadItem()) {
|
|
||||||
refreshAllFeeds();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (!file->refresh())
|
|
||||||
continue;
|
|
||||||
// Update UI
|
|
||||||
if (qSharedPointerDynamicCast<Rss::Feed>(file)) {
|
|
||||||
item->setData(0, Qt::DecorationRole, QVariant(QIcon(":/icons/loading.png")));
|
|
||||||
}
|
|
||||||
else if (qSharedPointerDynamicCast<Rss::Folder>(file)) {
|
|
||||||
// Update feeds in the folder
|
|
||||||
foreach (QTreeWidgetItem *feed, m_feedList->getAllFeedItems(item))
|
|
||||||
feed->setData(0, Qt::DecorationRole, QVariant(QIcon(":/icons/loading.png")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::copySelectedFeedsURL()
|
|
||||||
{
|
|
||||||
QStringList URLs;
|
|
||||||
QList<QTreeWidgetItem * > selectedItems = m_feedList->selectedItems();
|
|
||||||
QTreeWidgetItem *item;
|
|
||||||
foreach (item, selectedItems)
|
|
||||||
if (m_feedList->isFeed(item))
|
|
||||||
URLs << m_feedList->getItemID(item);
|
|
||||||
qApp->clipboard()->setText(URLs.join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::on_markReadButton_clicked()
|
|
||||||
{
|
|
||||||
QList<QTreeWidgetItem * > selectedItems = m_feedList->selectedItems();
|
|
||||||
foreach (QTreeWidgetItem *item, selectedItems) {
|
|
||||||
Rss::FilePtr rss_item = m_feedList->getRSSItem(item);
|
|
||||||
Q_ASSERT(rss_item);
|
|
||||||
rss_item->markAsRead();
|
|
||||||
updateItemInfos(item);
|
|
||||||
}
|
|
||||||
// Update article list
|
|
||||||
if (!selectedItems.isEmpty())
|
|
||||||
populateArticleList(m_feedList->currentItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
QTreeWidgetItem *RSSImp::createFolderListItem(const Rss::FilePtr &rssFile)
|
|
||||||
{
|
|
||||||
Q_ASSERT(rssFile);
|
|
||||||
QTreeWidgetItem *item = new QTreeWidgetItem;
|
|
||||||
item->setData(0, Qt::DisplayRole, QVariant(rssFile->displayName() + QString::fromUtf8(" (") + QString::number(rssFile->unreadCount()) + QString(")")));
|
|
||||||
item->setData(0, Qt::DecorationRole, QIcon(rssFile->iconPath()));
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::fillFeedsList(QTreeWidgetItem *parent, const Rss::FolderPtr &rss_parent)
|
|
||||||
{
|
|
||||||
QList<Rss::FilePtr> children;
|
|
||||||
if (parent)
|
|
||||||
children = rss_parent->getContent();
|
|
||||||
else
|
|
||||||
children = m_rssManager->rootFolder()->getContent();
|
|
||||||
foreach (const Rss::FilePtr &rssFile, children) {
|
|
||||||
QTreeWidgetItem *item = createFolderListItem(rssFile);
|
|
||||||
Q_ASSERT(item);
|
|
||||||
if (parent)
|
|
||||||
parent->addChild(item);
|
|
||||||
else
|
|
||||||
m_feedList->addTopLevelItem(item);
|
|
||||||
|
|
||||||
// Notify TreeWidget of item addition
|
|
||||||
m_feedList->itemAdded(item, rssFile);
|
|
||||||
|
|
||||||
// Recursive call if this is a folder.
|
|
||||||
if (Rss::FolderPtr folder = qSharedPointerDynamicCast<Rss::Folder>(rssFile))
|
|
||||||
fillFeedsList(item, folder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidgetItem *RSSImp::createArticleListItem(const Rss::ArticlePtr &article)
|
|
||||||
{
|
|
||||||
Q_ASSERT(article);
|
|
||||||
QListWidgetItem *item = new QListWidgetItem;
|
|
||||||
|
|
||||||
item->setData(Article::TitleRole, article->title());
|
|
||||||
item->setData(Article::FeedUrlRole, article->parent()->url());
|
|
||||||
item->setData(Article::IdRole, article->guid());
|
|
||||||
if (article->isRead()) {
|
|
||||||
item->setData(Article::ColorRole, QVariant(QColor("grey")));
|
|
||||||
item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere.png")));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
item->setData(Article::ColorRole, QVariant(QColor("blue")));
|
|
||||||
item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere2.png")));
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fills the newsList
|
|
||||||
void RSSImp::populateArticleList(QTreeWidgetItem *item)
|
|
||||||
{
|
|
||||||
if (!item) {
|
|
||||||
m_ui->listArticles->clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rss::FilePtr rss_item = m_feedList->getRSSItem(item);
|
|
||||||
if (!rss_item)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Clear the list first
|
|
||||||
m_ui->textBrowser->clear();
|
|
||||||
m_currentArticle = 0;
|
|
||||||
m_ui->listArticles->clear();
|
|
||||||
|
|
||||||
qDebug("Getting the list of news");
|
|
||||||
Rss::ArticleList articles;
|
|
||||||
if (rss_item == m_rssManager->rootFolder())
|
|
||||||
articles = rss_item->unreadArticleListByDateDesc();
|
|
||||||
else
|
|
||||||
articles = rss_item->articleListByDateDesc();
|
|
||||||
|
|
||||||
qDebug("Got the list of news");
|
|
||||||
foreach (const Rss::ArticlePtr &article, articles) {
|
|
||||||
QListWidgetItem *articleItem = createArticleListItem(article);
|
|
||||||
m_ui->listArticles->addItem(articleItem);
|
|
||||||
}
|
|
||||||
qDebug("Added all news to the GUI");
|
|
||||||
}
|
|
||||||
|
|
||||||
// display a news
|
|
||||||
void RSSImp::refreshTextBrowser()
|
|
||||||
{
|
|
||||||
QList<QListWidgetItem * > selection = m_ui->listArticles->selectedItems();
|
|
||||||
if (selection.empty()) return;
|
|
||||||
QListWidgetItem *item = selection.first();
|
|
||||||
Q_ASSERT(item);
|
|
||||||
if (item == m_currentArticle) return;
|
|
||||||
m_currentArticle = item;
|
|
||||||
|
|
||||||
Rss::FeedPtr feed = m_feedList->getRSSItemFromUrl(item->data(Article::FeedUrlRole).toString());
|
|
||||||
if (!feed) return;
|
|
||||||
Rss::ArticlePtr article = feed->getItem(item->data(Article::IdRole).toString());
|
|
||||||
if (!article) return;
|
|
||||||
QString html;
|
|
||||||
html += "<div style='border: 2px solid red; margin-left: 5px; margin-right: 5px; margin-bottom: 5px;'>";
|
|
||||||
html += "<div style='background-color: #678db2; font-weight: bold; color: #fff;'>" + article->title() + "</div>";
|
|
||||||
if (article->date().isValid())
|
|
||||||
html += "<div style='background-color: #efefef;'><b>" + tr("Date: ") + "</b>" + article->date().toLocalTime().toString(Qt::SystemLocaleLongDate) + "</div>";
|
|
||||||
if (!article->author().isEmpty())
|
|
||||||
html += "<div style='background-color: #efefef;'><b>" + tr("Author: ") + "</b>" + article->author() + "</div>";
|
|
||||||
html += "</div>";
|
|
||||||
html += "<div style='margin-left: 5px; margin-right: 5px;'>";
|
|
||||||
if (Qt::mightBeRichText(article->description())) {
|
|
||||||
html += article->description();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
QString description = article->description();
|
|
||||||
QRegExp rx;
|
|
||||||
// If description is plain text, replace BBCode tags with HTML and wrap everything in <pre></pre> so it looks nice
|
|
||||||
rx.setMinimal(true);
|
|
||||||
rx.setCaseSensitivity(Qt::CaseInsensitive);
|
|
||||||
|
|
||||||
rx.setPattern("\\[img\\](.+)\\[/img\\]");
|
|
||||||
description = description.replace(rx, "<img src=\"\\1\">");
|
|
||||||
|
|
||||||
rx.setPattern("\\[url=(\")?(.+)\\1\\]");
|
|
||||||
description = description.replace(rx, "<a href=\"\\2\">");
|
|
||||||
description = description.replace("[/url]", "</a>", Qt::CaseInsensitive);
|
|
||||||
|
|
||||||
rx.setPattern("\\[(/)?([bius])\\]");
|
|
||||||
description = description.replace(rx, "<\\1\\2>");
|
|
||||||
|
|
||||||
rx.setPattern("\\[color=(\")?(.+)\\1\\]");
|
|
||||||
description = description.replace(rx, "<span style=\"color:\\2\">");
|
|
||||||
description = description.replace("[/color]", "</span>", Qt::CaseInsensitive);
|
|
||||||
|
|
||||||
rx.setPattern("\\[size=(\")?(.+)\\d\\1\\]");
|
|
||||||
description = description.replace(rx, "<span style=\"font-size:\\2px\">");
|
|
||||||
description = description.replace("[/size]", "</span>", Qt::CaseInsensitive);
|
|
||||||
|
|
||||||
html += "<pre>" + description + "</pre>";
|
|
||||||
}
|
|
||||||
html += "</div>";
|
|
||||||
m_ui->textBrowser->setHtml(html);
|
|
||||||
article->markAsRead();
|
|
||||||
item->setData(Article::ColorRole, QVariant(QColor("grey")));
|
|
||||||
item->setData(Article::IconRole, QVariant(QIcon(":/icons/sphere.png")));
|
|
||||||
// Decrement feed nb unread news
|
|
||||||
updateItemInfos(m_feedList->stickyUnreadItem());
|
|
||||||
updateItemInfos(m_feedList->getTreeItemFromUrl(item->data(Article::FeedUrlRole).toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::saveSlidersPosition()
|
|
||||||
{
|
|
||||||
// Remember sliders positions
|
|
||||||
Preferences *const pref = Preferences::instance();
|
|
||||||
pref->setRssSideSplitterState(m_ui->splitterSide->saveState());
|
|
||||||
pref->setRssMainSplitterState(m_ui->splitterMain->saveState());
|
|
||||||
qDebug("Splitters position saved");
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::restoreSlidersPosition()
|
|
||||||
{
|
|
||||||
const Preferences *const pref = Preferences::instance();
|
|
||||||
const QByteArray stateSide = pref->getRssSideSplitterState();
|
|
||||||
if (!stateSide.isEmpty())
|
|
||||||
m_ui->splitterSide->restoreState(stateSide);
|
|
||||||
const QByteArray stateMain = pref->getRssMainSplitterState();
|
|
||||||
if (!stateMain.isEmpty())
|
|
||||||
m_ui->splitterMain->restoreState(stateMain);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::updateItemsInfos(const QList<QTreeWidgetItem *> &items)
|
|
||||||
{
|
|
||||||
foreach (QTreeWidgetItem *item, items)
|
|
||||||
updateItemInfos(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::updateItemInfos(QTreeWidgetItem *item)
|
|
||||||
{
|
|
||||||
Rss::FilePtr rss_item = m_feedList->getRSSItem(item);
|
|
||||||
if (!rss_item)
|
|
||||||
return;
|
|
||||||
|
|
||||||
QString name;
|
|
||||||
if (rss_item == m_rssManager->rootFolder()) {
|
|
||||||
name = tr("Unread");
|
|
||||||
emit updateRSSCount(rss_item->unreadCount());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
name = rss_item->displayName();
|
|
||||||
}
|
|
||||||
item->setText(0, name + QString::fromUtf8(" (") + QString::number(rss_item->unreadCount()) + QString(")"));
|
|
||||||
// If item has a parent, update it too
|
|
||||||
if (item->parent())
|
|
||||||
updateItemInfos(item->parent());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::updateFeedIcon(const QString &url, const QString &iconPath)
|
|
||||||
{
|
|
||||||
QTreeWidgetItem *item = m_feedList->getTreeItemFromUrl(url);
|
|
||||||
|
|
||||||
if (item)
|
|
||||||
item->setData(0, Qt::DecorationRole, QVariant(QIcon(iconPath)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::updateFeedInfos(const QString &url, const QString &display_name, uint nbUnread)
|
|
||||||
{
|
|
||||||
qDebug() << Q_FUNC_INFO << display_name;
|
|
||||||
QTreeWidgetItem *item = m_feedList->getTreeItemFromUrl(url);
|
|
||||||
|
|
||||||
if (item) {
|
|
||||||
Rss::FeedPtr stream = qSharedPointerCast<Rss::Feed>(m_feedList->getRSSItem(item));
|
|
||||||
item->setText(0, display_name + QString::fromUtf8(" (") + QString::number(nbUnread) + QString(")"));
|
|
||||||
if (!stream->isLoading())
|
|
||||||
item->setData(0, Qt::DecorationRole, QIcon(stream->iconPath()));
|
|
||||||
// Update parent
|
|
||||||
if (item->parent())
|
|
||||||
updateItemInfos(item->parent());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update Unread item
|
|
||||||
updateItemInfos(m_feedList->stickyUnreadItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::onFeedContentChanged(const QString &url)
|
|
||||||
{
|
|
||||||
qDebug() << Q_FUNC_INFO << url;
|
|
||||||
QTreeWidgetItem *item = m_feedList->getTreeItemFromUrl(url);
|
|
||||||
// If the feed is selected, update the displayed news
|
|
||||||
if (m_feedList->currentItem() == item)
|
|
||||||
populateArticleList(item);
|
|
||||||
// Update unread items
|
|
||||||
else if (m_feedList->currentItem() == m_feedList->stickyUnreadItem())
|
|
||||||
populateArticleList(m_feedList->stickyUnreadItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::updateRefreshInterval(uint val)
|
|
||||||
{
|
|
||||||
m_rssManager->updateRefreshInterval(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
RSSImp::RSSImp(QWidget *parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
, m_ui(new Ui::RSS())
|
|
||||||
, m_rssManager(new Rss::Manager)
|
|
||||||
{
|
|
||||||
m_ui->setupUi(this);
|
|
||||||
// Icons
|
|
||||||
m_ui->actionCopy_feed_URL->setIcon(GuiIconProvider::instance()->getIcon("edit-copy"));
|
|
||||||
m_ui->actionDelete->setIcon(GuiIconProvider::instance()->getIcon("edit-delete"));
|
|
||||||
m_ui->actionDownload_torrent->setIcon(GuiIconProvider::instance()->getIcon("download"));
|
|
||||||
m_ui->actionMark_items_read->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read"));
|
|
||||||
m_ui->actionNew_folder->setIcon(GuiIconProvider::instance()->getIcon("folder-new"));
|
|
||||||
m_ui->actionNew_subscription->setIcon(GuiIconProvider::instance()->getIcon("list-add"));
|
|
||||||
m_ui->actionOpen_news_URL->setIcon(GuiIconProvider::instance()->getIcon("application-x-mswinurl"));
|
|
||||||
m_ui->actionRename->setIcon(GuiIconProvider::instance()->getIcon("edit-rename"));
|
|
||||||
m_ui->actionUpdate->setIcon(GuiIconProvider::instance()->getIcon("view-refresh"));
|
|
||||||
m_ui->actionUpdate_all_feeds->setIcon(GuiIconProvider::instance()->getIcon("view-refresh"));
|
|
||||||
m_ui->newFeedButton->setIcon(GuiIconProvider::instance()->getIcon("list-add"));
|
|
||||||
m_ui->markReadButton->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read"));
|
|
||||||
m_ui->updateAllButton->setIcon(GuiIconProvider::instance()->getIcon("view-refresh"));
|
|
||||||
m_ui->rssDownloaderBtn->setIcon(GuiIconProvider::instance()->getIcon("download"));
|
|
||||||
m_ui->settingsButton->setIcon(GuiIconProvider::instance()->getIcon("configure", "preferences-system"));
|
|
||||||
|
|
||||||
m_feedList = new FeedListWidget(m_ui->splitterSide, m_rssManager);
|
|
||||||
m_ui->splitterSide->insertWidget(0, m_feedList);
|
|
||||||
editHotkey = new QShortcut(Qt::Key_F2, m_feedList, 0, 0, Qt::WidgetShortcut);
|
|
||||||
connect(editHotkey, SIGNAL(activated()), SLOT(renameSelectedRssFile()));
|
|
||||||
connect(m_feedList, SIGNAL(doubleClicked(QModelIndex)), SLOT(renameSelectedRssFile()));
|
|
||||||
deleteHotkey = new QShortcut(QKeySequence::Delete, m_feedList, 0, 0, Qt::WidgetShortcut);
|
|
||||||
connect(deleteHotkey, SIGNAL(activated()), SLOT(deleteSelectedItems()));
|
|
||||||
|
|
||||||
m_rssManager->loadStreamList();
|
|
||||||
m_feedList->setSortingEnabled(false);
|
|
||||||
fillFeedsList();
|
|
||||||
m_feedList->setSortingEnabled(true);
|
|
||||||
populateArticleList(m_feedList->currentItem());
|
|
||||||
|
|
||||||
loadFoldersOpenState();
|
|
||||||
connect(m_rssManager.data(), SIGNAL(feedInfosChanged(QString,QString,unsigned int)), SLOT(updateFeedInfos(QString,QString,unsigned int)));
|
|
||||||
connect(m_rssManager.data(), SIGNAL(feedContentChanged(QString)), SLOT(onFeedContentChanged(QString)));
|
|
||||||
connect(m_rssManager.data(), SIGNAL(feedIconChanged(QString,QString)), SLOT(updateFeedIcon(QString,QString)));
|
|
||||||
|
|
||||||
connect(m_feedList, SIGNAL(customContextMenuRequested(const QPoint&)), SLOT(displayRSSListMenu(const QPoint&)));
|
|
||||||
connect(m_ui->listArticles, SIGNAL(customContextMenuRequested(const QPoint&)), SLOT(displayItemsListMenu(const QPoint&)));
|
|
||||||
|
|
||||||
// Feeds list actions
|
|
||||||
connect(m_ui->actionDelete, SIGNAL(triggered()), this, SLOT(deleteSelectedItems()));
|
|
||||||
connect(m_ui->actionRename, SIGNAL(triggered()), this, SLOT(renameSelectedRssFile()));
|
|
||||||
connect(m_ui->actionUpdate, SIGNAL(triggered()), this, SLOT(refreshSelectedItems()));
|
|
||||||
connect(m_ui->actionNew_folder, SIGNAL(triggered()), this, SLOT(askNewFolder()));
|
|
||||||
connect(m_ui->actionNew_subscription, SIGNAL(triggered()), this, SLOT(on_newFeedButton_clicked()));
|
|
||||||
connect(m_ui->actionUpdate_all_feeds, SIGNAL(triggered()), this, SLOT(refreshAllFeeds()));
|
|
||||||
connect(m_ui->updateAllButton, SIGNAL(clicked()), SLOT(refreshAllFeeds()));
|
|
||||||
connect(m_ui->actionCopy_feed_URL, SIGNAL(triggered()), this, SLOT(copySelectedFeedsURL()));
|
|
||||||
connect(m_ui->actionMark_items_read, SIGNAL(triggered()), this, SLOT(on_markReadButton_clicked()));
|
|
||||||
// News list actions
|
|
||||||
connect(m_ui->actionOpen_news_URL, SIGNAL(triggered()), this, SLOT(openSelectedArticlesUrls()));
|
|
||||||
connect(m_ui->actionDownload_torrent, SIGNAL(triggered()), this, SLOT(downloadSelectedTorrents()));
|
|
||||||
|
|
||||||
connect(m_feedList, SIGNAL(currentItemChanged(QTreeWidgetItem *,QTreeWidgetItem *)), this, SLOT(populateArticleList(QTreeWidgetItem *)));
|
|
||||||
connect(m_feedList, SIGNAL(foldersAltered(QList<QTreeWidgetItem * >)), this, SLOT(updateItemsInfos(QList<QTreeWidgetItem * >)));
|
|
||||||
|
|
||||||
connect(m_ui->listArticles, SIGNAL(itemSelectionChanged()), this, SLOT(refreshTextBrowser()));
|
|
||||||
connect(m_ui->listArticles, SIGNAL(itemDoubleClicked(QListWidgetItem *)), this, SLOT(downloadSelectedTorrents()));
|
|
||||||
|
|
||||||
// Restore sliders position
|
|
||||||
restoreSlidersPosition();
|
|
||||||
// Bind saveSliders slots
|
|
||||||
connect(m_ui->splitterMain, SIGNAL(splitterMoved(int,int)), this, SLOT(saveSlidersPosition()));
|
|
||||||
connect(m_ui->splitterSide, SIGNAL(splitterMoved(int,int)), this, SLOT(saveSlidersPosition()));
|
|
||||||
|
|
||||||
qDebug("RSSImp constructed");
|
|
||||||
}
|
|
||||||
|
|
||||||
RSSImp::~RSSImp()
|
|
||||||
{
|
|
||||||
qDebug("Deleting RSSImp...");
|
|
||||||
saveFoldersOpenState();
|
|
||||||
delete editHotkey;
|
|
||||||
delete deleteHotkey;
|
|
||||||
delete m_feedList;
|
|
||||||
delete m_ui;
|
|
||||||
qDebug("RSSImp deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::on_settingsButton_clicked()
|
|
||||||
{
|
|
||||||
RssSettingsDlg dlg(this);
|
|
||||||
if (dlg.exec())
|
|
||||||
updateRefreshInterval(Preferences::instance()->getRSSRefreshInterval());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RSSImp::on_rssDownloaderBtn_clicked()
|
|
||||||
{
|
|
||||||
AutomatedRssDownloader dlg(m_rssManager, this);
|
|
||||||
dlg.exec();
|
|
||||||
if (dlg.isRssDownloaderEnabled()) {
|
|
||||||
m_rssManager->rootFolder()->recheckRssItemsForDownload();
|
|
||||||
refreshAllFeeds();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* Bittorrent Client using Qt4 and libtorrent.
|
|
||||||
* Copyright (C) 2010 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 "rsssettingsdlg.h"
|
|
||||||
#include "ui_rsssettingsdlg.h"
|
|
||||||
|
|
||||||
#include "base/preferences.h"
|
|
||||||
#include "base/utils/misc.h"
|
|
||||||
#include "guiiconprovider.h"
|
|
||||||
|
|
||||||
RssSettingsDlg::RssSettingsDlg(QWidget *parent) :
|
|
||||||
QDialog(parent),
|
|
||||||
ui(new Ui::RssSettingsDlg)
|
|
||||||
{
|
|
||||||
ui->setupUi(this);
|
|
||||||
ui->rssIcon->setPixmap(GuiIconProvider::instance()->getIcon("application-rss+xml").pixmap(Utils::Misc::largeIconSize()));
|
|
||||||
// Load settings
|
|
||||||
const Preferences* const pref = Preferences::instance();
|
|
||||||
ui->spinRSSRefresh->setValue(pref->getRSSRefreshInterval());
|
|
||||||
ui->spinRSSMaxArticlesPerFeed->setValue(pref->getRSSMaxArticlesPerFeed());
|
|
||||||
}
|
|
||||||
|
|
||||||
RssSettingsDlg::~RssSettingsDlg()
|
|
||||||
{
|
|
||||||
qDebug("Deleting the RSS settings dialog");
|
|
||||||
delete ui;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RssSettingsDlg::on_buttonBox_accepted() {
|
|
||||||
// Save settings
|
|
||||||
Preferences* const pref = Preferences::instance();
|
|
||||||
pref->setRSSRefreshInterval(ui->spinRSSRefresh->value());
|
|
||||||
pref->setRSSMaxArticlesPerFeed(ui->spinRSSMaxArticlesPerFeed->value());
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>RssSettingsDlg</class>
|
|
||||||
<widget class="QDialog" name="RssSettingsDlg">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>415</width>
|
|
||||||
<height>123</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>RSS Reader Settings</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>RSS feeds refresh interval:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="2">
|
|
||||||
<widget class="QSpinBox" name="spinRSSRefresh">
|
|
||||||
<property name="suffix">
|
|
||||||
<string> min</string>
|
|
||||||
</property>
|
|
||||||
<property name="minimum">
|
|
||||||
<number>1</number>
|
|
||||||
</property>
|
|
||||||
<property name="maximum">
|
|
||||||
<number>999999</number>
|
|
||||||
</property>
|
|
||||||
<property name="value">
|
|
||||||
<number>5</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string>Maximum number of articles per feed:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="QSpinBox" name="spinRSSMaxArticlesPerFeed">
|
|
||||||
<property name="maximum">
|
|
||||||
<number>9999</number>
|
|
||||||
</property>
|
|
||||||
<property name="value">
|
|
||||||
<number>100</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="0" rowspan="2">
|
|
||||||
<widget class="QLabel" name="rssIcon"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>0</width>
|
|
||||||
<height>0</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources>
|
|
||||||
<include location="../../icons.qrc"/>
|
|
||||||
</resources>
|
|
||||||
<connections>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>accepted()</signal>
|
|
||||||
<receiver>RssSettingsDlg</receiver>
|
|
||||||
<slot>accept()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>248</x>
|
|
||||||
<y>254</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>157</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>rejected()</signal>
|
|
||||||
<receiver>RssSettingsDlg</receiver>
|
|
||||||
<slot>reject()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>316</x>
|
|
||||||
<y>260</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>286</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
</connections>
|
|
||||||
</ui>
|
|
533
src/gui/rss/rsswidget.cpp
Normal file
533
src/gui/rss/rsswidget.cpp
Normal file
|
@ -0,0 +1,533 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2006 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rsswidget.h"
|
||||||
|
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QDragMoveEvent>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "base/bittorrent/session.h"
|
||||||
|
#include "base/net/downloadmanager.h"
|
||||||
|
#include "base/preferences.h"
|
||||||
|
#include "base/rss/rss_article.h"
|
||||||
|
#include "base/rss/rss_feed.h"
|
||||||
|
#include "base/rss/rss_folder.h"
|
||||||
|
#include "base/rss/rss_session.h"
|
||||||
|
#include "base/utils/misc.h"
|
||||||
|
#include "addnewtorrentdialog.h"
|
||||||
|
#include "articlelistwidget.h"
|
||||||
|
#include "autoexpandabledialog.h"
|
||||||
|
#include "automatedrssdownloader.h"
|
||||||
|
#include "feedlistwidget.h"
|
||||||
|
#include "guiiconprovider.h"
|
||||||
|
#include "ui_rsswidget.h"
|
||||||
|
|
||||||
|
RSSWidget::RSSWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_ui(new Ui::RSSWidget)
|
||||||
|
{
|
||||||
|
m_ui->setupUi(this);
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
m_ui->actionCopyFeedURL->setIcon(GuiIconProvider::instance()->getIcon("edit-copy"));
|
||||||
|
m_ui->actionDelete->setIcon(GuiIconProvider::instance()->getIcon("edit-delete"));
|
||||||
|
m_ui->actionDownloadTorrent->setIcon(GuiIconProvider::instance()->getIcon("download"));
|
||||||
|
m_ui->actionMarkItemsRead->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read"));
|
||||||
|
m_ui->actionNewFolder->setIcon(GuiIconProvider::instance()->getIcon("folder-new"));
|
||||||
|
m_ui->actionNewSubscription->setIcon(GuiIconProvider::instance()->getIcon("list-add"));
|
||||||
|
m_ui->actionOpenNewsURL->setIcon(GuiIconProvider::instance()->getIcon("application-x-mswinurl"));
|
||||||
|
m_ui->actionRename->setIcon(GuiIconProvider::instance()->getIcon("edit-rename"));
|
||||||
|
m_ui->actionUpdate->setIcon(GuiIconProvider::instance()->getIcon("view-refresh"));
|
||||||
|
m_ui->actionUpdateAllFeeds->setIcon(GuiIconProvider::instance()->getIcon("view-refresh"));
|
||||||
|
m_ui->newFeedButton->setIcon(GuiIconProvider::instance()->getIcon("list-add"));
|
||||||
|
m_ui->markReadButton->setIcon(GuiIconProvider::instance()->getIcon("mail-mark-read"));
|
||||||
|
m_ui->updateAllButton->setIcon(GuiIconProvider::instance()->getIcon("view-refresh"));
|
||||||
|
m_ui->rssDownloaderBtn->setIcon(GuiIconProvider::instance()->getIcon("download"));
|
||||||
|
|
||||||
|
m_articleListWidget = new ArticleListWidget(m_ui->splitterMain);
|
||||||
|
m_ui->splitterMain->insertWidget(0, m_articleListWidget);
|
||||||
|
connect(m_articleListWidget, &ArticleListWidget::customContextMenuRequested, this, &RSSWidget::displayItemsListMenu);
|
||||||
|
connect(m_articleListWidget, &ArticleListWidget::currentItemChanged, this, &RSSWidget::handleCurrentArticleItemChanged);
|
||||||
|
connect(m_articleListWidget, &ArticleListWidget::itemDoubleClicked, this, &RSSWidget::downloadSelectedTorrents);
|
||||||
|
|
||||||
|
m_feedListWidget = new FeedListWidget(m_ui->splitterSide);
|
||||||
|
m_ui->splitterSide->insertWidget(0, m_feedListWidget);
|
||||||
|
connect(m_feedListWidget, &QAbstractItemView::doubleClicked, this, &RSSWidget::renameSelectedRSSItem);
|
||||||
|
connect(m_feedListWidget, &QTreeWidget::currentItemChanged, this, &RSSWidget::handleCurrentFeedItemChanged);
|
||||||
|
connect(m_feedListWidget, &QWidget::customContextMenuRequested, this, &RSSWidget::displayRSSListMenu);
|
||||||
|
loadFoldersOpenState();
|
||||||
|
m_feedListWidget->setCurrentItem(m_feedListWidget->stickyUnreadItem());
|
||||||
|
|
||||||
|
m_editHotkey = new QShortcut(Qt::Key_F2, m_feedListWidget, 0, 0, Qt::WidgetShortcut);
|
||||||
|
connect(m_editHotkey, &QShortcut::activated, this, &RSSWidget::renameSelectedRSSItem);
|
||||||
|
m_deleteHotkey = new QShortcut(QKeySequence::Delete, m_feedListWidget, 0, 0, Qt::WidgetShortcut);
|
||||||
|
connect(m_deleteHotkey, &QShortcut::activated, this, &RSSWidget::deleteSelectedItems);
|
||||||
|
|
||||||
|
// Feeds list actions
|
||||||
|
connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems);
|
||||||
|
connect(m_ui->actionRename, &QAction::triggered, this, &RSSWidget::renameSelectedRSSItem);
|
||||||
|
connect(m_ui->actionUpdate, &QAction::triggered, this, &RSSWidget::refreshSelectedItems);
|
||||||
|
connect(m_ui->actionNewFolder, &QAction::triggered, this, &RSSWidget::askNewFolder);
|
||||||
|
connect(m_ui->actionNewSubscription, &QAction::triggered, this, &RSSWidget::on_newFeedButton_clicked);
|
||||||
|
connect(m_ui->actionUpdateAllFeeds, &QAction::triggered, this, &RSSWidget::refreshAllFeeds);
|
||||||
|
connect(m_ui->updateAllButton, &QAbstractButton::clicked, this, &RSSWidget::refreshAllFeeds);
|
||||||
|
connect(m_ui->actionCopyFeedURL, &QAction::triggered, this, &RSSWidget::copySelectedFeedsURL);
|
||||||
|
connect(m_ui->actionMarkItemsRead, &QAction::triggered, this, &RSSWidget::on_markReadButton_clicked);
|
||||||
|
|
||||||
|
// News list actions
|
||||||
|
connect(m_ui->actionOpenNewsURL, &QAction::triggered, this, &RSSWidget::openSelectedArticlesUrls);
|
||||||
|
connect(m_ui->actionDownloadTorrent, &QAction::triggered, this, &RSSWidget::downloadSelectedTorrents);
|
||||||
|
|
||||||
|
// Restore sliders position
|
||||||
|
restoreSlidersPosition();
|
||||||
|
// Bind saveSliders slots
|
||||||
|
connect(m_ui->splitterMain, &QSplitter::splitterMoved, this, &RSSWidget::saveSlidersPosition);
|
||||||
|
connect(m_ui->splitterSide, &QSplitter::splitterMoved, this, &RSSWidget::saveSlidersPosition);
|
||||||
|
|
||||||
|
if (RSS::Session::instance()->isProcessingEnabled())
|
||||||
|
m_ui->labelWarn->hide();
|
||||||
|
connect(RSS::Session::instance(), &RSS::Session::processingStateChanged
|
||||||
|
, this, &RSSWidget::handleSessionProcessingStateChanged);
|
||||||
|
connect(RSS::Session::instance()->rootFolder(), &RSS::Folder::unreadCountChanged
|
||||||
|
, this, &RSSWidget::handleUnreadCountChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
RSSWidget::~RSSWidget()
|
||||||
|
{
|
||||||
|
// we need it here to properly mark latest article
|
||||||
|
// as read without having additional code
|
||||||
|
m_articleListWidget->clear();
|
||||||
|
|
||||||
|
saveFoldersOpenState();
|
||||||
|
|
||||||
|
delete m_editHotkey;
|
||||||
|
delete m_deleteHotkey;
|
||||||
|
delete m_feedListWidget;
|
||||||
|
delete m_ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
// display a right-click menu
|
||||||
|
void RSSWidget::displayRSSListMenu(const QPoint &pos)
|
||||||
|
{
|
||||||
|
if (!m_feedListWidget->indexAt(pos).isValid())
|
||||||
|
// No item under the mouse, clear selection
|
||||||
|
m_feedListWidget->clearSelection();
|
||||||
|
QMenu myRSSListMenu(this);
|
||||||
|
QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
|
||||||
|
if (selectedItems.size() > 0) {
|
||||||
|
myRSSListMenu.addAction(m_ui->actionUpdate);
|
||||||
|
myRSSListMenu.addAction(m_ui->actionMarkItemsRead);
|
||||||
|
myRSSListMenu.addSeparator();
|
||||||
|
if (selectedItems.size() == 1) {
|
||||||
|
if (selectedItems.first() != m_feedListWidget->stickyUnreadItem()) {
|
||||||
|
myRSSListMenu.addAction(m_ui->actionRename);
|
||||||
|
myRSSListMenu.addAction(m_ui->actionDelete);
|
||||||
|
myRSSListMenu.addSeparator();
|
||||||
|
if (m_feedListWidget->isFolder(selectedItems.first()))
|
||||||
|
myRSSListMenu.addAction(m_ui->actionNewFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
myRSSListMenu.addAction(m_ui->actionDelete);
|
||||||
|
myRSSListMenu.addSeparator();
|
||||||
|
}
|
||||||
|
myRSSListMenu.addAction(m_ui->actionNewSubscription);
|
||||||
|
if (m_feedListWidget->isFeed(selectedItems.first())) {
|
||||||
|
myRSSListMenu.addSeparator();
|
||||||
|
myRSSListMenu.addAction(m_ui->actionCopyFeedURL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
myRSSListMenu.addAction(m_ui->actionNewSubscription);
|
||||||
|
myRSSListMenu.addAction(m_ui->actionNewFolder);
|
||||||
|
myRSSListMenu.addSeparator();
|
||||||
|
myRSSListMenu.addAction(m_ui->actionUpdateAllFeeds);
|
||||||
|
}
|
||||||
|
myRSSListMenu.exec(QCursor::pos());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::displayItemsListMenu(const QPoint &)
|
||||||
|
{
|
||||||
|
bool hasTorrent = false;
|
||||||
|
bool hasLink = false;
|
||||||
|
foreach (const QListWidgetItem *item, m_articleListWidget->selectedItems()) {
|
||||||
|
auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
|
||||||
|
Q_ASSERT(article);
|
||||||
|
|
||||||
|
if (!article->torrentUrl().isEmpty())
|
||||||
|
hasTorrent = true;
|
||||||
|
if (!article->link().isEmpty())
|
||||||
|
hasLink = true;
|
||||||
|
if (hasTorrent && hasLink)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu myItemListMenu(this);
|
||||||
|
if (hasTorrent)
|
||||||
|
myItemListMenu.addAction(m_ui->actionDownloadTorrent);
|
||||||
|
if (hasLink)
|
||||||
|
myItemListMenu.addAction(m_ui->actionOpenNewsURL);
|
||||||
|
if (hasTorrent || hasLink)
|
||||||
|
myItemListMenu.exec(QCursor::pos());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::askNewFolder()
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
QString newName = AutoExpandableDialog::getText(
|
||||||
|
this, tr("Please choose a folder name"), tr("Folder name:"), QLineEdit::Normal
|
||||||
|
, tr("New folder"), &ok);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
newName = newName.trimmed();
|
||||||
|
if (newName.isEmpty()) return;
|
||||||
|
|
||||||
|
// Determine destination folder for new item
|
||||||
|
QTreeWidgetItem *destItem = nullptr;
|
||||||
|
QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
|
||||||
|
if (!selectedItems.empty()) {
|
||||||
|
destItem = selectedItems.first();
|
||||||
|
if (!m_feedListWidget->isFolder(destItem))
|
||||||
|
destItem = destItem->parent();
|
||||||
|
}
|
||||||
|
// Consider the case where the user clicked on Unread item
|
||||||
|
RSS::Folder *rssDestFolder = ((destItem == m_feedListWidget->stickyUnreadItem())
|
||||||
|
? RSS::Session::instance()->rootFolder()
|
||||||
|
: qobject_cast<RSS::Folder *>(m_feedListWidget->getRSSItem(destItem)));
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
const QString newFolderPath = RSS::Item::joinPath(rssDestFolder->path(), newName);
|
||||||
|
if (!RSS::Session::instance()->addFolder(newFolderPath, &error))
|
||||||
|
QMessageBox::warning(this, "qBittorrent", error, QMessageBox::Ok);
|
||||||
|
|
||||||
|
// Expand destination folder to display new feed
|
||||||
|
if (destItem != m_feedListWidget->stickyUnreadItem())
|
||||||
|
destItem->setExpanded(true);
|
||||||
|
// As new RSS items are added synchronously, we can do the following here.
|
||||||
|
m_feedListWidget->setCurrentItem(m_feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFolderPath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a stream by a button
|
||||||
|
void RSSWidget::on_newFeedButton_clicked()
|
||||||
|
{
|
||||||
|
// Ask for feed URL
|
||||||
|
const QString clipText = qApp->clipboard()->text();
|
||||||
|
const QString defaultURL = (Utils::Misc::isUrl(clipText) ? clipText : "http://");
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
QString newURL = AutoExpandableDialog::getText(
|
||||||
|
this, tr("Please type a RSS feed URL"), tr("Feed URL:"), QLineEdit::Normal, defaultURL, &ok);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
newURL = newURL.trimmed();
|
||||||
|
if (newURL.isEmpty()) return;
|
||||||
|
|
||||||
|
// Determine destination folder for new item
|
||||||
|
QTreeWidgetItem *destItem = nullptr;
|
||||||
|
QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
|
||||||
|
if (!selectedItems.empty()) {
|
||||||
|
destItem = selectedItems.first();
|
||||||
|
if (!m_feedListWidget->isFolder(destItem))
|
||||||
|
destItem = destItem->parent();
|
||||||
|
}
|
||||||
|
// Consider the case where the user clicked on Unread item
|
||||||
|
RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_feedListWidget->stickyUnreadItem()))
|
||||||
|
? RSS::Session::instance()->rootFolder()
|
||||||
|
: qobject_cast<RSS::Folder *>(m_feedListWidget->getRSSItem(destItem)));
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
// NOTE: We still add feed using legacy way (with URL as feed name)
|
||||||
|
const QString newFeedPath = RSS::Item::joinPath(rssDestFolder->path(), newURL);
|
||||||
|
if (!RSS::Session::instance()->addFeed(newURL, newFeedPath, &error))
|
||||||
|
QMessageBox::warning(this, "qBittorrent", error, QMessageBox::Ok);
|
||||||
|
|
||||||
|
// Expand destination folder to display new feed
|
||||||
|
if (destItem && (destItem != m_feedListWidget->stickyUnreadItem()))
|
||||||
|
destItem->setExpanded(true);
|
||||||
|
// As new RSS items are added synchronously, we can do the following here.
|
||||||
|
m_feedListWidget->setCurrentItem(m_feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFeedPath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::deleteSelectedItems()
|
||||||
|
{
|
||||||
|
QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
|
||||||
|
if (selectedItems.isEmpty())
|
||||||
|
return;
|
||||||
|
if ((selectedItems.size() == 1) && (selectedItems.first() == m_feedListWidget->stickyUnreadItem()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
QMessageBox::StandardButton answer = QMessageBox::question(
|
||||||
|
this, tr("Deletion confirmation"), tr("Are you sure you want to delete the selected RSS feeds?")
|
||||||
|
, QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||||
|
if (answer == QMessageBox::No)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (QTreeWidgetItem *item, selectedItems)
|
||||||
|
if (item != m_feedListWidget->stickyUnreadItem())
|
||||||
|
RSS::Session::instance()->removeItem(m_feedListWidget->itemPath(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::loadFoldersOpenState()
|
||||||
|
{
|
||||||
|
const QStringList openedFolders = Preferences::instance()->getRssOpenFolders();
|
||||||
|
foreach (const QString &varPath, openedFolders) {
|
||||||
|
QTreeWidgetItem *parent = nullptr;
|
||||||
|
foreach (const QString &name, varPath.split("\\")) {
|
||||||
|
int nbChildren = (parent ? parent->childCount() : m_feedListWidget->topLevelItemCount());
|
||||||
|
for (int i = 0; i < nbChildren; ++i) {
|
||||||
|
QTreeWidgetItem *child = (parent ? parent->child(i) : m_feedListWidget->topLevelItem(i));
|
||||||
|
if (m_feedListWidget->getRSSItem(child)->name() == name) {
|
||||||
|
parent = child;
|
||||||
|
parent->setExpanded(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::saveFoldersOpenState()
|
||||||
|
{
|
||||||
|
QStringList openedFolders;
|
||||||
|
foreach (QTreeWidgetItem *item, m_feedListWidget->getAllOpenedFolders())
|
||||||
|
openedFolders << m_feedListWidget->itemPath(item);
|
||||||
|
Preferences::instance()->setRssOpenFolders(openedFolders);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::refreshAllFeeds()
|
||||||
|
{
|
||||||
|
RSS::Session::instance()->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::downloadSelectedTorrents()
|
||||||
|
{
|
||||||
|
foreach (QListWidgetItem *item, m_articleListWidget->selectedItems()) {
|
||||||
|
auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
|
||||||
|
Q_ASSERT(article);
|
||||||
|
|
||||||
|
// Mark as read
|
||||||
|
article->markAsRead();
|
||||||
|
|
||||||
|
if (!article->torrentUrl().isEmpty()) {
|
||||||
|
if (AddNewTorrentDialog::isEnabled())
|
||||||
|
AddNewTorrentDialog::show(article->torrentUrl());
|
||||||
|
else
|
||||||
|
BitTorrent::Session::instance()->addTorrent(article->torrentUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the url of the selected RSS articles in the Web browser
|
||||||
|
void RSSWidget::openSelectedArticlesUrls()
|
||||||
|
{
|
||||||
|
foreach (QListWidgetItem *item, m_articleListWidget->selectedItems()) {
|
||||||
|
auto article = reinterpret_cast<RSS::Article *>(item->data(Qt::UserRole).value<quintptr>());
|
||||||
|
Q_ASSERT(article);
|
||||||
|
|
||||||
|
// Mark as read
|
||||||
|
article->markAsRead();
|
||||||
|
|
||||||
|
if (!article->link().isEmpty())
|
||||||
|
QDesktopServices::openUrl(QUrl(article->link()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::renameSelectedRSSItem()
|
||||||
|
{
|
||||||
|
QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
|
||||||
|
if (selectedItems.size() != 1) return;
|
||||||
|
|
||||||
|
QTreeWidgetItem *item = selectedItems.first();
|
||||||
|
if (item == m_feedListWidget->stickyUnreadItem())
|
||||||
|
return;
|
||||||
|
|
||||||
|
RSS::Item *rssItem = m_feedListWidget->getRSSItem(item);
|
||||||
|
const QString parentPath = RSS::Item::parentPath(rssItem->path());
|
||||||
|
bool ok;
|
||||||
|
do {
|
||||||
|
QString newName = AutoExpandableDialog::getText(
|
||||||
|
this, tr("Please choose a new name for this RSS feed"), tr("New feed name:")
|
||||||
|
, QLineEdit::Normal, rssItem->name(), &ok);
|
||||||
|
// Check if name is already taken
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
if (!RSS::Session::instance()->moveItem(rssItem, RSS::Item::joinPath(parentPath, newName), &error)) {
|
||||||
|
QMessageBox::warning(0, tr("Rename failed"), error);
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
} while (!ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::refreshSelectedItems()
|
||||||
|
{
|
||||||
|
foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) {
|
||||||
|
if (item == m_feedListWidget->stickyUnreadItem()) {
|
||||||
|
refreshAllFeeds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_feedListWidget->getRSSItem(item)->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::copySelectedFeedsURL()
|
||||||
|
{
|
||||||
|
QStringList URLs;
|
||||||
|
foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) {
|
||||||
|
if (auto feed = qobject_cast<RSS::Feed *>(m_feedListWidget->getRSSItem(item)))
|
||||||
|
URLs << feed->url();
|
||||||
|
}
|
||||||
|
qApp->clipboard()->setText(URLs.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem)
|
||||||
|
{
|
||||||
|
if (!currentItem) {
|
||||||
|
m_articleListWidget->clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_articleListWidget->setRSSItem(m_feedListWidget->getRSSItem(currentItem)
|
||||||
|
, (currentItem == m_feedListWidget->stickyUnreadItem()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::on_markReadButton_clicked()
|
||||||
|
{
|
||||||
|
foreach (QTreeWidgetItem *item, m_feedListWidget->selectedItems()) {
|
||||||
|
m_feedListWidget->getRSSItem(item)->markAsRead();
|
||||||
|
if (item == m_feedListWidget->stickyUnreadItem())
|
||||||
|
break; // all items was read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// display a news
|
||||||
|
void RSSWidget::handleCurrentArticleItemChanged(QListWidgetItem *currentItem, QListWidgetItem *previousItem)
|
||||||
|
{
|
||||||
|
m_ui->textBrowser->clear();
|
||||||
|
|
||||||
|
if (previousItem) {
|
||||||
|
auto article = m_articleListWidget->getRSSArticle(previousItem);
|
||||||
|
Q_ASSERT(article);
|
||||||
|
article->markAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentItem) return;
|
||||||
|
|
||||||
|
auto article = m_articleListWidget->getRSSArticle(currentItem);
|
||||||
|
Q_ASSERT(article);
|
||||||
|
|
||||||
|
QString html;
|
||||||
|
html += "<div style='border: 2px solid red; margin-left: 5px; margin-right: 5px; margin-bottom: 5px;'>";
|
||||||
|
html += "<div style='background-color: #678db2; font-weight: bold; color: #fff;'>" + article->title() + "</div>";
|
||||||
|
if (article->date().isValid())
|
||||||
|
html += "<div style='background-color: #efefef;'><b>" + tr("Date: ") + "</b>" + article->date().toLocalTime().toString(Qt::SystemLocaleLongDate) + "</div>";
|
||||||
|
if (!article->author().isEmpty())
|
||||||
|
html += "<div style='background-color: #efefef;'><b>" + tr("Author: ") + "</b>" + article->author() + "</div>";
|
||||||
|
html += "</div>";
|
||||||
|
html += "<div style='margin-left: 5px; margin-right: 5px;'>";
|
||||||
|
if (Qt::mightBeRichText(article->description())) {
|
||||||
|
html += article->description();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QString description = article->description();
|
||||||
|
QRegExp rx;
|
||||||
|
// If description is plain text, replace BBCode tags with HTML and wrap everything in <pre></pre> so it looks nice
|
||||||
|
rx.setMinimal(true);
|
||||||
|
rx.setCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
|
||||||
|
rx.setPattern("\\[img\\](.+)\\[/img\\]");
|
||||||
|
description = description.replace(rx, "<img src=\"\\1\">");
|
||||||
|
|
||||||
|
rx.setPattern("\\[url=(\")?(.+)\\1\\]");
|
||||||
|
description = description.replace(rx, "<a href=\"\\2\">");
|
||||||
|
description = description.replace("[/url]", "</a>", Qt::CaseInsensitive);
|
||||||
|
|
||||||
|
rx.setPattern("\\[(/)?([bius])\\]");
|
||||||
|
description = description.replace(rx, "<\\1\\2>");
|
||||||
|
|
||||||
|
rx.setPattern("\\[color=(\")?(.+)\\1\\]");
|
||||||
|
description = description.replace(rx, "<span style=\"color:\\2\">");
|
||||||
|
description = description.replace("[/color]", "</span>", Qt::CaseInsensitive);
|
||||||
|
|
||||||
|
rx.setPattern("\\[size=(\")?(.+)\\d\\1\\]");
|
||||||
|
description = description.replace(rx, "<span style=\"font-size:\\2px\">");
|
||||||
|
description = description.replace("[/size]", "</span>", Qt::CaseInsensitive);
|
||||||
|
|
||||||
|
html += "<pre>" + description + "</pre>";
|
||||||
|
}
|
||||||
|
html += "</div>";
|
||||||
|
m_ui->textBrowser->setHtml(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::saveSlidersPosition()
|
||||||
|
{
|
||||||
|
// Remember sliders positions
|
||||||
|
Preferences *const pref = Preferences::instance();
|
||||||
|
pref->setRssSideSplitterState(m_ui->splitterSide->saveState());
|
||||||
|
pref->setRssMainSplitterState(m_ui->splitterMain->saveState());
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::restoreSlidersPosition()
|
||||||
|
{
|
||||||
|
const Preferences *const pref = Preferences::instance();
|
||||||
|
const QByteArray stateSide = pref->getRssSideSplitterState();
|
||||||
|
if (!stateSide.isEmpty())
|
||||||
|
m_ui->splitterSide->restoreState(stateSide);
|
||||||
|
const QByteArray stateMain = pref->getRssMainSplitterState();
|
||||||
|
if (!stateMain.isEmpty())
|
||||||
|
m_ui->splitterMain->restoreState(stateMain);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::updateRefreshInterval(uint val)
|
||||||
|
{
|
||||||
|
RSS::Session::instance()->setRefreshInterval(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::on_rssDownloaderBtn_clicked()
|
||||||
|
{
|
||||||
|
AutomatedRssDownloader(this).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::handleSessionProcessingStateChanged(bool enabled)
|
||||||
|
{
|
||||||
|
m_ui->labelWarn->setVisible(!enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RSSWidget::handleUnreadCountChanged()
|
||||||
|
{
|
||||||
|
emit unreadCountUpdated(RSS::Session::instance()->rootFolder()->unreadCount());
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt4 and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2006 Christophe Dumez, Arnaud Demaiziere
|
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||||
|
* Copyright (C) 2006 Arnaud Demaiziere <arnaud@qbittorrent.org>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -24,46 +26,38 @@
|
||||||
* modify file(s), you may extend this exception to your version of the file(s),
|
* 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
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
* exception statement from your version.
|
* exception statement from your version.
|
||||||
*
|
|
||||||
* Contact : chris@qbittorrent.org arnaud@qbittorrent.org
|
|
||||||
*/
|
*/
|
||||||
#ifndef __RSS_IMP_H__
|
|
||||||
#define __RSS_IMP_H__
|
|
||||||
|
|
||||||
#define REFRESH_MAX_LATENCY 600000
|
#ifndef RSSWIDGET_H
|
||||||
|
#define RSSWIDGET_H
|
||||||
|
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
|
|
||||||
#include "base/rss/rssfolder.h"
|
class ArticleListWidget;
|
||||||
#include "base/rss/rssmanager.h"
|
|
||||||
|
|
||||||
class FeedListWidget;
|
class FeedListWidget;
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
class QListWidgetItem;
|
class QListWidgetItem;
|
||||||
class QTreeWidgetItem;
|
class QTreeWidgetItem;
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
class RSS;
|
class RSSWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RSSImp: public QWidget
|
class RSSWidget: public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RSSImp(QWidget * parent);
|
RSSWidget(QWidget *parent);
|
||||||
~RSSImp();
|
~RSSWidget();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void deleteSelectedItems();
|
void deleteSelectedItems();
|
||||||
void updateRefreshInterval(uint val);
|
void updateRefreshInterval(uint val);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void updateRSSCount(int);
|
void unreadCountUpdated(int count);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void on_newFeedButton_clicked();
|
void on_newFeedButton_clicked();
|
||||||
|
@ -71,38 +65,28 @@ private slots:
|
||||||
void on_markReadButton_clicked();
|
void on_markReadButton_clicked();
|
||||||
void displayRSSListMenu(const QPoint &);
|
void displayRSSListMenu(const QPoint &);
|
||||||
void displayItemsListMenu(const QPoint &);
|
void displayItemsListMenu(const QPoint &);
|
||||||
void renameSelectedRssFile();
|
void renameSelectedRSSItem();
|
||||||
void refreshSelectedItems();
|
void refreshSelectedItems();
|
||||||
void copySelectedFeedsURL();
|
void copySelectedFeedsURL();
|
||||||
void populateArticleList(QTreeWidgetItem *item);
|
void handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem);
|
||||||
void refreshTextBrowser();
|
void handleCurrentArticleItemChanged(QListWidgetItem *currentItem, QListWidgetItem *previousItem);
|
||||||
void updateFeedIcon(const QString &url, const QString &icon_path);
|
|
||||||
void updateFeedInfos(const QString &url, const QString &display_name, uint nbUnread);
|
|
||||||
void onFeedContentChanged(const QString &url);
|
|
||||||
void updateItemsInfos(const QList<QTreeWidgetItem *> &items);
|
|
||||||
void updateItemInfos(QTreeWidgetItem *item);
|
|
||||||
void openSelectedArticlesUrls();
|
void openSelectedArticlesUrls();
|
||||||
void downloadSelectedTorrents();
|
void downloadSelectedTorrents();
|
||||||
void fillFeedsList(QTreeWidgetItem *parent = 0, const Rss::FolderPtr &rss_parent = Rss::FolderPtr());
|
|
||||||
void saveSlidersPosition();
|
void saveSlidersPosition();
|
||||||
void restoreSlidersPosition();
|
void restoreSlidersPosition();
|
||||||
void askNewFolder();
|
void askNewFolder();
|
||||||
void saveFoldersOpenState();
|
void saveFoldersOpenState();
|
||||||
void loadFoldersOpenState();
|
void loadFoldersOpenState();
|
||||||
void on_settingsButton_clicked();
|
|
||||||
void on_rssDownloaderBtn_clicked();
|
void on_rssDownloaderBtn_clicked();
|
||||||
|
void handleSessionProcessingStateChanged(bool enabled);
|
||||||
|
void handleUnreadCountChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QListWidgetItem *createArticleListItem(const Rss::ArticlePtr &article);
|
Ui::RSSWidget *m_ui;
|
||||||
static QTreeWidgetItem *createFolderListItem(const Rss::FilePtr &rssFile);
|
ArticleListWidget *m_articleListWidget;
|
||||||
|
FeedListWidget *m_feedListWidget;
|
||||||
private:
|
QShortcut *m_editHotkey;
|
||||||
Ui::RSS *m_ui;
|
QShortcut *m_deleteHotkey;
|
||||||
Rss::ManagerPtr m_rssManager;
|
|
||||||
FeedListWidget *m_feedList;
|
|
||||||
QListWidgetItem *m_currentArticle;
|
|
||||||
QShortcut *editHotkey;
|
|
||||||
QShortcut *deleteHotkey;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif // RSSWIDGET_H
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>RSS</class>
|
<class>RSSWidget</class>
|
||||||
<widget class="QWidget" name="RSS">
|
<widget class="QWidget" name="RSSWidget">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
|
@ -17,6 +17,24 @@
|
||||||
<string>Search</string>
|
<string>Search</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="labelWarn">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<italic>true</italic>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">color: red;</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Fetching of RSS feeds is disabled now! You can enable it in application settings.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<item>
|
<item>
|
||||||
|
@ -63,13 +81,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="settingsButton">
|
|
||||||
<property name="text">
|
|
||||||
<string>Settings...</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -109,14 +120,6 @@
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QListWidget" name="listArticles">
|
|
||||||
<property name="contextMenuPolicy">
|
|
||||||
<enum>Qt::CustomContextMenu</enum>
|
|
||||||
</property>
|
|
||||||
<property name="selectionMode">
|
|
||||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
<widget class="HtmlBrowser" name="textBrowser">
|
<widget class="HtmlBrowser" name="textBrowser">
|
||||||
<property name="openExternalLinks">
|
<property name="openExternalLinks">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
@ -153,12 +156,12 @@
|
||||||
<string>Update</string>
|
<string>Update</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionNew_subscription">
|
<action name="actionNewSubscription">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>New subscription...</string>
|
<string>New subscription...</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionUpdate_all_feeds">
|
<action name="actionUpdateAllFeeds">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Update all feeds</string>
|
<string>Update all feeds</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -166,7 +169,7 @@
|
||||||
<string>Update all feeds</string>
|
<string>Update all feeds</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionMark_items_read">
|
<action name="actionMarkItemsRead">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Mark items read</string>
|
<string>Mark items read</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -174,22 +177,22 @@
|
||||||
<string>Mark items read</string>
|
<string>Mark items read</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionDownload_torrent">
|
<action name="actionDownloadTorrent">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Download torrent</string>
|
<string>Download torrent</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionOpen_news_URL">
|
<action name="actionOpenNewsURL">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Open news URL</string>
|
<string>Open news URL</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionCopy_feed_URL">
|
<action name="actionCopyFeedURL">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Copy feed URL</string>
|
<string>Copy feed URL</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionNew_folder">
|
<action name="actionNewFolder">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>New folder...</string>
|
<string>New folder...</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -199,7 +202,7 @@
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>HtmlBrowser</class>
|
<class>HtmlBrowser</class>
|
||||||
<extends>QTextBrowser</extends>
|
<extends>QTextBrowser</extends>
|
||||||
<header>htmlbrowser.h</header>
|
<header>gui/rss/htmlbrowser.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
|
@ -1,5 +1,5 @@
|
||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>icons/qbittorrent.png</file>
|
<file>icons/qbittorrent.png</file>
|
||||||
<file>icons/3-state-checkbox.gif</file>
|
<file>icons/3-state-checkbox.gif</file>
|
||||||
<file>icons/L.gif</file>
|
<file>icons/L.gif</file>
|
||||||
|
@ -376,5 +376,6 @@
|
||||||
<file>icons/qbt-theme/go-top.png</file>
|
<file>icons/qbt-theme/go-top.png</file>
|
||||||
<file>icons/qbt-theme/checked.png</file>
|
<file>icons/qbt-theme/checked.png</file>
|
||||||
<file>icons/qbt-theme/office-chart-line.png</file>
|
<file>icons/qbt-theme/office-chart-line.png</file>
|
||||||
</qresource>
|
<file>icons/qbt-theme/rss-config.png</file>
|
||||||
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
BIN
src/icons/qbt-theme/rss-config.png
Normal file
BIN
src/icons/qbt-theme/rss-config.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -396,7 +396,7 @@ void WebApplication::action_command_download()
|
||||||
// TODO: Check if destination actually exists
|
// TODO: Check if destination actually exists
|
||||||
params.skipChecking = skipChecking;
|
params.skipChecking = skipChecking;
|
||||||
|
|
||||||
params.addPaused = addPaused;
|
params.addPaused = TriStateBool(addPaused);
|
||||||
params.savePath = savepath;
|
params.savePath = savepath;
|
||||||
params.category = category;
|
params.category = category;
|
||||||
|
|
||||||
|
@ -436,7 +436,7 @@ void WebApplication::action_command_upload()
|
||||||
// TODO: Check if destination actually exists
|
// TODO: Check if destination actually exists
|
||||||
params.skipChecking = skipChecking;
|
params.skipChecking = skipChecking;
|
||||||
|
|
||||||
params.addPaused = addPaused;
|
params.addPaused = TriStateBool(addPaused);
|
||||||
params.savePath = savepath;
|
params.savePath = savepath;
|
||||||
params.category = category;
|
params.category = category;
|
||||||
if (!BitTorrent::Session::instance()->addTorrent(torrentInfo, params)) {
|
if (!BitTorrent::Session::instance()->addTorrent(torrentInfo, params)) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue