Use Entry title to set downloaded file name

Instead of using the MD5 hash of the enclosure download URL, we
create a filename which follows `feedname/entry_title.hash.ext`, where
feedname is a uniquefied feed title (stored in the DB), a truncated version
of the entry title, a shortened hash based on the download URL, and the
original file extension extracted from the download URL.

BUG: 457848
This commit is contained in:
Eamonn Rea 2023-07-05 14:03:11 +00:00 committed by Bart De Vries
parent b2e79dbb51
commit dc311cac7b
12 changed files with 270 additions and 55 deletions

View File

@ -7,14 +7,19 @@
#include "database.h" #include "database.h"
#include <QCryptographicHash>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSqlDatabase> #include <QSqlDatabase>
#include <QSqlError> #include <QSqlError>
#include <QStandardPaths> #include <QStandardPaths>
#include <QUrl> #include <QUrl>
#include "settingsmanager.h"
#define TRUE_OR_RETURN(x) \ #define TRUE_OR_RETURN(x) \
if (!x) \ if (!x) \
return false; return false;
@ -64,6 +69,8 @@ bool Database::migrate()
TRUE_OR_RETURN(migrateTo6()); TRUE_OR_RETURN(migrateTo6());
if (dbversion < 7) if (dbversion < 7)
TRUE_OR_RETURN(migrateTo7()); TRUE_OR_RETURN(migrateTo7());
if (dbversion < 8)
TRUE_OR_RETURN(migrateTo8());
return true; return true;
} }
@ -163,6 +170,82 @@ bool Database::migrateTo7()
return true; return true;
} }
bool Database::migrateTo8()
{
qDebug() << "Migrating database to version 8; this can take a while";
const int maxFilenameLength = 200;
QString enclosurePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (!SettingsManager::self()->storagePath().isEmpty()) {
enclosurePath = SettingsManager::self()->storagePath().toLocalFile();
}
enclosurePath += QStringLiteral("/enclosures/");
TRUE_OR_RETURN(transaction());
TRUE_OR_RETURN(execute(QStringLiteral("ALTER TABLE Feeds ADD COLUMN dirname TEXT;")));
QStringList dirNameList;
QSqlQuery query(QStringLiteral("SELECT url, name FROM Feeds;"));
while (query.next()) {
QString url = query.value(QStringLiteral("url")).toString();
QString name = query.value(QStringLiteral("name")).toString();
// Generate directory name for enclosures based on feed name
QString dirBaseName = name.left(maxFilenameLength);
QString dirName = dirBaseName;
// Check for duplicate names
int numDups = 1; // Minimum to append is " (1)" if file already exists
while (dirNameList.contains(dirName)) {
dirName = QStringLiteral("%1 (%2)").arg(dirBaseName, QString::number(numDups));
numDups++;
}
dirNameList << dirName;
QSqlQuery writeQuery;
writeQuery.prepare(QStringLiteral("UPDATE Feeds SET dirname=:dirname WHERE url=:url;"));
writeQuery.bindValue(QStringLiteral(":dirname"), dirName);
writeQuery.bindValue(QStringLiteral(":url"), url);
TRUE_OR_RETURN(execute(writeQuery));
}
// Rename enclosures to new filename convention
query.prepare(
QStringLiteral("SELECT entry.title, enclosure.id, enclosure.url, feed.dirname FROM Enclosures enclosure JOIN Entries entry ON enclosure.id = entry.id "
"JOIN Feeds feed ON enclosure.feed = feed.url;"));
TRUE_OR_RETURN(execute(query));
while (query.next()) {
QString queryTitle = query.value(QStringLiteral("title")).toString();
QString queryId = query.value(QStringLiteral("id")).toString();
QString queryUrl = query.value(QStringLiteral("url")).toString();
QString feedDirName = query.value(QStringLiteral("dirname")).toString();
// Rename any existing files with the new filename generated above
QString legacyPath = enclosurePath + QString::fromStdString(QCryptographicHash::hash(queryUrl.toUtf8(), QCryptographicHash::Md5).toHex().toStdString());
if (QFileInfo::exists(legacyPath) && QFileInfo(legacyPath).isFile()) {
// Generate filename based on episode name and url hash with feedname as subdirectory
QString enclosureFilenameBase = queryTitle.left(maxFilenameLength) + QStringLiteral(".")
+ QString::fromStdString(QCryptographicHash::hash(queryUrl.toUtf8(), QCryptographicHash::Md5).toHex().toStdString()).left(6);
QString enclosureFilenameExt = QFileInfo(QUrl::fromUserInput(queryUrl).fileName()).suffix();
QString enclosureFilename =
!enclosureFilenameExt.isEmpty() ? enclosureFilenameBase + QStringLiteral(".") + enclosureFilenameExt : enclosureFilenameBase;
QString newDirPath = enclosurePath + feedDirName + QStringLiteral("/");
QString newFilePath = newDirPath + enclosureFilename;
QFileInfo().absoluteDir().mkpath(newDirPath);
QFile::rename(legacyPath, newFilePath);
}
}
TRUE_OR_RETURN(execute(QStringLiteral("PRAGMA user_version = 8;")));
TRUE_OR_RETURN(commit());
return true;
}
bool Database::execute(const QString &query, const QString &connectionName) bool Database::execute(const QString &query, const QString &connectionName)
{ {
QSqlQuery q(connectionName); QSqlQuery q(connectionName);

View File

@ -43,6 +43,7 @@ private:
bool migrateTo5(); bool migrateTo5();
bool migrateTo6(); bool migrateTo6();
bool migrateTo7(); bool migrateTo7();
bool migrateTo8();
void cleanup(); void cleanup();
void setWalMode(); void setWalMode();

View File

@ -27,28 +27,34 @@
DataManager::DataManager() DataManager::DataManager()
{ {
connect( connect(&Fetcher::instance(),
&Fetcher::instance(), &Fetcher::feedDetailsUpdated,
&Fetcher::feedDetailsUpdated, this,
this, [this](const QString &url,
[this](const QString &url, const QString &name, const QString &image, const QString &link, const QString &description, const QDateTime &lastUpdated) { const QString &name,
qCDebug(kastsDataManager) << "Start updating feed details for" << url; const QString &image,
Feed *feed = getFeed(url); const QString &link,
if (feed != nullptr) { const QString &description,
feed->setName(name); const QDateTime &lastUpdated,
feed->setImage(image); const QString &dirname) {
feed->setLink(link); qCDebug(kastsDataManager) << "Start updating feed details for" << url;
feed->setDescription(description); Feed *feed = getFeed(url);
feed->setLastUpdated(lastUpdated); if (feed != nullptr) {
qCDebug(kastsDataManager) << "Retrieving authors"; feed->setName(name);
feed->updateAuthors(); feed->setImage(image);
// For feeds that have just been added, this is probably the point feed->setLink(link);
// where the Feed object gets created; let's set refreshing to feed->setDescription(description);
// true in order to show user feedback that the feed is still feed->setLastUpdated(lastUpdated);
// being fetched feed->setDirname(dirname);
feed->setRefreshing(true); 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) { connect(&Fetcher::instance(), &Fetcher::entryAdded, this, [this](const QString &feedurl, const QString &id) {
Q_UNUSED(feedurl) Q_UNUSED(feedurl)
// Only add the new entry to m_entries // Only add the new entry to m_entries
@ -289,6 +295,11 @@ void DataManager::removeFeeds(const QList<Feed *> &feeds)
m_entrymap.remove(feedurl); // remove all the entry mappings belonging to the feed 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 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()) if (!feed->image().isEmpty())
StorageManager::instance().removeImage(feed->image()); StorageManager::instance().removeImage(feed->image());
m_feeds.remove(m_feedmap[index]); // remove from m_feeds m_feeds.remove(m_feedmap[index]); // remove from m_feeds
@ -362,7 +373,8 @@ void DataManager::addFeeds(const QStringList &urls, const bool fetch)
// TODO: Add more checks like checking if URLs exist; however this will mean async... // TODO: Add more checks like checking if URLs exist; however this will mean async...
QStringList newUrls; QStringList newUrls;
for (const QString &url : urls) { for (const QString &url : urls) {
if (!url.trimmed().isEmpty()) { if (!url.trimmed().isEmpty() && !feedExists(url)) {
qCDebug(kastsDataManager) << "Feed already exists or URL is empty" << url.trimmed();
newUrls << url.trimmed(); newUrls << url.trimmed();
} }
} }
@ -375,18 +387,13 @@ void DataManager::addFeeds(const QStringList &urls, const bool fetch)
// authors and enclosures) will be updated by calling Fetcher::fetch() which // authors and enclosures) will be updated by calling Fetcher::fetch() which
// will trigger a full update of the feed and all related items. // will trigger a full update of the feed and all related items.
for (const QString &url : newUrls) { for (const QString &url : newUrls) {
qCDebug(kastsDataManager) << "Adding feed"; qCDebug(kastsDataManager) << "Adding new feed:" << url;
if (feedExists(url)) {
qCDebug(kastsDataManager) << "Feed already exists";
continue;
}
qCDebug(kastsDataManager) << "Feed does not yet exist";
QUrl urlFromInput = QUrl::fromUserInput(url); QUrl urlFromInput = QUrl::fromUserInput(url);
QSqlQuery query; QSqlQuery query;
query.prepare( query.prepare(
QStringLiteral("INSERT INTO Feeds VALUES (:name, :url, :image, :link, :description, :deleteAfterCount, :deleteAfterType, :subscribed, " QStringLiteral("INSERT INTO Feeds VALUES (:name, :url, :image, :link, :description, :deleteAfterCount, :deleteAfterType, :subscribed, "
":lastUpdated, :new, :notify);")); ":lastUpdated, :new, :notify, :dirname);"));
query.bindValue(QStringLiteral(":name"), urlFromInput.toString()); query.bindValue(QStringLiteral(":name"), urlFromInput.toString());
query.bindValue(QStringLiteral(":url"), urlFromInput.toString()); query.bindValue(QStringLiteral(":url"), urlFromInput.toString());
query.bindValue(QStringLiteral(":image"), QLatin1String("")); query.bindValue(QStringLiteral(":image"), QLatin1String(""));
@ -398,6 +405,7 @@ void DataManager::addFeeds(const QStringList &urls, const bool fetch)
query.bindValue(QStringLiteral(":lastUpdated"), 0); query.bindValue(QStringLiteral(":lastUpdated"), 0);
query.bindValue(QStringLiteral(":new"), true); query.bindValue(QStringLiteral(":new"), true);
query.bindValue(QStringLiteral(":notify"), false); query.bindValue(QStringLiteral(":notify"), false);
query.bindValue(QStringLiteral(":dirname"), QLatin1String(""));
Database::instance().execute(query); Database::instance().execute(query);
m_feeds[urlFromInput.toString()] = nullptr; m_feeds[urlFromInput.toString()] = nullptr;

View File

@ -290,7 +290,7 @@ QString Enclosure::url() const
QString Enclosure::path() const QString Enclosure::path() const
{ {
return StorageManager::instance().enclosurePath(m_url); return StorageManager::instance().enclosurePath(m_entry->title(), m_url, m_entry->feed()->dirname());
} }
Enclosure::Status Enclosure::status() const Enclosure::Status Enclosure::status() const
@ -301,7 +301,7 @@ Enclosure::Status Enclosure::status() const
QString Enclosure::cachedEmbeddedImage() const QString Enclosure::cachedEmbeddedImage() const
{ {
// if image is already cached, then return the path // if image is already cached, then return the path
QString cachedpath = StorageManager::instance().imagePath(path()); QString cachedpath = StorageManager::instance().imagePath(m_url);
if (QFileInfo::exists(cachedpath)) { if (QFileInfo::exists(cachedpath)) {
if (QFileInfo(cachedpath).size() != 0) { if (QFileInfo(cachedpath).size() != 0) {
return QUrl::fromLocalFile(cachedpath).toString(); return QUrl::fromLocalFile(cachedpath).toString();

View File

@ -37,6 +37,7 @@ Feed::Feed(const QString &feedurl)
m_deleteAfterCount = query.value(QStringLiteral("deleteAfterCount")).toInt(); m_deleteAfterCount = query.value(QStringLiteral("deleteAfterCount")).toInt();
m_deleteAfterType = query.value(QStringLiteral("deleteAfterType")).toInt(); m_deleteAfterType = query.value(QStringLiteral("deleteAfterType")).toInt();
m_notify = query.value(QStringLiteral("notify")).toBool(); m_notify = query.value(QStringLiteral("notify")).toBool();
m_dirname = query.value(QStringLiteral("dirname")).toString();
m_errorId = 0; m_errorId = 0;
m_errorString = QLatin1String(""); m_errorString = QLatin1String("");
@ -200,6 +201,11 @@ bool Feed::notify() const
return m_notify; return m_notify;
} }
QString Feed::dirname() const
{
return m_dirname;
}
int Feed::entryCount() const int Feed::entryCount() const
{ {
return DataManager::instance().entryCount(this); return DataManager::instance().entryCount(this);
@ -306,6 +312,14 @@ void Feed::setNotify(bool notify)
} }
} }
void Feed::setDirname(const QString &dirname)
{
if (dirname != m_dirname) {
m_dirname = dirname;
Q_EMIT dirnameChanged(m_dirname);
}
}
void Feed::setUnreadEntryCount(const int count) void Feed::setUnreadEntryCount(const int count)
{ {
if (count != m_unreadEntryCount) { if (count != m_unreadEntryCount) {

View File

@ -32,6 +32,7 @@ class Feed : public QObject
Q_PROPERTY(QDateTime subscribed READ subscribed CONSTANT) Q_PROPERTY(QDateTime subscribed READ subscribed CONSTANT)
Q_PROPERTY(QDateTime lastUpdated READ lastUpdated WRITE setLastUpdated NOTIFY lastUpdatedChanged) Q_PROPERTY(QDateTime lastUpdated READ lastUpdated WRITE setLastUpdated NOTIFY lastUpdatedChanged)
Q_PROPERTY(bool notify READ notify WRITE setNotify NOTIFY notifyChanged) Q_PROPERTY(bool notify READ notify WRITE setNotify NOTIFY notifyChanged)
Q_PROPERTY(QString dirname READ dirname WRITE setDirname NOTIFY dirnameChanged)
Q_PROPERTY(int entryCount READ entryCount NOTIFY entryCountChanged) Q_PROPERTY(int entryCount READ entryCount NOTIFY entryCountChanged)
Q_PROPERTY(int unreadEntryCount READ unreadEntryCount WRITE setUnreadEntryCount NOTIFY unreadEntryCountChanged) Q_PROPERTY(int unreadEntryCount READ unreadEntryCount WRITE setUnreadEntryCount NOTIFY unreadEntryCountChanged)
Q_PROPERTY(int newEntryCount READ newEntryCount NOTIFY newEntryCountChanged) Q_PROPERTY(int newEntryCount READ newEntryCount NOTIFY newEntryCountChanged)
@ -59,6 +60,7 @@ public:
QDateTime subscribed() const; QDateTime subscribed() const;
QDateTime lastUpdated() const; QDateTime lastUpdated() const;
bool notify() const; bool notify() const;
QString dirname() const;
int entryCount() const; int entryCount() const;
int unreadEntryCount() const; int unreadEntryCount() const;
int newEntryCount() const; int newEntryCount() const;
@ -78,6 +80,7 @@ public:
void setDeleteAfterType(int type); void setDeleteAfterType(int type);
void setLastUpdated(const QDateTime &lastUpdated); void setLastUpdated(const QDateTime &lastUpdated);
void setNotify(bool notify); void setNotify(bool notify);
void setDirname(const QString &dirname);
void setUnreadEntryCount(const int count); void setUnreadEntryCount(const int count);
void setRefreshing(bool refreshing); void setRefreshing(bool refreshing);
void setErrorId(int errorId); void setErrorId(int errorId);
@ -96,6 +99,7 @@ Q_SIGNALS:
void deleteAfterTypeChanged(int type); void deleteAfterTypeChanged(int type);
void lastUpdatedChanged(const QDateTime &lastUpdated); void lastUpdatedChanged(const QDateTime &lastUpdated);
void notifyChanged(bool notify); void notifyChanged(bool notify);
void dirnameChanged(const QString &dirname);
void entryCountChanged(); void entryCountChanged();
void unreadEntryCountChanged(); void unreadEntryCountChanged();
void newEntryCountChanged(); void newEntryCountChanged();
@ -119,6 +123,7 @@ private:
QDateTime m_subscribed; QDateTime m_subscribed;
QDateTime m_lastUpdated; QDateTime m_lastUpdated;
bool m_notify; bool m_notify;
QString m_dirname;
int m_errorId; int m_errorId;
QString m_errorString; QString m_errorString;
int m_unreadEntryCount = -1; int m_unreadEntryCount = -1;

View File

@ -51,7 +51,8 @@ Q_SIGNALS:
const QString &image, const QString &image,
const QString &link, const QString &link,
const QString &description, const QString &description,
const QDateTime &lastUpdated); const QDateTime &lastUpdated,
const QString &dirname);
void feedUpdateStatusChanged(const QString &url, bool status); void feedUpdateStatusChanged(const QString &url, bool status);
void cancelFetching(); void cancelFetching();

View File

@ -65,7 +65,13 @@ void StorageManager::setStoragePath(QUrl url)
qCDebug(kastsStorageManager) << "New storage path will be:" << newPath; qCDebug(kastsStorageManager) << "New storage path will be:" << newPath;
if (oldPath != newPath) { if (oldPath != newPath) {
QStringList list = {QStringLiteral("enclosures"), QStringLiteral("images")}; // make list of dirs to be moved (images/ and enclosures/*/)
QStringList list = {QStringLiteral("images")};
for (const QString &subdir : QDir(oldPath + QStringLiteral("/enclosures/")).entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
list << QStringLiteral("enclosures/") + subdir;
}
list << QStringLiteral("enclosures");
StorageMoveJob *moveJob = new StorageMoveJob(oldPath, newPath, list); StorageMoveJob *moveJob = new StorageMoveJob(oldPath, newPath, list);
connect(moveJob, &KJob::processedAmountChanged, this, [this, moveJob]() { connect(moveJob, &KJob::processedAmountChanged, this, [this, moveJob]() {
m_storageMoveProgress = moveJob->processedAmount(KJob::Files); m_storageMoveProgress = moveJob->processedAmount(KJob::Files);
@ -112,24 +118,44 @@ QString StorageManager::imagePath(const QString &url) const
} }
QString StorageManager::enclosureDirPath() const QString StorageManager::enclosureDirPath() const
{
return enclosureDirPath(QStringLiteral(""));
}
QString StorageManager::enclosureDirPath(const QString &feedname) const
{ {
QString path = storagePath() + QStringLiteral("/enclosures/"); QString path = storagePath() + QStringLiteral("/enclosures/");
if (!feedname.isEmpty()) {
path += feedname + QStringLiteral("/");
}
// Create path if it doesn't exist yet // Create path if it doesn't exist yet
QFileInfo().absoluteDir().mkpath(path); QFileInfo().absoluteDir().mkpath(path);
return path; return path;
} }
QString StorageManager::enclosurePath(const QString &url) const QString StorageManager::enclosurePath(const QString &name, const QString &url, const QString &feedname) const
{ {
return enclosureDirPath() + QString::fromStdString(QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5).toHex().toStdString()); // Generate filename based on episode name and url hash with feedname as subdirectory
QString enclosureFilenameBase = name.left(maxFilenameLength) + QStringLiteral(".")
+ QString::fromStdString(QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5).toHex().toStdString()).left(6);
QString enclosureFilenameExt = QFileInfo(QUrl::fromUserInput(url).fileName()).suffix();
QString enclosureFilename = !enclosureFilenameExt.isEmpty() ? enclosureFilenameBase + QStringLiteral(".") + enclosureFilenameExt : enclosureFilenameBase;
return enclosureDirPath(feedname) + enclosureFilename;
} }
qint64 StorageManager::dirSize(const QString &path) const qint64 StorageManager::dirSize(const QString &path) const
{ {
qint64 size = 0; qint64 size = 0;
QFileInfoList files = QDir(path).entryInfoList(QDir::Files); QFileInfoList files = QDir(path).entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
for (QFileInfo info : files) { for (QFileInfo info : files) {
if (info.isDir()) {
size += dirSize(info.filePath());
}
size += info.size(); size += info.size();
} }
@ -145,11 +171,11 @@ void StorageManager::removeImage(const QString &url)
void StorageManager::clearImageCache() void StorageManager::clearImageCache()
{ {
qDebug() << imageDirPath(); qCDebug(kastsStorageManager) << imageDirPath();
QStringList images = QDir(imageDirPath()).entryList(QDir::Files); QStringList images = QDir(imageDirPath()).entryList(QDir::Files);
qDebug() << images; qCDebug(kastsStorageManager) << images;
for (QString image : images) { for (QString image : images) {
qDebug() << image; qCDebug(kastsStorageManager) << image;
QFile(QDir(imageDirPath()).absoluteFilePath(image)).remove(); QFile(QDir(imageDirPath()).absoluteFilePath(image)).remove();
} }
Q_EMIT imageDirSizeChanged(); Q_EMIT imageDirSizeChanged();

View File

@ -34,6 +34,8 @@ public:
return _instance; return _instance;
} }
static const int maxFilenameLength = 200;
QString storagePath() const; QString storagePath() const;
Q_INVOKABLE void setStoragePath(QUrl url); Q_INVOKABLE void setStoragePath(QUrl url);
@ -41,7 +43,8 @@ public:
QString imagePath(const QString &url) const; QString imagePath(const QString &url) const;
QString enclosureDirPath() const; QString enclosureDirPath() const;
QString enclosurePath(const QString &url) const; QString enclosureDirPath(const QString &feedname) const;
QString enclosurePath(const QString &name, const QString &url, const QString &feedname) const;
qint64 enclosureDirSize() const; qint64 enclosureDirSize() const;
qint64 imageDirSize() const; qint64 imageDirSize() const;

View File

@ -101,6 +101,13 @@ void StorageMoveJob::moveFiles()
QFile(QDir(m_from).absoluteFilePath(file)).remove(); QFile(QDir(m_from).absoluteFilePath(file)).remove();
qCDebug(kastsStorageMoveJob) << "Removing file" << QDir(m_from).absoluteFilePath(file); qCDebug(kastsStorageMoveJob) << "Removing file" << QDir(m_from).absoluteFilePath(file);
} }
// delete the directories as well
for (const QString &item : m_list) {
if (!item.isEmpty() && QFileInfo(m_from + QStringLiteral("/") + item).isDir()) {
QDir(m_from).rmdir(item);
}
}
} else { } else {
setError(2); setError(2);
setErrorText(i18n("An error occurred while copying data")); setErrorText(i18n("An error occurred while copying data"));

View File

@ -23,6 +23,7 @@
#include "fetcherlogging.h" #include "fetcherlogging.h"
#include "kasts-version.h" #include "kasts-version.h"
#include "settingsmanager.h" #include "settingsmanager.h"
#include "storagemanager.h"
using namespace ThreadWeaver; using namespace ThreadWeaver;
@ -69,27 +70,32 @@ void UpdateFeedJob::processFeed(Syndication::FeedPtr feed)
if (feed.isNull()) if (feed.isNull())
return; return;
// First check if this is a newly added feed // First check if this is a newly added feed and get current name and dirname
m_isNewFeed = false; m_isNewFeed = false;
QString oldName, oldDirname;
QSqlQuery query(QSqlDatabase::database(m_url)); QSqlQuery query(QSqlDatabase::database(m_url));
query.prepare(QStringLiteral("SELECT new FROM Feeds WHERE url=:url;")); query.prepare(QStringLiteral("SELECT new, name, dirname FROM Feeds WHERE url=:url;"));
query.bindValue(QStringLiteral(":url"), m_url); query.bindValue(QStringLiteral(":url"), m_url);
Database::execute(query); Database::execute(query);
if (query.next()) { if (query.next()) {
m_isNewFeed = query.value(QStringLiteral("new")).toBool(); m_isNewFeed = query.value(QStringLiteral("new")).toBool();
oldName = query.value(QStringLiteral("name")).toString();
oldDirname = query.value(QStringLiteral("dirname")).toString();
} else { } else {
qCDebug(kastsFetcher) << "Feed not found in database" << m_url; qCDebug(kastsFetcher) << "Feed not found in database" << m_url;
return; return;
} }
if (m_isNewFeed) if (m_isNewFeed) {
qCDebug(kastsFetcher) << "New feed" << feed->title(); qCDebug(kastsFetcher) << "New feed" << feed->title();
}
m_markUnreadOnNewFeed = !(SettingsManager::self()->markUnreadOnNewFeed() == 2); m_markUnreadOnNewFeed = !(SettingsManager::self()->markUnreadOnNewFeed() == 2);
// Retrieve "other" fields; this will include the "itunes" tags // Retrieve "other" fields; this will include the "itunes" tags
QMultiMap<QString, QDomElement> otherItems = feed->additionalProperties(); QMultiMap<QString, QDomElement> otherItems = feed->additionalProperties();
query.prepare(QStringLiteral("UPDATE Feeds SET name=:name, image=:image, link=:link, description=:description, lastUpdated=:lastUpdated WHERE url=:url;")); query.prepare(QStringLiteral(
"UPDATE Feeds SET name=:name, image=:image, link=:link, description=:description, lastUpdated=:lastUpdated, dirname=:dirname WHERE url=:url;"));
query.bindValue(QStringLiteral(":name"), feed->title()); query.bindValue(QStringLiteral(":name"), feed->title());
query.bindValue(QStringLiteral(":url"), m_url); query.bindValue(QStringLiteral(":url"), m_url);
query.bindValue(QStringLiteral(":link"), feed->link()); query.bindValue(QStringLiteral(":link"), feed->link());
@ -110,6 +116,23 @@ void UpdateFeedJob::processFeed(Syndication::FeedPtr feed)
image = QUrl(m_url).adjusted(QUrl::RemovePath).toString() + image; image = QUrl(m_url).adjusted(QUrl::RemovePath).toString() + image;
query.bindValue(QStringLiteral(":image"), image); query.bindValue(QStringLiteral(":image"), image);
// if the title has changed, we need to rename the corresponding enclosure
// download directory name and move the files
m_dirname = oldDirname;
if (oldName != feed->title() || oldDirname.isEmpty() || m_isNewFeed) {
QString generatedDirname = generateFeedDirname(feed->title());
if (generatedDirname != oldDirname) {
m_dirname = generatedDirname;
QString enclosurePath = StorageManager::instance().enclosureDirPath();
if (QDir(enclosurePath + oldDirname).exists()) {
QDir().rename(enclosurePath + oldDirname, enclosurePath + m_dirname);
} else {
QDir().mkpath(enclosurePath + m_dirname);
}
}
}
query.bindValue(QStringLiteral(":dirname"), m_dirname);
// Do the actual database UPDATE of this feed // Do the actual database UPDATE of this feed
Database::execute(query); Database::execute(query);
@ -210,7 +233,7 @@ void UpdateFeedJob::processFeed(Syndication::FeedPtr feed)
qCDebug(kastsFetcher) << "Updated feed details:" << feed->title(); qCDebug(kastsFetcher) << "Updated feed details:" << feed->title();
// TODO: Only emit signal if the details have really changed // TODO: Only emit signal if the details have really changed
Q_EMIT feedDetailsUpdated(m_url, feed->title(), image, feed->link(), feed->description(), current); Q_EMIT feedDetailsUpdated(m_url, feed->title(), image, feed->link(), feed->description(), current, m_dirname);
if (m_abort) if (m_abort)
return; return;
@ -359,7 +382,7 @@ bool UpdateFeedJob::processEntry(Syndication::ItemPtr entry)
// the first one is probably the podcast author's preferred version // the first one is probably the podcast author's preferred version
// TODO: handle more than one enclosure? // TODO: handle more than one enclosure?
if (entry->enclosures().count() > 0) { if (entry->enclosures().count() > 0) {
isUpdateDependencies = isUpdateDependencies | processEnclosure(entry->enclosures()[0], entry); isUpdateDependencies = isUpdateDependencies | processEnclosure(entry->enclosures()[0], entryDetails, currentEntry);
} }
return isNewEntry | isUpdateEntry | isUpdateDependencies; // this is a new or updated entry, or an enclosure, chapter or author has been changed/added return isNewEntry | isUpdateEntry | isUpdateDependencies; // this is a new or updated entry, or an enclosure, chapter or author has been changed/added
@ -402,7 +425,7 @@ bool UpdateFeedJob::processAuthor(const QString &entryId, const QString &authorN
return isNewAuthor | isUpdateAuthor; return isNewAuthor | isUpdateAuthor;
} }
bool UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry) bool UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, const EntryDetails &newEntry, const EntryDetails &oldEntry)
{ {
bool isNewEnclosure = true; bool isNewEnclosure = true;
bool isUpdateEnclosure = false; bool isUpdateEnclosure = false;
@ -410,7 +433,7 @@ bool UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, Syndic
// check against existing enclosures already in database // check against existing enclosures already in database
for (const EnclosureDetails &enclosureDetails : (m_enclosures + m_newEnclosures)) { for (const EnclosureDetails &enclosureDetails : (m_enclosures + m_newEnclosures)) {
if (enclosureDetails.id == entry->id()) { if (enclosureDetails.id == newEntry.id) {
isNewEnclosure = false; isNewEnclosure = false;
currentEnclosure = enclosureDetails; currentEnclosure = enclosureDetails;
} }
@ -418,7 +441,7 @@ bool UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, Syndic
EnclosureDetails enclosureDetails; EnclosureDetails enclosureDetails;
enclosureDetails.feed = m_url; enclosureDetails.feed = m_url;
enclosureDetails.id = entry->id(); enclosureDetails.id = newEntry.id;
enclosureDetails.duration = enclosure->duration(); enclosureDetails.duration = enclosure->duration();
enclosureDetails.size = enclosure->length(); enclosureDetails.size = enclosure->length();
enclosureDetails.title = enclosure->title(); enclosureDetails.title = enclosure->title();
@ -430,14 +453,32 @@ bool UpdateFeedJob::processEnclosure(Syndication::EnclosurePtr enclosure, Syndic
if (!isNewEnclosure) { if (!isNewEnclosure) {
if ((currentEnclosure.url != enclosureDetails.url) || (currentEnclosure.title != enclosureDetails.title) if ((currentEnclosure.url != enclosureDetails.url) || (currentEnclosure.title != enclosureDetails.title)
|| (currentEnclosure.type != enclosureDetails.type)) { || (currentEnclosure.type != enclosureDetails.type)) {
qCDebug(kastsFetcher) << "enclosure details have been updated for:" << entry->id(); qCDebug(kastsFetcher) << "enclosure details have been updated for:" << newEntry.id;
isUpdateEnclosure = true; isUpdateEnclosure = true;
m_updateEnclosures += enclosureDetails; m_updateEnclosures += enclosureDetails;
} else { } else {
qCDebug(kastsFetcher) << "enclosure details are unchanged:" << entry->id(); qCDebug(kastsFetcher) << "enclosure details are unchanged:" << newEntry.id;
}
// Check if entry title or enclosure URL has changed
if (newEntry.title != oldEntry.title) {
QString oldFilename = StorageManager::instance().enclosurePath(oldEntry.title, currentEnclosure.url, m_dirname);
QString newFilename = StorageManager::instance().enclosurePath(newEntry.title, enclosureDetails.url, m_dirname);
if (oldFilename != newFilename) {
if (currentEnclosure.url == enclosureDetails.url) {
// If entry title has changed but URL is still the same, the existing enclosure needs to be renamed
QFile::rename(oldFilename, newFilename);
} else {
// If enclosure URL has changed, the old enclosure needs to be deleted
if (QFile(oldFilename).exists()) {
QFile(oldFilename).remove();
}
}
}
} }
} else { } else {
qCDebug(kastsFetcher) << "this is a new enclosure:" << entry->id(); qCDebug(kastsFetcher) << "this is a new enclosure:" << newEntry.id;
m_newEnclosures += enclosureDetails; m_newEnclosures += enclosureDetails;
} }
@ -653,6 +694,29 @@ void UpdateFeedJob::writeToDatabase()
} }
} }
QString UpdateFeedJob::generateFeedDirname(const QString &name) const
{
// Generate directory name for enclosures based on feed name
// NOTE: Any changes here require a database migration!
QString dirBaseName = name.left(StorageManager::maxFilenameLength);
QString dirName = dirBaseName;
QStringList dirNameList;
QSqlQuery query(QSqlDatabase::database(m_url));
query.prepare(QStringLiteral("SELECT name FROM Feeds;"));
while (query.next()) {
dirNameList << query.value(QStringLiteral("name")).toString();
}
// Check for duplicate names in database and on filesystem
int numDups = 1; // Minimum to append is " (1)" if file already exists
while (dirNameList.contains(dirName) || QDir(StorageManager::instance().enclosureDirPath() + dirName).exists()) {
dirName = QStringLiteral("%1 (%2)").arg(dirBaseName, QString::number(numDups));
numDups++;
}
return dirName;
}
void UpdateFeedJob::abort() void UpdateFeedJob::abort()
{ {
m_abort = true; m_abort = true;

View File

@ -76,7 +76,8 @@ Q_SIGNALS:
const QString &image, const QString &image,
const QString &link, const QString &link,
const QString &description, const QString &description,
const QDateTime &lastUpdated); const QDateTime &lastUpdated,
const QString &dirname);
void feedUpdated(const QString &url); void feedUpdated(const QString &url);
void entryAdded(const QString &feedurl, const QString &id); void entryAdded(const QString &feedurl, const QString &id);
void entryUpdated(const QString &feedurl, const QString &id); void entryUpdated(const QString &feedurl, const QString &id);
@ -88,13 +89,15 @@ private:
void processFeed(Syndication::FeedPtr feed); void processFeed(Syndication::FeedPtr feed);
bool processEntry(Syndication::ItemPtr entry); bool processEntry(Syndication::ItemPtr entry);
bool processAuthor(const QString &entryId, const QString &authorName, const QString &authorUri, const QString &authorEmail); bool processAuthor(const QString &entryId, const QString &authorName, const QString &authorUri, const QString &authorEmail);
bool processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry); bool processEnclosure(Syndication::EnclosurePtr enclosure, const EntryDetails &newEntry, const EntryDetails &oldEntry);
bool processChapter(const QString &entryId, const int &start, const QString &chapterTitle, const QString &link, const QString &image); bool processChapter(const QString &entryId, const int &start, const QString &chapterTitle, const QString &link, const QString &image);
void writeToDatabase(); void writeToDatabase();
QString generateFeedDirname(const QString &name) const;
bool m_abort = false; bool m_abort = false;
QString m_url; QString m_url;
QString m_dirname;
QByteArray m_data; QByteArray m_data;
bool m_isNewFeed; bool m_isNewFeed;