From 5327b72f0bb773cf112817370152d809593fddc8 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Mon, 12 Mar 2012 19:35:47 +0000 Subject: [PATCH] Implement the "Delete after x days" feature for listened podcasts. --- data/schema/schema-37.sql | 1 + src/core/timeconstants.h | 2 ++ src/podcasts/podcastbackend.cpp | 26 ++++++++++++++++++++ src/podcasts/podcastbackend.h | 12 +++++++--- src/podcasts/podcastdownloader.cpp | 36 ++++++++++++++++++++++++++-- src/podcasts/podcastdownloader.h | 6 +++++ src/podcasts/podcastepisode.cpp | 15 ++++++++---- src/podcasts/podcastepisode.h | 2 ++ src/podcasts/podcastservice.cpp | 6 +++++ src/podcasts/podcastsettingspage.cpp | 7 +++--- src/podcasts/podcastsettingspage.ui | 11 +++------ 11 files changed, 102 insertions(+), 22 deletions(-) diff --git a/data/schema/schema-37.sql b/data/schema/schema-37.sql index 27ea81a6f..6e7b4eb48 100644 --- a/data/schema/schema-37.sql +++ b/data/schema/schema-37.sql @@ -27,6 +27,7 @@ CREATE TABLE podcast_episodes ( url TEXT, listened BOOLEAN, + listened_date INTEGER, downloaded BOOLEAN, local_url TEXT, diff --git a/src/core/timeconstants.h b/src/core/timeconstants.h index 42bf6208d..96242d659 100644 --- a/src/core/timeconstants.h +++ b/src/core/timeconstants.h @@ -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 diff --git a/src/podcasts/podcastbackend.cpp b/src/podcasts/podcastbackend.cpp index 49a564be1..f87eb8de3 100644 --- a/src/podcasts/podcastbackend.cpp +++ b/src/podcasts/podcastbackend.cpp @@ -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; +} diff --git a/src/podcasts/podcastbackend.h b/src/podcasts/podcastbackend.h index 3d5dc39b6..f09f4742b 100644 --- a/src/podcasts/podcastbackend.h +++ b/src/podcasts/podcastbackend.h @@ -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: diff --git a/src/podcasts/podcastdownloader.cpp b/src/podcasts/podcastdownloader.cpp index b01e5b17a..476b7d628 100644 --- a/src/podcasts/podcastdownloader.cpp +++ b/src/podcasts/podcastdownloader.cpp @@ -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 #include #include +#include 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)), SLOT(EpisodesAdded(QList))); 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& 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); + } +} diff --git a/src/podcasts/podcastdownloader.h b/src/podcasts/podcastdownloader.h index ad9f783e3..b12c9083a 100644 --- a/src/podcasts/podcastdownloader.h +++ b/src/podcasts/podcastdownloader.h @@ -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 queued_tasks_; QSet downloading_episode_ids_; time_t last_progress_signal_; + + QTimer* auto_delete_timer_; }; #endif // PODCASTDOWNLOADER_H diff --git a/src/podcasts/podcastepisode.cpp b/src/podcasts/podcastepisode.cpp index b2e0b0b87..1def8bb81 100644 --- a/src/podcasts/podcastepisode.cpp +++ b/src/podcasts/podcastepisode.cpp @@ -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()); diff --git a/src/podcasts/podcastepisode.h b/src/podcasts/podcastepisode.h index 429be2d09..92505f98c 100644 --- a/src/podcasts/podcastepisode.h +++ b/src/podcasts/podcastepisode.h @@ -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); diff --git a/src/podcasts/podcastservice.cpp b/src/podcasts/podcastservice.cpp index 129c4c62e..91ab8af70 100644 --- a/src/podcasts/podcastservice.cpp +++ b/src/podcasts/podcastservice.cpp @@ -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(); episode.set_listened(listened); + if (listened) { + episode.set_listened_date(current_date_time); + } episodes << episode; } diff --git a/src/podcasts/podcastsettingspage.cpp b/src/podcasts/podcastsettingspage.cpp index 2120993f0..f8f92e029 100644 --- a/src/podcasts/podcastsettingspage.cpp +++ b/src/podcasts/podcastsettingspage.cpp @@ -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()); } diff --git a/src/podcasts/podcastsettingspage.ui b/src/podcasts/podcastsettingspage.ui index 9151a0c8a..220799237 100644 --- a/src/podcasts/podcastsettingspage.ui +++ b/src/podcasts/podcastsettingspage.ui @@ -112,6 +112,9 @@ Cleaning up + + QFormLayout::AllNonFixedFieldsGrow + @@ -138,13 +141,6 @@ - - - - Also delete unplayed episodes - - - @@ -256,7 +252,6 @@ download_dir_browse auto_download delete_after - delete_unplayed username password device_name