JSON feed support added.
This commit is contained in:
parent
b51f2e6dd5
commit
e8ea98d3fa
@ -36,6 +36,8 @@ class Message {
|
|||||||
// Creates Message from given record, which contains
|
// Creates Message from given record, which contains
|
||||||
// row from query SELECT * FROM Messages WHERE ....;
|
// row from query SELECT * FROM Messages WHERE ....;
|
||||||
static Message fromSqlRecord(const QSqlRecord& record, bool* result = nullptr);
|
static Message fromSqlRecord(const QSqlRecord& record, bool* result = nullptr);
|
||||||
|
|
||||||
|
public:
|
||||||
QString m_title;
|
QString m_title;
|
||||||
QString m_url;
|
QString m_url;
|
||||||
QString m_author;
|
QString m_author;
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
#define FEED_INITIAL_OPML_PATTERN "feeds-%1.opml"
|
#define FEED_INITIAL_OPML_PATTERN "feeds-%1.opml"
|
||||||
|
|
||||||
#define FEED_REGEX_MATCHER "<link[^>]+type=\"application\\/(?:atom|rss)\\+xml\"[^>]*>"
|
#define FEED_REGEX_MATCHER "<link[^>]+type=\"application\\/(?:atom\\+xml|rss\\+xml|feed\\+json|json)\"[^>]*>"
|
||||||
#define FEED_HREF_REGEX_MATCHER "href=\"([^\"]+)\""
|
#define FEED_HREF_REGEX_MATCHER "href=\"([^\"]+)\""
|
||||||
|
|
||||||
#define PLACEHOLDER_UNREAD_COUNTS "%unread"
|
#define PLACEHOLDER_UNREAD_COUNTS "%unread"
|
||||||
|
@ -62,10 +62,10 @@ void DiscoverFeedsButton::fillMenu() {
|
|||||||
menu()->clear();
|
menu()->clear();
|
||||||
|
|
||||||
for (const ServiceRoot* root : qApp->feedReader()->feedsModel()->serviceRoots()) {
|
for (const ServiceRoot* root : qApp->feedReader()->feedsModel()->serviceRoots()) {
|
||||||
|
if (root->supportsFeedAdding()) {
|
||||||
QMenu* root_menu = menu()->addMenu(root->icon(), root->title());
|
QMenu* root_menu = menu()->addMenu(root->icon(), root->title());
|
||||||
|
|
||||||
for (const QString& url : m_addresses) {
|
for (const QString& url : m_addresses) {
|
||||||
if (root->supportsFeedAdding()) {
|
|
||||||
QAction* url_action = root_menu->addAction(root->icon(), url);
|
QAction* url_action = root_menu->addAction(root->icon(), url);
|
||||||
|
|
||||||
url_action->setProperty("url", url);
|
url_action->setProperty("url", url);
|
||||||
@ -73,4 +73,8 @@ void DiscoverFeedsButton::fillMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (menu()->isEmpty()) {
|
||||||
|
menu()->addAction(tr("Feeds were detected, but no suitable accounts are configured."))->setEnabled(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,6 +158,7 @@ HEADERS += core/feeddownloader.h \
|
|||||||
services/standard/gui/formstandardcategorydetails.h \
|
services/standard/gui/formstandardcategorydetails.h \
|
||||||
services/standard/gui/formstandardfeeddetails.h \
|
services/standard/gui/formstandardfeeddetails.h \
|
||||||
services/standard/gui/formstandardimportexport.h \
|
services/standard/gui/formstandardimportexport.h \
|
||||||
|
services/standard/jsonparser.h \
|
||||||
services/standard/rdfparser.h \
|
services/standard/rdfparser.h \
|
||||||
services/standard/rssparser.h \
|
services/standard/rssparser.h \
|
||||||
services/standard/standardcategory.h \
|
services/standard/standardcategory.h \
|
||||||
@ -302,6 +303,7 @@ SOURCES += core/feeddownloader.cpp \
|
|||||||
services/standard/gui/formstandardcategorydetails.cpp \
|
services/standard/gui/formstandardcategorydetails.cpp \
|
||||||
services/standard/gui/formstandardfeeddetails.cpp \
|
services/standard/gui/formstandardfeeddetails.cpp \
|
||||||
services/standard/gui/formstandardimportexport.cpp \
|
services/standard/gui/formstandardimportexport.cpp \
|
||||||
|
services/standard/jsonparser.cpp \
|
||||||
services/standard/rdfparser.cpp \
|
services/standard/rdfparser.cpp \
|
||||||
services/standard/rssparser.cpp \
|
services/standard/rssparser.cpp \
|
||||||
services/standard/standardcategory.cpp \
|
services/standard/standardcategory.cpp \
|
||||||
|
@ -196,7 +196,8 @@ inline void DatabaseQueries::fillFeedData(StandardFeed* feed, const QSqlRecord&
|
|||||||
case StandardFeed::Type::Atom10:
|
case StandardFeed::Type::Atom10:
|
||||||
case StandardFeed::Type::Rdf:
|
case StandardFeed::Type::Rdf:
|
||||||
case StandardFeed::Type::Rss0X:
|
case StandardFeed::Type::Rss0X:
|
||||||
case StandardFeed::Type::Rss2X: {
|
case StandardFeed::Type::Rss2X:
|
||||||
|
case StandardFeed::Type::Json: {
|
||||||
feed->setType(type);
|
feed->setType(type);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -143,6 +143,22 @@ QNetworkReply::NetworkError NetworkFactory::downloadIcon(const QList<QString>& u
|
|||||||
QNetworkReply::NetworkError network_result = QNetworkReply::UnknownNetworkError;
|
QNetworkReply::NetworkError network_result = QNetworkReply::UnknownNetworkError;
|
||||||
|
|
||||||
for (const QString& url : urls) {
|
for (const QString& url : urls) {
|
||||||
|
QByteArray icon_data;
|
||||||
|
|
||||||
|
network_result = performNetworkOperation(url, timeout, QByteArray(), icon_data,
|
||||||
|
QNetworkAccessManager::GetOperation).first;
|
||||||
|
|
||||||
|
if (network_result == QNetworkReply::NoError) {
|
||||||
|
QPixmap icon_pixmap;
|
||||||
|
|
||||||
|
icon_pixmap.loadFromData(icon_data);
|
||||||
|
output = QIcon(icon_pixmap);
|
||||||
|
|
||||||
|
if (!output.isNull()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString host = QUrl(url).host();
|
QString host = QUrl(url).host();
|
||||||
|
|
||||||
if (host.startsWith(QSL("www."))) {
|
if (host.startsWith(QSL("www."))) {
|
||||||
@ -150,7 +166,6 @@ QNetworkReply::NetworkError NetworkFactory::downloadIcon(const QList<QString>& u
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QString google_s2_with_url = QString("http://www.google.com/s2/favicons?domain=%1").arg(host);
|
const QString google_s2_with_url = QString("http://www.google.com/s2/favicons?domain=%1").arg(host);
|
||||||
QByteArray icon_data;
|
|
||||||
|
|
||||||
network_result = performNetworkOperation(google_s2_with_url, timeout, QByteArray(), icon_data,
|
network_result = performNetworkOperation(google_s2_with_url, timeout, QByteArray(), icon_data,
|
||||||
QNetworkAccessManager::GetOperation).first;
|
QNetworkAccessManager::GetOperation).first;
|
||||||
@ -160,9 +175,12 @@ QNetworkReply::NetworkError NetworkFactory::downloadIcon(const QList<QString>& u
|
|||||||
|
|
||||||
icon_pixmap.loadFromData(icon_data);
|
icon_pixmap.loadFromData(icon_data);
|
||||||
output = QIcon(icon_pixmap);
|
output = QIcon(icon_pixmap);
|
||||||
|
|
||||||
|
if (!output.isNull()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return network_result;
|
return network_result;
|
||||||
}
|
}
|
||||||
|
@ -315,6 +315,7 @@ void FormFeedDetails::initialize() {
|
|||||||
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::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::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::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.
|
// Load available encodings.
|
||||||
const QList<QByteArray> encodings = QTextCodec::availableCodecs();
|
const QList<QByteArray> encodings = QTextCodec::availableCodecs();
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "core/message.h"
|
#include "core/message.h"
|
||||||
|
|
||||||
|
// Base class for all XML-based feed parsers.
|
||||||
class FeedParser {
|
class FeedParser {
|
||||||
public:
|
public:
|
||||||
explicit FeedParser(QString data);
|
explicit FeedParser(QString data);
|
||||||
|
67
src/librssguard/services/standard/jsonparser.cpp
Normal file
67
src/librssguard/services/standard/jsonparser.cpp
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// For license of this file, see <project-root-folder>/LICENSE.md.
|
||||||
|
|
||||||
|
#include "services/standard/jsonparser.h"
|
||||||
|
|
||||||
|
#include "miscellaneous/textfactory.h"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
JsonParser::JsonParser(const QString& data) : m_jsonData(data) {}
|
||||||
|
|
||||||
|
QList<Message> JsonParser::messages() const {
|
||||||
|
QList<Message> msgs;
|
||||||
|
QJsonDocument json = QJsonDocument::fromJson(m_jsonData.toUtf8());
|
||||||
|
QString global_author = json.object()["author"].toObject()["name"].toString();
|
||||||
|
|
||||||
|
if (global_author.isEmpty()) {
|
||||||
|
global_author = json.object()["authors"].toArray().at(0).toObject()["name"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QJsonValue& msg_val : json.object()["items"].toArray()) {
|
||||||
|
QJsonObject msg_obj = msg_val.toObject();
|
||||||
|
Message msg;
|
||||||
|
|
||||||
|
msg.m_title = msg_obj["title"].toString();
|
||||||
|
msg.m_url = msg_obj["url"].toString();
|
||||||
|
msg.m_contents = msg_obj.contains("content_html") ? msg_obj["content_html"].toString() : msg_obj["content_text"].toString();
|
||||||
|
|
||||||
|
msg.m_created = TextFactory::parseDateTime(msg_obj.contains("date_modified")
|
||||||
|
? msg_obj["date_modified"].toString()
|
||||||
|
: msg_obj["date_published"].toString());
|
||||||
|
|
||||||
|
if (!msg.m_created.isValid()) {
|
||||||
|
msg.m_created = QDateTime::currentDateTime();
|
||||||
|
msg.m_createdFromFeed = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg.m_createdFromFeed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg_obj.contains("author")) {
|
||||||
|
msg.m_author = msg_obj["author"].toObject()["name"].toString();
|
||||||
|
}
|
||||||
|
else if (msg_obj.contains("authors")) {
|
||||||
|
msg.m_author = msg_obj["authors"].toArray().at(0).toObject()["name"].toString();
|
||||||
|
}
|
||||||
|
else if (!global_author.isEmpty()) {
|
||||||
|
msg.m_author = global_author;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QJsonValue& att : msg_obj["attachments"].toArray()) {
|
||||||
|
QJsonObject att_obj = att.toObject();
|
||||||
|
auto xx = att_obj["url"].toString();
|
||||||
|
|
||||||
|
msg.m_enclosures.append(Enclosure(att_obj["url"].toString(), att_obj["mime_type"].toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.m_title.isEmpty() && !msg.m_url.isEmpty()) {
|
||||||
|
msg.m_title = msg.m_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs.append(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgs;
|
||||||
|
}
|
18
src/librssguard/services/standard/jsonparser.h
Normal file
18
src/librssguard/services/standard/jsonparser.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// For license of this file, see <project-root-folder>/LICENSE.md.
|
||||||
|
|
||||||
|
#ifndef JSONPARSER_H
|
||||||
|
#define JSONPARSER_H
|
||||||
|
|
||||||
|
#include "core/message.h"
|
||||||
|
|
||||||
|
class JsonParser {
|
||||||
|
public:
|
||||||
|
explicit JsonParser(const QString& data);
|
||||||
|
|
||||||
|
QList<Message> messages() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_jsonData;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // JSONPARSER_H
|
@ -15,6 +15,7 @@
|
|||||||
#include "services/abstract/recyclebin.h"
|
#include "services/abstract/recyclebin.h"
|
||||||
#include "services/standard/atomparser.h"
|
#include "services/standard/atomparser.h"
|
||||||
#include "services/standard/gui/formstandardfeeddetails.h"
|
#include "services/standard/gui/formstandardfeeddetails.h"
|
||||||
|
#include "services/standard/jsonparser.h"
|
||||||
#include "services/standard/rdfparser.h"
|
#include "services/standard/rdfparser.h"
|
||||||
#include "services/standard/rssparser.h"
|
#include "services/standard/rssparser.h"
|
||||||
#include "services/standard/standardserviceroot.h"
|
#include "services/standard/standardserviceroot.h"
|
||||||
@ -22,6 +23,8 @@
|
|||||||
#include <QDomDocument>
|
#include <QDomDocument>
|
||||||
#include <QDomElement>
|
#include <QDomElement>
|
||||||
#include <QDomNode>
|
#include <QDomNode>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QTextCodec>
|
#include <QTextCodec>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
@ -104,6 +107,9 @@ QString StandardFeed::typeToString(StandardFeed::Type type) {
|
|||||||
case Type::Rss0X:
|
case Type::Rss0X:
|
||||||
return QSL("RSS 0.91/0.92/0.93");
|
return QSL("RSS 0.91/0.92/0.93");
|
||||||
|
|
||||||
|
case Type::Json:
|
||||||
|
return QSL("JSON 1.0/1.1");
|
||||||
|
|
||||||
case Type::Rss2X:
|
case Type::Rss2X:
|
||||||
default:
|
default:
|
||||||
return QSL("RSS 2.0/2.0.1");
|
return QSL("RSS 2.0/2.0.1");
|
||||||
@ -162,6 +168,31 @@ QPair<StandardFeed*, QNetworkReply::NetworkError> StandardFeed::guessFeed(const
|
|||||||
result.first = new StandardFeed();
|
result.first = new StandardFeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<QString> icon_possible_locations;
|
||||||
|
|
||||||
|
icon_possible_locations.append(url);
|
||||||
|
|
||||||
|
if (network_result.second.toString().contains(QSL("json"), Qt::CaseSensitivity::CaseInsensitive)) {
|
||||||
|
// We have JSON feed.
|
||||||
|
result.first->setEncoding(DEFAULT_FEED_ENCODING);
|
||||||
|
result.first->setType(Type::Json);
|
||||||
|
|
||||||
|
QJsonDocument json = QJsonDocument::fromJson(feed_contents);
|
||||||
|
|
||||||
|
result.first->setTitle(json.object()["title"].toString());
|
||||||
|
result.first->setDescription(json.object()["description"].toString());
|
||||||
|
|
||||||
|
auto icon = json.object()["icon"].toString();
|
||||||
|
|
||||||
|
if (icon.isEmpty()) {
|
||||||
|
icon = json.object()["favicon"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!icon.isEmpty()) {
|
||||||
|
icon_possible_locations.prepend(icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
// Feed XML was obtained, now we need to try to guess
|
// Feed XML was obtained, now we need to try to guess
|
||||||
// its encoding before we can read further data.
|
// its encoding before we can read further data.
|
||||||
QString xml_schema_encoding;
|
QString xml_schema_encoding;
|
||||||
@ -210,9 +241,6 @@ QPair<StandardFeed*, QNetworkReply::NetworkError> StandardFeed::guessFeed(const
|
|||||||
|
|
||||||
QDomElement root_element = xml_document.documentElement();
|
QDomElement root_element = xml_document.documentElement();
|
||||||
QString root_tag_name = root_element.tagName();
|
QString root_tag_name = root_element.tagName();
|
||||||
QList<QString> icon_possible_locations;
|
|
||||||
|
|
||||||
icon_possible_locations.append(url);
|
|
||||||
|
|
||||||
if (root_tag_name == QL1S("rdf:RDF")) {
|
if (root_tag_name == QL1S("rdf:RDF")) {
|
||||||
// We found RDF feed.
|
// We found RDF feed.
|
||||||
@ -264,6 +292,7 @@ QPair<StandardFeed*, QNetworkReply::NetworkError> StandardFeed::guessFeed(const
|
|||||||
// but feed format was NOT recognized.
|
// but feed format was NOT recognized.
|
||||||
result.second = QNetworkReply::UnknownContentError;
|
result.second = QNetworkReply::UnknownContentError;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try to obtain icon.
|
// Try to obtain icon.
|
||||||
QIcon icon_data;
|
QIcon icon_data;
|
||||||
@ -280,7 +309,7 @@ QPair<StandardFeed*, QNetworkReply::NetworkError> StandardFeed::guessFeed(const
|
|||||||
}
|
}
|
||||||
|
|
||||||
Qt::ItemFlags StandardFeed::additionalFlags() const {
|
Qt::ItemFlags StandardFeed::additionalFlags() const {
|
||||||
return Qt::ItemIsDragEnabled;
|
return Qt::ItemFlag::ItemIsDragEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StandardFeed::performDragDropChange(RootItem* target_item) {
|
bool StandardFeed::performDragDropChange(RootItem* target_item) {
|
||||||
@ -457,6 +486,11 @@ QList<Message> StandardFeed::obtainNewMessages(bool* error_during_obtaining) {
|
|||||||
|
|
||||||
case StandardFeed::Type::Atom10:
|
case StandardFeed::Type::Atom10:
|
||||||
messages = AtomParser(formatted_feed_contents).messages();
|
messages = AtomParser(formatted_feed_contents).messages();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case StandardFeed::Type::Json:
|
||||||
|
messages = JsonParser(formatted_feed_contents).messages();
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -24,7 +24,8 @@ class StandardFeed : public Feed {
|
|||||||
Rss0X = 0,
|
Rss0X = 0,
|
||||||
Rss2X = 1,
|
Rss2X = 1,
|
||||||
Rdf = 2, // Sometimes denoted as RSS 1.0.
|
Rdf = 2, // Sometimes denoted as RSS 1.0.
|
||||||
Atom10 = 3
|
Atom10 = 3,
|
||||||
|
Json = 4
|
||||||
};
|
};
|
||||||
|
|
||||||
// Constructors and destructors.
|
// Constructors and destructors.
|
||||||
|
@ -108,6 +108,10 @@ bool FeedsImportExportModel::exportToOMPL20(QByteArray& result) {
|
|||||||
outline_feed.setAttribute(QSL("version"), QSL("ATOM"));
|
outline_feed.setAttribute(QSL("version"), QSL("ATOM"));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case StandardFeed::Type::Json:
|
||||||
|
outline_feed.setAttribute(QSL("version"), QSL("JSON"));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -202,6 +206,9 @@ void FeedsImportExportModel::importAsOPML20(const QByteArray& data, bool fetch_m
|
|||||||
if (feed_type == QL1S("RSS1")) {
|
if (feed_type == QL1S("RSS1")) {
|
||||||
new_feed->setType(StandardFeed::Type::Rdf);
|
new_feed->setType(StandardFeed::Type::Rdf);
|
||||||
}
|
}
|
||||||
|
else if (feed_type == QL1S("JSON")) {
|
||||||
|
new_feed->setType(StandardFeed::Type::Json);
|
||||||
|
}
|
||||||
else if (feed_type == QL1S("ATOM")) {
|
else if (feed_type == QL1S("ATOM")) {
|
||||||
new_feed->setType(StandardFeed::Type::Atom10);
|
new_feed->setType(StandardFeed::Type::Atom10);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user