Allow to edit RSS feed URL

PR #18807.
Closes #5489.
This commit is contained in:
Vladimir Golovnev 2023-04-07 14:22:50 +03:00 committed by GitHub
parent cecf2d28e6
commit b8cd614775
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 170 additions and 18 deletions

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2017-2023 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
@ -133,6 +133,8 @@ AutoDownloader::AutoDownloader()
load();
connect(Session::instance(), &Session::feedURLChanged, this, &AutoDownloader::handleFeedURLChanged);
m_processingTimer->setSingleShot(true);
connect(m_processingTimer, &QTimer::timeout, this, &AutoDownloader::process);
@ -331,22 +333,28 @@ void AutoDownloader::setDownloadRepacks(const bool enabled)
void AutoDownloader::process()
{
if (m_processingQueue.isEmpty()) return; // processing was disabled
if (m_processingQueue.isEmpty()) // processing was disabled
return;
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)
{
const auto job = m_waitingJobs.take(url);
if (!job) return;
if (!job)
return;
if (Feed *feed = Session::instance()->feedByURL(job->feedURL))
{
if (Article *article = feed->articleByGUID(job->articleData.value(Article::KeyId).toString()))
article->markAsRead();
}
}
void AutoDownloader::handleTorrentDownloadFailed(const QString &url)
@ -361,6 +369,34 @@ void AutoDownloader::handleNewArticle(const Article *article)
addJobForArticle(article);
}
void AutoDownloader::handleFeedURLChanged(Feed *feed, const QString &oldURL)
{
for (AutoDownloadRule &rule : m_rules)
{
if (const auto i = rule.feedURLs().indexOf(oldURL); i >= 0)
{
auto feedURLs = rule.feedURLs();
feedURLs.replace(i, feed->url());
rule.setFeedURLs(feedURLs);
m_dirty = true;
}
}
for (QSharedPointer<ProcessingJob> job : asConst(m_processingQueue))
{
if (job->feedURL == oldURL)
job->feedURL = feed->url();
}
for (QSharedPointer<ProcessingJob> job : asConst(m_waitingJobs))
{
if (job->feedURL == oldURL)
job->feedURL = feed->url();
}
store();
}
void AutoDownloader::setRule_impl(const AutoDownloadRule &rule)
{
m_rules.insert(rule.name(), rule);
@ -369,9 +405,10 @@ void AutoDownloader::setRule_impl(const AutoDownloadRule &rule)
void AutoDownloader::addJobForArticle(const Article *article)
{
const QString torrentURL = article->torrentUrl();
if (m_waitingJobs.contains(torrentURL)) return;
if (m_waitingJobs.contains(torrentURL))
return;
QSharedPointer<ProcessingJob> job(new ProcessingJob);
auto job = QSharedPointer<ProcessingJob>::create();
job->feedURL = article->feed()->url();
job->articleData = article->data();
m_processingQueue.append(job);
@ -383,9 +420,12 @@ 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.accepts(job->articleData)) continue;
if (!rule.isEnabled())
continue;
if (!rule.feedURLs().contains(job->feedURL))
continue;
if (!rule.accepts(job->articleData))
continue;
m_dirty = true;
storeDeferred();
@ -423,12 +463,18 @@ void AutoDownloader::load()
QFile rulesFile {(m_fileStorage->storageDir() / Path(RULES_FILE_NAME)).data()};
if (!rulesFile.exists())
{
loadRulesLegacy();
}
else if (rulesFile.open(QFile::ReadOnly))
{
loadRules(rulesFile.readAll());
}
else
{
LogMsg(tr("Couldn't read RSS AutoDownloader rules from %1. Error: %2")
.arg(rulesFile.fileName(), rulesFile.errorString()), Log::CRITICAL);
}
}
void AutoDownloader::loadRules(const QByteArray &data)
@ -460,7 +506,8 @@ void AutoDownloader::loadRulesLegacy()
void AutoDownloader::store()
{
if (!m_dirty) return;
if (!m_dirty)
return;
m_dirty = false;
m_savingTimer.stop();
@ -486,7 +533,8 @@ bool AutoDownloader::isProcessingEnabled() const
void AutoDownloader::resetProcessingQueue()
{
m_processingQueue.clear();
if (!isProcessingEnabled()) return;
if (!isProcessingEnabled())
return;
for (Article *article : asConst(Session::instance()->rootFolder()->articles()))
{

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2017-2023 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
@ -113,6 +113,7 @@ namespace RSS
void handleTorrentDownloadFinished(const QString &url);
void handleTorrentDownloadFailed(const QString &url);
void handleNewArticle(const Article *article);
void handleFeedURLChanged(Feed *feed, const QString &oldURL);
private:
void timerEvent(QTimerEvent *event) override;

View file

@ -455,6 +455,13 @@ Path Feed::iconPath() const
return m_iconPath;
}
void Feed::setURL(const QString &url)
{
const QString oldURL = m_url;
m_url = url;
emit urlChanged(oldURL);
}
QJsonValue Feed::toJsonValue(const bool withData) const
{
QJsonObject jsonObj;

View file

@ -91,6 +91,7 @@ namespace RSS
void iconLoaded(Feed *feed = nullptr);
void titleChanged(Feed *feed = nullptr);
void stateChanged(Feed *feed = nullptr);
void urlChanged(const QString &oldURL);
private slots:
void handleSessionProcessingEnabledChanged(bool enabled);
@ -113,12 +114,13 @@ namespace RSS
void decreaseUnreadCount();
void downloadIcon();
int updateArticles(const QList<QVariantHash> &loadedArticles);
void setURL(const QString &url);
Session *m_session = nullptr;
Private::Parser *m_parser = nullptr;
Private::FeedSerializer *m_serializer = nullptr;
const QUuid m_uid;
const QString m_url;
QString m_url;
QString m_title;
QString m_lastBuildDate;
bool m_hasError = false;

View file

@ -156,10 +156,39 @@ nonstd::expected<void, QString> Session::addFeed(const QString &url, const QStri
return result.get_unexpected();
const auto destFolder = result.value();
addItem(new Feed(generateUID(), url, path, this), destFolder);
auto *feed = new Feed(generateUID(), url, path, this);
addItem(feed, destFolder);
store();
if (isProcessingEnabled())
feedByURL(url)->refresh();
feed->refresh();
return {};
}
nonstd::expected<void, QString> Session::setFeedURL(const QString &path, const QString &url)
{
auto *feed = qobject_cast<Feed *>(m_itemsByPath.value(path));
if (!feed)
return nonstd::make_unexpected(tr("Feed doesn't exist: %1.").arg(path));
return setFeedURL(feed, url);
}
nonstd::expected<void, QString> Session::setFeedURL(Feed *feed, const QString &url)
{
Q_ASSERT(feed);
if (url == feed->url())
return {};
if (m_feedsByURL.contains(url))
return nonstd::make_unexpected(tr("RSS feed with given URL already exists: %1.").arg(url));
m_feedsByURL[url] = m_feedsByURL.take(feed->url());
feed->setURL(url);
store();
if (isProcessingEnabled())
feed->refresh();
return {};
}
@ -409,6 +438,16 @@ void Session::addItem(Item *item, Folder *destFolder)
connect(feed, &Feed::titleChanged, this, &Session::handleFeedTitleChanged);
connect(feed, &Feed::iconLoaded, this, &Session::feedIconLoaded);
connect(feed, &Feed::stateChanged, this, &Session::feedStateChanged);
connect(feed, &Feed::urlChanged, this, [this, feed](const QString &oldURL)
{
if (feed->name() == oldURL)
{
// If feed still use an URL as a name trying to rename it to match new URL...
moveItem(feed, Item::joinPath(Item::parentPath(feed->path()), feed->url()));
}
emit feedURLChanged(feed, oldURL);
});
m_feedsByUID[feed->uid()] = feed;
m_feedsByURL[feed->url()] = feed;
}
@ -502,9 +541,11 @@ void Session::handleItemAboutToBeDestroyed(Item *item)
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()));
}
}
QUuid Session::generateUID() const

View file

@ -116,6 +116,8 @@ namespace RSS
nonstd::expected<void, QString> addFolder(const QString &path);
nonstd::expected<void, QString> addFeed(const QString &url, const QString &path);
nonstd::expected<void, QString> setFeedURL(const QString &path, const QString &url);
nonstd::expected<void, QString> setFeedURL(Feed *feed, const QString &url);
nonstd::expected<void, QString> moveItem(const QString &itemPath, const QString &destPath);
nonstd::expected<void, QString> moveItem(Item *item, const QString &destPath);
nonstd::expected<void, QString> removeItem(const QString &itemPath);
@ -138,6 +140,7 @@ namespace RSS
void itemAboutToBeRemoved(Item *item);
void feedIconLoaded(Feed *feed);
void feedStateChanged(Feed *feed);
void feedURLChanged(Feed *feed, const QString &oldURL);
private slots:
void handleItemAboutToBeDestroyed(Item *item);

View file

@ -65,6 +65,7 @@ RSSWidget::RSSWidget(QWidget *parent)
m_ui->actionCopyFeedURL->setIcon(UIThemeManager::instance()->getIcon(u"edit-copy"_qs));
m_ui->actionDelete->setIcon(UIThemeManager::instance()->getIcon(u"edit-clear"_qs));
m_ui->actionDownloadTorrent->setIcon(UIThemeManager::instance()->getIcon(u"downloading"_qs, u"download"_qs));
m_ui->actionEditFeedURL->setIcon(UIThemeManager::instance()->getIcon(u"edit-rename"_qs));
m_ui->actionMarkItemsRead->setIcon(UIThemeManager::instance()->getIcon(u"task-complete"_qs, u"mail-mark-read"_qs));
m_ui->actionNewFolder->setIcon(UIThemeManager::instance()->getIcon(u"folder-new"_qs));
m_ui->actionNewSubscription->setIcon(UIThemeManager::instance()->getIcon(u"list-add"_qs));
@ -101,6 +102,7 @@ RSSWidget::RSSWidget(QWidget *parent)
// Feeds list actions
connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems);
connect(m_ui->actionRename, &QAction::triggered, this, &RSSWidget::renameSelectedRSSItem);
connect(m_ui->actionEditFeedURL, &QAction::triggered, this, &RSSWidget::editSelectedRSSFeedURL);
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);
@ -158,12 +160,15 @@ void RSSWidget::displayRSSListMenu(const QPoint &pos)
if (selectedItems.size() == 1)
{
if (selectedItems.first() != m_feedListWidget->stickyUnreadItem())
QTreeWidgetItem *selectedItem = selectedItems.first();
if (selectedItem != m_feedListWidget->stickyUnreadItem())
{
menu->addAction(m_ui->actionRename);
if (m_feedListWidget->isFeed(selectedItem))
menu->addAction(m_ui->actionEditFeedURL);
menu->addAction(m_ui->actionDelete);
menu->addSeparator();
if (m_feedListWidget->isFolder(selectedItems.first()))
if (m_feedListWidget->isFolder(selectedItem))
menu->addAction(m_ui->actionNewFolder);
}
}
@ -420,6 +425,29 @@ void RSSWidget::renameSelectedRSSItem()
} while (!ok);
}
void RSSWidget::editSelectedRSSFeedURL()
{
QList<QTreeWidgetItem *> selectedItems = m_feedListWidget->selectedItems();
if (selectedItems.size() != 1)
return;
QTreeWidgetItem *item = selectedItems.first();
RSS::Feed *rssFeed = qobject_cast<RSS::Feed *>(m_feedListWidget->getRSSItem(item));
Q_ASSERT(rssFeed);
if (Q_UNLIKELY(!rssFeed))
return;
bool ok = false;
QString newURL = AutoExpandableDialog::getText(this, tr("Please type a RSS feed URL")
, tr("Feed URL:"), QLineEdit::Normal, rssFeed->url(), &ok).trimmed();
if (!ok || newURL.isEmpty())
return;
const nonstd::expected<void, QString> result = RSS::Session::instance()->setFeedURL(rssFeed, newURL);
if (!result)
QMessageBox::warning(this, u"qBittorrent"_qs, result.error(), QMessageBox::Ok);
}
void RSSWidget::refreshSelectedItems()
{
for (QTreeWidgetItem *item : asConst(m_feedListWidget->selectedItems()))

View file

@ -66,6 +66,7 @@ private slots:
void displayRSSListMenu(const QPoint &pos);
void displayItemsListMenu();
void renameSelectedRSSItem();
void editSelectedRSSFeedURL();
void refreshSelectedItems();
void copySelectedFeedsURL();
void handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem);

View file

@ -197,6 +197,14 @@
<string>New folder...</string>
</property>
</action>
<action name="actionEditFeedURL">
<property name="text">
<string>Edit feed URL...</string>
</property>
<property name="toolTip">
<string>Edit feed URL</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View file

@ -66,6 +66,17 @@ void RSSController::addFeedAction()
throw APIError(APIErrorType::Conflict, result.error());
}
void RSSController::setFeedURLAction()
{
requireParams({u"path"_qs, u"url"_qs});
const QString path = params()[u"path"_qs].trimmed();
const QString url = params()[u"url"_qs].trimmed();
const nonstd::expected<void, QString> result = RSS::Session::instance()->setFeedURL(path, url);
if (!result)
throw APIError(APIErrorType::Conflict, result.error());
}
void RSSController::removeItemAction()
{
requireParams({u"path"_qs});

View file

@ -41,6 +41,7 @@ public:
private slots:
void addFolderAction();
void addFeedAction();
void setFeedURLAction();
void removeItemAction();
void moveItemAction();
void itemsAction();

View file

@ -148,6 +148,7 @@ private:
{{u"auth"_qs, u"login"_qs}, Http::METHOD_POST},
{{u"auth"_qs, u"logout"_qs}, Http::METHOD_POST},
{{u"rss"_qs, u"addFeed"_qs}, Http::METHOD_POST},
{{u"rss"_qs, u"setFeedURL"_qs}, Http::METHOD_POST},
{{u"rss"_qs, u"addFolder"_qs}, Http::METHOD_POST},
{{u"rss"_qs, u"markAsRead"_qs}, Http::METHOD_POST},
{{u"rss"_qs, u"moveItem"_qs}, Http::METHOD_POST},