File indexing completed on 2024-05-12 16:23:40
0001 /** 0002 * SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include <KLocalizedString> 0008 #include <QCryptographicHash> 0009 #include <QDateTime> 0010 #include <QDebug> 0011 #include <QFile> 0012 #include <QFileInfo> 0013 #include <QNetworkReply> 0014 #include <QStandardPaths> 0015 #include <QTextDocumentFragment> 0016 #include <Syndication/Syndication> 0017 0018 #include "database.h" 0019 #include "fetcher.h" 0020 0021 Fetcher::Fetcher() 0022 : m_fetchCount(0) 0023 { 0024 manager = new QNetworkAccessManager(this); 0025 manager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); 0026 manager->setStrictTransportSecurityEnabled(true); 0027 manager->enableStrictTransportSecurityStore(true); 0028 } 0029 0030 void Fetcher::fetch(const QString &url, const bool markEntriesRead) 0031 { 0032 qDebug() << "Starting to fetch" << url; 0033 0034 Q_EMIT startedFetchingFeed(url); 0035 setFetchCount(m_fetchCount + 1); 0036 0037 QNetworkRequest request((QUrl(url))); 0038 QNetworkReply *reply = get(request); 0039 connect(reply, &QNetworkReply::finished, this, [this, url, reply, markEntriesRead]() { 0040 setFetchCount(m_fetchCount - 1); 0041 if (reply->error()) { 0042 qWarning() << "Error fetching feed"; 0043 qWarning() << reply->errorString(); 0044 Q_EMIT error(url, reply->error(), reply->errorString()); 0045 } else { 0046 QByteArray data = reply->readAll(); 0047 Syndication::DocumentSource document(data, url); 0048 Syndication::FeedPtr feed = Syndication::parserCollection()->parse(document, QStringLiteral("Atom")); 0049 processFeed(feed, url, markEntriesRead); 0050 } 0051 delete reply; 0052 }); 0053 } 0054 0055 void Fetcher::fetchAll() 0056 { 0057 QSqlQuery query; 0058 if (query.prepare(QStringLiteral("SELECT url FROM Feeds;"))) { 0059 Database::instance().execute(query); 0060 while (query.next()) { 0061 fetch(query.value(0).toString()); 0062 } 0063 } 0064 } 0065 0066 void Fetcher::setFetchCount(int count) 0067 { 0068 m_fetchCount = count; 0069 Q_EMIT refreshingChanged(refreshing()); 0070 } 0071 0072 void Fetcher::processFeed(Syndication::FeedPtr feed, const QString &url, const bool markEntriesRead) 0073 { 0074 if (feed.isNull()) { 0075 Syndication::ErrorCode errorCode = Syndication::parserCollection()->lastError(); 0076 QString errorString = syndicationErrorToString(errorCode); 0077 Q_EMIT error(url, errorCode, errorString); 0078 return; 0079 } 0080 0081 QSqlQuery query; 0082 if (query.prepare( 0083 QStringLiteral("UPDATE Feeds SET name=:name, image=:image, link=:link, description=:description, lastUpdated=:lastUpdated WHERE url=:url;"))) { 0084 query.bindValue(QStringLiteral(":name"), feed->title()); 0085 query.bindValue(QStringLiteral(":url"), url); 0086 query.bindValue(QStringLiteral(":link"), feed->link()); 0087 query.bindValue(QStringLiteral(":description"), feed->description()); 0088 0089 QDateTime current = QDateTime::currentDateTime(); 0090 query.bindValue(QStringLiteral(":lastUpdated"), current.toSecsSinceEpoch()); 0091 0092 for (auto &author : feed->authors()) { 0093 processAuthor(author, QLatin1String(""), url); 0094 } 0095 0096 QString imagePath; 0097 if (feed->image()->url().startsWith(QStringLiteral("/"))) { 0098 imagePath = QUrl(url).adjusted(QUrl::RemovePath).toString() + feed->image()->url(); 0099 } else { 0100 imagePath = feed->image()->url(); 0101 } 0102 query.bindValue(QStringLiteral(":image"), imagePath); 0103 Database::instance().execute(query); 0104 0105 qDebug() << "Updated feed title:" << feed->title(); 0106 0107 Q_EMIT feedDetailsUpdated(url, feed->title(), imagePath, feed->link(), feed->description(), current); 0108 } 0109 0110 for (const auto &entry : feed->items()) { 0111 processEntry(entry, url, markEntriesRead); 0112 } 0113 0114 Q_EMIT feedUpdated(url); 0115 } 0116 0117 void Fetcher::processEntry(Syndication::ItemPtr entry, const QString &url, const bool markEntriesRead) 0118 { 0119 qDebug() << "Processing" << entry->title(); 0120 QSqlQuery query; 0121 if (query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries WHERE id=:id;"))) { 0122 query.bindValue(QStringLiteral(":id"), entry->id()); 0123 Database::instance().execute(query); 0124 query.next(); 0125 0126 if (query.value(0).toInt() != 0) { 0127 return; 0128 } 0129 } 0130 0131 if (query.prepare(QStringLiteral("INSERT INTO Entries VALUES (:feed, :id, :title, :content, :created, :updated, :link, :read);"))) { 0132 query.bindValue(QStringLiteral(":feed"), url); 0133 query.bindValue(QStringLiteral(":id"), entry->id()); 0134 query.bindValue(QStringLiteral(":title"), QTextDocumentFragment::fromHtml(entry->title()).toPlainText()); 0135 query.bindValue(QStringLiteral(":created"), static_cast<int>(entry->datePublished())); 0136 query.bindValue(QStringLiteral(":updated"), static_cast<int>(entry->dateUpdated())); 0137 query.bindValue(QStringLiteral(":link"), entry->link()); 0138 query.bindValue(QStringLiteral(":read"), markEntriesRead); 0139 0140 if (!entry->content().isEmpty()) { 0141 query.bindValue(QStringLiteral(":content"), entry->content()); 0142 } else { 0143 query.bindValue(QStringLiteral(":content"), entry->description()); 0144 } 0145 0146 Database::instance().execute(query); 0147 } 0148 0149 for (const auto &author : entry->authors()) { 0150 processAuthor(author, entry->id(), url); 0151 } 0152 0153 for (const auto &enclosure : entry->enclosures()) { 0154 processEnclosure(enclosure, entry, url); 0155 } 0156 } 0157 0158 void Fetcher::processAuthor(Syndication::PersonPtr author, const QString &entryId, const QString &url) 0159 { 0160 QSqlQuery query; 0161 if (query.prepare(QStringLiteral("INSERT INTO Authors VALUES(:feed, :id, :name, :uri, :email);"))) { 0162 query.bindValue(QStringLiteral(":feed"), url); 0163 query.bindValue(QStringLiteral(":id"), entryId); 0164 query.bindValue(QStringLiteral(":name"), author->name()); 0165 query.bindValue(QStringLiteral(":uri"), author->uri()); 0166 query.bindValue(QStringLiteral(":email"), author->email()); 0167 Database::instance().execute(query); 0168 } 0169 } 0170 0171 void Fetcher::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry, const QString &feedUrl) 0172 { 0173 QSqlQuery query; 0174 if (query.prepare(QStringLiteral("INSERT INTO Enclosures VALUES (:feed, :id, :duration, :size, :title, :type, :url);"))) { 0175 query.bindValue(QStringLiteral(":feed"), feedUrl); 0176 query.bindValue(QStringLiteral(":id"), entry->id()); 0177 query.bindValue(QStringLiteral(":duration"), enclosure->duration()); 0178 query.bindValue(QStringLiteral(":size"), enclosure->length()); 0179 query.bindValue(QStringLiteral(":title"), enclosure->title()); 0180 query.bindValue(QStringLiteral(":type"), enclosure->type()); 0181 query.bindValue(QStringLiteral(":url"), enclosure->url()); 0182 Database::instance().execute(query); 0183 } 0184 } 0185 0186 QString Fetcher::image(const QString &url) 0187 { 0188 QString path = filePath(url); 0189 if (QFileInfo::exists(path)) { 0190 return path; 0191 } 0192 0193 download(url); 0194 0195 return QLatin1String(""); 0196 } 0197 0198 void Fetcher::download(const QString &url) 0199 { 0200 QNetworkRequest request((QUrl(url))); 0201 QNetworkReply *reply = get(request); 0202 connect(reply, &QNetworkReply::finished, this, [this, url, reply]() { 0203 QByteArray data = reply->readAll(); 0204 QFile file(filePath(url)); 0205 file.open(QIODevice::WriteOnly); 0206 file.write(data); 0207 file.close(); 0208 0209 Q_EMIT imageDownloadFinished(url); 0210 delete reply; 0211 }); 0212 } 0213 0214 void Fetcher::removeImage(const QString &url) 0215 { 0216 qDebug() << filePath(url); 0217 QFile(filePath(url)).remove(); 0218 } 0219 0220 QString Fetcher::filePath(const QString &url) 0221 { 0222 return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/") 0223 + QString::fromStdString(QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5).toHex().toStdString()); 0224 } 0225 0226 QNetworkReply *Fetcher::get(QNetworkRequest &request) 0227 { 0228 request.setRawHeader("User-Agent", "Alligator/0.1; Syndication"); 0229 return manager->get(request); 0230 } 0231 0232 QString Fetcher::syndicationErrorToString(Syndication::ErrorCode errorCode) 0233 { 0234 switch (errorCode) { 0235 case Syndication::InvalidXml: 0236 return i18n("Invalid XML"); 0237 case Syndication::XmlNotAccepted: 0238 return i18n("No parser accepted the XML"); 0239 default: 0240 return i18n("Error while parsing feed"); 0241 } 0242 }