418 lines
15 KiB
C++
Executable File
418 lines
15 KiB
C++
Executable File
#include "core/feedsmodelstandardfeed.h"
|
|
|
|
#include "core/defs.h"
|
|
#include "core/settings.h"
|
|
#include "core/parsingfactory.h"
|
|
#include "core/databasefactory.h"
|
|
#include "core/networkfactory.h"
|
|
#include "core/textfactory.h"
|
|
#include "gui/iconfactory.h"
|
|
#include "gui/iconthemefactory.h"
|
|
|
|
#include <QVariant>
|
|
#include <QTextCodec>
|
|
#include <QSqlQuery>
|
|
|
|
#include <QDomDocument>
|
|
#include <QDomNode>
|
|
#include <QDomElement>
|
|
#include <QXmlStreamReader>
|
|
|
|
|
|
FeedsModelStandardFeed::FeedsModelStandardFeed(FeedsModelRootItem *parent_item)
|
|
: FeedsModelFeed(parent_item),
|
|
m_autoUpdateType(DontAutoUpdate),
|
|
m_autoUpdateInitialInterval(DEFAULT_AUTO_UPDATE_INTERVAL) {
|
|
}
|
|
|
|
FeedsModelStandardFeed::~FeedsModelStandardFeed() {
|
|
qDebug("Destroying FeedsModelStandardFeed instance.");
|
|
}
|
|
|
|
FeedsModelStandardFeed *FeedsModelStandardFeed::loadFromRecord(const QSqlRecord &record) {
|
|
FeedsModelStandardFeed *feed = new FeedsModelStandardFeed(NULL);
|
|
|
|
feed->setTitle(record.value(FDS_DB_TITLE_INDEX).toString());
|
|
feed->setId(record.value(FDS_DB_ID_INDEX).toInt());
|
|
feed->setDescription(record.value(FDS_DB_DESCRIPTION_INDEX).toString());
|
|
feed->setCreationDate(TextFactory::parseDateTime(record.value(FDS_DB_DCREATED_INDEX).value<qint64>()).toLocalTime());
|
|
feed->setIcon(IconFactory::fromByteArray(record.value(FDS_DB_ICON_INDEX).toByteArray()));
|
|
feed->setEncoding(record.value(FDS_DB_ENCODING_INDEX).toString());
|
|
feed->setUrl(record.value(FDS_DB_URL_INDEX).toString());
|
|
feed->setPasswordProtected(record.value(FDS_DB_PROTECTED_INDEX).toBool());
|
|
feed->setUsername(record.value(FDS_DB_USERNAME_INDEX).toString());
|
|
feed->setPassword(record.value(FDS_DB_PASSWORD_INDEX).toString());
|
|
feed->setAutoUpdateType(static_cast<FeedsModelStandardFeed::AutoUpdateType>(record.value(FDS_DB_UPDATE_TYPE_INDEX).toInt()));
|
|
feed->setAutoUpdateInitialInterval(record.value(FDS_DB_UPDATE_INTERVAL_INDEX).toInt());
|
|
feed->updateCounts();
|
|
|
|
return feed;
|
|
}
|
|
|
|
QPair<FeedsModelStandardFeed*, QNetworkReply::NetworkError> FeedsModelStandardFeed::guessFeed(const QString &url,
|
|
const QString &username,
|
|
const QString &password) {
|
|
QPair<FeedsModelStandardFeed*, QNetworkReply::NetworkError> result; result.first = NULL;
|
|
|
|
// Try to obtain icon.
|
|
QIcon icon_data;
|
|
|
|
if ((result.second = NetworkFactory::downloadIcon(url,
|
|
5000,
|
|
icon_data)) ==
|
|
QNetworkReply::NoError) {
|
|
// Icon for feed was downloaded and is stored now in _icon_data.
|
|
result.first = new FeedsModelStandardFeed();
|
|
result.first->setIcon(icon_data);
|
|
}
|
|
|
|
QByteArray feed_contents;
|
|
if ((result.second = NetworkFactory::downloadFeedFile(url,
|
|
Settings::instance()->value(APP_CFG_FEEDS, "feed_update_timeout", DOWNLOAD_TIMEOUT).toInt(),
|
|
feed_contents,
|
|
true,
|
|
username,
|
|
password)) == QNetworkReply::NoError) {
|
|
// Feed XML was obtained, now we need to try to guess
|
|
// its encoding before we can read further data.
|
|
QXmlStreamReader xml_stream_reader(feed_contents);
|
|
QString xml_schema_encoding;
|
|
QString xml_contents_encoded;
|
|
|
|
// TODO: Use QRegExp and capture encoding attribute with it
|
|
// instead of heavy QXmlStreamReader.
|
|
|
|
// We have several chances to read the XML version directly
|
|
// from XML declaration.
|
|
for (int i = 0; i < 2 && !xml_stream_reader.atEnd(); i++) {
|
|
if ((xml_schema_encoding = xml_stream_reader.documentEncoding().toString()).isEmpty()) {
|
|
xml_stream_reader.readNext();
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (result.first == NULL) {
|
|
result.first = new FeedsModelStandardFeed();
|
|
}
|
|
|
|
QTextCodec *custom_codec = QTextCodec::codecForName(xml_schema_encoding.toLocal8Bit());
|
|
|
|
if (custom_codec != NULL) {
|
|
// Feed encoding was probably guessed.
|
|
xml_contents_encoded = custom_codec->toUnicode(feed_contents);
|
|
result.first->setEncoding(xml_schema_encoding);
|
|
}
|
|
else {
|
|
// Feed encoding probably not guessed, set it as
|
|
// default.
|
|
xml_contents_encoded = feed_contents;
|
|
result.first->setEncoding(DEFAULT_FEED_ENCODING);
|
|
}
|
|
|
|
// Feed XML was obtained, guess it now.
|
|
QDomDocument xml_document;
|
|
|
|
if (!xml_document.setContent(xml_contents_encoded)) {
|
|
qDebug("XML of feed '%s' is not valid and cannot be loaded.",
|
|
qPrintable(url));
|
|
|
|
result.second = QNetworkReply::UnknownContentError;
|
|
|
|
// XML is invalid, exit.
|
|
return result;
|
|
}
|
|
|
|
QDomElement root_element = xml_document.documentElement();
|
|
QString root_tag_name = root_element.tagName();
|
|
|
|
if (root_tag_name == "rdf:RDF") {
|
|
// We found RDF feed.
|
|
QDomElement channel_element = root_element.namedItem("channel").toElement();
|
|
|
|
result.first->setType(StandardRdf);
|
|
result.first->setTitle(channel_element.namedItem("title").toElement().text());
|
|
result.first->setDescription(channel_element.namedItem("description").toElement().text());
|
|
}
|
|
else if (root_tag_name == "rss") {
|
|
// We found RSS 0.91/0.92/0.93/2.0/2.0.1 feed.
|
|
QString rss_type = root_element.attribute("version", "2.0");
|
|
|
|
if (rss_type == "0.91" || rss_type == "0.92" || rss_type == "0.93") {
|
|
result.first->setType(StandardRss0X);
|
|
}
|
|
else {
|
|
result.first->setType(StandardRss2X);
|
|
}
|
|
|
|
QDomElement channel_element = root_element.namedItem("channel").toElement();
|
|
|
|
result.first->setTitle(channel_element.namedItem("title").toElement().text());
|
|
result.first->setDescription(channel_element.namedItem("description").toElement().text());
|
|
}
|
|
else if (root_tag_name == "feed") {
|
|
// We found ATOM feed.
|
|
result.first->setType(StandardAtom10);
|
|
result.first->setTitle(root_element.namedItem("title").toElement().text());
|
|
result.first->setDescription(root_element.namedItem("subtitle").toElement().text());
|
|
}
|
|
else {
|
|
// File was downloaded and it really was XML file
|
|
// but feed format was NOT recognized.
|
|
result.second = QNetworkReply::UnknownContentError;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QVariant FeedsModelStandardFeed::data(int column, int role) const {
|
|
switch (role) {
|
|
case Qt::DisplayRole:
|
|
if (column == FDS_MODEL_TITLE_INDEX) {
|
|
return m_title;
|
|
}
|
|
else if (column == FDS_MODEL_COUNTS_INDEX) {
|
|
// TODO: Changeable text.
|
|
return QString("%1").arg(QString::number(countOfUnreadMessages()),
|
|
QString::number(countOfAllMessages()));
|
|
}
|
|
else {
|
|
return QVariant();
|
|
}
|
|
|
|
case Qt::EditRole:
|
|
if (column == FDS_MODEL_TITLE_INDEX) {
|
|
return m_title;
|
|
}
|
|
else if (column == FDS_MODEL_COUNTS_INDEX) {
|
|
return countOfUnreadMessages();
|
|
}
|
|
else {
|
|
return QVariant();
|
|
}
|
|
|
|
case Qt::DecorationRole:
|
|
if (column == FDS_MODEL_TITLE_INDEX) {
|
|
return m_icon;
|
|
}
|
|
else {
|
|
return QVariant();
|
|
}
|
|
|
|
case Qt::ToolTipRole:
|
|
if (column == FDS_MODEL_TITLE_INDEX) {
|
|
QString auto_update_string;
|
|
|
|
switch (m_autoUpdateType) {
|
|
case DontAutoUpdate:
|
|
auto_update_string = QObject::tr("does not use auto-update");
|
|
break;
|
|
|
|
case DefaultAutoUpdate:
|
|
auto_update_string = QObject::tr("uses global settings");
|
|
break;
|
|
|
|
case SpecificAutoUpdate:
|
|
default:
|
|
auto_update_string = QObject::tr("uses specific settings "
|
|
"(%n minute(s) to next auto-update)",
|
|
0,
|
|
m_autoUpdateRemainingInterval);
|
|
break;
|
|
}
|
|
|
|
return QObject::tr("%1 (%2)\n"
|
|
"%3\n\n"
|
|
"Encoding: %4\n"
|
|
"Auto-update status: %5").arg(m_title,
|
|
FeedsModelFeed::typeToString(m_type),
|
|
m_description,
|
|
m_encoding,
|
|
auto_update_string);
|
|
}
|
|
else if (column == FDS_MODEL_COUNTS_INDEX) {
|
|
return QObject::tr("%n unread message(s).", 0, countOfUnreadMessages());
|
|
}
|
|
else {
|
|
return QVariant();
|
|
}
|
|
|
|
case Qt::TextAlignmentRole:
|
|
if (column == FDS_MODEL_COUNTS_INDEX) {
|
|
return Qt::AlignCenter;
|
|
}
|
|
else {
|
|
return QVariant();
|
|
}
|
|
|
|
default:
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
void FeedsModelStandardFeed::update() {
|
|
QByteArray feed_contents;
|
|
int download_timeout = Settings::instance()->value(APP_CFG_FEEDS, "feed_update_timeout", DOWNLOAD_TIMEOUT).toInt();
|
|
QNetworkReply::NetworkError download_result = NetworkFactory::downloadFeedFile(url(),
|
|
download_timeout,
|
|
feed_contents,
|
|
passwordProtected(),
|
|
username(),
|
|
password());
|
|
|
|
if (download_result != QNetworkReply::NoError) {
|
|
qWarning("Error during fetching of new messages for feed '%s' (id %d).",
|
|
qPrintable(url()),
|
|
id());
|
|
return;
|
|
}
|
|
|
|
// Encode downloaded data for further parsing.
|
|
QTextCodec *codec = QTextCodec::codecForName(encoding().toLocal8Bit());
|
|
QString formatted_feed_contents;
|
|
|
|
if (codec == NULL) {
|
|
// No suitable codec for this encoding was found.
|
|
// Use non-converted data.
|
|
formatted_feed_contents = feed_contents;
|
|
}
|
|
else {
|
|
formatted_feed_contents = codec->toUnicode(feed_contents);
|
|
}
|
|
|
|
// Feed data are downloaded and encoded.
|
|
// Parse data and obtain messages.
|
|
QList<Message> messages;
|
|
|
|
switch (type()) {
|
|
case FeedsModelFeed::StandardRss0X:
|
|
case FeedsModelFeed::StandardRss2X:
|
|
messages = ParsingFactory::parseAsRSS20(formatted_feed_contents);
|
|
break;
|
|
|
|
case FeedsModelFeed::StandardRdf:
|
|
messages = ParsingFactory::parseAsRDF(formatted_feed_contents);
|
|
break;
|
|
|
|
case FeedsModelFeed::StandardAtom10:
|
|
messages = ParsingFactory::parseAsATOM10(formatted_feed_contents);
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
updateMessages(messages);
|
|
}
|
|
|
|
bool FeedsModelStandardFeed::removeItself() {
|
|
QSqlDatabase database = DatabaseFactory::instance()->connection("FeedsModelStandardFeed",
|
|
DatabaseFactory::FromSettings);
|
|
QSqlQuery query_remove(database);
|
|
|
|
query_remove.setForwardOnly(true);
|
|
|
|
// Remove all messages from this standard feed.
|
|
query_remove.prepare("DELETE FROM Messages WHERE feed = :feed;");
|
|
query_remove.bindValue(":feed", id());
|
|
|
|
if (!query_remove.exec()) {
|
|
return false;
|
|
}
|
|
|
|
// Remove feed itself.
|
|
query_remove.prepare("DELETE FROM Feeds WHERE id = :feed;");
|
|
query_remove.bindValue(":feed", id());
|
|
|
|
return query_remove.exec();
|
|
}
|
|
|
|
void FeedsModelStandardFeed::updateMessages(const QList<Message> &messages) {
|
|
int feed_id = id();
|
|
QSqlDatabase database = DatabaseFactory::instance()->connection("FeedsModelStandardFeed",
|
|
DatabaseFactory::FromSettings);
|
|
|
|
// Prepare queries.
|
|
QSqlQuery query_select(database);
|
|
QSqlQuery query_insert(database);
|
|
|
|
// Used to check if give feed contains with message with given
|
|
// title, url and date_created.
|
|
// WARNING: One feed CANNOT contain
|
|
// two (or more) messages with same
|
|
// AUTHOR AND TITLE AND URL AND DATE_CREATED.
|
|
query_select.setForwardOnly(true);
|
|
query_select.prepare("SELECT id, feed, date_created FROM Messages "
|
|
"WHERE feed = :feed AND title = :title AND url = :url AND author = :author;");
|
|
|
|
// Used to insert new messages.
|
|
query_insert.setForwardOnly(true);
|
|
query_insert.prepare("INSERT INTO Messages "
|
|
"(feed, title, url, author, date_created, contents) "
|
|
"VALUES (:feed, :title, :url, :author, :date_created, :contents);");
|
|
|
|
if (!database.transaction()) {
|
|
database.rollback();
|
|
|
|
qDebug("Transaction start for message downloader failed.");
|
|
return;
|
|
}
|
|
|
|
foreach (const Message &message, messages) {
|
|
query_select.bindValue(":feed", feed_id);
|
|
query_select.bindValue(":title", message.m_title);
|
|
query_select.bindValue(":url", message.m_url);
|
|
query_select.bindValue(":author", message.m_author);
|
|
query_select.exec();
|
|
|
|
QList<qint64> datetime_stamps;
|
|
|
|
while (query_select.next()) {
|
|
datetime_stamps << query_select.value(2).value<qint64>();
|
|
}
|
|
|
|
query_select.finish();
|
|
|
|
// TODO: potreba opravit nacitani URL
|
|
// pro http://forum.tea-earth.net/feed.php
|
|
// a taky vyresit problem v situaci
|
|
// kdy nastane situace ze message_id == -1
|
|
// tedy zprava s danym nazvem, autorem, url a casem
|
|
// neexistuje, ale existuje ta sama starsi s
|
|
// datem ktery se neziskalo z kanalu ale vygenerovalo
|
|
// a ja tam ted vkladam tu samou zpravu s opet novym
|
|
// vygenerovanym datem, takze se ty zpravy duplikujou a
|
|
// duplikujou
|
|
|
|
if (datetime_stamps.size() == 0 ||
|
|
(message.m_createdFromFeed &&
|
|
!datetime_stamps.contains(message.m_created.toMSecsSinceEpoch()))) {
|
|
// Message is not fetched in this feed yet
|
|
// or it is. If it is, then go
|
|
// through datetime stamps of stored messages
|
|
// and check if new (not auto-generated timestamp
|
|
// is among them and add this message if it is not.
|
|
query_insert.bindValue(":feed", feed_id);
|
|
query_insert.bindValue(":title", message.m_title);
|
|
query_insert.bindValue(":url", message.m_url);
|
|
query_insert.bindValue(":author", message.m_author);
|
|
query_insert.bindValue(":date_created", message.m_created.toMSecsSinceEpoch());
|
|
query_insert.bindValue(":contents", message.m_contents);
|
|
|
|
query_insert.exec();
|
|
query_insert.finish();
|
|
}
|
|
}
|
|
|
|
if (!database.commit()) {
|
|
database.rollback();
|
|
|
|
qDebug("Transaction commit for message downloader failed.");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|