diff --git a/resources/docs/Feed-formats.md b/resources/docs/Feed-formats.md index 3490d3648..777085b25 100755 --- a/resources/docs/Feed-formats.md +++ b/resources/docs/Feed-formats.md @@ -4,6 +4,7 @@ RSS Guard is a modular application which supports plugins. It offers well-mainta * [Tiny Tiny RSS](https://tt-rss.org) plugin: Adds ability to synchronize messages with TT-RSS instances, either self-hosted or via 3rd-party external service. * [Inoreader](https://www.inoreader.com) plugin: Adds ability to synchronize messages with Inoreader. All you need to do is create free account on their website and start rocking. * [Nextcloud News](https://apps.nextcloud.com/apps/news) plugin: Nextcloud News is a Nextcloud app which adds feed reader abilities into your Nextcloud instances. +* Google Reader API plugin: This plugin was added in RSS Guard 3.9.0 and offers two-way synchronization with services which implement Google Reader API. At this point, plugin was tested and works with Bazqux, The Old Reader and FreshRSS. * [Gmail](https://www.google.com/gmail) plugin: Yes, you are reading it right. RSS Guard can be used as very lightweight and simple e-mail client. This plugins uses [Gmail API](https://developers.google.com/gmail/api) and offers even e-mail sending. All plugins share almost all core RSS Guard's features, including labels, recycle bins, podcasts fetching or newspaper view. They are implemented in a very transparent way, making it easy to maintain them or add new ones. @@ -27,4 +28,33 @@ OPML files can be exported/imported in simple dialog. -You just select output file (in case of OPML export), check desired feeds and hit `Export to file`. \ No newline at end of file +You just select output file (in case of OPML export), check desired feeds and hit `Export to file`. + +### Websites scraping and other related advanced features +RSS Guard 3.9.0+ offers extra advanced features which were inspired by [Liferea](https://lzone.de/liferea/). + +**Only proceed if you consider yourself as power user and you know you are doing!** + +You can select source type of each feed. If you select `URL`, then RSS Guard simply downloads feed file from given location. + +However, if you choose `Script` option, then you cannot provide URL of your feed and you rely on custom script to obtain your script and provide its contents to **standard output**. Resulting data written to standard output **MUST** be valid feed file, for example RSS or ATOM XML file. + + + +Any errors in your script must be written to **error output**. + +Note that you must provide full execution line to your custom script, including interpreter binary path and name. Some examples of valid execution lines are: + +| Command | Explanation | +|---------|-------------| +| `bash -c "curl 'https://github.com/martinrotter.atom'"` | Downloads ATOM feed file with Bash and Curl. | +| `Powershell "Invoke-WebRequest 'https://github.com/martinrotter.atom' | Select-Object -ExpandProperty Content"` | Downloads ATOM feed file with Powershell. | +| `php tweeper.php https://twitter.com/NSACareers` | Downloads RSS feed file with [Tweeper](https://git.ao2.it/tweeper.git/). Tweeper is utility which is able to produce RSS feed from Twitter. | + + + +Note that the above examples are cross-platform and you can use the exact same command on Windows, Linux or Mac OS X, if your operating system is properly configured. + +RSS Guard offers placeholder `%data%` which is automatically replaced with full path to RSS Guard's [user data folder](Documentation.md#portable-user-data). You can, therefore, use something like this as source script line: `bash %data%/scripts/download-feed.sh`. + +Also, working directory of process executing the script is set to RSS Guard's user data folder. \ No newline at end of file diff --git a/resources/docs/images/scrape-source-type.png b/resources/docs/images/scrape-source-type.png new file mode 100755 index 000000000..c2295d390 Binary files /dev/null and b/resources/docs/images/scrape-source-type.png differ diff --git a/resources/docs/images/scrape-source.png b/resources/docs/images/scrape-source.png new file mode 100755 index 000000000..7d5bcc65a Binary files /dev/null and b/resources/docs/images/scrape-source.png differ diff --git a/src/librssguard/definitions/definitions.h b/src/librssguard/definitions/definitions.h index 11fca67ec..42765833d 100755 --- a/src/librssguard/definitions/definitions.h +++ b/src/librssguard/definitions/definitions.h @@ -39,6 +39,7 @@ #define MSG_FILTERING_HELP "https://github.com/martinrotter/rssguard/blob/master/resources/docs/Message-filters.md#message-filtering" #define DEFAULT_FEED_TYPE "RSS" #define URL_REGEXP "^(http|https|feed|ftp):\\/\\/[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?$" +#define SCRIPT_SOURCE_TYPE_REGEXP "^.+#.*$" #define TEXT_TITLE_LIMIT 30 #define RESELECT_MESSAGE_THRESSHOLD 500 #define ICON_SIZE_SETTINGS 16 diff --git a/src/librssguard/miscellaneous/databasequeries.cpp b/src/librssguard/miscellaneous/databasequeries.cpp index 3c7ba7f43..cef60d9bf 100755 --- a/src/librssguard/miscellaneous/databasequeries.cpp +++ b/src/librssguard/miscellaneous/databasequeries.cpp @@ -2089,15 +2089,16 @@ int DatabaseQueries::addStandardFeed(const QSqlDatabase& db, int parent_id, int const QString& description, const QDateTime& creation_date, const QIcon& icon, const QString& encoding, const QString& url, bool is_protected, const QString& username, const QString& password, - Feed::AutoUpdateType auto_update_type, - int auto_update_interval, StandardFeed::Type feed_format, bool* ok) { + Feed::AutoUpdateType auto_update_type, int auto_update_interval, + StandardFeed::SourceType source_type, const QString& post_process_script, + StandardFeed::Type feed_format, bool* ok) { QSqlQuery q(db); qDebug() << "Adding feed with title '" << title.toUtf8() << "' to DB."; q.setForwardOnly(true); q.prepare("INSERT INTO Feeds " - "(title, description, date_created, icon, category, encoding, url, protected, username, password, update_type, update_interval, type, account_id) " - "VALUES (:title, :description, :date_created, :icon, :category, :encoding, :url, :protected, :username, :password, :update_type, :update_interval, :type, :account_id);"); + "(title, description, date_created, icon, category, encoding, url, source_type, post_process, protected, username, password, update_type, update_interval, type, account_id) " + "VALUES (:title, :description, :date_created, :icon, :category, :encoding, :url, :source_type, :post_process, :protected, :username, :password, :update_type, :update_interval, :type, :account_id);"); q.bindValue(QSL(":title"), title.toUtf8()); q.bindValue(QSL(":description"), description.toUtf8()); q.bindValue(QSL(":date_created"), creation_date.toMSecsSinceEpoch()); @@ -2105,6 +2106,8 @@ int DatabaseQueries::addStandardFeed(const QSqlDatabase& db, int parent_id, int q.bindValue(QSL(":category"), parent_id); q.bindValue(QSL(":encoding"), encoding); q.bindValue(QSL(":url"), url); + q.bindValue(QSL(":source_type"), int(source_type)); + q.bindValue(QSL(":post_process"), post_process_script); q.bindValue(QSL(":protected"), is_protected ? 1 : 0); q.bindValue(QSL(":username"), username); q.bindValue(QSL(":account_id"), account_id); @@ -2153,12 +2156,13 @@ bool DatabaseQueries::editStandardFeed(const QSqlDatabase& db, int parent_id, in const QString& encoding, const QString& url, bool is_protected, const QString& username, const QString& password, Feed::AutoUpdateType auto_update_type, - int auto_update_interval, StandardFeed::Type feed_format) { + int auto_update_interval, StandardFeed::SourceType source_type, + const QString& post_process_script, StandardFeed::Type feed_format) { QSqlQuery q(db); q.setForwardOnly(true); q.prepare("UPDATE Feeds " - "SET title = :title, description = :description, icon = :icon, category = :category, encoding = :encoding, url = :url, protected = :protected, username = :username, password = :password, update_type = :update_type, update_interval = :update_interval, type = :type " + "SET title = :title, description = :description, icon = :icon, category = :category, encoding = :encoding, url = :url, source_type = :source_type, post_process = :post_process, protected = :protected, username = :username, password = :password, update_type = :update_type, update_interval = :update_interval, type = :type " "WHERE id = :id;"); q.bindValue(QSL(":title"), title); q.bindValue(QSL(":description"), description); @@ -2166,6 +2170,8 @@ bool DatabaseQueries::editStandardFeed(const QSqlDatabase& db, int parent_id, in q.bindValue(QSL(":category"), parent_id); q.bindValue(QSL(":encoding"), encoding); q.bindValue(QSL(":url"), url); + q.bindValue(QSL(":source_type"), int(source_type)); + q.bindValue(QSL(":post_process"), post_process_script); q.bindValue(QSL(":protected"), is_protected ? 1 : 0); q.bindValue(QSL(":username"), username); diff --git a/src/librssguard/miscellaneous/databasequeries.h b/src/librssguard/miscellaneous/databasequeries.h index cfeef4b42..5c6335a96 100644 --- a/src/librssguard/miscellaneous/databasequeries.h +++ b/src/librssguard/miscellaneous/databasequeries.h @@ -136,13 +136,15 @@ class DatabaseQueries { const QString& description, const QDateTime& creation_date, const QIcon& icon, const QString& encoding, const QString& url, bool is_protected, const QString& username, const QString& password, - Feed::AutoUpdateType auto_update_type, - int auto_update_interval, StandardFeed::Type feed_format, bool* ok = nullptr); + Feed::AutoUpdateType auto_update_type, int auto_update_interval, + StandardFeed::SourceType source_type, const QString& post_process_script, + StandardFeed::Type feed_format, bool* ok = nullptr); static bool editStandardFeed(const QSqlDatabase& db, int parent_id, int feed_id, const QString& title, const QString& description, const QIcon& icon, const QString& encoding, const QString& url, bool is_protected, const QString& username, const QString& password, Feed::AutoUpdateType auto_update_type, - int auto_update_interval, StandardFeed::Type feed_format); + int auto_update_interval, StandardFeed::SourceType source_type, + const QString& post_process_script, StandardFeed::Type feed_format); static QList getStandardAccounts(const QSqlDatabase& db, bool* ok = nullptr); template diff --git a/src/librssguard/services/abstract/gui/formaccountdetails.ui b/src/librssguard/services/abstract/gui/formaccountdetails.ui index c38f978a4..8d2c579f0 100644 --- a/src/librssguard/services/abstract/gui/formaccountdetails.ui +++ b/src/librssguard/services/abstract/gui/formaccountdetails.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 500 + 450 diff --git a/src/librssguard/services/abstract/gui/formfeeddetails.ui b/src/librssguard/services/abstract/gui/formfeeddetails.ui index bdd44ddcf..016b3d393 100644 --- a/src/librssguard/services/abstract/gui/formfeeddetails.ui +++ b/src/librssguard/services/abstract/gui/formfeeddetails.ui @@ -6,8 +6,8 @@ 0 0 - 471 - 352 + 500 + 450 diff --git a/src/librssguard/services/standard/gui/formstandardfeeddetails.cpp b/src/librssguard/services/standard/gui/formstandardfeeddetails.cpp index 015aabe37..ff8ee9c15 100644 --- a/src/librssguard/services/standard/gui/formstandardfeeddetails.cpp +++ b/src/librssguard/services/standard/gui/formstandardfeeddetails.cpp @@ -22,7 +22,7 @@ FormStandardFeedDetails::FormStandardFeedDetails(ServiceRoot* service_root, QWid insertCustomTab(m_authDetails, tr("Network"), 2); activateTab(0); - connect(m_standardFeedDetails->ui.m_btnFetchMetadata, &QPushButton::clicked, this, &FormStandardFeedDetails::guessFeed); + connect(m_standardFeedDetails->m_ui.m_btnFetchMetadata, &QPushButton::clicked, this, &FormStandardFeedDetails::guessFeed); connect(m_standardFeedDetails->m_actionFetchIcon, &QAction::triggered, this, &FormStandardFeedDetails::guessIconOnly); } @@ -47,34 +47,36 @@ int FormStandardFeedDetails::addEditFeed(StandardFeed* input_feed, RootItem* par } void FormStandardFeedDetails::guessFeed() { - m_standardFeedDetails->guessFeed(m_standardFeedDetails->ui.m_txtUrl->lineEdit()->text(), + m_standardFeedDetails->guessFeed(m_standardFeedDetails->m_ui.m_txtSource->lineEdit()->text(), m_authDetails->m_txtUsername->lineEdit()->text(), m_authDetails->m_txtPassword->lineEdit()->text()); } void FormStandardFeedDetails::guessIconOnly() { - m_standardFeedDetails->guessIconOnly(m_standardFeedDetails->ui.m_txtUrl->lineEdit()->text(), + m_standardFeedDetails->guessIconOnly(m_standardFeedDetails->m_ui.m_txtSource->lineEdit()->text(), m_authDetails->m_txtUsername->lineEdit()->text(), m_authDetails->m_txtPassword->lineEdit()->text()); } void FormStandardFeedDetails::apply() { RootItem* parent = - static_cast(m_standardFeedDetails->ui.m_cmbParentCategory->itemData( - m_standardFeedDetails->ui.m_cmbParentCategory->currentIndex()).value()); + static_cast(m_standardFeedDetails->m_ui.m_cmbParentCategory->itemData( + m_standardFeedDetails->m_ui.m_cmbParentCategory->currentIndex()).value()); StandardFeed::Type type = - static_cast(m_standardFeedDetails->ui.m_cmbType->itemData(m_standardFeedDetails->ui.m_cmbType->currentIndex()).value()); + static_cast(m_standardFeedDetails->m_ui.m_cmbType->itemData(m_standardFeedDetails->m_ui.m_cmbType->currentIndex()).value()); auto* new_feed = new StandardFeed(); // Setup data for new_feed. - new_feed->setTitle(m_standardFeedDetails->ui.m_txtTitle->lineEdit()->text()); + new_feed->setTitle(m_standardFeedDetails->m_ui.m_txtTitle->lineEdit()->text()); new_feed->setCreationDate(QDateTime::currentDateTime()); - new_feed->setDescription(m_standardFeedDetails->ui.m_txtDescription->lineEdit()->text()); - new_feed->setIcon(m_standardFeedDetails->ui.m_btnIcon->icon()); - new_feed->setEncoding(m_standardFeedDetails->ui.m_cmbEncoding->currentText()); + new_feed->setDescription(m_standardFeedDetails->m_ui.m_txtDescription->lineEdit()->text()); + new_feed->setIcon(m_standardFeedDetails->m_ui.m_btnIcon->icon()); + new_feed->setEncoding(m_standardFeedDetails->m_ui.m_cmbEncoding->currentText()); new_feed->setType(type); - new_feed->setUrl(m_standardFeedDetails->ui.m_txtUrl->lineEdit()->text()); + new_feed->setSourceType(m_standardFeedDetails->sourceType()); + new_feed->setPostProcessScript(m_standardFeedDetails->m_ui.m_txtPostProcessScript->lineEdit()->text()); + new_feed->setUrl(m_standardFeedDetails->m_ui.m_txtSource->lineEdit()->text()); new_feed->setPasswordProtected(m_authDetails->m_gbAuthentication->isChecked()); new_feed->setUsername(m_authDetails->m_txtUsername->lineEdit()->text()); new_feed->setPassword(m_authDetails->m_txtPassword->lineEdit()->text()); diff --git a/src/librssguard/services/standard/gui/standardfeeddetails.cpp b/src/librssguard/services/standard/gui/standardfeeddetails.cpp index 928ca7ac8..e39cc3a91 100755 --- a/src/librssguard/services/standard/gui/standardfeeddetails.cpp +++ b/src/librssguard/services/standard/gui/standardfeeddetails.cpp @@ -2,33 +2,44 @@ #include "services/standard/gui/standardfeeddetails.h" +#include "gui/guiutilities.h" #include "miscellaneous/iconfactory.h" #include "network-web/networkfactory.h" #include "services/abstract/category.h" -#include "services/standard/standardfeed.h" #include #include #include #include +#include #include StandardFeedDetails::StandardFeedDetails(QWidget* parent) : QWidget(parent) { - ui.setupUi(this); + m_ui.setupUi(this); - ui.m_txtTitle->lineEdit()->setPlaceholderText(tr("Feed title")); - ui.m_txtTitle->lineEdit()->setToolTip(tr("Set title for your feed.")); - ui.m_txtDescription->lineEdit()->setPlaceholderText(tr("Feed description")); - ui.m_txtDescription->lineEdit()->setToolTip(tr("Set description for your feed.")); - ui.m_txtUrl->lineEdit()->setPlaceholderText(tr("Full feed url including scheme")); - ui.m_txtUrl->lineEdit()->setToolTip(tr("Set url for your feed.")); + m_ui.m_txtTitle->lineEdit()->setPlaceholderText(tr("Feed title")); + m_ui.m_txtTitle->lineEdit()->setToolTip(tr("Set title for your feed.")); + m_ui.m_txtDescription->lineEdit()->setPlaceholderText(tr("Feed description")); + m_ui.m_txtDescription->lineEdit()->setToolTip(tr("Set description for your feed.")); + m_ui.m_txtSource->lineEdit()->setPlaceholderText(tr("Full feed source identifier")); + m_ui.m_txtSource->lineEdit()->setToolTip(tr("Full feed source identifier which can be URL.")); + m_ui.m_txtPostProcessScript->lineEdit()->setPlaceholderText(tr("Full command to execute")); + m_ui.m_txtPostProcessScript->lineEdit()->setToolTip(tr("You can enter full command including interpreter here.")); + + // Add source types. + m_ui.m_cmbSourceType->addItem(StandardFeed::sourceTypeToString(StandardFeed::SourceType::Url), + QVariant::fromValue(StandardFeed::SourceType::Url)); + m_ui.m_cmbSourceType->addItem(StandardFeed::sourceTypeToString(StandardFeed::SourceType::Script), + QVariant::fromValue(StandardFeed::SourceType::Script)); + m_ui.m_txtPostProcessScript->setStatus(WidgetWithStatus::StatusType::Ok, + tr("Here you can enter script executaion line, including interpreter.")); // Add standard feed types. - ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Atom10), QVariant::fromValue(int(StandardFeed::Type::Atom10))); - ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Rdf), QVariant::fromValue(int(StandardFeed::Type::Rdf))); - ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Rss0X), QVariant::fromValue(int(StandardFeed::Type::Rss0X))); - ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Rss2X), QVariant::fromValue(int(StandardFeed::Type::Rss2X))); - ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Json), QVariant::fromValue(int(StandardFeed::Type::Json))); + m_ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Atom10), QVariant::fromValue(int(StandardFeed::Type::Atom10))); + m_ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Rdf), QVariant::fromValue(int(StandardFeed::Type::Rdf))); + m_ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Rss0X), QVariant::fromValue(int(StandardFeed::Type::Rss0X))); + m_ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Rss2X), QVariant::fromValue(int(StandardFeed::Type::Rss2X))); + m_ui.m_cmbType->addItem(StandardFeed::typeToString(StandardFeed::Type::Json), QVariant::fromValue(int(StandardFeed::Type::Json))); // Load available encodings. const QList encodings = QTextCodec::availableCodecs(); @@ -43,7 +54,7 @@ StandardFeedDetails::StandardFeedDetails(QWidget* parent) : QWidget(parent) { return lhs.toLower() < rhs.toLower(); }); - ui.m_cmbEncoding->addItems(encoded_encodings); + m_ui.m_cmbEncoding->addItems(encoded_encodings); // Setup menu & actions for icon selection. m_iconMenu = new QMenu(tr("Icon selection"), this); @@ -59,20 +70,36 @@ StandardFeedDetails::StandardFeedDetails(QWidget* parent) : QWidget(parent) { m_iconMenu->addAction(m_actionFetchIcon); m_iconMenu->addAction(m_actionLoadIconFromFile); m_iconMenu->addAction(m_actionUseDefaultIcon); - ui.m_btnIcon->setMenu(m_iconMenu); - ui.m_txtUrl->lineEdit()->setFocus(Qt::TabFocusReason); + m_ui.m_btnIcon->setMenu(m_iconMenu); + m_ui.m_txtSource->lineEdit()->setFocus(Qt::TabFocusReason); // Set feed metadata fetch label. - ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Information, - tr("No metadata fetched so far."), - tr("No metadata fetched so far.")); + m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Information, + tr("No metadata fetched so far."), + tr("No metadata fetched so far.")); - connect(ui.m_txtTitle->lineEdit(), &BaseLineEdit::textChanged, this, &StandardFeedDetails::onTitleChanged); - connect(ui.m_txtDescription->lineEdit(), &BaseLineEdit::textChanged, this, &StandardFeedDetails::onDescriptionChanged); - connect(ui.m_txtUrl->lineEdit(), &BaseLineEdit::textChanged, this, &StandardFeedDetails::onUrlChanged); + connect(m_ui.m_txtTitle->lineEdit(), &BaseLineEdit::textChanged, this, &StandardFeedDetails::onTitleChanged); + connect(m_ui.m_txtDescription->lineEdit(), &BaseLineEdit::textChanged, this, &StandardFeedDetails::onDescriptionChanged); + connect(m_ui.m_cmbSourceType, QOverload::of(&QComboBox::currentIndexChanged), + this, [this]() { + onUrlChanged(m_ui.m_txtSource->lineEdit()->text()); + }); + connect(m_ui.m_txtSource->lineEdit(), &BaseLineEdit::textChanged, this, &StandardFeedDetails::onUrlChanged); connect(m_actionLoadIconFromFile, &QAction::triggered, this, &StandardFeedDetails::onLoadIconFromFile); connect(m_actionUseDefaultIcon, &QAction::triggered, this, &StandardFeedDetails::onUseDefaultIcon); + setTabOrder(m_ui.m_cmbParentCategory, m_ui.m_cmbType); + setTabOrder(m_ui.m_cmbType, m_ui.m_cmbEncoding); + setTabOrder(m_ui.m_cmbEncoding, m_ui.m_txtTitle->lineEdit()); + setTabOrder(m_ui.m_txtTitle->lineEdit(), m_ui.m_txtDescription->lineEdit()); + setTabOrder(m_ui.m_txtDescription->lineEdit(), m_ui.m_cmbSourceType); + setTabOrder(m_ui.m_cmbSourceType, m_ui.m_txtSource->lineEdit()); + setTabOrder(m_ui.m_txtSource->lineEdit(), m_ui.m_txtPostProcessScript->lineEdit()); + setTabOrder(m_ui.m_txtPostProcessScript->lineEdit(), m_ui.m_btnFetchMetadata); + setTabOrder(m_ui.m_btnFetchMetadata, m_ui.m_btnIcon); + + GuiUtilities::setLabelAsNotice(*m_ui.m_lblScriptInfo, false); + onTitleChanged(QString()); onDescriptionChanged(QString()); onUrlChanged(QString()); @@ -87,17 +114,17 @@ void StandardFeedDetails::guessIconOnly(const QString& url, const QString& usern if (result.first != nullptr) { // Icon or whole feed was guessed. - ui.m_btnIcon->setIcon(result.first->icon()); + m_ui.m_btnIcon->setIcon(result.first->icon()); if (result.second == QNetworkReply::NoError) { - ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Ok, - tr("Icon fetched successfully."), - tr("Icon metadata fetched.")); + m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Ok, + tr("Icon fetched successfully."), + tr("Icon metadata fetched.")); } else { - ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Warning, - tr("Result: %1.").arg(NetworkFactory::networkErrorText(result.second)), - tr("Icon metadata not fetched.")); + m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Warning, + tr("Result: %1.").arg(NetworkFactory::networkErrorText(result.second)), + tr("Icon metadata not fetched.")); } // Remove temporary feed object. @@ -105,9 +132,9 @@ void StandardFeedDetails::guessIconOnly(const QString& url, const QString& usern } else { // No feed guessed, even no icon available. - ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Error, - tr("Error: %1.").arg(NetworkFactory::networkErrorText(result.second)), - tr("No icon fetched.")); + m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Error, + tr("Error: %1.").arg(NetworkFactory::networkErrorText(result.second)), + tr("No icon fetched.")); } } @@ -120,28 +147,28 @@ void StandardFeedDetails::guessFeed(const QString& url, const QString& username, if (result.first != nullptr) { // Icon or whole feed was guessed. - ui.m_btnIcon->setIcon(result.first->icon()); - ui.m_txtTitle->lineEdit()->setText(result.first->title()); - ui.m_txtDescription->lineEdit()->setText(result.first->description()); - ui.m_cmbType->setCurrentIndex(ui.m_cmbType->findData(QVariant::fromValue((int) result.first->type()))); - int encoding_index = ui.m_cmbEncoding->findText(result.first->encoding(), Qt::MatchFixedString); + m_ui.m_btnIcon->setIcon(result.first->icon()); + m_ui.m_txtTitle->lineEdit()->setText(result.first->title()); + m_ui.m_txtDescription->lineEdit()->setText(result.first->description()); + m_ui.m_cmbType->setCurrentIndex(m_ui.m_cmbType->findData(QVariant::fromValue((int) result.first->type()))); + int encoding_index = m_ui.m_cmbEncoding->findText(result.first->encoding(), Qt::MatchFixedString); if (encoding_index >= 0) { - ui.m_cmbEncoding->setCurrentIndex(encoding_index); + m_ui.m_cmbEncoding->setCurrentIndex(encoding_index); } else { - ui.m_cmbEncoding->setCurrentIndex(ui.m_cmbEncoding->findText(DEFAULT_FEED_ENCODING, Qt::MatchFixedString)); + m_ui.m_cmbEncoding->setCurrentIndex(m_ui.m_cmbEncoding->findText(DEFAULT_FEED_ENCODING, Qt::MatchFixedString)); } if (result.second == QNetworkReply::NoError) { - ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Ok, - tr("All metadata fetched successfully."), - tr("Feed and icon metadata fetched.")); + m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Ok, + tr("All metadata fetched successfully."), + tr("Feed and icon metadata fetched.")); } else { - ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Warning, - tr("Result: %1.").arg(NetworkFactory::networkErrorText(result.second)), - tr("Feed or icon metadata not fetched.")); + m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Warning, + tr("Result: %1.").arg(NetworkFactory::networkErrorText(result.second)), + tr("Feed or icon metadata not fetched.")); } // Remove temporary feed object. @@ -149,43 +176,58 @@ void StandardFeedDetails::guessFeed(const QString& url, const QString& username, } else { // No feed guessed, even no icon available. - ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Error, - tr("Error: %1.").arg(NetworkFactory::networkErrorText(result.second)), - tr("No metadata fetched.")); + m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Error, + tr("Error: %1.").arg(NetworkFactory::networkErrorText(result.second)), + tr("No metadata fetched.")); } } void StandardFeedDetails::onTitleChanged(const QString& new_title) { if (new_title.simplified().size() >= MIN_CATEGORY_NAME_LENGTH) { - ui.m_txtTitle->setStatus(LineEditWithStatus::StatusType::Ok, tr("Feed name is ok.")); + m_ui.m_txtTitle->setStatus(LineEditWithStatus::StatusType::Ok, tr("Feed name is ok.")); } else { - ui.m_txtTitle->setStatus(LineEditWithStatus::StatusType::Error, tr("Feed name is too short.")); + m_ui.m_txtTitle->setStatus(LineEditWithStatus::StatusType::Error, tr("Feed name is too short.")); } } void StandardFeedDetails::onDescriptionChanged(const QString& new_description) { if (new_description.simplified().isEmpty()) { - ui.m_txtDescription->setStatus(LineEditWithStatus::StatusType::Warning, tr("Description is empty.")); + m_ui.m_txtDescription->setStatus(LineEditWithStatus::StatusType::Warning, tr("Description is empty.")); } else { - ui.m_txtDescription->setStatus(LineEditWithStatus::StatusType::Ok, tr("The description is ok.")); + m_ui.m_txtDescription->setStatus(LineEditWithStatus::StatusType::Ok, tr("The description is ok.")); } } void StandardFeedDetails::onUrlChanged(const QString& new_url) { - if (QRegularExpression(URL_REGEXP).match(new_url).hasMatch()) { - // New url is well-formed. - ui.m_txtUrl->setStatus(LineEditWithStatus::StatusType::Ok, tr("The URL is ok.")); + if (sourceType() == StandardFeed::SourceType::Url) { + if (QRegularExpression(URL_REGEXP).match(new_url).hasMatch()) { + m_ui.m_txtSource->setStatus(LineEditWithStatus::StatusType::Ok, tr("The URL is ok.")); + } + else if (!new_url.simplified().isEmpty()) { + m_ui.m_txtSource->setStatus(LineEditWithStatus::StatusType::Warning, + tr("The URL does not meet standard pattern. " + "Does your URL start with \"http://\" or \"https://\" prefix.")); + } + else { + m_ui.m_txtSource->setStatus(LineEditWithStatus::StatusType::Error, tr("The URL is empty.")); + } } - else if (!new_url.simplified().isEmpty()) { - // New url is not well-formed but is not empty on the other hand. - ui.m_txtUrl->setStatus(LineEditWithStatus::StatusType::Warning, - tr(R"(The URL does not meet standard pattern. Does your URL start with "http://" or "https://" prefix.)")); + else if (sourceType() == StandardFeed::SourceType::Script) { + if (QRegularExpression(SCRIPT_SOURCE_TYPE_REGEXP).match(new_url).hasMatch()) { + m_ui.m_txtSource->setStatus(LineEditWithStatus::StatusType::Ok, tr("The source is ok.")); + } + else if (!new_url.simplified().isEmpty()) { + m_ui.m_txtSource->setStatus(LineEditWithStatus::StatusType::Warning, + tr("The source needs to include \"#\" separator.")); + } + else { + m_ui.m_txtSource->setStatus(LineEditWithStatus::StatusType::Error, tr("The source is empty.")); + } } else { - // New url is empty. - ui.m_txtUrl->setStatus(LineEditWithStatus::StatusType::Error, tr("The URL is empty.")); + m_ui.m_txtSource->setStatus(LineEditWithStatus::StatusType::Ok, tr("The source is ok.")); } } @@ -206,63 +248,69 @@ void StandardFeedDetails::onLoadIconFromFile() { dialog.setLabelText(QFileDialog::DialogLabel::FileType, tr("Icon type:")); if (dialog.exec() == QDialog::DialogCode::Accepted) { - ui.m_btnIcon->setIcon(QIcon(dialog.selectedFiles().value(0))); + m_ui.m_btnIcon->setIcon(QIcon(dialog.selectedFiles().value(0))); } } void StandardFeedDetails::onUseDefaultIcon() { - ui.m_btnIcon->setIcon(QIcon()); + m_ui.m_btnIcon->setIcon(QIcon()); +} + +StandardFeed::SourceType StandardFeedDetails::sourceType() const { + return m_ui.m_cmbSourceType->currentData().value(); } void StandardFeedDetails::prepareForNewFeed(RootItem* parent_to_select, const QString& url) { // Make sure that "default" icon is used as the default option for new // feed. m_actionUseDefaultIcon->trigger(); - int default_encoding_index = ui.m_cmbEncoding->findText(DEFAULT_FEED_ENCODING); + int default_encoding_index = m_ui.m_cmbEncoding->findText(DEFAULT_FEED_ENCODING); if (default_encoding_index >= 0) { - ui.m_cmbEncoding->setCurrentIndex(default_encoding_index); + m_ui.m_cmbEncoding->setCurrentIndex(default_encoding_index); } if (parent_to_select != nullptr) { if (parent_to_select->kind() == RootItem::Kind::Category) { - ui.m_cmbParentCategory->setCurrentIndex(ui.m_cmbParentCategory->findData(QVariant::fromValue((void*)parent_to_select))); + m_ui.m_cmbParentCategory->setCurrentIndex(m_ui.m_cmbParentCategory->findData(QVariant::fromValue((void*)parent_to_select))); } else if (parent_to_select->kind() == RootItem::Kind::Feed) { - int target_item = ui.m_cmbParentCategory->findData(QVariant::fromValue((void*)parent_to_select->parent())); + int target_item = m_ui.m_cmbParentCategory->findData(QVariant::fromValue((void*)parent_to_select->parent())); if (target_item >= 0) { - ui.m_cmbParentCategory->setCurrentIndex(target_item); + m_ui.m_cmbParentCategory->setCurrentIndex(target_item); } } } if (!url.isEmpty()) { - ui.m_txtUrl->lineEdit()->setText(url); + m_ui.m_txtSource->lineEdit()->setText(url); } else if (Application::clipboard()->mimeData()->hasText()) { - ui.m_txtUrl->lineEdit()->setText(Application::clipboard()->text()); + m_ui.m_txtSource->lineEdit()->setText(Application::clipboard()->text()); } - ui.m_txtUrl->setFocus(); + m_ui.m_txtSource->setFocus(); } void StandardFeedDetails::setExistingFeed(StandardFeed* feed) { - ui.m_cmbParentCategory->setCurrentIndex(ui.m_cmbParentCategory->findData(QVariant::fromValue((void*)feed->parent()))); - ui.m_txtTitle->lineEdit()->setText(feed->title()); - ui.m_txtDescription->lineEdit()->setText(feed->description()); - ui.m_btnIcon->setIcon(feed->icon()); - ui.m_txtUrl->lineEdit()->setText(feed->url()); - ui.m_cmbType->setCurrentIndex(ui.m_cmbType->findData(QVariant::fromValue(int(feed->type())))); - ui.m_cmbEncoding->setCurrentIndex(ui.m_cmbEncoding->findData(feed->encoding(), - Qt::ItemDataRole::DisplayRole, - Qt::MatchFlag::MatchFixedString)); + m_ui.m_cmbSourceType->setCurrentIndex(m_ui.m_cmbSourceType->findData(QVariant::fromValue(feed->sourceType()))); + m_ui.m_cmbParentCategory->setCurrentIndex(m_ui.m_cmbParentCategory->findData(QVariant::fromValue((void*)feed->parent()))); + m_ui.m_txtTitle->lineEdit()->setText(feed->title()); + m_ui.m_txtDescription->lineEdit()->setText(feed->description()); + m_ui.m_btnIcon->setIcon(feed->icon()); + m_ui.m_txtSource->lineEdit()->setText(feed->url()); + m_ui.m_txtPostProcessScript->lineEdit()->setText(feed->postProcessScript()); + m_ui.m_cmbType->setCurrentIndex(m_ui.m_cmbType->findData(QVariant::fromValue(int(feed->type())))); + m_ui.m_cmbEncoding->setCurrentIndex(m_ui.m_cmbEncoding->findData(feed->encoding(), + Qt::ItemDataRole::DisplayRole, + Qt::MatchFlag::MatchFixedString)); } void StandardFeedDetails::loadCategories(const QList& categories, RootItem* root_item) { - ui.m_cmbParentCategory->addItem(root_item->fullIcon(), root_item->title(), QVariant::fromValue((void*) root_item)); + m_ui.m_cmbParentCategory->addItem(root_item->fullIcon(), root_item->title(), QVariant::fromValue((void*) root_item)); for (Category* category : categories) { - ui.m_cmbParentCategory->addItem(category->fullIcon(), category->title(), QVariant::fromValue((void*) category)); + m_ui.m_cmbParentCategory->addItem(category->fullIcon(), category->title(), QVariant::fromValue((void*) category)); } } diff --git a/src/librssguard/services/standard/gui/standardfeeddetails.h b/src/librssguard/services/standard/gui/standardfeeddetails.h index 6c83dfadd..cabfbb8ab 100755 --- a/src/librssguard/services/standard/gui/standardfeeddetails.h +++ b/src/librssguard/services/standard/gui/standardfeeddetails.h @@ -7,11 +7,12 @@ #include "ui_standardfeeddetails.h" +#include "services/standard/standardfeed.h" + #include class Category; class RootItem; -class StandardFeed; class StandardFeedDetails : public QWidget { Q_OBJECT @@ -37,13 +38,15 @@ class StandardFeedDetails : public QWidget { void onLoadIconFromFile(); void onUseDefaultIcon(); + StandardFeed::SourceType sourceType() const; + private: void prepareForNewFeed(RootItem* parent_to_select, const QString& url); void setExistingFeed(StandardFeed* feed); void loadCategories(const QList& categories, RootItem* root_item); private: - Ui::StandardFeedDetails ui; + Ui::StandardFeedDetails m_ui; QMenu* m_iconMenu{}; QAction* m_actionLoadIconFromFile{}; QAction* m_actionUseDefaultIcon{}; diff --git a/src/librssguard/services/standard/gui/standardfeeddetails.ui b/src/librssguard/services/standard/gui/standardfeeddetails.ui index 95cb8be88..8fbce5426 100755 --- a/src/librssguard/services/standard/gui/standardfeeddetails.ui +++ b/src/librssguard/services/standard/gui/standardfeeddetails.ui @@ -7,7 +7,7 @@ 0 0 429 - 260 + 321 @@ -103,17 +103,31 @@ - URL + Source - m_txtUrl + m_txtSource - + + + + + + + + + 1 + 0 + + + + + - + Fetch metadata @@ -123,7 +137,7 @@ - + @@ -147,7 +161,7 @@ - + Icon @@ -157,7 +171,7 @@ - + @@ -194,6 +208,29 @@ + + + + + + + Post-process script + + + + + + + You can use URL as a source of your feed or you can produce your feed with custom script. Also, you can post-process generated feed data with yet another script if you wish. These are advanced features and make sure to read the documentation before your use them. + + + Qt::AlignCenter + + + true + + + @@ -210,13 +247,6 @@ 1 - - m_cmbParentCategory - m_cmbType - m_cmbEncoding - m_btnFetchMetadata - m_btnIcon - diff --git a/src/librssguard/services/standard/standardfeed.cpp b/src/librssguard/services/standard/standardfeed.cpp index f79ed3ee7..ed3740321 100644 --- a/src/librssguard/services/standard/standardfeed.cpp +++ b/src/librssguard/services/standard/standardfeed.cpp @@ -35,13 +35,14 @@ StandardFeed::StandardFeed(RootItem* parent_item) m_networkError = QNetworkReply::NetworkError::NoError; m_type = Type::Rss0X; m_sourceType = SourceType::Url; - m_encoding = QString(); + m_encoding = m_postProcessScript = QString(); } StandardFeed::StandardFeed(const StandardFeed& other) : Feed(other) { m_networkError = other.networkError(); m_type = other.type(); + m_postProcessScript = other.postProcessScript(); m_sourceType = other.sourceType(); m_encoding = other.encoding(); } @@ -112,6 +113,22 @@ QString StandardFeed::typeToString(StandardFeed::Type type) { } } +QString StandardFeed::sourceTypeToString(StandardFeed::SourceType type) { + switch (type) { + case StandardFeed::SourceType::Url: + return QSL("URL"); + + case StandardFeed::SourceType::Script: + return tr("Script"); + + case StandardFeed::SourceType::LocalFile: + return tr("Local file"); + + default: + return tr("Unknown"); + } +} + void StandardFeed::fetchMetadataForItself() { QPair metadata = guessFeed(url(), username(), @@ -141,6 +158,14 @@ void StandardFeed::fetchMetadataForItself() { } } +QString StandardFeed::postProcessScript() const { + return m_postProcessScript; +} + +void StandardFeed::setPostProcessScript(const QString& post_process_script) { + m_postProcessScript = post_process_script; +} + StandardFeed::SourceType StandardFeed::sourceType() const { return m_sourceType; } @@ -370,9 +395,11 @@ bool StandardFeed::addItself(RootItem* parent) { // Now, add feed to persistent storage. QSqlDatabase database = qApp->database()->connection(metaObject()->className()); bool ok; - int new_id = DatabaseQueries::addStandardFeed(database, parent->id(), parent->getParentServiceRoot()->accountId(), title(), - description(), creationDate(), icon(), encoding(), url(), passwordProtected(), - username(), password(), autoUpdateType(), autoUpdateInitialInterval(), type(), &ok); + int new_id = DatabaseQueries::addStandardFeed(database, parent->id(), parent->getParentServiceRoot()->accountId(), + title(), description(), creationDate(), icon(), encoding(), url(), + passwordProtected(), username(), password(), autoUpdateType(), + autoUpdateInitialInterval(), sourceType(), postProcessScript(), + type(), &ok); if (!ok) { // Query failed. @@ -396,6 +423,7 @@ bool StandardFeed::editItself(StandardFeed* new_feed_data) { new_feed_data->encoding(), new_feed_data->url(), new_feed_data->passwordProtected(), new_feed_data->username(), new_feed_data->password(), new_feed_data->autoUpdateType(), new_feed_data->autoUpdateInitialInterval(), + new_feed_data->sourceType(), new_feed_data->postProcessScript(), new_feed_data->type())) { // Persistent storage update failed, no way to continue now. qWarningNN << LOGSEC_CORE @@ -417,6 +445,7 @@ bool StandardFeed::editItself(StandardFeed* new_feed_data) { original_feed->setAutoUpdateInitialInterval(new_feed_data->autoUpdateInitialInterval()); original_feed->setType(new_feed_data->type()); original_feed->setSourceType(new_feed_data->sourceType()); + original_feed->setPostProcessScript(new_feed_data->postProcessScript()); // Editing is done. return true; diff --git a/src/librssguard/services/standard/standardfeed.h b/src/librssguard/services/standard/standardfeed.h index e3578aee8..355bc61d7 100644 --- a/src/librssguard/services/standard/standardfeed.h +++ b/src/librssguard/services/standard/standardfeed.h @@ -71,6 +71,9 @@ class StandardFeed : public Feed { QString encoding() const; void setEncoding(const QString& encoding); + QString postProcessScript() const; + void setPostProcessScript(const QString& post_process_script); + QNetworkReply::NetworkError networkError() const; QList obtainNewMessages(bool* error_during_obtaining); @@ -87,6 +90,7 @@ class StandardFeed : public Feed { // Converts particular feed type to string. static QString typeToString(Type type); + static QString sourceTypeToString(SourceType type); public slots: void fetchMetadataForItself(); @@ -94,11 +98,13 @@ class StandardFeed : public Feed { private: SourceType m_sourceType; Type m_type; + QString m_postProcessScript; QNetworkReply::NetworkError m_networkError; QString m_encoding; }; +Q_DECLARE_METATYPE(StandardFeed::SourceType) Q_DECLARE_METATYPE(StandardFeed::Type) #endif // FEEDSMODELFEED_H