kasts/src/datamanager.cpp

834 lines
28 KiB
C++

/**
* SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "datamanager.h"
#include "datamanagerlogging.h"
#include <QDateTime>
#include <QDir>
#include <QSqlDatabase>
#include <QSqlError>
#include <QStandardPaths>
#include <QUrl>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include "audiomanager.h"
#include "database.h"
#include "entry.h"
#include "feed.h"
#include "fetcher.h"
#include "settingsmanager.h"
#include "sync/sync.h"
#include "utils/storagemanager.h"
DataManager::DataManager()
{
connect(&Fetcher::instance(),
&Fetcher::feedDetailsUpdated,
this,
[this](const QString &url,
const QString &name,
const QString &image,
const QString &link,
const QString &description,
const QDateTime &lastUpdated,
const QString &dirname) {
qCDebug(kastsDataManager) << "Start updating feed details for" << url;
Feed *feed = getFeed(url);
if (feed != nullptr) {
feed->setName(name);
feed->setImage(image);
feed->setLink(link);
feed->setDescription(description);
feed->setLastUpdated(lastUpdated);
feed->setDirname(dirname);
qCDebug(kastsDataManager) << "Retrieving authors";
feed->updateAuthors();
// For feeds that have just been added, this is probably the point
// where the Feed object gets created; let's set refreshing to
// true in order to show user feedback that the feed is still
// being fetched
feed->setRefreshing(true);
}
});
connect(&Fetcher::instance(), &Fetcher::entryAdded, this, [this](const QString &feedurl, const QString &id) {
Q_UNUSED(feedurl)
// Only add the new entry to m_entries
// we will repopulate m_entrymap once all new entries have been added,
// such that m_entrymap will show all new entries in the correct order
m_entries[id] = nullptr;
});
connect(&Fetcher::instance(), &Fetcher::feedUpdated, this, [this](const QString &feedurl) {
// Update m_entrymap for feedurl, such that the new and old entries show
// up in the correct order
// TODO: put this code into a separate method and re-use this in the constructor
QSqlQuery query;
m_entrymap[feedurl].clear();
query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed ORDER BY updated DESC;"));
query.bindValue(QStringLiteral(":feed"), feedurl);
Database::instance().execute(query);
while (query.next()) {
m_entrymap[feedurl] += query.value(QStringLiteral("id")).toString();
}
// Check for "new" entries
if (SettingsManager::self()->autoQueue()) {
// start an immediate transaction since this non-blocking read query
// can change into a blocking write query if the entry needs to be
// queued; this can create a deadlock with other concurrent write
// operations
Database::instance().transaction();
query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed AND new=:new ORDER BY updated ASC;"));
query.bindValue(QStringLiteral(":feed"), feedurl);
query.bindValue(QStringLiteral(":new"), true);
Database::instance().execute(query);
while (query.next()) {
QString id = query.value(QStringLiteral("id")).toString();
getEntry(id)->setQueueStatusInternal(true);
if (SettingsManager::self()->autoDownload()) {
if (getEntry(id) && getEntry(id)->hasEnclosure() && getEntry(id)->enclosure()) {
qCDebug(kastsDataManager) << "Start downloading" << getEntry(id)->title();
getEntry(id)->enclosure()->download();
}
}
}
Database::instance().commit();
}
Q_EMIT feedEntriesUpdated(feedurl);
});
// Only read unique feedurls and entry ids from the database.
// The feed and entry datastructures will be loaded lazily.
QSqlQuery query;
query.prepare(QStringLiteral("SELECT url FROM Feeds;"));
Database::instance().execute(query);
while (query.next()) {
m_feedmap += query.value(QStringLiteral("url")).toString();
m_feeds[query.value(QStringLiteral("url")).toString()] = nullptr;
}
for (auto &feedurl : m_feedmap) {
query.prepare(QStringLiteral("SELECT id FROM Entries WHERE feed=:feed ORDER BY updated DESC;"));
query.bindValue(QStringLiteral(":feed"), feedurl);
Database::instance().execute(query);
while (query.next()) {
m_entrymap[feedurl] += query.value(QStringLiteral("id")).toString();
m_entries[query.value(QStringLiteral("id")).toString()] = nullptr;
}
}
// qCDebug(kastsDataManager) << "entrymap contains:" << m_entrymap;
query.prepare(QStringLiteral("SELECT id FROM Queue ORDER BY listnr;"));
Database::instance().execute(query);
while (query.next()) {
m_queuemap += query.value(QStringLiteral("id")).toString();
}
qCDebug(kastsDataManager) << "Queuemap contains:" << m_queuemap;
}
Feed *DataManager::getFeed(const int index) const
{
if (index < m_feedmap.size()) {
return getFeed(m_feedmap[index]);
}
return nullptr;
}
Feed *DataManager::getFeed(const QString &feedurl) const
{
if (m_feeds.contains(feedurl)) {
if (m_feeds[feedurl] == nullptr) {
loadFeed(feedurl);
}
return m_feeds[feedurl];
}
return nullptr;
}
Entry *DataManager::getEntry(const int feed_index, const int entry_index) const
{
if (feed_index < m_feedmap.size() && entry_index < m_entrymap[m_feedmap[feed_index]].size()) {
return getEntry(m_entrymap[m_feedmap[feed_index]][entry_index]);
}
return nullptr;
}
Entry *DataManager::getEntry(const Feed *feed, const int entry_index) const
{
if (feed && entry_index < m_entrymap[feed->url()].size()) {
return getEntry(m_entrymap[feed->url()][entry_index]);
}
return nullptr;
}
Entry *DataManager::getEntry(const QString &id) const
{
if (m_entries.contains(id)) {
if (m_entries[id] == nullptr)
loadEntry(id);
return m_entries[id];
}
return nullptr;
}
int DataManager::feedCount() const
{
return m_feedmap.count();
}
QStringList DataManager::getIdList(const Feed *feed) const
{
return m_entrymap[feed->url()];
}
int DataManager::entryCount(const int feed_index) const
{
return m_entrymap[m_feedmap[feed_index]].count();
}
int DataManager::entryCount(const Feed *feed) const
{
return m_entrymap[feed->url()].count();
}
void DataManager::removeFeed(Feed *feed)
{
QList<Feed *> feeds;
feeds << feed;
removeFeeds(feeds);
}
void DataManager::removeFeed(const int index)
{
// Get feed pointer
Feed *feed = getFeed(m_feedmap[index]);
if (feed) {
removeFeed(feed);
}
}
void DataManager::removeFeeds(const QStringList &feedurls)
{
QList<Feed *> feeds;
for (QString feedurl : feedurls) {
Feed *feed = getFeed(feedurl);
if (feed) {
feeds << feed;
}
}
removeFeeds(feeds);
}
void DataManager::removeFeeds(const QVariantList feedVariantList)
{
QList<Feed *> feeds;
for (QVariant feedVariant : feedVariantList) {
if (feedVariant.canConvert<Feed *>()) {
if (feedVariant.value<Feed *>()) {
feeds << feedVariant.value<Feed *>();
}
}
}
removeFeeds(feeds);
}
void DataManager::removeFeeds(const QList<Feed *> &feeds)
{
for (Feed *feed : feeds) {
if (feed) {
const QString feedurl = feed->url();
int index = m_feedmap.indexOf(feedurl);
qCDebug(kastsDataManager) << "deleting feed" << feedurl << "with index" << index;
// Delete the object instances and mappings
// First delete entries in Queue
qCDebug(kastsDataManager) << "delete queueentries of" << feedurl;
QStringList removeFromQueueList;
for (auto &id : m_queuemap) {
if (getEntry(id)->feed()->url() == feedurl) {
if (AudioManager::instance().entry() == getEntry(id)) {
AudioManager::instance().next();
}
removeFromQueueList += id;
}
}
bulkQueueStatus(false, removeFromQueueList);
// Delete entries themselves
qCDebug(kastsDataManager) << "delete entries of" << feedurl;
for (auto &id : m_entrymap[feedurl]) {
if (getEntry(id)->hasEnclosure())
getEntry(id)->enclosure()->deleteFile(); // delete enclosure (if it exists)
if (!getEntry(id)->image().isEmpty())
StorageManager::instance().removeImage(getEntry(id)->image()); // delete entry images
delete m_entries[id]; // delete pointer
m_entries.remove(id); // delete the hash key
}
m_entrymap.remove(feedurl); // remove all the entry mappings belonging to the feed
qCDebug(kastsDataManager) << "Remove feed image" << feed->image() << "for feed" << feedurl;
qCDebug(kastsDataManager) << "Remove feed enclosure download directory" << feed->dirname() << "for feed" << feedurl;
QDir enclosureDir = QDir(StorageManager::instance().enclosureDirPath() + feed->dirname());
if (!feed->dirname().isEmpty() && enclosureDir.exists()) {
enclosureDir.removeRecursively();
}
if (!feed->image().isEmpty())
StorageManager::instance().removeImage(feed->image());
m_feeds.remove(m_feedmap[index]); // remove from m_feeds
m_feedmap.removeAt(index); // remove from m_feedmap
delete feed; // remove the pointer
// Then delete everything from the database
qCDebug(kastsDataManager) << "delete database part of" << feedurl;
// Delete related Errors
QSqlQuery query;
query.prepare(QStringLiteral("DELETE FROM Errors WHERE url=:url;"));
query.bindValue(QStringLiteral(":url"), feedurl);
Database::instance().execute(query);
// Delete Authors
query.prepare(QStringLiteral("DELETE FROM Authors WHERE feed=:feed;"));
query.bindValue(QStringLiteral(":feed"), feedurl);
Database::instance().execute(query);
// Delete Chapters
query.prepare(QStringLiteral("DELETE FROM Chapters WHERE feed=:feed;"));
query.bindValue(QStringLiteral(":feed"), feedurl);
Database::instance().execute(query);
// Delete Entries
query.prepare(QStringLiteral("DELETE FROM Entries WHERE feed=:feed;"));
query.bindValue(QStringLiteral(":feed"), feedurl);
Database::instance().execute(query);
// Delete Enclosures
query.prepare(QStringLiteral("DELETE FROM Enclosures WHERE feed=:feed;"));
query.bindValue(QStringLiteral(":feed"), feedurl);
Database::instance().execute(query);
// Delete Feed
query.prepare(QStringLiteral("DELETE FROM Feeds WHERE url=:url;"));
query.bindValue(QStringLiteral(":url"), feedurl);
Database::instance().execute(query);
// Save this action to the database (including timestamp) in order to be
// able to sync with remote services
Sync::instance().storeRemoveFeedAction(feedurl);
Q_EMIT feedRemoved(index);
}
}
// if settings allow, then upload these changes immediately to sync server
Sync::instance().doQuickSync();
}
void DataManager::addFeed(const QString &url)
{
addFeed(url, true);
}
void DataManager::addFeed(const QString &url, const bool fetch)
{
addFeeds(QStringList(url), fetch);
}
void DataManager::addFeeds(const QStringList &urls)
{
addFeeds(urls, true);
}
void DataManager::addFeeds(const QStringList &urls, const bool fetch)
{
// First check if the URLs are not empty
// TODO: Add more checks like checking if URLs exist; however this will mean async...
QStringList newUrls;
for (const QString &url : urls) {
if (!url.trimmed().isEmpty() && !feedExists(url)) {
qCDebug(kastsDataManager) << "Feed already exists or URL is empty" << url.trimmed();
newUrls << url.trimmed();
}
}
if (newUrls.count() == 0)
return;
// This method will add the relevant internal data structures, and then add
// a preliminary entry into the database. Those details (as well as entries,
// authors and enclosures) will be updated by calling Fetcher::fetch() which
// will trigger a full update of the feed and all related items.
for (const QString &url : newUrls) {
qCDebug(kastsDataManager) << "Adding new feed:" << url;
QString urlFromInput = QUrl::fromUserInput(url).toString();
QSqlQuery query;
query.prepare(
QStringLiteral("INSERT INTO Feeds VALUES (:name, :url, :image, :link, :description, :deleteAfterCount, :deleteAfterType, :subscribed, "
":lastUpdated, :new, :notify, :dirname, :lastHash, :filterType);"));
query.bindValue(QStringLiteral(":name"), urlFromInput);
query.bindValue(QStringLiteral(":url"), urlFromInput);
query.bindValue(QStringLiteral(":image"), QLatin1String(""));
query.bindValue(QStringLiteral(":link"), QLatin1String(""));
query.bindValue(QStringLiteral(":description"), QLatin1String(""));
query.bindValue(QStringLiteral(":deleteAfterCount"), 0);
query.bindValue(QStringLiteral(":deleteAfterType"), 0);
query.bindValue(QStringLiteral(":subscribed"), QDateTime::currentDateTime().toSecsSinceEpoch());
query.bindValue(QStringLiteral(":lastUpdated"), 0);
query.bindValue(QStringLiteral(":new"), true);
query.bindValue(QStringLiteral(":notify"), false);
query.bindValue(QStringLiteral(":dirname"), QLatin1String(""));
query.bindValue(QStringLiteral(":lastHash"), QLatin1String(""));
query.bindValue(QStringLiteral(":filterType"), 0);
Database::instance().execute(query);
// TODO: check whether the entry in the database happened correctly?
m_feeds[urlFromInput] = new Feed(urlFromInput);
m_feedmap.append(urlFromInput);
// Save this action to the database (including timestamp) in order to be
// able to sync with remote services
Sync::instance().storeAddFeedAction(urlFromInput);
Q_EMIT feedAdded(urlFromInput);
}
if (fetch) {
Fetcher::instance().fetch(urls);
}
// if settings allow, upload these changes immediately to sync servers
Sync::instance().doQuickSync();
}
Entry *DataManager::getQueueEntry(int index) const
{
return getEntry(m_queuemap[index]);
}
int DataManager::queueCount() const
{
return m_queuemap.count();
}
QStringList DataManager::queue() const
{
return m_queuemap;
}
bool DataManager::entryInQueue(const Entry *entry)
{
return entryInQueue(entry->id());
}
bool DataManager::entryInQueue(const QString &id) const
{
return m_queuemap.contains(id);
}
void DataManager::moveQueueItem(const int from, const int to)
{
// First move the items in the internal data structure
m_queuemap.move(from, to);
// Then make sure that the database Queue table reflects these changes
updateQueueListnrs();
// Make sure that the QueueModel is aware of the changes so it can update
Q_EMIT queueEntryMoved(from, to);
}
void DataManager::addToQueue(const QString &id)
{
// If item is already in queue, then stop here
if (m_queuemap.contains(id))
return;
// Add to internal queuemap data structure
m_queuemap += id;
qCDebug(kastsDataManager) << "Queue mapping is now:" << m_queuemap;
// Get index of this entry
const int index = m_queuemap.indexOf(id); // add new entry to end of queue
// Add to Queue database
QSqlQuery query;
query.prepare(QStringLiteral("INSERT INTO Queue VALUES (:index, :feedurl, :id, :playing);"));
query.bindValue(QStringLiteral(":index"), index);
query.bindValue(QStringLiteral(":feedurl"), getEntry(id)->feed()->url());
query.bindValue(QStringLiteral(":id"), id);
query.bindValue(QStringLiteral(":playing"), false);
Database::instance().execute(query);
// Make sure that the QueueModel is aware of the changes
Q_EMIT queueEntryAdded(index, id);
}
void DataManager::removeFromQueue(const QString &id)
{
if (!entryInQueue(id)) {
return;
}
const int index = m_queuemap.indexOf(id);
qCDebug(kastsDataManager) << "Queuemap is now:" << m_queuemap;
qCDebug(kastsDataManager) << "Queue index of item to be removed" << index;
// Move to next track if it's currently playing
if (AudioManager::instance().entry() == getEntry(id)) {
AudioManager::instance().next();
}
// Remove the item from the internal data structure
m_queuemap.removeAt(index);
// Then make sure that the database Queue table reflects these changes
QSqlQuery query;
query.prepare(QStringLiteral("DELETE FROM Queue WHERE id=:id;"));
query.bindValue(QStringLiteral(":id"), id);
Database::instance().execute(query);
// Make sure that the QueueModel is aware of the change so it can update
Q_EMIT queueEntryRemoved(index, id);
}
void DataManager::sortQueue(AbstractEpisodeProxyModel::SortType sortType)
{
QString columnName;
QString order;
switch (sortType) {
case AbstractEpisodeProxyModel::SortType::DateAscending:
order = QStringLiteral("ASC");
columnName = QStringLiteral("updated");
break;
case AbstractEpisodeProxyModel::SortType::DateDescending:
order = QStringLiteral("DESC");
columnName = QStringLiteral("updated");
break;
}
QStringList newQueuemap;
QSqlQuery query;
query.prepare(QStringLiteral("SELECT * FROM Queue INNER JOIN Entries ON Queue.id = Entries.id ORDER BY %1 %2;").arg(columnName, order));
Database::instance().execute(query);
while (query.next()) {
qCDebug(kastsDataManager) << "new queue order:" << query.value(QStringLiteral("id")).toString();
newQueuemap += query.value(QStringLiteral("id")).toString();
}
Database::instance().transaction();
for (int i = 0; i < m_queuemap.length(); i++) {
query.prepare(QStringLiteral("UPDATE Queue SET listnr=:listnr WHERE id=:id;"));
query.bindValue(QStringLiteral(":id"), newQueuemap[i]);
query.bindValue(QStringLiteral(":listnr"), i);
Database::instance().execute(query);
}
Database::instance().commit();
m_queuemap.clear();
m_queuemap = newQueuemap;
Q_EMIT queueSorted();
}
QString DataManager::lastPlayingEntry()
{
QSqlQuery query;
query.prepare(QStringLiteral("SELECT id FROM Queue WHERE playing=:playing;"));
query.bindValue(QStringLiteral(":playing"), true);
Database::instance().execute(query);
if (!query.next())
return QStringLiteral("none");
return query.value(QStringLiteral("id")).toString();
}
void DataManager::setLastPlayingEntry(const QString &id)
{
QSqlQuery query;
// First set playing to false for all Queue items
query.prepare(QStringLiteral("UPDATE Queue SET playing=:playing;"));
query.bindValue(QStringLiteral(":playing"), false);
Database::instance().execute(query);
// Now set the correct track to playing=true
query.prepare(QStringLiteral("UPDATE Queue SET playing=:playing WHERE id=:id;"));
query.bindValue(QStringLiteral(":playing"), true);
query.bindValue(QStringLiteral(":id"), id);
Database::instance().execute(query);
}
void DataManager::deletePlayedEnclosures()
{
QSqlQuery query;
query.prepare(
QStringLiteral("SELECT * FROM Enclosures INNER JOIN Entries ON Enclosures.id = Entries.id WHERE"
"(downloaded=:downloaded OR downloaded=:partiallydownloaded) AND (read=:read);"));
query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloaded));
query.bindValue(QStringLiteral(":partiallydownloaded"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded));
query.bindValue(QStringLiteral(":read"), true);
Database::instance().execute(query);
while (query.next()) {
QString feed = query.value(QStringLiteral("feed")).toString();
QString id = query.value(QStringLiteral("id")).toString();
qCDebug(kastsDataManager) << "Found entry which has been downloaded and is marked as played; deleting now:" << id;
Entry *entry = getEntry(id);
if (entry->hasEnclosure()) {
entry->enclosure()->deleteFile();
}
}
}
void DataManager::importFeeds(const QString &path)
{
QUrl url(path);
QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
file.open(QIODevice::ReadOnly);
QStringList urls;
QXmlStreamReader xmlReader(&file);
while (!xmlReader.atEnd()) {
xmlReader.readNext();
if (xmlReader.tokenType() == 4 && xmlReader.attributes().hasAttribute(QStringLiteral("xmlUrl"))) {
urls += xmlReader.attributes().value(QStringLiteral("xmlUrl")).toString();
}
}
qCDebug(kastsDataManager) << "Start importing urls:" << urls;
addFeeds(urls);
}
void DataManager::exportFeeds(const QString &path)
{
QUrl url(path);
QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
file.open(QIODevice::WriteOnly);
QXmlStreamWriter xmlWriter(&file);
xmlWriter.setAutoFormatting(true);
xmlWriter.writeStartDocument(QStringLiteral("1.0"));
xmlWriter.writeStartElement(QStringLiteral("opml"));
xmlWriter.writeEmptyElement(QStringLiteral("head"));
xmlWriter.writeStartElement(QStringLiteral("body"));
xmlWriter.writeAttribute(QStringLiteral("version"), QStringLiteral("1.0"));
QSqlQuery query;
query.prepare(QStringLiteral("SELECT url, name FROM Feeds;"));
Database::instance().execute(query);
while (query.next()) {
xmlWriter.writeEmptyElement(QStringLiteral("outline"));
xmlWriter.writeAttribute(QStringLiteral("xmlUrl"), query.value(0).toString());
xmlWriter.writeAttribute(QStringLiteral("title"), query.value(1).toString());
}
xmlWriter.writeEndElement();
xmlWriter.writeEndElement();
xmlWriter.writeEndDocument();
}
void DataManager::loadFeed(const QString &feedurl) const
{
if (m_feeds[feedurl]) {
// nothing to do if Feed object already exists
return;
}
QSqlQuery query;
query.prepare(QStringLiteral("SELECT url FROM Feeds WHERE url=:feedurl;"));
query.bindValue(QStringLiteral(":feedurl"), feedurl);
Database::instance().execute(query);
if (!query.next()) {
qWarning() << "Failed to load feed" << feedurl;
} else {
m_feeds[feedurl] = new Feed(feedurl);
}
}
void DataManager::loadEntry(const QString id) const
{
// First find the feed that this entry belongs to
Feed *feed = nullptr;
QHashIterator<QString, QStringList> i(m_entrymap);
while (i.hasNext()) {
i.next();
if (i.value().contains(id))
feed = getFeed(i.key());
}
if (!feed) {
qCDebug(kastsDataManager) << "Failed to find feed belonging to entry" << id;
return;
}
m_entries[id] = new Entry(feed, id);
}
bool DataManager::feedExists(const QString &url)
{
// using cleanUrl to do "fuzzy" check on the podcast URL
QString cleanedUrl = cleanUrl(url);
for (QString listUrl : m_feedmap) {
if (cleanedUrl == cleanUrl(listUrl)) {
return true;
}
}
return false;
}
void DataManager::updateQueueListnrs() const
{
QSqlQuery query;
query.prepare(QStringLiteral("UPDATE Queue SET listnr=:i WHERE id=:id;"));
for (int i = 0; i < m_queuemap.count(); i++) {
query.bindValue(QStringLiteral(":i"), i);
query.bindValue(QStringLiteral(":id"), m_queuemap[i]);
Database::instance().execute(query);
}
}
void DataManager::bulkMarkReadByIndex(bool state, QModelIndexList list)
{
bulkMarkRead(state, getIdsFromModelIndexList(list));
}
void DataManager::bulkMarkRead(bool state, QStringList list)
{
Database::instance().transaction();
if (state) { // Mark as read
// This needs special attention as the DB operations are very intensive.
// Reversing the loop is much faster
for (int i = list.count() - 1; i >= 0; i--) {
getEntry(list[i])->setReadInternal(state);
}
updateQueueListnrs(); // update queue after modification
} else { // Mark as unread
for (QString id : list) {
getEntry(id)->setReadInternal(state);
}
}
Database::instance().commit();
Q_EMIT bulkReadStatusActionFinished();
// if settings allow, upload these changes immediately to sync servers
if (state) {
Sync::instance().doQuickSync();
}
}
void DataManager::bulkMarkNewByIndex(bool state, QModelIndexList list)
{
bulkMarkNew(state, getIdsFromModelIndexList(list));
}
void DataManager::bulkMarkNew(bool state, QStringList list)
{
Database::instance().transaction();
for (QString id : list) {
getEntry(id)->setNewInternal(state);
}
Database::instance().commit();
Q_EMIT bulkNewStatusActionFinished();
}
void DataManager::bulkMarkFavoriteByIndex(bool state, QModelIndexList list)
{
bulkMarkFavorite(state, getIdsFromModelIndexList(list));
}
void DataManager::bulkMarkFavorite(bool state, QStringList list)
{
Database::instance().transaction();
for (QString id : list) {
getEntry(id)->setFavoriteInternal(state);
}
Database::instance().commit();
Q_EMIT bulkFavoriteStatusActionFinished();
}
void DataManager::bulkQueueStatusByIndex(bool state, QModelIndexList list)
{
bulkQueueStatus(state, getIdsFromModelIndexList(list));
}
void DataManager::bulkQueueStatus(bool state, QStringList list)
{
Database::instance().transaction();
if (state) { // i.e. add to queue
for (QString id : list) {
getEntry(id)->setQueueStatusInternal(state);
}
} else { // i.e. remove from queue
// This needs special attention as the DB operations are very intensive.
// Reversing the loop is much faster.
for (int i = list.count() - 1; i >= 0; i--) {
qCDebug(kastsDataManager) << "getting entry" << getEntry(list[i])->id();
getEntry(list[i])->setQueueStatusInternal(state);
}
updateQueueListnrs();
}
Database::instance().commit();
Q_EMIT bulkReadStatusActionFinished();
Q_EMIT bulkNewStatusActionFinished();
}
void DataManager::bulkDownloadEnclosuresByIndex(QModelIndexList list)
{
bulkDownloadEnclosures(getIdsFromModelIndexList(list));
}
void DataManager::bulkDownloadEnclosures(QStringList list)
{
bulkQueueStatus(true, list);
for (QString id : list) {
if (getEntry(id)->hasEnclosure()) {
getEntry(id)->enclosure()->download();
}
}
}
void DataManager::bulkDeleteEnclosuresByIndex(QModelIndexList list)
{
bulkDeleteEnclosures(getIdsFromModelIndexList(list));
}
void DataManager::bulkDeleteEnclosures(QStringList list)
{
Database::instance().transaction();
for (QString id : list) {
if (getEntry(id)->hasEnclosure()) {
getEntry(id)->enclosure()->deleteFile();
}
}
Database::instance().commit();
}
QStringList DataManager::getIdsFromModelIndexList(const QModelIndexList &list) const
{
QStringList ids;
for (QModelIndex index : list) {
ids += index.data(EpisodeModel::Roles::IdRole).value<QString>();
}
qCDebug(kastsDataManager) << "Ids of selection:" << ids;
return ids;
}
QString DataManager::cleanUrl(const QString &url)
{
// this is a method to create a "canonical" version of a podcast url which
// would account for some common cases where the URL is different but is
// actually pointing to the same data. Currently covering:
// - http vs https (scheme is actually removed altogether!)
// - encoded vs non-encoded URLs
return QUrl(url).authority() + QUrl(url).path(QUrl::FullyDecoded)
+ (QUrl(url).hasQuery() ? QStringLiteral("?") + QUrl(url).query(QUrl::FullyDecoded) : QStringLiteral(""));
}