File indexing completed on 2025-09-14 04:47:30
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 "sync/sync.h" 0026 #include "utils/storagemanager.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, :lastHash);")); 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 query.bindValue(QStringLiteral(":lastHash"), QLatin1String("")); 0388 Database::instance().execute(query); 0389 0390 m_feeds[urlFromInput.toString()] = nullptr; 0391 m_feedmap.append(urlFromInput.toString()); 0392 0393 // Save this action to the database (including timestamp) in order to be 0394 // able to sync with remote services 0395 Sync::instance().storeAddFeedAction(urlFromInput.toString()); 0396 0397 Q_EMIT feedAdded(urlFromInput.toString()); 0398 } 0399 0400 if (fetch) { 0401 Fetcher::instance().fetch(urls); 0402 } 0403 0404 // if settings allow, upload these changes immediately to sync servers 0405 Sync::instance().doQuickSync(); 0406 } 0407 0408 Entry *DataManager::getQueueEntry(int index) const 0409 { 0410 return getEntry(m_queuemap[index]); 0411 } 0412 0413 int DataManager::queueCount() const 0414 { 0415 return m_queuemap.count(); 0416 } 0417 0418 QStringList DataManager::queue() const 0419 { 0420 return m_queuemap; 0421 } 0422 0423 bool DataManager::entryInQueue(const Entry *entry) 0424 { 0425 return entryInQueue(entry->id()); 0426 } 0427 0428 bool DataManager::entryInQueue(const QString &id) const 0429 { 0430 return m_queuemap.contains(id); 0431 } 0432 0433 void DataManager::moveQueueItem(const int from, const int to) 0434 { 0435 // First move the items in the internal data structure 0436 m_queuemap.move(from, to); 0437 0438 // Then make sure that the database Queue table reflects these changes 0439 updateQueueListnrs(); 0440 0441 // Make sure that the QueueModel is aware of the changes so it can update 0442 Q_EMIT queueEntryMoved(from, to); 0443 } 0444 0445 void DataManager::addToQueue(const QString &id) 0446 { 0447 // If item is already in queue, then stop here 0448 if (m_queuemap.contains(id)) 0449 return; 0450 0451 // Add to internal queuemap data structure 0452 m_queuemap += id; 0453 qCDebug(kastsDataManager) << "Queue mapping is now:" << m_queuemap; 0454 0455 // Get index of this entry 0456 const int index = m_queuemap.indexOf(id); // add new entry to end of queue 0457 0458 // Add to Queue database 0459 QSqlQuery query; 0460 query.prepare(QStringLiteral("INSERT INTO Queue VALUES (:index, :feedurl, :id, :playing);")); 0461 query.bindValue(QStringLiteral(":index"), index); 0462 query.bindValue(QStringLiteral(":feedurl"), getEntry(id)->feed()->url()); 0463 query.bindValue(QStringLiteral(":id"), id); 0464 query.bindValue(QStringLiteral(":playing"), false); 0465 Database::instance().execute(query); 0466 0467 // Make sure that the QueueModel is aware of the changes 0468 Q_EMIT queueEntryAdded(index, id); 0469 } 0470 0471 void DataManager::removeFromQueue(const QString &id) 0472 { 0473 if (!entryInQueue(id)) { 0474 return; 0475 } 0476 0477 const int index = m_queuemap.indexOf(id); 0478 qCDebug(kastsDataManager) << "Queuemap is now:" << m_queuemap; 0479 qCDebug(kastsDataManager) << "Queue index of item to be removed" << index; 0480 0481 // Move to next track if it's currently playing 0482 if (AudioManager::instance().entry() == getEntry(id)) { 0483 AudioManager::instance().next(); 0484 } 0485 0486 // Remove the item from the internal data structure 0487 m_queuemap.removeAt(index); 0488 0489 // Then make sure that the database Queue table reflects these changes 0490 QSqlQuery query; 0491 query.prepare(QStringLiteral("DELETE FROM Queue WHERE id=:id;")); 0492 query.bindValue(QStringLiteral(":id"), id); 0493 Database::instance().execute(query); 0494 0495 // Make sure that the QueueModel is aware of the change so it can update 0496 Q_EMIT queueEntryRemoved(index, id); 0497 } 0498 0499 void DataManager::sortQueue(AbstractEpisodeProxyModel::SortType sortType) 0500 { 0501 QString columnName; 0502 QString order; 0503 0504 switch (sortType) { 0505 case AbstractEpisodeProxyModel::SortType::DateAscending: 0506 order = QStringLiteral("ASC"); 0507 columnName = QStringLiteral("updated"); 0508 break; 0509 case AbstractEpisodeProxyModel::SortType::DateDescending: 0510 order = QStringLiteral("DESC"); 0511 columnName = QStringLiteral("updated"); 0512 break; 0513 } 0514 0515 QStringList newQueuemap; 0516 0517 QSqlQuery query; 0518 query.prepare(QStringLiteral("SELECT * FROM Queue INNER JOIN Entries ON Queue.id = Entries.id ORDER BY %1 %2;").arg(columnName, order)); 0519 Database::instance().execute(query); 0520 0521 while (query.next()) { 0522 qCDebug(kastsDataManager) << "new queue order:" << query.value(QStringLiteral("id")).toString(); 0523 newQueuemap += query.value(QStringLiteral("id")).toString(); 0524 } 0525 0526 Database::instance().transaction(); 0527 for (int i = 0; i < m_queuemap.length(); i++) { 0528 query.prepare(QStringLiteral("UPDATE Queue SET listnr=:listnr WHERE id=:id;")); 0529 query.bindValue(QStringLiteral(":id"), newQueuemap[i]); 0530 query.bindValue(QStringLiteral(":listnr"), i); 0531 Database::instance().execute(query); 0532 } 0533 Database::instance().commit(); 0534 0535 m_queuemap.clear(); 0536 m_queuemap = newQueuemap; 0537 0538 Q_EMIT queueSorted(); 0539 } 0540 0541 QString DataManager::lastPlayingEntry() 0542 { 0543 QSqlQuery query; 0544 query.prepare(QStringLiteral("SELECT id FROM Queue WHERE playing=:playing;")); 0545 query.bindValue(QStringLiteral(":playing"), true); 0546 Database::instance().execute(query); 0547 if (!query.next()) 0548 return QStringLiteral("none"); 0549 return query.value(QStringLiteral("id")).toString(); 0550 } 0551 0552 void DataManager::setLastPlayingEntry(const QString &id) 0553 { 0554 QSqlQuery query; 0555 // First set playing to false for all Queue items 0556 query.prepare(QStringLiteral("UPDATE Queue SET playing=:playing;")); 0557 query.bindValue(QStringLiteral(":playing"), false); 0558 Database::instance().execute(query); 0559 // Now set the correct track to playing=true 0560 query.prepare(QStringLiteral("UPDATE Queue SET playing=:playing WHERE id=:id;")); 0561 query.bindValue(QStringLiteral(":playing"), true); 0562 query.bindValue(QStringLiteral(":id"), id); 0563 Database::instance().execute(query); 0564 } 0565 0566 void DataManager::deletePlayedEnclosures() 0567 { 0568 QSqlQuery query; 0569 query.prepare( 0570 QStringLiteral("SELECT * FROM Enclosures INNER JOIN Entries ON Enclosures.id = Entries.id WHERE" 0571 "(downloaded=:downloaded OR downloaded=:partiallydownloaded) AND (read=:read);")); 0572 query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloaded)); 0573 query.bindValue(QStringLiteral(":partiallydownloaded"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded)); 0574 query.bindValue(QStringLiteral(":read"), true); 0575 Database::instance().execute(query); 0576 while (query.next()) { 0577 QString feed = query.value(QStringLiteral("feed")).toString(); 0578 QString id = query.value(QStringLiteral("id")).toString(); 0579 qCDebug(kastsDataManager) << "Found entry which has been downloaded and is marked as played; deleting now:" << id; 0580 Entry *entry = getEntry(id); 0581 if (entry->hasEnclosure()) { 0582 entry->enclosure()->deleteFile(); 0583 } 0584 } 0585 } 0586 0587 void DataManager::importFeeds(const QString &path) 0588 { 0589 QUrl url(path); 0590 QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); 0591 0592 file.open(QIODevice::ReadOnly); 0593 0594 QStringList urls; 0595 QXmlStreamReader xmlReader(&file); 0596 while (!xmlReader.atEnd()) { 0597 xmlReader.readNext(); 0598 if (xmlReader.tokenType() == 4 && xmlReader.attributes().hasAttribute(QStringLiteral("xmlUrl"))) { 0599 urls += xmlReader.attributes().value(QStringLiteral("xmlUrl")).toString(); 0600 } 0601 } 0602 qCDebug(kastsDataManager) << "Start importing urls:" << urls; 0603 addFeeds(urls); 0604 } 0605 0606 void DataManager::exportFeeds(const QString &path) 0607 { 0608 QUrl url(path); 0609 QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); 0610 file.open(QIODevice::WriteOnly); 0611 0612 QXmlStreamWriter xmlWriter(&file); 0613 xmlWriter.setAutoFormatting(true); 0614 xmlWriter.writeStartDocument(QStringLiteral("1.0")); 0615 xmlWriter.writeStartElement(QStringLiteral("opml")); 0616 xmlWriter.writeEmptyElement(QStringLiteral("head")); 0617 xmlWriter.writeStartElement(QStringLiteral("body")); 0618 xmlWriter.writeAttribute(QStringLiteral("version"), QStringLiteral("1.0")); 0619 QSqlQuery query; 0620 query.prepare(QStringLiteral("SELECT url, name FROM Feeds;")); 0621 Database::instance().execute(query); 0622 while (query.next()) { 0623 xmlWriter.writeEmptyElement(QStringLiteral("outline")); 0624 xmlWriter.writeAttribute(QStringLiteral("xmlUrl"), query.value(0).toString()); 0625 xmlWriter.writeAttribute(QStringLiteral("title"), query.value(1).toString()); 0626 } 0627 xmlWriter.writeEndElement(); 0628 xmlWriter.writeEndElement(); 0629 xmlWriter.writeEndDocument(); 0630 } 0631 0632 void DataManager::loadFeed(const QString &feedurl) const 0633 { 0634 QSqlQuery query; 0635 query.prepare(QStringLiteral("SELECT url FROM Feeds WHERE url=:feedurl;")); 0636 query.bindValue(QStringLiteral(":feedurl"), feedurl); 0637 Database::instance().execute(query); 0638 if (!query.next()) { 0639 qWarning() << "Failed to load feed" << feedurl; 0640 } else { 0641 m_feeds[feedurl] = new Feed(feedurl); 0642 } 0643 } 0644 0645 void DataManager::loadEntry(const QString id) const 0646 { 0647 // First find the feed that this entry belongs to 0648 Feed *feed = nullptr; 0649 QHashIterator<QString, QStringList> i(m_entrymap); 0650 while (i.hasNext()) { 0651 i.next(); 0652 if (i.value().contains(id)) 0653 feed = getFeed(i.key()); 0654 } 0655 if (!feed) { 0656 qCDebug(kastsDataManager) << "Failed to find feed belonging to entry" << id; 0657 return; 0658 } 0659 m_entries[id] = new Entry(feed, id); 0660 } 0661 0662 bool DataManager::feedExists(const QString &url) 0663 { 0664 // using cleanUrl to do "fuzzy" check on the podcast URL 0665 QString cleanedUrl = cleanUrl(url); 0666 for (QString listUrl : m_feedmap) { 0667 if (cleanedUrl == cleanUrl(listUrl)) { 0668 return true; 0669 } 0670 } 0671 return false; 0672 } 0673 0674 void DataManager::updateQueueListnrs() const 0675 { 0676 QSqlQuery query; 0677 query.prepare(QStringLiteral("UPDATE Queue SET listnr=:i WHERE id=:id;")); 0678 for (int i = 0; i < m_queuemap.count(); i++) { 0679 query.bindValue(QStringLiteral(":i"), i); 0680 query.bindValue(QStringLiteral(":id"), m_queuemap[i]); 0681 Database::instance().execute(query); 0682 } 0683 } 0684 0685 void DataManager::bulkMarkReadByIndex(bool state, QModelIndexList list) 0686 { 0687 bulkMarkRead(state, getIdsFromModelIndexList(list)); 0688 } 0689 0690 void DataManager::bulkMarkRead(bool state, QStringList list) 0691 { 0692 Database::instance().transaction(); 0693 0694 if (state) { // Mark as read 0695 // This needs special attention as the DB operations are very intensive. 0696 // Reversing the loop is much faster 0697 for (int i = list.count() - 1; i >= 0; i--) { 0698 getEntry(list[i])->setReadInternal(state); 0699 } 0700 updateQueueListnrs(); // update queue after modification 0701 } else { // Mark as unread 0702 for (QString id : list) { 0703 getEntry(id)->setReadInternal(state); 0704 } 0705 } 0706 Database::instance().commit(); 0707 0708 Q_EMIT bulkReadStatusActionFinished(); 0709 0710 // if settings allow, upload these changes immediately to sync servers 0711 if (state) { 0712 Sync::instance().doQuickSync(); 0713 } 0714 } 0715 0716 void DataManager::bulkMarkNewByIndex(bool state, QModelIndexList list) 0717 { 0718 bulkMarkNew(state, getIdsFromModelIndexList(list)); 0719 } 0720 0721 void DataManager::bulkMarkNew(bool state, QStringList list) 0722 { 0723 Database::instance().transaction(); 0724 for (QString id : list) { 0725 getEntry(id)->setNewInternal(state); 0726 } 0727 Database::instance().commit(); 0728 0729 Q_EMIT bulkNewStatusActionFinished(); 0730 } 0731 0732 void DataManager::bulkMarkFavoriteByIndex(bool state, QModelIndexList list) 0733 { 0734 bulkMarkFavorite(state, getIdsFromModelIndexList(list)); 0735 } 0736 0737 void DataManager::bulkMarkFavorite(bool state, QStringList list) 0738 { 0739 Database::instance().transaction(); 0740 for (QString id : list) { 0741 getEntry(id)->setFavoriteInternal(state); 0742 } 0743 Database::instance().commit(); 0744 0745 Q_EMIT bulkFavoriteStatusActionFinished(); 0746 } 0747 0748 void DataManager::bulkQueueStatusByIndex(bool state, QModelIndexList list) 0749 { 0750 bulkQueueStatus(state, getIdsFromModelIndexList(list)); 0751 } 0752 0753 void DataManager::bulkQueueStatus(bool state, QStringList list) 0754 { 0755 Database::instance().transaction(); 0756 if (state) { // i.e. add to queue 0757 for (QString id : list) { 0758 getEntry(id)->setQueueStatusInternal(state); 0759 } 0760 } else { // i.e. remove from queue 0761 // This needs special attention as the DB operations are very intensive. 0762 // Reversing the loop is much faster. 0763 for (int i = list.count() - 1; i >= 0; i--) { 0764 qCDebug(kastsDataManager) << "getting entry" << getEntry(list[i])->id(); 0765 getEntry(list[i])->setQueueStatusInternal(state); 0766 } 0767 updateQueueListnrs(); 0768 } 0769 Database::instance().commit(); 0770 0771 Q_EMIT bulkReadStatusActionFinished(); 0772 Q_EMIT bulkNewStatusActionFinished(); 0773 } 0774 0775 void DataManager::bulkDownloadEnclosuresByIndex(QModelIndexList list) 0776 { 0777 bulkDownloadEnclosures(getIdsFromModelIndexList(list)); 0778 } 0779 0780 void DataManager::bulkDownloadEnclosures(QStringList list) 0781 { 0782 bulkQueueStatus(true, list); 0783 for (QString id : list) { 0784 if (getEntry(id)->hasEnclosure()) { 0785 getEntry(id)->enclosure()->download(); 0786 } 0787 } 0788 } 0789 0790 void DataManager::bulkDeleteEnclosuresByIndex(QModelIndexList list) 0791 { 0792 bulkDeleteEnclosures(getIdsFromModelIndexList(list)); 0793 } 0794 0795 void DataManager::bulkDeleteEnclosures(QStringList list) 0796 { 0797 Database::instance().transaction(); 0798 for (QString id : list) { 0799 if (getEntry(id)->hasEnclosure()) { 0800 getEntry(id)->enclosure()->deleteFile(); 0801 } 0802 } 0803 Database::instance().commit(); 0804 } 0805 0806 QStringList DataManager::getIdsFromModelIndexList(const QModelIndexList &list) const 0807 { 0808 QStringList ids; 0809 for (QModelIndex index : list) { 0810 ids += index.data(EpisodeModel::Roles::IdRole).value<QString>(); 0811 } 0812 qCDebug(kastsDataManager) << "Ids of selection:" << ids; 0813 return ids; 0814 } 0815 0816 QString DataManager::cleanUrl(const QString &url) 0817 { 0818 // this is a method to create a "canonical" version of a podcast url which 0819 // would account for some common cases where the URL is different but is 0820 // actually pointing to the same data. Currently covering: 0821 // - http vs https (scheme is actually removed altogether!) 0822 // - encoded vs non-encoded URLs 0823 return QUrl(url).authority() + QUrl(url).path(QUrl::FullyDecoded) 0824 + (QUrl(url).hasQuery() ? QStringLiteral("?") + QUrl(url).query(QUrl::FullyDecoded) : QStringLiteral("")); 0825 }