Implement the "Delete after x days" feature for listened podcasts.

This commit is contained in:
David Sansome 2012-03-12 19:35:47 +00:00
parent a5107b7dfc
commit 5327b72f0b
11 changed files with 102 additions and 22 deletions

View File

@ -27,6 +27,7 @@ CREATE TABLE podcast_episodes (
url TEXT,
listened BOOLEAN,
listened_date INTEGER,
downloaded BOOLEAN,
local_url TEXT,

View File

@ -31,4 +31,6 @@ const qint64 kNsecPerUsec = 1000ll;
const qint64 kNsecPerMsec = 1000000ll;
const qint64 kNsecPerSec = 1000000000ll;
const qint64 kSecsPerDay = 24 * 60 * 60;
#endif // TIMECONSTANTS_H

View File

@ -136,12 +136,14 @@ void PodcastBackend::UpdateEpisodes(const PodcastEpisodeList& episodes) {
QSqlQuery q("UPDATE podcast_episodes"
" SET listened = :listened,"
" listened_date = :listened_date,"
" downloaded = :downloaded,"
" local_url = :local_url"
" WHERE ROWID = :id", db);
foreach (const PodcastEpisode& episode, episodes) {
q.bindValue(":listened", episode.listened());
q.bindValue(":listened_date", episode.listened_date().toTime_t());
q.bindValue(":downloaded", episode.downloaded());
q.bindValue(":local_url", episode.local_url().toEncoded());
q.bindValue(":id", episode.database_id());
@ -287,3 +289,27 @@ PodcastEpisode PodcastBackend::GetEpisodeByUrlOrLocalUrl(const QUrl& url) {
return ret;
}
PodcastEpisodeList PodcastBackend::GetOldDownloadedEpisodes(const QDateTime& max_listened_date) {
PodcastEpisodeList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q("SELECT ROWID, " + PodcastEpisode::kColumnSpec +
" FROM podcast_episodes"
" WHERE downloaded = 'true'"
" AND listened_date <= :max_listened_date", db);
q.bindValue(":max_listened_date", max_listened_date.toTime_t());
q.exec();
if (db_->CheckErrors(q))
return ret;
while (q.next()) {
PodcastEpisode episode;
episode.InitFromQuery(q);
ret << episode;
}
return ret;
}

View File

@ -48,18 +48,24 @@ public:
Podcast GetSubscriptionById(int id);
Podcast GetSubscriptionByUrl(const QUrl& url);
// Returns a list of the episodes in the podcast with the given ID.
// Returns podcast episodes that match various keys. All these queries are
// indexed.
PodcastEpisodeList GetEpisodes(int podcast_id);
PodcastEpisode GetEpisodeById(int id);
PodcastEpisode GetEpisodeByUrl(const QUrl& url);
PodcastEpisode GetEpisodeByUrlOrLocalUrl(const QUrl& url);
// Returns a list of episodes that have local data (downloaded=true) but were
// last listened to before the given QDateTime. This query is NOT indexed so
// it involves a full search of the table.
PodcastEpisodeList GetOldDownloadedEpisodes(const QDateTime& max_listened_date);
// Adds episodes to the database. Every episode must have a valid
// podcast_database_id set already.
void AddEpisodes(PodcastEpisodeList* episodes);
// Updates the editable fields (listened, downloaded, and local_url) on
// episodes that must already exist in the database.
// Updates the editable fields (listened, listened_date, downloaded, and
// local_url) on episodes that must already exist in the database.
void UpdateEpisodes(const PodcastEpisodeList& episodes);
signals:

View File

@ -20,6 +20,7 @@
#include "core/application.h"
#include "core/logging.h"
#include "core/network.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "library/librarydirectorymodel.h"
#include "library/librarymodel.h"
@ -29,9 +30,10 @@
#include <QFile>
#include <QNetworkReply>
#include <QSettings>
#include <QTimer>
const char* PodcastDownloader::kSettingsGroup = "Podcasts";
const int PodcastDownloader::kAutoDeleteCheckIntervalMsec = 15 * 60 * kMsecPerSec; // 15 minutes
struct PodcastDownloader::Task {
Task() : file(NULL) {}
@ -47,14 +49,21 @@ PodcastDownloader::PodcastDownloader(Application* app, QObject* parent)
backend_(app_->podcast_backend()),
network_(new NetworkAccessManager(this)),
disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"),
auto_download_(false),
delete_after_secs_(0),
current_task_(NULL),
last_progress_signal_(0)
last_progress_signal_(0),
auto_delete_timer_(new QTimer(this))
{
connect(backend_, SIGNAL(EpisodesAdded(QList<PodcastEpisode>)),
SLOT(EpisodesAdded(QList<PodcastEpisode>)));
connect(backend_, SIGNAL(SubscriptionAdded(Podcast)),
SLOT(SubscriptionAdded(Podcast)));
connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
connect(auto_delete_timer_, SIGNAL(timeout()), SLOT(AutoDelete()));
auto_delete_timer_->setInterval(kAutoDeleteCheckIntervalMsec);
auto_delete_timer_->start();
ReloadSettings();
}
@ -77,6 +86,7 @@ void PodcastDownloader::ReloadSettings() {
auto_download_ = s.value("auto_download", false).toBool();
download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString();
delete_after_secs_ = s.value("delete_after", 0).toInt();
}
void PodcastDownloader::DownloadEpisode(const PodcastEpisode& episode) {
@ -244,3 +254,25 @@ void PodcastDownloader::EpisodesAdded(const QList<PodcastEpisode>& episodes) {
}
}
}
void PodcastDownloader::AutoDelete() {
if (delete_after_secs_ <= 0) {
return;
}
QDateTime max_date = QDateTime::currentDateTime();
max_date.addSecs(-delete_after_secs_);
PodcastEpisodeList old_episodes = backend_->GetOldDownloadedEpisodes(max_date);
if (old_episodes.isEmpty())
return;
qLog(Info) << "Deleting" << old_episodes.count()
<< "episodes because they were last listened to"
<< (delete_after_secs_ / kSecsPerDay)
<< "days ago";
foreach (const PodcastEpisode& episode, old_episodes) {
DeleteEpisode(episode);
}
}

View File

@ -46,6 +46,7 @@ public:
};
static const char* kSettingsGroup;
static const int kAutoDeleteCheckIntervalMsec;
QString DefaultDownloadDir() const;
@ -70,6 +71,8 @@ private slots:
void ReplyFinished();
void ReplyDownloadProgress(qint64 received, qint64 total);
void AutoDelete();
private:
struct Task;
@ -89,12 +92,15 @@ private:
bool auto_download_;
QString download_dir_;
int delete_after_secs_;
Task* current_task_;
QQueue<Task*> queued_tasks_;
QSet<int> downloading_episode_ids_;
time_t last_progress_signal_;
QTimer* auto_delete_timer_;
};
#endif // PODCASTDOWNLOADER_H

View File

@ -29,7 +29,7 @@
const QStringList PodcastEpisode::kColumns = QStringList()
<< "podcast_id" << "title" << "description" << "author"
<< "publication_date" << "duration_secs" << "url" << "listened"
<< "downloaded" << "local_url" << "extra";
<< "listened_date" << "downloaded" << "local_url" << "extra";
const QString PodcastEpisode::kColumnSpec = PodcastEpisode::kColumns.join(", ");
const QString PodcastEpisode::kJoinSpec = Utilities::Prepend("e.", PodcastEpisode::kColumns).join(", ");
@ -50,8 +50,9 @@ struct PodcastEpisode::Private : public QSharedData {
QUrl url_;
bool listened_;
bool downloaded_;
QDateTime listened_date_;
bool downloaded_;
QUrl local_url_;
QVariantMap extra_;
@ -93,6 +94,7 @@ const QDateTime& PodcastEpisode::publication_date() const { return d->publicatio
int PodcastEpisode::duration_secs() const { return d->duration_secs_; }
const QUrl& PodcastEpisode::url() const { return d->url_; }
bool PodcastEpisode::listened() const { return d->listened_; }
const QDateTime& PodcastEpisode::listened_date() const { return d->listened_date_; }
bool PodcastEpisode::downloaded() const { return d->downloaded_; }
const QUrl& PodcastEpisode::local_url() const { return d->local_url_; }
const QVariantMap& PodcastEpisode::extra() const { return d->extra_; }
@ -107,6 +109,7 @@ void PodcastEpisode::set_publication_date(const QDateTime& v) { d->publication_d
void PodcastEpisode::set_duration_secs(int v) { d->duration_secs_ = v; }
void PodcastEpisode::set_url(const QUrl& v) { d->url_ = v; }
void PodcastEpisode::set_listened(bool v) { d->listened_ = v; }
void PodcastEpisode::set_listened_date(const QDateTime& v) { d->listened_date_ = v; }
void PodcastEpisode::set_downloaded(bool v) { d->downloaded_ = v; }
void PodcastEpisode::set_local_url(const QUrl& v) { d->local_url_ = v; }
void PodcastEpisode::set_extra(const QVariantMap& v) { d->extra_ = v; }
@ -122,10 +125,11 @@ void PodcastEpisode::InitFromQuery(const QSqlQuery& query) {
d->duration_secs_ = query.value(6).toInt();
d->url_ = QUrl::fromEncoded(query.value(7).toByteArray());
d->listened_ = query.value(8).toBool();
d->downloaded_ = query.value(9).toBool();
d->local_url_ = QUrl::fromEncoded(query.value(10).toByteArray());
d->listened_date_ = QDateTime::fromTime_t(query.value(9).toUInt());
d->downloaded_ = query.value(10).toBool();
d->local_url_ = QUrl::fromEncoded(query.value(11).toByteArray());
QDataStream extra_stream(query.value(11).toByteArray());
QDataStream extra_stream(query.value(12).toByteArray());
extra_stream >> d->extra_;
}
@ -138,6 +142,7 @@ void PodcastEpisode::BindToQuery(QSqlQuery* query) const {
query->bindValue(":duration_secs", d->duration_secs_);
query->bindValue(":url", d->url_.toEncoded());
query->bindValue(":listened", d->listened_);
query->bindValue(":listened_date", d->listened_date_.toTime_t());
query->bindValue(":downloaded", d->downloaded_);
query->bindValue(":local_url", d->local_url_.toEncoded());

View File

@ -55,6 +55,7 @@ public:
int duration_secs() const;
const QUrl& url() const;
bool listened() const;
const QDateTime& listened_date() const;
bool downloaded() const;
const QUrl& local_url() const;
const QVariantMap& extra() const;
@ -69,6 +70,7 @@ public:
void set_duration_secs(int v);
void set_url(const QUrl& v);
void set_listened(bool v);
void set_listened_date(const QDateTime& v);
void set_downloaded(bool v);
void set_local_url(const QUrl& v);
void set_extra(const QVariantMap& v);

View File

@ -511,6 +511,7 @@ void PodcastService::CurrentSongChanged(const Song& metadata) {
// Mark it as listened if it's not already
if (!episode.listened()) {
episode.set_listened(true);
episode.set_listened_date(QDateTime::currentDateTime());
backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
}
}
@ -526,9 +527,14 @@ void PodcastService::SetListened() {
void PodcastService::SetListened(const QModelIndexList& indexes, bool listened) {
PodcastEpisodeList episodes;
QDateTime current_date_time = QDateTime::currentDateTime();
foreach (const QModelIndex& index, indexes) {
PodcastEpisode episode = index.data(Role_Episode).value<PodcastEpisode>();
episode.set_listened(listened);
if (listened) {
episode.set_listened_date(current_date_time);
}
episodes << episode;
}

View File

@ -21,6 +21,7 @@
#include "ui_podcastsettingspage.h"
#include "core/application.h"
#include "core/closure.h"
#include "core/timeconstants.h"
#include "library/librarydirectorymodel.h"
#include "library/librarymodel.h"
#include "ui/settingsdialog.h"
@ -73,8 +74,7 @@ void PodcastSettingsPage::Load() {
s.value("download_dir", default_download_dir).toString()));
ui_->auto_download->setChecked(s.value("auto_download", false).toBool());
ui_->delete_after->setValue(s.value("delete_after", 0).toInt() / (24*60*60));
ui_->delete_unplayed->setChecked(s.value("delete_unplayed", false).toBool());
ui_->delete_after->setValue(s.value("delete_after", 0).toInt() / kSecsPerDay);
ui_->username->setText(s.value("gpodder_username").toString());
ui_->device_name->setText(s.value("gpodder_device_name", GPodderSync::DefaultDeviceName()).toString());
@ -93,8 +93,7 @@ void PodcastSettingsPage::Save() {
ui_->check_interval->itemData(ui_->check_interval->currentIndex()));
s.setValue("download_dir", QDir::fromNativeSeparators(ui_->download_dir->text()));
s.setValue("auto_download", ui_->auto_download->isChecked());
s.setValue("delete_after", ui_->delete_after->value());
s.setValue("delete_unplayed", ui_->delete_unplayed->isChecked());
s.setValue("delete_after", ui_->delete_after->value() * kSecsPerDay);
s.setValue("gpodder_device_name", ui_->device_name->text());
}

View File

@ -112,6 +112,9 @@
<string>Cleaning up</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
@ -138,13 +141,6 @@
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="delete_unplayed">
<property name="text">
<string>Also delete unplayed episodes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -256,7 +252,6 @@
<tabstop>download_dir_browse</tabstop>
<tabstop>auto_download</tabstop>
<tabstop>delete_after</tabstop>
<tabstop>delete_unplayed</tabstop>
<tabstop>username</tabstop>
<tabstop>password</tabstop>
<tabstop>device_name</tabstop>