From b8cd6147757d94b638d8ea7d2c122372866c8e1e Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Fri, 7 Apr 2023 14:22:50 +0300 Subject: [PATCH] Allow to edit RSS feed URL PR #18807. Closes #5489. --- src/base/rss/rss_autodownloader.cpp | 72 ++++++++++++++++++++++++----- src/base/rss/rss_autodownloader.h | 3 +- src/base/rss/rss_feed.cpp | 7 +++ src/base/rss/rss_feed.h | 4 +- src/base/rss/rss_session.cpp | 45 +++++++++++++++++- src/base/rss/rss_session.h | 3 ++ src/gui/rss/rsswidget.cpp | 32 ++++++++++++- src/gui/rss/rsswidget.h | 1 + src/gui/rss/rsswidget.ui | 8 ++++ src/webui/api/rsscontroller.cpp | 11 +++++ src/webui/api/rsscontroller.h | 1 + src/webui/webapplication.h | 1 + 12 files changed, 170 insertions(+), 18 deletions(-) diff --git a/src/base/rss/rss_autodownloader.cpp b/src/base/rss/rss_autodownloader.cpp index c6b94b5e6..1c74e169c 100644 --- a/src/base/rss/rss_autodownloader.cpp +++ b/src/base/rss/rss_autodownloader.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2017-2023 Vladimir Golovnev * * 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 job : asConst(m_processingQueue)) + { + if (job->feedURL == oldURL) + job->feedURL = feed->url(); + } + + for (QSharedPointer 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 job(new ProcessingJob); + auto job = QSharedPointer::create(); job->feedURL = article->feed()->url(); job->articleData = article->data(); m_processingQueue.append(job); @@ -383,9 +420,12 @@ void AutoDownloader::processJob(const QSharedPointer &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); + .arg(rulesFile.fileName(), rulesFile.errorString()), Log::CRITICAL); + } } void AutoDownloader::loadRules(const QByteArray &data) @@ -442,7 +488,7 @@ void AutoDownloader::loadRules(const QByteArray &data) catch (const ParsingError &error) { 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() { - 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())) { diff --git a/src/base/rss/rss_autodownloader.h b/src/base/rss/rss_autodownloader.h index a3e95be9d..da4f5e3b7 100644 --- a/src/base/rss/rss_autodownloader.h +++ b/src/base/rss/rss_autodownloader.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2017 Vladimir Golovnev + * Copyright (C) 2017-2023 Vladimir Golovnev * * 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; diff --git a/src/base/rss/rss_feed.cpp b/src/base/rss/rss_feed.cpp index 1502db0ad..3de73fe3c 100644 --- a/src/base/rss/rss_feed.cpp +++ b/src/base/rss/rss_feed.cpp @@ -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; diff --git a/src/base/rss/rss_feed.h b/src/base/rss/rss_feed.h index 715dbf1f1..491985c2d 100644 --- a/src/base/rss/rss_feed.h +++ b/src/base/rss/rss_feed.h @@ -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 &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; diff --git a/src/base/rss/rss_session.cpp b/src/base/rss/rss_session.cpp index 1d1ed81b1..fc3990e44 100644 --- a/src/base/rss/rss_session.cpp +++ b/src/base/rss/rss_session.cpp @@ -156,10 +156,39 @@ nonstd::expected 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 Session::setFeedURL(const QString &path, const QString &url) +{ + auto *feed = qobject_cast(m_itemsByPath.value(path)); + if (!feed) + return nonstd::make_unexpected(tr("Feed doesn't exist: %1.").arg(path)); + + return setFeedURL(feed, url); +} + +nonstd::expected 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 diff --git a/src/base/rss/rss_session.h b/src/base/rss/rss_session.h index 1472802ce..550ff9a99 100644 --- a/src/base/rss/rss_session.h +++ b/src/base/rss/rss_session.h @@ -116,6 +116,8 @@ namespace RSS nonstd::expected addFolder(const QString &path); nonstd::expected addFeed(const QString &url, const QString &path); + nonstd::expected setFeedURL(const QString &path, const QString &url); + nonstd::expected setFeedURL(Feed *feed, const QString &url); nonstd::expected moveItem(const QString &itemPath, const QString &destPath); nonstd::expected moveItem(Item *item, const QString &destPath); nonstd::expected 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); diff --git a/src/gui/rss/rsswidget.cpp b/src/gui/rss/rsswidget.cpp index e0bfec041..9069b5301 100644 --- a/src/gui/rss/rsswidget.cpp +++ b/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->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 selectedItems = m_feedListWidget->selectedItems(); + if (selectedItems.size() != 1) + return; + + QTreeWidgetItem *item = selectedItems.first(); + RSS::Feed *rssFeed = qobject_cast(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 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())) diff --git a/src/gui/rss/rsswidget.h b/src/gui/rss/rsswidget.h index 9d1f296a0..a2b44c5bf 100644 --- a/src/gui/rss/rsswidget.h +++ b/src/gui/rss/rsswidget.h @@ -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); diff --git a/src/gui/rss/rsswidget.ui b/src/gui/rss/rsswidget.ui index 74fb81130..97f06194c 100644 --- a/src/gui/rss/rsswidget.ui +++ b/src/gui/rss/rsswidget.ui @@ -197,6 +197,14 @@ New folder... + + + Edit feed URL... + + + Edit feed URL + + diff --git a/src/webui/api/rsscontroller.cpp b/src/webui/api/rsscontroller.cpp index 321020cfd..0ed2b35e3 100644 --- a/src/webui/api/rsscontroller.cpp +++ b/src/webui/api/rsscontroller.cpp @@ -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 result = RSS::Session::instance()->setFeedURL(path, url); + if (!result) + throw APIError(APIErrorType::Conflict, result.error()); +} + void RSSController::removeItemAction() { requireParams({u"path"_qs}); diff --git a/src/webui/api/rsscontroller.h b/src/webui/api/rsscontroller.h index cb58b41f8..0d10ba5c5 100644 --- a/src/webui/api/rsscontroller.h +++ b/src/webui/api/rsscontroller.h @@ -41,6 +41,7 @@ public: private slots: void addFolderAction(); void addFeedAction(); + void setFeedURLAction(); void removeItemAction(); void moveItemAction(); void itemsAction(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index cb7e62f89..51443a2ad 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -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},