Move RSS parsing to another thread to avoid freezing the UI

RSS items are now displayed as soon as they have been parsed
instead of displaying all of them in one batch once the whole
feed is parsed.

First step to address issue #34.
This commit is contained in:
Christophe Dumez
2012-08-18 18:06:29 +03:00
parent 820d94e014
commit c50c9239ea
10 changed files with 529 additions and 374 deletions

View File

@@ -28,168 +28,12 @@
* Contact: chris@qbittorrent.org, arnaud@qbittorrent.org
*/
#include <QRegExp>
#include <QVariant>
#include <QStringList>
#include <QDebug>
#include <iostream>
#include "rssarticle.h"
static const char shortDay[][4] = {
"Mon", "Tue", "Wed",
"Thu", "Fri", "Sat",
"Sun"
};
static const char longDay[][10] = {
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday",
"Sunday"
};
static const char shortMonth[][4] = {
"Jan", "Feb", "Mar", "Apr",
"May", "Jun", "Jul", "Aug",
"Sep", "Oct", "Nov", "Dec"
};
static const char longMonth[][10] = {
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
};
// Ported to Qt4 from KDElibs4
QDateTime RssArticle::parseDate(const QString &string) {
const QString str = string.trimmed();
if (str.isEmpty())
return QDateTime::currentDateTime();
int nyear = 6; // indexes within string to values
int nmonth = 4;
int nday = 2;
int nwday = 1;
int nhour = 7;
int nmin = 8;
int nsec = 9;
// Also accept obsolete form "Weekday, DD-Mon-YY HH:MM:SS ±hhmm"
QRegExp rx("^(?:([A-Z][a-z]+),\\s*)?(\\d{1,2})(\\s+|-)([^-\\s]+)(\\s+|-)(\\d{2,4})\\s+(\\d\\d):(\\d\\d)(?::(\\d\\d))?\\s+(\\S+)$");
QStringList parts;
if (!str.indexOf(rx)) {
// Check that if date has '-' separators, both separators are '-'.
parts = rx.capturedTexts();
bool h1 = (parts[3] == QLatin1String("-"));
bool h2 = (parts[5] == QLatin1String("-"));
if (h1 != h2)
return QDateTime::currentDateTime();
} else {
// Check for the obsolete form "Wdy Mon DD HH:MM:SS YYYY"
rx = QRegExp("^([A-Z][a-z]+)\\s+(\\S+)\\s+(\\d\\d)\\s+(\\d\\d):(\\d\\d):(\\d\\d)\\s+(\\d\\d\\d\\d)$");
if (str.indexOf(rx))
return QDateTime::currentDateTime();
nyear = 7;
nmonth = 2;
nday = 3;
nwday = 1;
nhour = 4;
nmin = 5;
nsec = 6;
parts = rx.capturedTexts();
}
bool ok[4];
const int day = parts[nday].toInt(&ok[0]);
int year = parts[nyear].toInt(&ok[1]);
const int hour = parts[nhour].toInt(&ok[2]);
const int minute = parts[nmin].toInt(&ok[3]);
if (!ok[0] || !ok[1] || !ok[2] || !ok[3])
return QDateTime::currentDateTime();
int second = 0;
if (!parts[nsec].isEmpty()) {
second = parts[nsec].toInt(&ok[0]);
if (!ok[0])
return QDateTime::currentDateTime();
}
bool leapSecond = (second == 60);
if (leapSecond)
second = 59; // apparently a leap second - validate below, once time zone is known
int month = 0;
for ( ; month < 12 && parts[nmonth] != shortMonth[month]; ++month) ;
int dayOfWeek = -1;
if (!parts[nwday].isEmpty()) {
// Look up the weekday name
while (++dayOfWeek < 7 && shortDay[dayOfWeek] != parts[nwday]) ;
if (dayOfWeek >= 7)
for (dayOfWeek = 0; dayOfWeek < 7 && longDay[dayOfWeek] != parts[nwday]; ++dayOfWeek) ;
}
// if (month >= 12 || dayOfWeek >= 7
// || (dayOfWeek < 0 && format == RFCDateDay))
// return QDateTime;
int i = parts[nyear].size();
if (i < 4) {
// It's an obsolete year specification with less than 4 digits
year += (i == 2 && year < 50) ? 2000: 1900;
}
// Parse the UTC offset part
int offset = 0; // set default to '-0000'
bool negOffset = false;
if (parts.count() > 10) {
rx = QRegExp("^([+-])(\\d\\d)(\\d\\d)$");
if (!parts[10].indexOf(rx)) {
// It's a UTC offset ±hhmm
parts = rx.capturedTexts();
offset = parts[2].toInt(&ok[0]) * 3600;
int offsetMin = parts[3].toInt(&ok[1]);
if (!ok[0] || !ok[1] || offsetMin > 59)
return QDateTime();
offset += offsetMin * 60;
negOffset = (parts[1] == QLatin1String("-"));
if (negOffset)
offset = -offset;
} else {
// Check for an obsolete time zone name
QByteArray zone = parts[10].toLatin1();
if (zone.length() == 1 && isalpha(zone[0]) && toupper(zone[0]) != 'J')
negOffset = true; // military zone: RFC 2822 treats as '-0000'
else if (zone != "UT" && zone != "GMT") { // treated as '+0000'
offset = (zone == "EDT") ? -4*3600
: (zone == "EST" || zone == "CDT") ? -5*3600
: (zone == "CST" || zone == "MDT") ? -6*3600
: (zone == "MST" || zone == "PDT") ? -7*3600
: (zone == "PST") ? -8*3600
: 0;
if (!offset) {
// Check for any other alphabetic time zone
bool nonalpha = false;
for (int i = 0, end = zone.size(); i < end && !nonalpha; ++i)
nonalpha = !isalpha(zone[i]);
if (nonalpha)
return QDateTime();
// TODO: Attempt to recognize the time zone abbreviation?
negOffset = true; // unknown time zone: RFC 2822 treats as '-0000'
}
}
}
}
QDate qdate(year, month+1, day); // convert date, and check for out-of-range
if (!qdate.isValid())
return QDateTime::currentDateTime();
QDateTime result(qdate, QTime(hour, minute, second));
if (!result.isValid()
|| (dayOfWeek >= 0 && result.date().dayOfWeek() != dayOfWeek+1))
return QDateTime::currentDateTime(); // invalid date/time, or weekday doesn't correspond with date
if (!offset) {
result.setTimeSpec(Qt::UTC);
}
if (leapSecond) {
// Validate a leap second time. Leap seconds are inserted after 23:59:59 UTC.
// Convert the time to UTC and check that it is 00:00:00.
if ((hour*3600 + minute*60 + 60 - offset + 86400*5) % 86400) // (max abs(offset) is 100 hours)
return QDateTime::currentDateTime(); // the time isn't the last second of the day
}
return result;
}
// public constructor
RssArticle::RssArticle(RssFeed* parent, const QString &guid):
m_parent(parent), m_guid(guid), m_read(false) {}
@@ -211,65 +55,6 @@ QVariantHash RssArticle::toHash() const {
return item;
}
RssArticlePtr xmlToRssArticle(RssFeed* parent, QXmlStreamReader& xml)
{
QString guid;
QString title;
QString torrentUrl;
QString link;
QString description;
QDateTime date;
QString author;
while(!xml.atEnd()) {
xml.readNext();
if(xml.isEndElement() && xml.name() == "item")
break;
if (xml.isStartElement()) {
if (xml.name() == "title")
title = xml.readElementText();
else if (xml.name() == "enclosure") {
if (xml.attributes().value("type") == "application/x-bittorrent")
torrentUrl = xml.attributes().value("url").toString();
}
else if (xml.name() == "link")
link = xml.readElementText();
else if (xml.name() == "description")
description = xml.readElementText();
else if (xml.name() == "pubDate")
date = RssArticle::parseDate(xml.readElementText());
else if (xml.name() == "author")
author = xml.readElementText();
else if (xml.name() == "guid")
guid = xml.readElementText();
}
}
if (guid.isEmpty()) {
// Item does not have a guid, fall back to some other identifier
if (!link.isEmpty())
guid = link;
else if (!title.isEmpty())
guid = title;
else {
qWarning() << "Item has no guid, link or title, ignoring it...";
return RssArticlePtr();
}
}
RssArticlePtr art(new RssArticle(parent, guid));
art->m_title = title;
art->m_torrentUrl = torrentUrl;
art->m_link = link;
art->m_description = description;
art->m_date = date;
art->m_author = author;
return art;
}
RssArticlePtr hashToRssArticle(RssFeed* parent, const QVariantHash &h) {
const QString guid = h.value("id").toString();
if (guid.isEmpty()) return RssArticlePtr();
@@ -281,7 +66,7 @@ RssArticlePtr hashToRssArticle(RssFeed* parent, const QVariantHash &h) {
art->m_description = h.value("description").toString();
art->m_date = h.value("date").toDateTime();
art->m_author = h.value("author").toString();
art->m_read = h.value("read").toBool();
art->m_read = h.value("read", false).toBool();
return art;
}
@@ -320,7 +105,7 @@ void RssArticle::markAsRead() {
m_read = true;
}
QString RssArticle::guid() const
const QString& RssArticle::guid() const
{
return m_guid;
}