Browse Source

Allow to edit RSS feed URL

PR #18807.
Closes #5489.
adaptive-webui-19844
Vladimir Golovnev 2 years ago committed by GitHub
parent
commit
b8cd614775
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 72
      src/base/rss/rss_autodownloader.cpp
  2. 3
      src/base/rss/rss_autodownloader.h
  3. 7
      src/base/rss/rss_feed.cpp
  4. 4
      src/base/rss/rss_feed.h
  5. 45
      src/base/rss/rss_session.cpp
  6. 3
      src/base/rss/rss_session.h
  7. 32
      src/gui/rss/rsswidget.cpp
  8. 1
      src/gui/rss/rsswidget.h
  9. 8
      src/gui/rss/rsswidget.ui
  10. 11
      src/webui/api/rsscontroller.cpp
  11. 1
      src/webui/api/rsscontroller.h
  12. 1
      src/webui/webapplication.h

72
src/base/rss/rss_autodownloader.cpp

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

3
src/base/rss/rss_autodownloader.h

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

7
src/base/rss/rss_feed.cpp

@ -455,6 +455,13 @@ Path Feed::iconPath() const
return m_iconPath; 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 QJsonValue Feed::toJsonValue(const bool withData) const
{ {
QJsonObject jsonObj; QJsonObject jsonObj;

4
src/base/rss/rss_feed.h

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

45
src/base/rss/rss_session.cpp

@ -156,10 +156,39 @@ nonstd::expected<void, QString> Session::addFeed(const QString &url, const QStri
return result.get_unexpected(); return result.get_unexpected();
const auto destFolder = result.value(); 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(); store();
if (isProcessingEnabled()) 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 {}; return {};
} }
@ -409,6 +438,16 @@ void Session::addItem(Item *item, Folder *destFolder)
connect(feed, &Feed::titleChanged, this, &Session::handleFeedTitleChanged); connect(feed, &Feed::titleChanged, this, &Session::handleFeedTitleChanged);
connect(feed, &Feed::iconLoaded, this, &Session::feedIconLoaded); connect(feed, &Feed::iconLoaded, this, &Session::feedIconLoaded);
connect(feed, &Feed::stateChanged, this, &Session::feedStateChanged); 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_feedsByUID[feed->uid()] = feed;
m_feedsByURL[feed->url()] = feed; m_feedsByURL[feed->url()] = feed;
} }
@ -502,9 +541,11 @@ void Session::handleItemAboutToBeDestroyed(Item *item)
void Session::handleFeedTitleChanged(Feed *feed) void Session::handleFeedTitleChanged(Feed *feed)
{ {
if (feed->name() == feed->url()) if (feed->name() == feed->url())
{
// Now we have something better than a URL. // Now we have something better than a URL.
// Trying to rename feed... // Trying to rename feed...
moveItem(feed, Item::joinPath(Item::parentPath(feed->path()), feed->title())); moveItem(feed, Item::joinPath(Item::parentPath(feed->path()), feed->title()));
}
} }
QUuid Session::generateUID() const QUuid Session::generateUID() const

3
src/base/rss/rss_session.h

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

32
src/gui/rss/rsswidget.cpp

@ -65,6 +65,7 @@ RSSWidget::RSSWidget(QWidget *parent)
m_ui->actionCopyFeedURL->setIcon(UIThemeManager::instance()->getIcon(u"edit-copy"_qs)); m_ui->actionCopyFeedURL->setIcon(UIThemeManager::instance()->getIcon(u"edit-copy"_qs));
m_ui->actionDelete->setIcon(UIThemeManager::instance()->getIcon(u"edit-clear"_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->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->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->actionNewFolder->setIcon(UIThemeManager::instance()->getIcon(u"folder-new"_qs));
m_ui->actionNewSubscription->setIcon(UIThemeManager::instance()->getIcon(u"list-add"_qs)); m_ui->actionNewSubscription->setIcon(UIThemeManager::instance()->getIcon(u"list-add"_qs));
@ -101,6 +102,7 @@ RSSWidget::RSSWidget(QWidget *parent)
// Feeds list actions // Feeds list actions
connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems); connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems);
connect(m_ui->actionRename, &QAction::triggered, this, &RSSWidget::renameSelectedRSSItem); 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->actionUpdate, &QAction::triggered, this, &RSSWidget::refreshSelectedItems);
connect(m_ui->actionNewFolder, &QAction::triggered, this, &RSSWidget::askNewFolder); connect(m_ui->actionNewFolder, &QAction::triggered, this, &RSSWidget::askNewFolder);
connect(m_ui->actionNewSubscription, &QAction::triggered, this, &RSSWidget::on_newFeedButton_clicked); 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.size() == 1)
{ {
if (selectedItems.first() != m_feedListWidget->stickyUnreadItem()) QTreeWidgetItem *selectedItem = selectedItems.first();
if (selectedItem != m_feedListWidget->stickyUnreadItem())
{ {
menu->addAction(m_ui->actionRename); menu->addAction(m_ui->actionRename);
if (m_feedListWidget->isFeed(selectedItem))
menu->addAction(m_ui->actionEditFeedURL);
menu->addAction(m_ui->actionDelete); menu->addAction(m_ui->actionDelete);
menu->addSeparator(); menu->addSeparator();
if (m_feedListWidget->isFolder(selectedItems.first())) if (m_feedListWidget->isFolder(selectedItem))
menu->addAction(m_ui->actionNewFolder); menu->addAction(m_ui->actionNewFolder);
} }
} }
@ -420,6 +425,29 @@ void RSSWidget::renameSelectedRSSItem()
} while (!ok); } 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() void RSSWidget::refreshSelectedItems()
{ {
for (QTreeWidgetItem *item : asConst(m_feedListWidget->selectedItems())) for (QTreeWidgetItem *item : asConst(m_feedListWidget->selectedItems()))

1
src/gui/rss/rsswidget.h

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

8
src/gui/rss/rsswidget.ui

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

11
src/webui/api/rsscontroller.cpp

@ -66,6 +66,17 @@ void RSSController::addFeedAction()
throw APIError(APIErrorType::Conflict, result.error()); 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() void RSSController::removeItemAction()
{ {
requireParams({u"path"_qs}); requireParams({u"path"_qs});

1
src/webui/api/rsscontroller.h

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

1
src/webui/webapplication.h

@ -148,6 +148,7 @@ private:
{{u"auth"_qs, u"login"_qs}, Http::METHOD_POST}, {{u"auth"_qs, u"login"_qs}, Http::METHOD_POST},
{{u"auth"_qs, u"logout"_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"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"addFolder"_qs}, Http::METHOD_POST},
{{u"rss"_qs, u"markAsRead"_qs}, Http::METHOD_POST}, {{u"rss"_qs, u"markAsRead"_qs}, Http::METHOD_POST},
{{u"rss"_qs, u"moveItem"_qs}, Http::METHOD_POST}, {{u"rss"_qs, u"moveItem"_qs}, Http::METHOD_POST},

Loading…
Cancel
Save