File indexing completed on 2024-05-12 16:21:28
0001 /** 0002 * SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "datamanager.h" 0008 #include "datamanagerlogging.h" 0009 0010 #include <QDateTime> 0011 #include <QDir> 0012 #include <QSqlDatabase> 0013 #include <QSqlError> 0014 #include <QStandardPaths> 0015 #include <QUrl> 0016 #include <QXmlStreamReader> 0017 #include <QXmlStreamWriter> 0018 0019 #include "audiomanager.h" 0020 #include "database.h" 0021 #include "entry.h" 0022 #include "feed.h" 0023 #include "fetcher.h" 0024 #include "settingsmanager.h" 0025 #include "storagemanager.h" 0026 #include "sync/sync.h" 0027 0028 DataManager::DataManager() 0029 { 0030 connect(&Fetcher::instance(), 0031 &Fetcher::feedDetailsUpdated, 0032 this, 0033 [this](const QString &url, 0034 const QString &name, 0035 const QString &image, 0036 const QString &link, 0037 const QString &description, 0038 const QDateTime &lastUpdated, 0039 const QString &dirname) { 0040 qCDebug(kastsDataManager) << "Start updating feed details for" << url; 0041 Feed *feed = getFeed(url); 0042 if (feed != nullptr) { 0043 feed->setName(name); 0044 feed->setImage(image); 0045 feed->setLink(link); 0046 feed->setDescription(description); 0047 feed->setLastUpdated(lastUpdated); 0048 feed->setDirname(dirname); 0049 qCDebug(kastsDataManager) << "Retrieving authors"; 0050 feed->updateAuthors(); 0051 // For feeds that have just been added, this is probably the point 0052 // where the Feed object gets created; let's set refreshing to 0053 // true in order to show user feedback that the feed is still 0054 // being fetched 0055 feed->setRefreshing(true); 0056 } 0057 }); 0058 connect(&Fetcher::instance(), &Fetcher::entryAdded, this, [this](const QString &feedurl, const QString &id) { 0059 Q_UNUSED(feedurl) 0060 // Only add the new entry to m_entries 0061 // we will repopulate m_entrymap once all new entries have been added, 0062 // such that m_entrymap will show all new entries in the correct order 0063 m_entries[id] = nullptr; 0064 }); 0065 connect(&Fetcher::instance(), &Fetcher::feedUpdated, this, [this](const QString &feedurl) { 0066 // Update m_entrymap for feedurl, such that the new and old entries show 0067 // up in the correct order 0068 // TODO: put this code into a separate method and re-use this in the constructor 0069 QSqlQuery query; 0070 m_entrymap[feedurl].clear(); 0071 query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed ORDER BY updated DESC;")); 0072 query.bindValue(QStringLiteral(":feed"), feedurl); 0073 Database::instance().execute(query); 0074 while (query.next()) { 0075 m_entrymap[feedurl] += query.value(QStringLiteral("id")).toString(); 0076 } 0077 0078 // Check for "new" entries 0079 if (SettingsManager::self()->autoQueue()) { 0080 // start an immediate transaction since this non-blocking read query 0081 // can change into a blocking write query if the entry needs to be 0082 // queued; this can create a deadlock with other concurrent write 0083 // operations 0084 Database::instance().transaction(); 0085 query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed AND new=:new ORDER BY updated ASC;")); 0086 query.bindValue(QStringLiteral(":feed"), feedurl); 0087 query.bindValue(QStringLiteral(":new"), true); 0088 Database::instance().execute(query); 0089 while (query.next()) { 0090 QString id = query.value(QStringLiteral("id")).toString(); 0091 getEntry(id)->setQueueStatusInternal(true); 0092 if (SettingsManager::self()->autoDownload()) { 0093 if (getEntry(id) && getEntry(id)->hasEnclosure() && getEntry(id)->enclosure()) { 0094 qCDebug(kastsDataManager) << "Start downloading" << getEntry(id)->title(); 0095 getEntry(id)->enclosure()->download(); 0096 } 0097 } 0098 } 0099 Database::instance().commit(); 0100 } 0101 0102 Q_EMIT feedEntriesUpdated(feedurl); 0103 }); 0104 0105 // Only read unique feedurls and entry ids from the database. 0106 // The feed and entry datastructures will be loaded lazily. 0107 QSqlQuery query; 0108 query.prepare(QStringLiteral("SELECT url FROM Feeds;")); 0109 Database::instance().execute(query); 0110 while (query.next()) { 0111 m_feedmap += query.value(QStringLiteral("url")).toString(); 0112 m_feeds[query.value(QStringLiteral("url")).toString()] = nullptr; 0113 } 0114 0115 for (auto &feedurl : m_feedmap) { 0116 query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed ORDER BY updated DESC;")); 0117 query.bindValue(QStringLiteral(":feed"), feedurl); 0118 Database::instance().execute(query); 0119 while (query.next()) { 0120 m_entrymap[feedurl] += query.value(QStringLiteral("id")).toString(); 0121 m_entries[query.value(QStringLiteral("id")).toString()] = nullptr; 0122 } 0123 } 0124 // qCDebug(kastsDataManager) << "entrymap contains:" << m_entrymap; 0125 0126 query.prepare(QStringLiteral("SELECT id FROM Queue ORDER BY listnr;")); 0127 Database::instance().execute(query); 0128 while (query.next()) { 0129 m_queuemap += query.value(QStringLiteral("id")).toString(); 0130 } 0131 qCDebug(kastsDataManager) << "Queuemap contains:" << m_queuemap; 0132 } 0133 0134 Feed *DataManager::getFeed(const int index) const 0135 { 0136 if (index < m_feedmap.size()) { 0137 return getFeed(m_feedmap[index]); 0138 } 0139 return nullptr; 0140 } 0141 0142 Feed *DataManager::getFeed(const QString &feedurl) const 0143 { 0144 if (m_feeds.contains(feedurl)) { 0145 if (m_feeds[feedurl] == nullptr) { 0146 loadFeed(feedurl); 0147 } 0148 return m_feeds[feedurl]; 0149 } 0150 return nullptr; 0151 } 0152 0153 Entry *DataManager::getEntry(const int feed_index, const int entry_index) const 0154 { 0155 if (feed_index < m_feedmap.size() && entry_index < m_entrymap[m_feedmap[feed_index]].size()) { 0156 return getEntry(m_entrymap[m_feedmap[feed_index]][entry_index]); 0157 } 0158 return nullptr; 0159 } 0160 0161 Entry *DataManager::getEntry(const Feed *feed, const int entry_index) const 0162 { 0163 if (feed && entry_index < m_entrymap[feed->url()].size()) { 0164 return getEntry(m_entrymap[feed->url()][entry_index]); 0165 } 0166 return nullptr; 0167 } 0168 0169 Entry *DataManager::getEntry(const QString &id) const 0170 { 0171 if (m_entries.contains(id)) { 0172 if (m_entries[id] == nullptr) 0173 loadEntry(id); 0174 return m_entries[id]; 0175 } 0176 return nullptr; 0177 } 0178 0179 int DataManager::feedCount() const 0180 { 0181 return m_feedmap.count(); 0182 } 0183 0184 QStringList DataManager::getIdList(const Feed *feed) const 0185 { 0186 return m_entrymap[feed->url()]; 0187 } 0188 0189 int DataManager::entryCount(const int feed_index) const 0190 { 0191 return m_entrymap[m_feedmap[feed_index]].count(); 0192 } 0193 0194 int DataManager::entryCount(const Feed *feed) const 0195 { 0196 return m_entrymap[feed->url()].count(); 0197 } 0198 0199 void DataManager::removeFeed(Feed *feed) 0200 { 0201 QList<Feed *> feeds; 0202 feeds << feed; 0203 removeFeeds(feeds); 0204 } 0205 0206 void DataManager::removeFeed(const int index) 0207 { 0208 // Get feed pointer 0209 Feed *feed = getFeed(m_feedmap[index]); 0210 if (feed) { 0211 removeFeed(feed); 0212 } 0213 } 0214 0215 void DataManager::removeFeeds(const QStringList &feedurls) 0216 { 0217 QList<Feed *> feeds; 0218 for (QString feedurl : feedurls) { 0219 Feed *feed = getFeed(feedurl); 0220 if (feed) { 0221 feeds << feed; 0222 } 0223 } 0224 removeFeeds(feeds); 0225 } 0226 0227 void DataManager::removeFeeds(const QVariantList feedVariantList) 0228 { 0229 QList<Feed *> feeds; 0230 for (QVariant feedVariant : feedVariantList) { 0231 if (feedVariant.canConvert<Feed *>()) { 0232 if (feedVariant.value<Feed *>()) { 0233 feeds << feedVariant.value<Feed *>(); 0234 } 0235 } 0236 } 0237 removeFeeds(feeds); 0238 } 0239 0240 void DataManager::removeFeeds(const QList<Feed *> &feeds) 0241 { 0242 for (Feed *feed : feeds) { 0243 if (feed) { 0244 const QString feedurl = feed->url(); 0245 int index = m_feedmap.indexOf(feedurl); 0246 0247 qCDebug(kastsDataManager) << "deleting feed" << feedurl << "with index" << index; 0248 0249 // Delete the object instances and mappings 0250 // First delete entries in Queue 0251 qCDebug(kastsDataManager) << "delete queueentries of" << feedurl; 0252 QStringList removeFromQueueList; 0253 for (auto &id : m_queuemap) { 0254 if (getEntry(id)->feed()->url() == feedurl) { 0255 if (AudioManager::instance().entry() == getEntry(id)) { 0256 AudioManager::instance().next(); 0257 } 0258 removeFromQueueList += id; 0259 } 0260 } 0261 bulkQueueStatus(false, removeFromQueueList); 0262 0263 // Delete entries themselves 0264 qCDebug(kastsDataManager) << "delete entries of" << feedurl; 0265 for (auto &id : m_entrymap[feedurl]) { 0266 if (getEntry(id)->hasEnclosure()) 0267 getEntry(id)->enclosure()->deleteFile(); // delete enclosure (if it exists) 0268 if (!getEntry(id)->image().isEmpty()) 0269 StorageManager::instance().removeImage(getEntry(id)->image()); // delete entry images 0270 delete m_entries[id]; // delete pointer 0271 m_entries.remove(id); // delete the hash key 0272 } 0273 m_entrymap.remove(feedurl); // remove all the entry mappings belonging to the feed 0274 0275 qCDebug(kastsDataManager) << "Remove feed image" << feed->image() << "for feed" << feedurl; 0276 qCDebug(kastsDataManager) << "Remove feed enclosure download directory" << feed->dirname() << "for feed" << feedurl; 0277 QDir enclosureDir = QDir(StorageManager::instance().enclosureDirPath() + feed->dirname()); 0278 if (!feed->dirname().isEmpty() && enclosureDir.exists()) { 0279 enclosureDir.removeRecursively(); 0280 } 0281 if (!feed->image().isEmpty()) 0282 StorageManager::instance().removeImage(feed->image()); 0283 m_feeds.remove(m_feedmap[index]); // remove from m_feeds 0284 m_feedmap.removeAt(index); // remove from m_feedmap 0285 delete feed; // remove the pointer 0286 0287 // Then delete everything from the database 0288 qCDebug(kastsDataManager) << "delete database part of" << feedurl; 0289 0290 // Delete related Errors 0291 QSqlQuery query; 0292 query.prepare(QStringLiteral("DELETE FROM Errors WHERE url=:url;")); 0293 query.bindValue(QStringLiteral(":url"), feedurl); 0294 Database::instance().execute(query); 0295 0296 // Delete Authors 0297 query.prepare(QStringLiteral("DELETE FROM Authors WHERE feed=:feed;")); 0298 query.bindValue(QStringLiteral(":feed"), feedurl); 0299 Database::instance().execute(query); 0300 0301 // Delete Chapters 0302 query.prepare(QStringLiteral("DELETE FROM Chapters WHERE feed=:feed;")); 0303 query.bindValue(QStringLiteral(":feed"), feedurl); 0304 Database::instance().execute(query); 0305 0306 // Delete Entries 0307 query.prepare(QStringLiteral("DELETE FROM Entries WHERE feed=:feed;")); 0308 query.bindValue(QStringLiteral(":feed"), feedurl); 0309 Database::instance().execute(query); 0310 0311 // Delete Enclosures 0312 query.prepare(QStringLiteral("DELETE FROM Enclosures WHERE feed=:feed;")); 0313 query.bindValue(QStringLiteral(":feed"), feedurl); 0314 Database::instance().execute(query); 0315 0316 // Delete Feed 0317 query.prepare(QStringLiteral("DELETE FROM Feeds WHERE url=:url;")); 0318 query.bindValue(QStringLiteral(":url"), feedurl); 0319 Database::instance().execute(query); 0320 0321 // Save this action to the database (including timestamp) in order to be 0322 // able to sync with remote services 0323 Sync::instance().storeRemoveFeedAction(feedurl); 0324 0325 Q_EMIT feedRemoved(index); 0326 } 0327 } 0328 0329 // if settings allow, then upload these changes immediately to sync server 0330 Sync::instance().doQuickSync(); 0331 } 0332 0333 void DataManager::addFeed(const QString &url) 0334 { 0335 addFeed(url, true); 0336 } 0337 0338 void DataManager::addFeed(const QString &url, const bool fetch) 0339 { 0340 addFeeds(QStringList(url), fetch); 0341 } 0342 0343 void DataManager::addFeeds(const QStringList &urls) 0344 { 0345 addFeeds(urls, true); 0346 } 0347 0348 void DataManager::addFeeds(const QStringList &urls, const bool fetch) 0349 { 0350 // First check if the URLs are not empty 0351 // TODO: Add more checks like checking if URLs exist; however this will mean async... 0352 QStringList newUrls; 0353 for (const QString &url : urls) { 0354 if (!url.trimmed().isEmpty() && !feedExists(url)) { 0355 qCDebug(kastsDataManager) << "Feed already exists or URL is empty" << url.trimmed(); 0356 newUrls << url.trimmed(); 0357 } 0358 } 0359 0360 if (newUrls.count() == 0) 0361 return; 0362 0363 // This method will add the relevant internal data structures, and then add 0364 // a preliminary entry into the database. Those details (as well as entries, 0365 // authors and enclosures) will be updated by calling Fetcher::fetch() which 0366 // will trigger a full update of the feed and all related items. 0367 for (const QString &url : newUrls) { 0368 qCDebug(kastsDataManager) << "Adding new feed:" << url; 0369 0370 QUrl urlFromInput = QUrl::fromUserInput(url); 0371 QSqlQuery query; 0372 query.prepare( 0373 QStringLiteral("INSERT INTO Feeds VALUES (:name, :url, :image, :link, :description, :deleteAfterCount, :deleteAfterType, :subscribed, " 0374 ":lastUpdated, :new, :notify, :dirname);")); 0375 query.bindValue(QStringLiteral(":name"), urlFromInput.toString()); 0376 query.bindValue(QStringLiteral(":url"), urlFromInput.toString()); 0377 query.bindValue(QStringLiteral(":image"), QLatin1String("")); 0378 query.bindValue(QStringLiteral(":link"), QLatin1String("")); 0379 query.bindValue(QStringLiteral(":description"), QLatin1String("")); 0380 query.bindValue(QStringLiteral(":deleteAfterCount"), 0); 0381 query.bindValue(QStringLiteral(":deleteAfterType"), 0); 0382 query.bindValue(QStringLiteral(":subscribed"), QDateTime::currentDateTime().toSecsSinceEpoch()); 0383 query.bindValue(QStringLiteral(":lastUpdated"), 0); 0384 query.bindValue(QStringLiteral(":new"), true); 0385 query.bindValue(QStringLiteral(":notify"), false); 0386 query.bindValue(QStringLiteral(":dirname"), QLatin1String("")); 0387 Database::instance().execute(query); 0388 0389 m_feeds[urlFromInput.toString()] = nullptr; 0390 m_feedmap.append(urlFromInput.toString()); 0391 0392 // Save this action to the database (including timestamp) in order to be 0393 // able to sync with remote services 0394 Sync::instance().storeAddFeedAction(urlFromInput.toString()); 0395 0396 Q_EMIT feedAdded(urlFromInput.toString()); 0397 } 0398 0399 if (fetch) { 0400 Fetcher::instance().fetch(urls); 0401 } 0402 0403 // if settings allow, upload these changes immediately to sync servers 0404 Sync::instance().doQuickSync(); 0405 } 0406 0407 Entry *DataManager::getQueueEntry(int index) const 0408 { 0409 return getEntry(m_queuemap[index]); 0410 } 0411 0412 int DataManager::queueCount() const 0413 { 0414 return m_queuemap.count(); 0415 } 0416 0417 QStringList DataManager::queue() const 0418 { 0419 return m_queuemap; 0420 } 0421 0422 bool DataManager::entryInQueue(const Entry *entry) 0423 { 0424 return entryInQueue(entry->id()); 0425 } 0426 0427 bool DataManager::entryInQueue(const QString &id) const 0428 { 0429 return m_queuemap.contains(id); 0430 } 0431 0432 void DataManager::moveQueueItem(const int from, const int to) 0433 { 0434 // First move the items in the internal data structure 0435 m_queuemap.move(from, to); 0436 0437 // Then make sure that the database Queue table reflects these changes 0438 updateQueueListnrs(); 0439 0440 // Make sure that the QueueModel is aware of the changes so it can update 0441 Q_EMIT queueEntryMoved(from, to); 0442 } 0443 0444 void DataManager::addToQueue(const QString &id) 0445 { 0446 // If item is already in queue, then stop here 0447 if (m_queuemap.contains(id)) 0448 return; 0449 0450 // Add to internal queuemap data structure 0451 m_queuemap += id; 0452 qCDebug(kastsDataManager) << "Queue mapping is now:" << m_queuemap; 0453 0454 // Get index of this entry 0455 const int index = m_queuemap.indexOf(id); // add new entry to end of queue 0456 0457 // Add to Queue database 0458 QSqlQuery query; 0459 query.prepare(QStringLiteral("INSERT INTO Queue VALUES (:index, :feedurl, :id, :playing);")); 0460 query.bindValue(QStringLiteral(":index"), index); 0461 query.bindValue(QStringLiteral(":feedurl"), getEntry(id)->feed()->url()); 0462 query.bindValue(QStringLiteral(":id"), id); 0463 query.bindValue(QStringLiteral(":playing"), false); 0464 Database::instance().execute(query); 0465 0466 // Make sure that the QueueModel is aware of the changes 0467 Q_EMIT queueEntryAdded(index, id); 0468 } 0469 0470 void DataManager::removeFromQueue(const QString &id) 0471 { 0472 if (!entryInQueue(id)) { 0473 return; 0474 } 0475 0476 const int index = m_queuemap.indexOf(id); 0477 qCDebug(kastsDataManager) << "Queuemap is now:" << m_queuemap; 0478 qCDebug(kastsDataManager) << "Queue index of item to be removed" << index; 0479 0480 // Move to next track if it's currently playing 0481 if (AudioManager::instance().entry() == getEntry(id)) { 0482 AudioManager::instance().next(); 0483 } 0484 0485 // Remove the item from the internal data structure 0486 m_queuemap.removeAt(index); 0487 0488 // Then make sure that the database Queue table reflects these changes 0489 QSqlQuery query; 0490 query.prepare(QStringLiteral("DELETE FROM Queue WHERE id=:id;")); 0491 query.bindValue(QStringLiteral(":id"), id); 0492 Database::instance().execute(query); 0493 0494 // Make sure that the QueueModel is aware of the change so it can update 0495 Q_EMIT queueEntryRemoved(index, id); 0496 } 0497 0498 void DataManager::sortQueue(AbstractEpisodeProxyModel::SortType sortType) 0499 { 0500 QString columnName; 0501 QString order; 0502 0503 switch (sortType) { 0504 case AbstractEpisodeProxyModel::SortType::DateAscending: 0505 order = QStringLiteral("ASC"); 0506 columnName = QStringLiteral("updated"); 0507 break; 0508 case AbstractEpisodeProxyModel::SortType::DateDescending: 0509 order = QStringLiteral("DESC"); 0510 columnName = QStringLiteral("updated"); 0511 break; 0512 } 0513 0514 QStringList newQueuemap; 0515 0516 QSqlQuery query; 0517 query.prepare(QStringLiteral("SELECT * FROM Queue INNER JOIN Entries ON Queue.id = Entries.id ORDER BY %1 %2;").arg(columnName, order)); 0518 Database::instance().execute(query); 0519 0520 while (query.next()) { 0521 qCDebug(kastsDataManager) << "new queue order:" << query.value(QStringLiteral("id")).toString(); 0522 newQueuemap += query.value(QStringLiteral("id")).toString(); 0523 } 0524 0525 Database::instance().transaction(); 0526 for (int i = 0; i < m_queuemap.length(); i++) { 0527 query.prepare(QStringLiteral("UPDATE Queue SET listnr=:listnr WHERE id=:id;")); 0528 query.bindValue(QStringLiteral(":id"), newQueuemap[i]); 0529 query.bindValue(QStringLiteral(":listnr"), i); 0530 Database::instance().execute(query); 0531 } 0532 Database::instance().commit(); 0533 0534 m_queuemap.clear(); 0535 m_queuemap = newQueuemap; 0536 0537 Q_EMIT queueSorted(); 0538 } 0539 0540 QString DataManager::lastPlayingEntry() 0541 { 0542 QSqlQuery query; 0543 query.prepare(QStringLiteral("SELECT id FROM Queue WHERE playing=:playing;")); 0544 query.bindValue(QStringLiteral(":playing"), true); 0545 Database::instance().execute(query); 0546 if (!query.next()) 0547 return QStringLiteral("none"); 0548 return query.value(QStringLiteral("id")).toString(); 0549 } 0550 0551 void DataManager::setLastPlayingEntry(const QString &id) 0552 { 0553 QSqlQuery query; 0554 // First set playing to false for all Queue items 0555 query.prepare(QStringLiteral("UPDATE Queue SET playing=:playing;")); 0556 query.bindValue(QStringLiteral(":playing"), false); 0557 Database::instance().execute(query); 0558 // Now set the correct track to playing=true 0559 query.prepare(QStringLiteral("UPDATE Queue SET playing=:playing WHERE id=:id;")); 0560 query.bindValue(QStringLiteral(":playing"), true); 0561 query.bindValue(QStringLiteral(":id"), id); 0562 Database::instance().execute(query); 0563 } 0564 0565 void DataManager::deletePlayedEnclosures() 0566 { 0567 QSqlQuery query; 0568 query.prepare( 0569 QStringLiteral("SELECT * FROM Enclosures INNER JOIN Entries ON Enclosures.id = Entries.id WHERE" 0570 "(downloaded=:downloaded OR downloaded=:partiallydownloaded) AND (read=:read);")); 0571 query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloaded)); 0572 query.bindValue(QStringLiteral(":partiallydownloaded"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded)); 0573 query.bindValue(QStringLiteral(":read"), true); 0574 Database::instance().execute(query); 0575 while (query.next()) { 0576 QString feed = query.value(QStringLiteral("feed")).toString(); 0577 QString id = query.value(QStringLiteral("id")).toString(); 0578 qCDebug(kastsDataManager) << "Found entry which has been downloaded and is marked as played; deleting now:" << id; 0579 Entry *entry = getEntry(id); 0580 if (entry->hasEnclosure()) { 0581 entry->enclosure()->deleteFile(); 0582 } 0583 } 0584 } 0585 0586 void DataManager::importFeeds(const QString &path) 0587 { 0588 QUrl url(path); 0589 QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); 0590 0591 file.open(QIODevice::ReadOnly); 0592 0593 QStringList urls; 0594 QXmlStreamReader xmlReader(&file); 0595 while (!xmlReader.atEnd()) { 0596 xmlReader.readNext(); 0597 if (xmlReader.tokenType() == 4 && xmlReader.attributes().hasAttribute(QStringLiteral("xmlUrl"))) { 0598 urls += xmlReader.attributes().value(QStringLiteral("xmlUrl")).toString(); 0599 } 0600 } 0601 qCDebug(kastsDataManager) << "Start importing urls:" << urls; 0602 addFeeds(urls); 0603 } 0604 0605 void DataManager::exportFeeds(const QString &path) 0606 { 0607 QUrl url(path); 0608 QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); 0609 file.open(QIODevice::WriteOnly); 0610 0611 QXmlStreamWriter xmlWriter(&file); 0612 xmlWriter.setAutoFormatting(true); 0613 xmlWriter.writeStartDocument(QStringLiteral("1.0")); 0614 xmlWriter.writeStartElement(QStringLiteral("opml")); 0615 xmlWriter.writeEmptyElement(QStringLiteral("head")); 0616 xmlWriter.writeStartElement(QStringLiteral("body")); 0617 xmlWriter.writeAttribute(QStringLiteral("version"), QStringLiteral("1.0")); 0618 QSqlQuery query; 0619 query.prepare(QStringLiteral("SELECT url, name FROM Feeds;")); 0620 Database::instance().execute(query); 0621 while (query.next()) { 0622 xmlWriter.writeEmptyElement(QStringLiteral("outline")); 0623 xmlWriter.writeAttribute(QStringLiteral("xmlUrl"), query.value(0).toString()); 0624 xmlWriter.writeAttribute(QStringLiteral("title"), query.value(1).toString()); 0625 } 0626 xmlWriter.writeEndElement(); 0627 xmlWriter.writeEndElement(); 0628 xmlWriter.writeEndDocument(); 0629 } 0630 0631 void DataManager::loadFeed(const QString &feedurl) const 0632 { 0633 QSqlQuery query; 0634 query.prepare(QStringLiteral("SELECT url FROM Feeds WHERE url=:feedurl;")); 0635 query.bindValue(QStringLiteral(":feedurl"), feedurl); 0636 Database::instance().execute(query); 0637 if (!query.next()) { 0638 qWarning() << "Failed to load feed" << feedurl; 0639 } else { 0640 m_feeds[feedurl] = new Feed(feedurl); 0641 } 0642 } 0643 0644 void DataManager::loadEntry(const QString id) const 0645 { 0646 // First find the feed that this entry belongs to 0647 Feed *feed = nullptr; 0648 QHashIterator<QString, QStringList> i(m_entrymap); 0649 while (i.hasNext()) { 0650 i.next(); 0651 if (i.value().contains(id)) 0652 feed = getFeed(i.key()); 0653 } 0654 if (!feed) { 0655 qCDebug(kastsDataManager) << "Failed to find feed belonging to entry" << id; 0656 return; 0657 } 0658 m_entries[id] = new Entry(feed, id); 0659 } 0660 0661 bool DataManager::feedExists(const QString &url) 0662 { 0663 // using cleanUrl to do "fuzzy" check on the podcast URL 0664 QString cleanedUrl = cleanUrl(url); 0665 for (QString listUrl : m_feedmap) { 0666 if (cleanedUrl == cleanUrl(listUrl)) { 0667 return true; 0668 } 0669 } 0670 return false; 0671 } 0672 0673 void DataManager::updateQueueListnrs() const 0674 { 0675 QSqlQuery query; 0676 query.prepare(QStringLiteral("UPDATE Queue SET listnr=:i WHERE id=:id;")); 0677 for (int i = 0; i < m_queuemap.count(); i++) { 0678 query.bindValue(QStringLiteral(":i"), i); 0679 query.bindValue(QStringLiteral(":id"), m_queuemap[i]); 0680 Database::instance().execute(query); 0681 } 0682 } 0683 0684 void DataManager::bulkMarkReadByIndex(bool state, QModelIndexList list) 0685 { 0686 bulkMarkRead(state, getIdsFromModelIndexList(list)); 0687 } 0688 0689 void DataManager::bulkMarkRead(bool state, QStringList list) 0690 { 0691 Database::instance().transaction(); 0692 0693 if (state) { // Mark as read 0694 // This needs special attention as the DB operations are very intensive. 0695 // Reversing the loop is much faster 0696 for (int i = list.count() - 1; i >= 0; i--) { 0697 getEntry(list[i])->setReadInternal(state); 0698 } 0699 updateQueueListnrs(); // update queue after modification 0700 } else { // Mark as unread 0701 for (QString id : list) { 0702 getEntry(id)->setReadInternal(state); 0703 } 0704 } 0705 Database::instance().commit(); 0706 0707 Q_EMIT bulkReadStatusActionFinished(); 0708 0709 // if settings allow, upload these changes immediately to sync servers 0710 if (state) { 0711 Sync::instance().doQuickSync(); 0712 } 0713 } 0714 0715 void DataManager::bulkMarkNewByIndex(bool state, QModelIndexList list) 0716 { 0717 bulkMarkNew(state, getIdsFromModelIndexList(list)); 0718 } 0719 0720 void DataManager::bulkMarkNew(bool state, QStringList list) 0721 { 0722 Database::instance().transaction(); 0723 for (QString id : list) { 0724 getEntry(id)->setNewInternal(state); 0725 } 0726 Database::instance().commit(); 0727 0728 Q_EMIT bulkNewStatusActionFinished(); 0729 } 0730 0731 void DataManager::bulkMarkFavoriteByIndex(bool state, QModelIndexList list) 0732 { 0733 bulkMarkFavorite(state, getIdsFromModelIndexList(list)); 0734 } 0735 0736 void DataManager::bulkMarkFavorite(bool state, QStringList list) 0737 { 0738 Database::instance().transaction(); 0739 for (QString id : list) { 0740 getEntry(id)->setFavoriteInternal(state); 0741 } 0742 Database::instance().commit(); 0743 0744 Q_EMIT bulkFavoriteStatusActionFinished(); 0745 } 0746 0747 void DataManager::bulkQueueStatusByIndex(bool state, QModelIndexList list) 0748 { 0749 bulkQueueStatus(state, getIdsFromModelIndexList(list)); 0750 } 0751 0752 void DataManager::bulkQueueStatus(bool state, QStringList list) 0753 { 0754 Database::instance().transaction(); 0755 if (state) { // i.e. add to queue 0756 for (QString id : list) { 0757 getEntry(id)->setQueueStatusInternal(state); 0758 } 0759 } else { // i.e. remove from queue 0760 // This needs special attention as the DB operations are very intensive. 0761 // Reversing the loop is much faster. 0762 for (int i = list.count() - 1; i >= 0; i--) { 0763 qCDebug(kastsDataManager) << "getting entry" << getEntry(list[i])->id(); 0764 getEntry(list[i])->setQueueStatusInternal(state); 0765 } 0766 updateQueueListnrs(); 0767 } 0768 Database::instance().commit(); 0769 0770 Q_EMIT bulkReadStatusActionFinished(); 0771 Q_EMIT bulkNewStatusActionFinished(); 0772 } 0773 0774 void DataManager::bulkDownloadEnclosuresByIndex(QModelIndexList list) 0775 { 0776 bulkDownloadEnclosures(getIdsFromModelIndexList(list)); 0777 } 0778 0779 void DataManager::bulkDownloadEnclosures(QStringList list) 0780 { 0781 bulkQueueStatus(true, list); 0782 for (QString id : list) { 0783 if (getEntry(id)->hasEnclosure()) { 0784 getEntry(id)->enclosure()->download(); 0785 } 0786 } 0787 } 0788 0789 void DataManager::bulkDeleteEnclosuresByIndex(QModelIndexList list) 0790 { 0791 bulkDeleteEnclosures(getIdsFromModelIndexList(list)); 0792 } 0793 0794 void DataManager::bulkDeleteEnclosures(QStringList list) 0795 { 0796 Database::instance().transaction(); 0797 for (QString id : list) { 0798 if (getEntry(id)->hasEnclosure()) { 0799 getEntry(id)->enclosure()->deleteFile(); 0800 } 0801 } 0802 Database::instance().commit(); 0803 } 0804 0805 QStringList DataManager::getIdsFromModelIndexList(const QModelIndexList &list) const 0806 { 0807 QStringList ids; 0808 for (QModelIndex index : list) { 0809 ids += index.data(EpisodeModel::Roles::IdRole).value<QString>(); 0810 } 0811 qCDebug(kastsDataManager) << "Ids of selection:" << ids; 0812 return ids; 0813 } 0814 0815 QString DataManager::cleanUrl(const QString &url) 0816 { 0817 // this is a method to create a "canonical" version of a podcast url which 0818 // would account for some common cases where the URL is different but is 0819 // actually pointing to the same data. Currently covering: 0820 // - http vs https (scheme is actually removed altogether!) 0821 // - encoded vs non-encoded URLs 0822 return QUrl(url).authority() + QUrl(url).path(QUrl::FullyDecoded) 0823 + (QUrl(url).hasQuery() ? QStringLiteral("?") + QUrl(url).query(QUrl::FullyDecoded) : QStringLiteral("")); 0824 }