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 }