From f197e6ab028d530b1ebca6de3691acffec598e10 Mon Sep 17 00:00:00 2001 From: Bart De Vries Date: Wed, 23 Jun 2021 21:11:04 +0200 Subject: [PATCH] Implement Enclosure::PartiallyDownloaded status Still to be done: - Update Download Page to show partial downloads. - Connect signals to Download Page to update whenever an enclosure changes status. This is broken by this commit because downloadCountChanged has been removed. --- src/database.cpp | 13 ++ src/database.h | 1 + src/datamanager.cpp | 18 ++- src/datamanager.h | 1 - src/enclosure.cpp | 198 ++++++++++++++++++------------- src/enclosure.h | 9 +- src/episodemodel.cpp | 8 -- src/episodemodel.h | 1 + src/fetcher.cpp | 3 +- src/qml/EntryPage.qml | 15 ++- src/qml/GenericEntryDelegate.qml | 2 +- 11 files changed, 164 insertions(+), 105 deletions(-) diff --git a/src/database.cpp b/src/database.cpp index 798ff277..46f0be32 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -44,6 +44,8 @@ bool Database::migrate() TRUE_OR_RETURN(migrateTo1()); if (dbversion < 2) TRUE_OR_RETURN(migrateTo2()); + if (dbversion < 3) + TRUE_OR_RETURN(migrateTo3()); return true; } @@ -75,6 +77,17 @@ bool Database::migrateTo2() return true; } +bool Database::migrateTo3() +{ + qDebug() << "Migrating database to version 3"; + TRUE_OR_RETURN(execute(QStringLiteral("ALTER TABLE Enclosures RENAME COLUMN downloaded TO downloaded_temp;"))); + TRUE_OR_RETURN(execute(QStringLiteral("ALTER TABLE Enclosures ADD COLUMN downloaded INTEGER DEFAULT 0;"))); + TRUE_OR_RETURN(execute(QStringLiteral("UPDATE Enclosures SET downloaded=3 where downloaded_temp=1;"))); + TRUE_OR_RETURN(execute(QStringLiteral("ALTER TABLE Enclosures DROP COLUMN downloaded_temp;"))); + TRUE_OR_RETURN(execute(QStringLiteral("PRAGMA user_version = 3;"))); + return true; +} + bool Database::execute(const QString &query) { QSqlQuery q; diff --git a/src/database.h b/src/database.h index 2ddf4213..511ff66e 100644 --- a/src/database.h +++ b/src/database.h @@ -29,5 +29,6 @@ private: bool migrate(); bool migrateTo1(); bool migrateTo2(); + bool migrateTo3(); void cleanup(); }; diff --git a/src/datamanager.cpp b/src/datamanager.cpp index a7ad9058..d48ed732 100644 --- a/src/datamanager.cpp +++ b/src/datamanager.cpp @@ -157,11 +157,16 @@ Entry *DataManager::getEntry(const EpisodeModel::Type type, const int entry_inde entryQuery.bindValue(QStringLiteral(":read"), false); } else if (type == EpisodeModel::All) { entryQuery.prepare(QStringLiteral("SELECT id FROM Entries ORDER BY updated DESC LIMIT 1 OFFSET :index;")); - } else { // i.e. EpisodeModel::Downloaded + } else if (type == EpisodeModel::Downloaded) { entryQuery.prepare( QStringLiteral("SELECT * FROM Enclosures INNER JOIN Entries ON Enclosures.id = Entries.id WHERE downloaded=:downloaded ORDER BY updated DESC " "LIMIT 1 OFFSET :index;")); - entryQuery.bindValue(QStringLiteral(":downloaded"), true); + entryQuery.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloaded)); + } else { // i.e. EpisodeModel::PartiallyDownloaded + entryQuery.prepare( + QStringLiteral("SELECT * FROM Enclosures INNER JOIN Entries ON Enclosures.id = Entries.id WHERE downloaded=:downloaded ORDER BY updated DESC " + "LIMIT 1 OFFSET :index;")); + entryQuery.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded)); } entryQuery.bindValue(QStringLiteral(":index"), entry_index); Database::instance().execute(entryQuery); @@ -202,9 +207,12 @@ int DataManager::entryCount(const EpisodeModel::Type type) const query.bindValue(QStringLiteral(":read"), false); } else if (type == EpisodeModel::All) { query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries;")); - } else { // i.e. EpisodeModel::Downloaded + } else if (type == EpisodeModel::Downloaded) { query.prepare(QStringLiteral("SELECT COUNT (id) FROM Enclosures WHERE downloaded=:downloaded;")); - query.bindValue(QStringLiteral(":downloaded"), true); + query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloaded)); + } else { // i.e. EpisodeModel::PartiallyDownloaded + query.prepare(QStringLiteral("SELECT COUNT (id) FROM Enclosures WHERE downloaded=:downloaded;")); + query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded)); } Database::instance().execute(query); if (!query.next()) @@ -577,4 +585,4 @@ void DataManager::updateQueueListnrs() const query.bindValue(QStringLiteral(":id"), m_queuemap[i]); Database::instance().execute(query); } -} \ No newline at end of file +} diff --git a/src/datamanager.h b/src/datamanager.h index a8ff4e09..b735e21f 100644 --- a/src/datamanager.h +++ b/src/datamanager.h @@ -74,7 +74,6 @@ Q_SIGNALS: void unreadEntryCountChanged(const QString &url); void newEntryCountChanged(const QString &url); - void downloadCountChanged(const QString &url); private: DataManager(); diff --git a/src/enclosure.cpp b/src/enclosure.cpp index 29976ada..549e52cb 100644 --- a/src/enclosure.cpp +++ b/src/enclosure.cpp @@ -33,6 +33,9 @@ Enclosure::Enclosure(Entry *entry) // reported. Other times only the remaining part. // Sometimes the value is rubbish (e.g. 2) // We assume that the value when starting a new download is correct. + if (fileSize < resumedAt) { + fileSize += resumedAt; + } qDebug() << "Correct filesize for enclosure" << url << "from" << m_size << "to" << fileSize; setSize(fileSize); } @@ -53,7 +56,7 @@ Enclosure::Enclosure(Entry *entry) m_type = query.value(QStringLiteral("type")).toString(); m_url = query.value(QStringLiteral("url")).toString(); m_playposition = query.value(QStringLiteral("playposition")).toLongLong(); - m_status = query.value(QStringLiteral("downloaded")).toBool() ? Downloaded : Downloadable; + m_status = dbToStatus(query.value(QStringLiteral("downloaded")).toInt()); m_playposition_dbsave = m_playposition; // In principle the database contains this status, we check anyway in case @@ -61,38 +64,53 @@ Enclosure::Enclosure(Entry *entry) QFile file(path()); if (file.exists()) { if (file.size() == m_size && file.size() > 0) { - if (m_status == Downloadable) { - // file is on disk, but was not expected, write to database - // this should, in principle, never happen unless the db was deleted - m_status = Downloaded; - query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); - query.bindValue(QStringLiteral(":id"), entry->id()); - query.bindValue(QStringLiteral(":downloaded"), true); - Database::instance().execute(query); - } + // file is on disk and has correct size, write to database if it + // wasn't already registered so + // this should, in principle, never happen unless the db was deleted + setStatus(Downloaded); + } else if (file.size() > 0) { + // file was downloaded, but there is a size mismatch + // set to PartiallyDownloaded such that download can be resumed + setStatus(PartiallyDownloaded); } else { - if (m_status == Downloaded) { - // file was downloaded, but there is a size mismatch or file is empty - // update status in database - // don't actually delete the file such that the download can be resumed - m_status = Downloadable; - query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); - query.bindValue(QStringLiteral(":id"), entry->id()); - query.bindValue(QStringLiteral(":downloaded"), false); - Database::instance().execute(query); - } + // file is empty + setStatus(Downloadable); } } else { - if (m_status == Downloaded) { - // file was supposed to be on disk, but isn't there - // update status and write to the database - file.remove(); - m_status = Downloadable; - query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); - query.bindValue(QStringLiteral(":id"), entry->id()); - query.bindValue(QStringLiteral(":downloaded"), false); - Database::instance().execute(query); - } + // file does not exist + setStatus(Downloadable); + } +} + +int Enclosure::statusToDb(Enclosure::Status status) +{ + switch (status) { + case Enclosure::Status::Downloadable: + return 0; + case Enclosure::Status::Downloading: + return 1; + case Enclosure::Status::PartiallyDownloaded: + return 2; + case Enclosure::Status::Downloaded: + return 3; + default: + return -1; + } +} + +Enclosure::Status Enclosure::dbToStatus(int value) +{ + switch (value) { + case 0: + return Enclosure::Status::Downloadable; + case 1: + return Enclosure::Status::Downloading; + case 2: + return Enclosure::Status::PartiallyDownloaded; + case 3: + return Enclosure::Status::Downloaded; + default: + return Enclosure::Status::Error; } } @@ -111,7 +129,12 @@ void Enclosure::download() if (downloadJob->error() == 0) { processDownloadedFile(); } else { - m_status = Downloadable; + QFile file(path()); + if (file.exists() && file.size() > 0) { + setStatus(PartiallyDownloaded); + } else { + setStatus(Downloadable); + } if (downloadJob->error() != QNetworkReply::OperationCanceledError) { m_entry->feed()->setErrorId(downloadJob->error()); m_entry->feed()->setErrorString(downloadJob->errorString()); @@ -124,9 +147,12 @@ void Enclosure::download() connect(this, &Enclosure::cancelDownload, this, [this, downloadJob]() { downloadJob->doKill(); - m_status = Downloadable; - Q_EMIT statusChanged(m_entry, m_status); - Q_EMIT DataManager::instance().downloadCountChanged(m_entry->feed()->url()); + QFile file(path()); + if (file.exists() && file.size() > 0) { + setStatus(PartiallyDownloaded); + } else { + setStatus(Downloadable); + } disconnect(this, &Enclosure::cancelDownload, this, nullptr); }); @@ -135,8 +161,7 @@ void Enclosure::download() Q_EMIT downloadProgressChanged(); }); - m_status = Downloading; - Q_EMIT statusChanged(m_entry, m_status); + setStatus(Downloading); } void Enclosure::processDownloadedFile() @@ -159,17 +184,10 @@ void Enclosure::processDownloadedFile() setSize(file.size()); } - m_status = Downloaded; - QSqlQuery query; - query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); - query.bindValue(QStringLiteral(":id"), m_entry->id()); - query.bindValue(QStringLiteral(":downloaded"), true); - Database::instance().execute(query); + setStatus(Downloaded); // Unset "new" status of item if (m_entry->getNew()) m_entry->setNew(false); - - Q_EMIT DataManager::instance().downloadCountChanged(m_entry->feed()->url()); } void Enclosure::deleteFile() @@ -179,14 +197,7 @@ void Enclosure::deleteFile() if (QFile(path()).exists()) QFile(path()).remove(); // If file disappeared unexpectedly, then still change status to downloadable - m_status = Downloadable; - QSqlQuery query; - query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id;")); - query.bindValue(QStringLiteral(":id"), m_entry->id()); - query.bindValue(QStringLiteral(":downloaded"), false); - Database::instance().execute(query); - Q_EMIT statusChanged(m_entry, m_status); - Q_EMIT DataManager::instance().downloadCountChanged(m_entry->feed()->url()); + setStatus(Downloadable); } QString Enclosure::path() const @@ -214,52 +225,77 @@ int Enclosure::size() const return m_size; } -void Enclosure::setPlayPosition(const qint64 &position) +void Enclosure::setStatus(Enclosure::Status status) { - m_playposition = position; - qCDebug(kastsEnclosure) << "save playPosition" << position << m_entry->title(); - Q_EMIT playPositionChanged(); + if (m_status != status) { + m_status = status; - // let's only save the play position to the database every 15 seconds - if ((abs(m_playposition - m_playposition_dbsave) > 15000) || position == 0) { - qCDebug(kastsEnclosure) << "save playPosition to database" << position << m_entry->title(); QSqlQuery query; - query.prepare(QStringLiteral("UPDATE Enclosures SET playposition=:playposition WHERE id=:id AND feed=:feed")); + query.prepare(QStringLiteral("UPDATE Enclosures SET downloaded=:downloaded WHERE id=:id AND feed=:feed;")); query.bindValue(QStringLiteral(":id"), m_entry->id()); query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url()); - query.bindValue(QStringLiteral(":playposition"), m_playposition); + query.bindValue(QStringLiteral(":downloaded"), statusToDb(m_status)); Database::instance().execute(query); - m_playposition_dbsave = m_playposition; + + Q_EMIT statusChanged(m_entry, m_status); + } +} + +void Enclosure::setPlayPosition(const qint64 &position) +{ + if (m_playposition != position) { + m_playposition = position; + qCDebug(kastsEnclosure) << "save playPosition" << position << m_entry->title(); + + // let's only save the play position to the database every 15 seconds + if ((abs(m_playposition - m_playposition_dbsave) > 15000) || position == 0) { + qCDebug(kastsEnclosure) << "save playPosition to database" << position << m_entry->title(); + QSqlQuery query; + query.prepare(QStringLiteral("UPDATE Enclosures SET playposition=:playposition WHERE id=:id AND feed=:feed")); + query.bindValue(QStringLiteral(":id"), m_entry->id()); + query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url()); + query.bindValue(QStringLiteral(":playposition"), m_playposition); + Database::instance().execute(query); + m_playposition_dbsave = m_playposition; + } + + Q_EMIT playPositionChanged(); } } void Enclosure::setDuration(const qint64 &duration) { - m_duration = duration; - Q_EMIT durationChanged(); + if (m_duration != duration) { + m_duration = duration; - // also save to database - qCDebug(kastsEnclosure) << "updating entry duration" << duration << m_entry->title(); - QSqlQuery query; - query.prepare(QStringLiteral("UPDATE Enclosures SET duration=:duration WHERE id=:id AND feed=:feed")); - query.bindValue(QStringLiteral(":id"), m_entry->id()); - query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url()); - query.bindValue(QStringLiteral(":duration"), m_duration); - Database::instance().execute(query); + // also save to database + qCDebug(kastsEnclosure) << "updating entry duration" << duration << m_entry->title(); + QSqlQuery query; + query.prepare(QStringLiteral("UPDATE Enclosures SET duration=:duration WHERE id=:id AND feed=:feed")); + query.bindValue(QStringLiteral(":id"), m_entry->id()); + query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url()); + query.bindValue(QStringLiteral(":duration"), m_duration); + Database::instance().execute(query); + + Q_EMIT durationChanged(); + } } void Enclosure::setSize(const int &size) { - m_size = size; - Q_EMIT sizeChanged(); + if (m_size != size) { + m_size = size; - // also save to database - QSqlQuery query; - query.prepare(QStringLiteral("UPDATE Enclosures SET size=:size WHERE id=:id AND feed=:feed")); - query.bindValue(QStringLiteral(":id"), m_entry->id()); - query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url()); - query.bindValue(QStringLiteral(":size"), m_size); - Database::instance().execute(query); + // also save to database + QSqlQuery query; + query.prepare(QStringLiteral("UPDATE Enclosures SET size=:size WHERE id=:id AND feed=:feed")); + query.bindValue(QStringLiteral(":id"), m_entry->id()); + query.bindValue(QStringLiteral(":feed"), m_entry->feed()->url()); + query.bindValue(QStringLiteral(":size"), m_size); + Database::instance().execute(query); + + Q_EMIT sizeChanged(); + } } QString Enclosure::formattedSize() const diff --git a/src/enclosure.h b/src/enclosure.h index ae6e2489..f8dac263 100644 --- a/src/enclosure.h +++ b/src/enclosure.h @@ -26,7 +26,7 @@ class Enclosure : public QObject Q_PROPERTY(QString title MEMBER m_title CONSTANT) Q_PROPERTY(QString type MEMBER m_type CONSTANT) Q_PROPERTY(QString url MEMBER m_url CONSTANT) - Q_PROPERTY(Status status READ status NOTIFY statusChanged) + Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged) Q_PROPERTY(double downloadProgress MEMBER m_downloadProgress NOTIFY downloadProgressChanged) Q_PROPERTY(QString path READ path CONSTANT) Q_PROPERTY(qint64 playPosition READ playPosition WRITE setPlayPosition NOTIFY playPositionChanged) @@ -40,11 +40,15 @@ public: enum Status { Downloadable, Downloading, + PartiallyDownloaded, Downloaded, Error, }; Q_ENUM(Status) + static int statusToDb(Status status); // needed to translate Enclosure::Status values to int for sqlite + static Status dbToStatus(int value); // needed to translate from int to Enclosure::Status values for sqlite + Q_INVOKABLE void download(); Q_INVOKABLE void deleteFile(); @@ -57,6 +61,7 @@ public: QString formattedDuration() const; QString formattedPlayPosition() const; + void setStatus(Status status); void setPlayPosition(const qint64 &position); void setDuration(const qint64 &duration); void setSize(const int &size); @@ -84,4 +89,4 @@ private: double m_downloadProgress = 0; Status m_status; KFormat m_kformat; -}; \ No newline at end of file +}; diff --git a/src/episodemodel.cpp b/src/episodemodel.cpp index 79d73885..c8b520c4 100644 --- a/src/episodemodel.cpp +++ b/src/episodemodel.cpp @@ -65,13 +65,5 @@ void EpisodeModel::setType(EpisodeModel::Type type) beginResetModel(); endResetModel(); }); - } else if (m_type == EpisodeModel::Downloaded) { // TODO: this needs to be removed !!!!!! - connect(&DataManager::instance(), &DataManager::downloadCountChanged, this, [this](const QString &url) { - Q_UNUSED(url) - // we have to reset the entire model in case entries are removed or added - // because we have no way of knowing where those entries will be added/removed - beginResetModel(); - endResetModel(); - }); } } diff --git a/src/episodemodel.h b/src/episodemodel.h index 5f988cf3..7852d613 100644 --- a/src/episodemodel.h +++ b/src/episodemodel.h @@ -22,6 +22,7 @@ public: Unread, Downloading, Downloaded, + PartiallyDownloaded, }; Q_ENUM(Type) diff --git a/src/fetcher.cpp b/src/fetcher.cpp index c4b114d5..dbd6325e 100644 --- a/src/fetcher.cpp +++ b/src/fetcher.cpp @@ -21,6 +21,7 @@ #include #include "database.h" +#include "enclosure.h" #include "fetcher.h" #include "fetcherlogging.h" #include "settingsmanager.h" @@ -339,7 +340,7 @@ void Fetcher::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication: query.bindValue(QStringLiteral(":type"), enclosure->type()); query.bindValue(QStringLiteral(":url"), enclosure->url()); query.bindValue(QStringLiteral(":playposition"), 0); - query.bindValue(QStringLiteral(":downloaded"), false); + query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloadable)); Database::instance().execute(query); } diff --git a/src/qml/EntryPage.qml b/src/qml/EntryPage.qml index 3d177d28..fa085753 100644 --- a/src/qml/EntryPage.qml +++ b/src/qml/EntryPage.qml @@ -69,22 +69,25 @@ Kirigami.ScrollablePage { actions.main: Kirigami.Action { text: !entry.enclosure ? i18n("Open in Browser") : - entry.enclosure.status === Enclosure.Downloadable ? i18n("Download") : + (entry.enclosure.status === Enclosure.Downloadable || entry.enclosure.status === Enclosure.PartiallyDownloaded) ? i18n("Download") : entry.enclosure.status === Enclosure.Downloading ? i18n("Cancel Download") : !entry.queueStatus ? i18n("Delete Download") : (AudioManager.entry === entry && AudioManager.playbackState === Audio.PlayingState) ? i18n("Pause") : i18n("Play") icon.name: !entry.enclosure ? "globe" : - entry.enclosure.status === Enclosure.Downloadable ? "download" : + (entry.enclosure.status === Enclosure.Downloadable || entry.enclosure.status === Enclosure.PartiallyDownloaded) ? "download" : entry.enclosure.status === Enclosure.Downloading ? "edit-delete-remove" : !entry.queueStatus ? "delete" : (AudioManager.entry === entry && AudioManager.playbackState === Audio.PlayingState) ? "media-playback-pause" : "media-playback-start" onTriggered: { - if(!entry.enclosure) Qt.openUrlExternally(entry.link) - else if(entry.enclosure.status === Enclosure.Downloadable) entry.enclosure.download() - else if(entry.enclosure.status === Enclosure.Downloading) entry.enclosure.cancelDownload() - else if(!entry.queueStatus) { + if (!entry.enclosure) { + Qt.openUrlExternally(entry.link) + } else if (entry.enclosure.status === Enclosure.Downloadable || entry.enclosure.status === Enclosure.PartiallyDownloaded) { + entry.enclosure.download() + } else if (entry.enclosure.status === Enclosure.Downloading) { + entry.enclosure.cancelDownload() + } else if (!entry.queueStatus) { entry.enclosure.deleteFile() } else { if(AudioManager.entry === entry && AudioManager.playbackState === Audio.PlayingState) { diff --git a/src/qml/GenericEntryDelegate.qml b/src/qml/GenericEntryDelegate.qml index 88f018be..3eb9b4cb 100644 --- a/src/qml/GenericEntryDelegate.qml +++ b/src/qml/GenericEntryDelegate.qml @@ -184,7 +184,7 @@ Kirigami.SwipeListItem { entry.queueStatus = true; entry.enclosure.download(); } - visible: !isDownloads && entry.enclosure && entry.enclosure.status === Enclosure.Downloadable + visible: !isDownloads && entry.enclosure && (entry.enclosure.status === Enclosure.Downloadable || entry.enclosure.status === Enclosure.PartiallyDownloaded) }, Kirigami.Action { text: i18n("Cancel Download")