File indexing completed on 2024-05-05 05:13:01
0001 /* 0002 This file is part of Akregator. 0003 0004 SPDX-FileCopyrightText: 2004 Frank Osterfeld <osterfeld@kde.org> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0 0007 */ 0008 0009 #include "feedlist.h" 0010 #include "article.h" 0011 #include "feed.h" 0012 #include "folder.h" 0013 #include "storage/storage.h" 0014 #include "treenode.h" 0015 #include "treenodevisitor.h" 0016 0017 #include "akregator_debug.h" 0018 #include "kernel.h" 0019 #include "subscriptionlistjobs.h" 0020 #include <KLocalizedString> 0021 #include <limits> 0022 0023 #include <QElapsedTimer> 0024 #include <QHash> 0025 #include <QRandomGenerator> 0026 #include <QSet> 0027 #include <qdom.h> 0028 0029 using namespace Akregator; 0030 class Akregator::FeedListPrivate 0031 { 0032 FeedList *const q; 0033 0034 public: 0035 FeedListPrivate(Backend::Storage *st, FeedList *qq); 0036 0037 Akregator::Backend::Storage *storage; 0038 QList<TreeNode *> flatList; 0039 Folder *rootNode; 0040 QHash<uint, TreeNode *> idMap; 0041 FeedList::AddNodeVisitor *addNodeVisitor; 0042 FeedList::RemoveNodeVisitor *removeNodeVisitor; 0043 QHash<QString, QList<Feed *>> urlMap; 0044 mutable int unreadCache; 0045 }; 0046 0047 class FeedList::AddNodeVisitor : public TreeNodeVisitor 0048 { 0049 public: 0050 AddNodeVisitor(FeedList *list) 0051 : m_list(list) 0052 { 0053 } 0054 0055 bool visitFeed(Feed *node) override 0056 { 0057 m_list->d->idMap.insert(node->id(), node); 0058 m_list->d->flatList.append(node); 0059 m_list->d->urlMap[node->xmlUrl()].append(node); 0060 connect(node, &Feed::fetchStarted, m_list, &FeedList::fetchStarted); 0061 connect(node, &Feed::fetched, m_list, &FeedList::fetched); 0062 connect(node, &Feed::fetchAborted, m_list, &FeedList::fetchAborted); 0063 connect(node, &Feed::fetchError, m_list, &FeedList::fetchError); 0064 connect(node, &Feed::fetchDiscovery, m_list, &FeedList::fetchDiscovery); 0065 0066 visitTreeNode(node); 0067 return true; 0068 } 0069 0070 void visit2(TreeNode *node, bool preserveID) 0071 { 0072 m_preserveID = preserveID; 0073 TreeNodeVisitor::visit(node); 0074 } 0075 0076 bool visitTreeNode(TreeNode *node) override 0077 { 0078 if (!m_preserveID) { 0079 node->setId(m_list->generateID()); 0080 } 0081 m_list->d->idMap[node->id()] = node; 0082 m_list->d->flatList.append(node); 0083 0084 connect(node, &TreeNode::signalDestroyed, m_list, &FeedList::slotNodeDestroyed); 0085 connect(node, &TreeNode::signalChanged, m_list, &FeedList::signalNodeChanged); 0086 Q_EMIT m_list->signalNodeAdded(node); 0087 0088 return true; 0089 } 0090 0091 bool visitFolder(Folder *node) override 0092 { 0093 connect(node, &Folder::signalChildAdded, m_list, &FeedList::slotNodeAdded); 0094 connect(node, &Folder::signalAboutToRemoveChild, m_list, &FeedList::signalAboutToRemoveNode); 0095 connect(node, &Folder::signalChildRemoved, m_list, &FeedList::slotNodeRemoved); 0096 0097 visitTreeNode(node); 0098 0099 for (TreeNode *i = node->firstChild(); i && i != node; i = i->next()) { 0100 m_list->slotNodeAdded(i); 0101 } 0102 0103 return true; 0104 } 0105 0106 private: 0107 FeedList *m_list = nullptr; 0108 bool m_preserveID = false; 0109 }; 0110 0111 class FeedList::RemoveNodeVisitor : public TreeNodeVisitor 0112 { 0113 public: 0114 RemoveNodeVisitor(FeedList *list) 0115 : m_list(list) 0116 { 0117 } 0118 0119 bool visitFeed(Feed *node) override 0120 { 0121 visitTreeNode(node); 0122 m_list->d->urlMap[node->xmlUrl()].removeAll(node); 0123 return true; 0124 } 0125 0126 bool visitTreeNode(TreeNode *node) override 0127 { 0128 m_list->d->idMap.remove(node->id()); 0129 m_list->d->flatList.removeAll(node); 0130 m_list->disconnect(node); 0131 return true; 0132 } 0133 0134 bool visitFolder(Folder *node) override 0135 { 0136 visitTreeNode(node); 0137 0138 return true; 0139 } 0140 0141 private: 0142 FeedList *m_list; 0143 }; 0144 0145 FeedListPrivate::FeedListPrivate(Backend::Storage *st, FeedList *qq) 0146 : q(qq) 0147 , storage(st) 0148 , rootNode(nullptr) 0149 , addNodeVisitor(new FeedList::AddNodeVisitor(q)) 0150 , removeNodeVisitor(new FeedList::RemoveNodeVisitor(q)) 0151 , unreadCache(-1) 0152 { 0153 Q_ASSERT(storage); 0154 } 0155 0156 FeedList::FeedList(Backend::Storage *storage) 0157 : QObject(nullptr) 0158 , d(new FeedListPrivate(storage, this)) 0159 { 0160 auto rootNode = new Folder(i18n("All Feeds")); 0161 rootNode->setId(1); 0162 setRootNode(rootNode); 0163 addNode(rootNode, true); 0164 } 0165 0166 QList<uint> FeedList::feedIds() const 0167 { 0168 QList<uint> ids; 0169 const auto f = feeds(); 0170 for (const Feed *const i : f) { 0171 ids += i->id(); 0172 } 0173 return ids; 0174 } 0175 0176 QList<const Akregator::Feed *> FeedList::feeds() const 0177 { 0178 QList<const Akregator::Feed *> constList; 0179 const auto rootNodeFeeds = d->rootNode->feeds(); 0180 for (const Akregator::Feed *const i : rootNodeFeeds) { 0181 constList.append(i); 0182 } 0183 return constList; 0184 } 0185 0186 QList<Akregator::Feed *> FeedList::feeds() 0187 { 0188 return d->rootNode->feeds(); 0189 } 0190 0191 QList<const Folder *> FeedList::folders() const 0192 { 0193 QList<const Folder *> constList; 0194 const auto nodeFolders = d->rootNode->folders(); 0195 for (const Folder *const i : nodeFolders) { 0196 constList.append(i); 0197 } 0198 return constList; 0199 } 0200 0201 QList<Folder *> FeedList::folders() 0202 { 0203 return d->rootNode->folders(); 0204 } 0205 0206 void FeedList::addNode(TreeNode *node, bool preserveID) 0207 { 0208 d->addNodeVisitor->visit2(node, preserveID); 0209 } 0210 0211 void FeedList::removeNode(TreeNode *node) 0212 { 0213 d->removeNodeVisitor->visit(node); 0214 } 0215 0216 void FeedList::parseChildNodes(QDomNode &node, Folder *parent) 0217 { 0218 QDomElement e = node.toElement(); // try to convert the node to an element. 0219 0220 if (!e.isNull()) { 0221 // QString title = e.hasAttribute("text") ? e.attribute("text") : e.attribute("title"); 0222 0223 if (e.hasAttribute(QStringLiteral("xmlUrl")) || e.hasAttribute(QStringLiteral("xmlurl")) || e.hasAttribute(QStringLiteral("xmlURL"))) { 0224 Feed *feed = Feed::fromOPML(e, d->storage); 0225 if (feed) { 0226 if (!d->urlMap[feed->xmlUrl()].contains(feed)) { 0227 d->urlMap[feed->xmlUrl()].append(feed); 0228 } 0229 parent->appendChild(feed); 0230 } 0231 } else { 0232 Folder *fg = Folder::fromOPML(e); 0233 parent->appendChild(fg); 0234 0235 if (e.hasChildNodes()) { 0236 QDomNode child = e.firstChild(); 0237 while (!child.isNull()) { 0238 parseChildNodes(child, fg); 0239 child = child.nextSibling(); 0240 } 0241 } 0242 } 0243 } 0244 } 0245 0246 bool FeedList::readFromOpml(const QDomDocument &doc) 0247 { 0248 QDomElement root = doc.documentElement(); 0249 0250 qCDebug(AKREGATOR_LOG) << "loading OPML feed" << root.tagName().toLower(); 0251 0252 qCDebug(AKREGATOR_LOG) << "measuring startup time: START"; 0253 QElapsedTimer spent; 0254 spent.start(); 0255 0256 if (root.tagName().toLower() != QLatin1StringView("opml")) { 0257 return false; 0258 } 0259 QDomNode bodyNode = root.firstChild(); 0260 0261 while (!bodyNode.isNull() && bodyNode.toElement().tagName().toLower() != QLatin1StringView("body")) { 0262 bodyNode = bodyNode.nextSibling(); 0263 } 0264 0265 if (bodyNode.isNull()) { 0266 qCDebug(AKREGATOR_LOG) << "Failed to acquire body node, markup broken?"; 0267 return false; 0268 } 0269 0270 QDomElement body = bodyNode.toElement(); 0271 0272 QDomNode i = body.firstChild(); 0273 0274 while (!i.isNull()) { 0275 parseChildNodes(i, allFeedsFolder()); 0276 i = i.nextSibling(); 0277 } 0278 0279 for (TreeNode *i = allFeedsFolder()->firstChild(); i && i != allFeedsFolder(); i = i->next()) { 0280 if (i->id() == 0) { 0281 uint id = generateID(); 0282 i->setId(id); 0283 d->idMap.insert(id, i); 0284 } 0285 } 0286 0287 qCDebug(AKREGATOR_LOG) << "measuring startup time: STOP," << spent.elapsed() << "ms"; 0288 qCDebug(AKREGATOR_LOG) << "Number of articles loaded:" << allFeedsFolder()->totalCount(); 0289 return true; 0290 } 0291 0292 FeedList::~FeedList() 0293 { 0294 Q_EMIT signalDestroyed(this); 0295 setRootNode(nullptr); 0296 delete d->addNodeVisitor; 0297 delete d->removeNodeVisitor; 0298 } 0299 0300 const Akregator::Feed *FeedList::findByURL(const QString &feedURL) const 0301 { 0302 if (!d->urlMap.contains(feedURL)) { 0303 return nullptr; 0304 } 0305 const QList<Feed *> &v = d->urlMap[feedURL]; 0306 return !v.isEmpty() ? v.front() : nullptr; 0307 } 0308 0309 Akregator::Feed *FeedList::findByURL(const QString &feedURL) 0310 { 0311 if (!d->urlMap.contains(feedURL)) { 0312 return nullptr; 0313 } 0314 const QList<Akregator::Feed *> &v = d->urlMap[feedURL]; 0315 return !v.isEmpty() ? v.front() : nullptr; 0316 } 0317 0318 const Article FeedList::findArticle(const QString &feedURL, const QString &guid) const 0319 { 0320 const Akregator::Feed *feed = findByURL(feedURL); 0321 return feed ? feed->findArticle(guid) : Article(); 0322 } 0323 0324 void FeedList::append(FeedList *list, Folder *parent, TreeNode *after) 0325 { 0326 if (list == this) { 0327 return; 0328 } 0329 0330 if (!d->flatList.contains(parent)) { 0331 parent = allFeedsFolder(); 0332 } 0333 0334 QList<TreeNode *> children = list->allFeedsFolder()->children(); 0335 0336 QList<TreeNode *>::ConstIterator end(children.constEnd()); 0337 for (QList<TreeNode *>::ConstIterator it = children.constBegin(); it != end; ++it) { 0338 list->allFeedsFolder()->removeChild(*it); 0339 parent->insertChild(*it, after); 0340 after = *it; 0341 } 0342 } 0343 0344 QDomDocument FeedList::toOpml() const 0345 { 0346 QDomDocument doc; 0347 doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\""))); 0348 0349 QDomElement root = doc.createElement(QStringLiteral("opml")); 0350 root.setAttribute(QStringLiteral("version"), QStringLiteral("1.0")); 0351 doc.appendChild(root); 0352 0353 QDomElement head = doc.createElement(QStringLiteral("head")); 0354 root.appendChild(head); 0355 0356 QDomElement ti = doc.createElement(QStringLiteral("text")); 0357 head.appendChild(ti); 0358 0359 QDomElement body = doc.createElement(QStringLiteral("body")); 0360 root.appendChild(body); 0361 0362 const auto children = allFeedsFolder()->children(); 0363 for (const TreeNode *const i : children) { 0364 body.appendChild(i->toOPML(body, doc)); 0365 } 0366 0367 return doc; 0368 } 0369 0370 const TreeNode *FeedList::findByID(uint id) const 0371 { 0372 return d->idMap[id]; 0373 } 0374 0375 TreeNode *FeedList::findByID(uint id) 0376 { 0377 return d->idMap[id]; 0378 } 0379 0380 QList<const TreeNode *> FeedList::findByTitle(const QString &title) const 0381 { 0382 return allFeedsFolder()->namedChildren(title); 0383 } 0384 0385 QList<TreeNode *> FeedList::findByTitle(const QString &title) 0386 { 0387 return allFeedsFolder()->namedChildren(title); 0388 } 0389 0390 const Folder *FeedList::allFeedsFolder() const 0391 { 0392 return d->rootNode; 0393 } 0394 0395 Folder *FeedList::allFeedsFolder() 0396 { 0397 return d->rootNode; 0398 } 0399 0400 bool FeedList::isEmpty() const 0401 { 0402 return d->rootNode->firstChild() == nullptr; 0403 } 0404 0405 void FeedList::rootNodeChanged() 0406 { 0407 Q_ASSERT(d->rootNode); 0408 const int newUnread = d->rootNode->unread(); 0409 if (newUnread == d->unreadCache) { 0410 return; 0411 } 0412 d->unreadCache = newUnread; 0413 Q_EMIT unreadCountChanged(newUnread); 0414 } 0415 0416 void FeedList::setRootNode(Folder *folder) 0417 { 0418 if (folder == d->rootNode) { 0419 return; 0420 } 0421 0422 delete d->rootNode; 0423 d->rootNode = folder; 0424 d->unreadCache = -1; 0425 0426 if (d->rootNode) { 0427 d->rootNode->setOpen(true); 0428 connect(d->rootNode, &Folder::signalChildAdded, this, &FeedList::slotNodeAdded); 0429 connect(d->rootNode, &Folder::signalAboutToRemoveChild, this, &FeedList::signalAboutToRemoveNode); 0430 connect(d->rootNode, &Folder::signalChildRemoved, this, &FeedList::slotNodeRemoved); 0431 connect(d->rootNode, &Folder::signalChanged, this, &FeedList::signalNodeChanged); 0432 connect(d->rootNode, &Folder::signalChanged, this, &FeedList::rootNodeChanged); 0433 } 0434 } 0435 0436 uint FeedList::generateID() const 0437 { 0438 // The values 0 and 1 are reserved, see TreeNode::id() 0439 return QRandomGenerator::global()->bounded(2u, std::numeric_limits<quint32>::max()); 0440 } 0441 0442 void FeedList::slotNodeAdded(TreeNode *node) 0443 { 0444 if (!node) { 0445 return; 0446 } 0447 0448 Folder *parent = node->parent(); 0449 if (!parent || !d->flatList.contains(parent) || d->flatList.contains(node)) { 0450 return; 0451 } 0452 0453 addNode(node, false); 0454 } 0455 0456 void FeedList::slotNodeDestroyed(TreeNode *node) 0457 { 0458 if (!node || !d->flatList.contains(node)) { 0459 return; 0460 } 0461 removeNode(node); 0462 } 0463 0464 void FeedList::slotNodeRemoved(Folder * /*parent*/, TreeNode *node) 0465 { 0466 if (!node || !d->flatList.contains(node)) { 0467 return; 0468 } 0469 removeNode(node); 0470 Q_EMIT signalNodeRemoved(node); 0471 } 0472 0473 int FeedList::unread() const 0474 { 0475 if (d->unreadCache == -1) { 0476 d->unreadCache = d->rootNode ? d->rootNode->unread() : 0; 0477 } 0478 return d->unreadCache; 0479 } 0480 0481 void FeedList::addToFetchQueue(FetchQueue *qu, bool intervalOnly) 0482 { 0483 if (d->rootNode) { 0484 d->rootNode->slotAddToFetchQueue(qu, intervalOnly); 0485 } 0486 } 0487 0488 KJob *FeedList::createMarkAsReadJob() 0489 { 0490 return d->rootNode ? d->rootNode->createMarkAsReadJob() : nullptr; 0491 } 0492 0493 FeedListManagementImpl::FeedListManagementImpl(const QSharedPointer<FeedList> &list) 0494 : m_feedList(list) 0495 { 0496 } 0497 0498 void FeedListManagementImpl::setFeedList(const QSharedPointer<FeedList> &list) 0499 { 0500 m_feedList = list; 0501 } 0502 0503 static QString path_of_folder(const Folder *fol) 0504 { 0505 Q_ASSERT(fol); 0506 QString path; 0507 const Folder *i = fol; 0508 while (i) { 0509 path = QString::number(i->id()) + QLatin1Char('/') + path; 0510 i = i->parent(); 0511 } 0512 return path; 0513 } 0514 0515 QStringList FeedListManagementImpl::categories() const 0516 { 0517 if (!m_feedList) { 0518 return {}; 0519 } 0520 QStringList cats; 0521 const auto folders = m_feedList->folders(); 0522 for (const Folder *const i : folders) { 0523 cats.append(path_of_folder(i)); 0524 } 0525 return cats; 0526 } 0527 0528 QStringList FeedListManagementImpl::feeds(const QString &catId) const 0529 { 0530 if (!m_feedList) { 0531 return {}; 0532 } 0533 0534 const uint lastcatid = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt(); 0535 0536 QSet<QString> urls; 0537 const auto feeds = m_feedList->feeds(); 0538 for (const Feed *const i : feeds) { 0539 if (lastcatid == i->parent()->id()) { 0540 urls.insert(i->xmlUrl()); 0541 } 0542 } 0543 return urls.values(); 0544 } 0545 0546 void FeedListManagementImpl::addFeed(const QString &url, const QString &catId) 0547 { 0548 if (!m_feedList) { 0549 return; 0550 } 0551 0552 qCDebug(AKREGATOR_LOG) << "Name:" << url.left(20) << "Cat:" << catId; 0553 const uint folder_id = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt(); 0554 0555 // Get the folder 0556 Folder *m_folder = nullptr; 0557 const QList<Folder *> vector = m_feedList->folders(); 0558 for (int i = 0; i < vector.size(); ++i) { 0559 if (vector.at(i)->id() == folder_id) { 0560 m_folder = vector.at(i); 0561 break; 0562 } 0563 } 0564 0565 // Create new feed 0566 QScopedPointer<FeedList> new_feedlist(new FeedList(Kernel::self()->storage())); 0567 Feed *new_feed = new Feed(Kernel::self()->storage()); 0568 new_feed->setXmlUrl(url); 0569 // new_feed->setTitle(url); 0570 new_feedlist->allFeedsFolder()->appendChild(new_feed); 0571 0572 // Get last in the folder 0573 TreeNode *m_last = m_folder->childAt(m_folder->totalCount()); 0574 0575 // Add the feed 0576 m_feedList->append(new_feedlist.data(), m_folder, m_last); 0577 } 0578 0579 void FeedListManagementImpl::removeFeed(const QString &url, const QString &catId) 0580 { 0581 qCDebug(AKREGATOR_LOG) << "Name:" << url.left(20) << "Cat:" << catId; 0582 0583 uint lastcatid = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt(); 0584 0585 const auto feeds = m_feedList->feeds(); 0586 for (const Feed *const i : feeds) { 0587 if (lastcatid == i->parent()->id()) { 0588 if (i->xmlUrl().compare(url) == 0) { 0589 qCDebug(AKREGATOR_LOG) << "id:" << i->id(); 0590 auto job = new DeleteSubscriptionJob; 0591 job->setSubscriptionId(i->id()); 0592 job->start(); 0593 } 0594 } 0595 } 0596 } 0597 0598 QString FeedListManagementImpl::getCategoryName(const QString &catId) const 0599 { 0600 QString catname; 0601 0602 if (!m_feedList) { 0603 return catname; 0604 } 0605 0606 const QStringList list = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts); 0607 for (int i = 0; i < list.size(); ++i) { 0608 int index = list.at(i).toInt(); 0609 catname += m_feedList->findByID(index)->title() + QLatin1Char('/'); 0610 } 0611 0612 return catname; 0613 } 0614 0615 #include "moc_feedlist.cpp"