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 }