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.
This commit is contained in:
Bart De Vries 2021-06-23 21:11:04 +02:00
parent 03081edc66
commit f197e6ab02
11 changed files with 164 additions and 105 deletions

View File

@ -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;

View File

@ -29,5 +29,6 @@ private:
bool migrate();
bool migrateTo1();
bool migrateTo2();
bool migrateTo3();
void cleanup();
};

View File

@ -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())

View File

@ -74,7 +74,6 @@ Q_SIGNALS:
void unreadEntryCountChanged(const QString &url);
void newEntryCountChanged(const QString &url);
void downloadCountChanged(const QString &url);
private:
DataManager();

View File

@ -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

View File

@ -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);

View File

@ -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();
});
}
}

View File

@ -22,6 +22,7 @@ public:
Unread,
Downloading,
Downloaded,
PartiallyDownloaded,
};
Q_ENUM(Type)

View File

@ -21,6 +21,7 @@
#include <Syndication/Syndication>
#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);
}

View File

@ -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) {

View File

@ -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")