From 691f5d99ca878c6e546b5cf09e231f4ed13a307c Mon Sep 17 00:00:00 2001 From: "Gavin D. Howard" Date: Fri, 7 Feb 2020 15:18:18 -0700 Subject: [PATCH] Implement disk caching of album art (#360) * Implement disk caching of album art This includes a button to clear the cache in the settings, as requested. Closes #358 * Make the cache size defaults match * Implement the review by jonaski * Fix more problems with the PR --- src/collection/collection.cpp | 1 + src/collection/collectionbackend.cpp | 4 ++ src/collection/collectionbackend.h | 2 + src/collection/collectionmodel.cpp | 85 ++++++++++++++++++++-- src/collection/collectionmodel.h | 11 ++- src/core/application.h | 1 + src/core/song.cpp | 20 +++--- src/settings/collectionsettingspage.cpp | 36 ++++++++++ src/settings/collectionsettingspage.h | 15 ++++ src/settings/collectionsettingspage.ui | 95 ++++++++++++++++++++++++- 10 files changed, 254 insertions(+), 16 deletions(-) diff --git a/src/collection/collection.cpp b/src/collection/collection.cpp index 79831e541..f286740ed 100644 --- a/src/collection/collection.cpp +++ b/src/collection/collection.cpp @@ -158,6 +158,7 @@ void SCollection::ResumeWatcher() { watcher_->SetRescanPausedAsync(false); } void SCollection::ReloadSettings() { watcher_->ReloadSettingsAsync(); + model_->ReloadSettings(); } diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index 585d65600..6cea2c052 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -899,6 +899,10 @@ SongList CollectionBackend::GetCompilationSongs(const QString &album, const Quer } +Song::Source CollectionBackend::Source() const { + return source_; +} + void CollectionBackend::UpdateCompilations() { QMutexLocker l(db_->Mutex()); diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index bd49a2d15..cdfad377e 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -188,6 +188,8 @@ class CollectionBackend : public CollectionBackendInterface { SongList GetSongsBySongId(const QList &song_ids); SongList GetSongsBySongId(const QStringList &song_ids); + Song::Source Source() const; + public slots: void Exit(); void LoadDirectories(); diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp index b206fb627..30b4ae9b4 100644 --- a/src/collection/collectionmodel.cpp +++ b/src/collection/collectionmodel.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include "core/application.h" @@ -63,6 +64,7 @@ #include "playlist/playlistmanager.h" #include "playlist/songmimedata.h" #include "covermanager/albumcoverloader.h" +#include "settings/collectionsettingspage.h" using std::bind; using std::sort; @@ -71,7 +73,9 @@ using std::placeholders::_2; const char *CollectionModel::kSavedGroupingsSettingsGroup = "SavedGroupings"; const int CollectionModel::kPrettyCoverSize = 32; -const int CollectionModel::kPixmapCacheLimit = QPixmapCache::cacheLimit() * 8; +const char *CollectionModel::kPixmapDiskCacheDir = "/pixmapcache"; + +QNetworkDiskCache *CollectionModel::sIconCache = nullptr; static bool IsArtistGroupBy(const CollectionModel::GroupBy by) { return by == CollectionModel::GroupBy_Artist || by == CollectionModel::GroupBy_AlbumArtist; @@ -96,7 +100,8 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q playlist_icon_(IconLoader::Load("albums")), init_task_id_(-1), use_pretty_covers_(false), - show_dividers_(true) { + show_dividers_(true), + use_disk_cache_(false) { root_->lazy_loaded = true; @@ -115,6 +120,11 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); //no_cover_icon_ = QPixmap(":/pictures/noalbumart.png").scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + // When running under gdb, all calls to this constructor came from the same thread. + // If this ever changes, these two lines might need to be protected by a mutex. + if (sIconCache == nullptr) + sIconCache = new QNetworkDiskCache(this); + connect(backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList))); connect(backend_, SIGNAL(SongsDeleted(SongList)), SLOT(SongsDeleted(SongList))); connect(backend_, SIGNAL(DatabaseReset()), SLOT(Reset())); @@ -127,7 +137,9 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q backend_->UpdateTotalArtistCountAsync(); backend_->UpdateTotalAlbumCountAsync(); - QPixmapCache::setCacheLimit(kPixmapCacheLimit); + connect(app_, SIGNAL(ClearPixmapDiskCache()), SLOT(ClearDiskCache())); + + ReloadSettings(); } @@ -165,6 +177,26 @@ void CollectionModel::SaveGrouping(QString name) { } +void CollectionModel::ReloadSettings() { + + QSettings s; + + s.beginGroup(CollectionSettingsPage::kSettingsGroup); + + use_disk_cache_ = s.value(CollectionSettingsPage::kSettingsDiskCacheEnable, false).toBool(); + + if (!use_disk_cache_) { + sIconCache->clear(); + } + + sIconCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + kPixmapDiskCacheDir); + sIconCache->setMaximumCacheSize(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsDiskCacheSize, CollectionSettingsPage::kSettingsDiskCacheSizeUnit)); + + QPixmapCache::setCacheLimit(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsCacheSize, CollectionSettingsPage::kSettingsCacheSizeUnit) / 1024); + + s.endGroup(); +} + void CollectionModel::Init(bool async) { if (async) { @@ -476,6 +508,7 @@ void CollectionModel::SongsDeleted(const SongList &songs) { // Remove from pixmap cache const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(node)); QPixmapCache::remove(cache_key); + if (use_disk_cache_) sIconCache->remove(QUrl(cache_key)); if (pending_cache_keys_.contains(cache_key)) { pending_cache_keys_.remove(cache_key); } @@ -532,7 +565,7 @@ QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const { idx_copy = idx_copy.parent(); } - return "collectionart:" + path.join("/"); + return Song::TextForSource(backend_->Source()) + path.join("/"); } @@ -549,6 +582,18 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) { return cached_pixmap; } + // Try to load it from the disk cache + if (use_disk_cache_) { + std::unique_ptr cache(sIconCache->data(QUrl(cache_key))); + if (cache) { + QImage cached_pixmap; + if (cached_pixmap.load(cache.get(), "XPM")) { + QPixmapCache::insert(cache_key, QPixmap::fromImage(cached_pixmap)); + return QPixmap::fromImage(cached_pixmap); + } + } + } + // Maybe we're loading a pixmap already? if (pending_cache_keys_.contains(cache_key)) { return no_cover_icon_; @@ -591,6 +636,21 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, QPixmapCache::insert(cache_key, image_pixmap); } + // If we have a valid cover not already in the disk cache + if (use_disk_cache_) { + std::unique_ptr cached_img(sIconCache->data(QUrl(cache_key))); + if (!cached_img && !image.isNull()) { + QNetworkCacheMetaData item_metadata; + item_metadata.setSaveToDisk(true); + item_metadata.setUrl(QUrl(cache_key)); + QIODevice* cache = sIconCache->prepare(item_metadata); + if (cache) { + image.save(cache, "XPM"); + sIconCache->insert(cache); + } + } + } + const QModelIndex idx = ItemToIndex(item); if (!idx.isValid()) return; @@ -1479,6 +1539,19 @@ bool CollectionModel::CompareItems(const CollectionItem *a, const CollectionItem } +int CollectionModel::MaximumCacheSize(QSettings *s, const char *size_id, const char *size_unit_id) const { + + int size = s->value(size_id, 80).toInt(); + int unit = s->value(size_unit_id, CollectionSettingsPage::CacheSizeUnit::CacheSizeUnit_MB).toInt() + 1; + + do { + size *= 1024; + unit -= 1; + } while (unit > 0); + + return size; +} + void CollectionModel::GetChildSongs(CollectionItem *item, QList *urls, SongList *songs, QSet *song_ids) const { switch (item->type) { @@ -1606,6 +1679,10 @@ void CollectionModel::TotalAlbumCountUpdatedSlot(int count) { } +void CollectionModel::ClearDiskCache() { + sIconCache->clear(); +} + QDataStream &operator<<(QDataStream &s, const CollectionModel::Grouping &g) { s << quint32(g.first) << quint32(g.second) << quint32(g.third); return s; diff --git a/src/collection/collectionmodel.h b/src/collection/collectionmodel.h index bff1618c9..d8e898621 100644 --- a/src/collection/collectionmodel.h +++ b/src/collection/collectionmodel.h @@ -43,6 +43,7 @@ #include #include #include +#include #include #include "core/simpletreemodel.h" @@ -69,7 +70,7 @@ class CollectionModel : public SimpleTreeModel { static const char *kSavedGroupingsSettingsGroup; static const int kPrettyCoverSize; - static const int kPixmapCacheLimit; + static const char *kPixmapDiskCacheDir; enum Role { Role_Type = Qt::UserRole + 1, @@ -162,6 +163,9 @@ class CollectionModel : public SimpleTreeModel { // Save the current grouping void SaveGrouping(QString name); + // Reload settings. + void ReloadSettings(); + // Utility functions for manipulating text static QString TextOrUnknown(const QString &text); static QString PrettyYearAlbum(const int year, const QString &album); @@ -203,6 +207,7 @@ signals: void TotalSongCountUpdatedSlot(int count); void TotalArtistCountUpdatedSlot(int count); void TotalAlbumCountUpdatedSlot(int count); + void ClearDiskCache(); // Called after ResetAsync void ResetAsyncQueryFinished(QFuture future); @@ -244,6 +249,7 @@ signals: QVariant AlbumIcon(const QModelIndex &idx); QVariant data(const CollectionItem *item, int role) const; bool CompareItems(const CollectionItem *a, const CollectionItem *b) const; + int MaximumCacheSize(QSettings *s, const char *size_id, const char *size_unit_id) const; private: CollectionBackend *backend_; @@ -274,10 +280,13 @@ signals: QIcon playlists_dir_icon_; QIcon playlist_icon_; + static QNetworkDiskCache *sIconCache; + int init_task_id_; bool use_pretty_covers_; bool show_dividers_; + bool use_disk_cache_; AlbumCoverLoaderOptions cover_loader_options_; diff --git a/src/core/application.h b/src/core/application.h index 60bd726ed..d94d5fb2a 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -126,6 +126,7 @@ signals: void SettingsChanged(); void SettingsDialogRequested(SettingsDialog::Page page); void ExitFinished(); + void ClearPixmapDiskCache(); private: std::unique_ptr p_; diff --git a/src/core/song.cpp b/src/core/song.cpp index 40029b8ef..d7bca02c0 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -469,17 +469,17 @@ Song::Source Song::SourceFromURL(const QUrl &url) { QString Song::TextForSource(Source source) { switch (source) { - case Song::Source_LocalFile: return QObject::tr("File"); - case Song::Source_Collection: return QObject::tr("Collection"); - case Song::Source_CDDA: return QObject::tr("CD"); - case Song::Source_Device: return QObject::tr("Device"); - case Song::Source_Stream: return QObject::tr("Stream"); - case Song::Source_Tidal: return QObject::tr("Tidal"); - case Song::Source_Subsonic: return QObject::tr("subsonic"); - case Song::Source_Qobuz: return QObject::tr("qobuz"); - case Song::Source_Unknown: return QObject::tr("Unknown"); + case Song::Source_LocalFile: return "file"; + case Song::Source_Collection: return "collection"; + case Song::Source_CDDA: return "cd"; + case Song::Source_Device: return "device"; + case Song::Source_Stream: return "stream"; + case Song::Source_Tidal: return "tidal"; + case Song::Source_Subsonic: return "subsonic"; + case Song::Source_Qobuz: return "qobuz"; + case Song::Source_Unknown: return "unknown"; } - return QObject::tr("Unknown"); + return "unknown"; } diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp index c4f4d791e..041c13bc8 100644 --- a/src/settings/collectionsettingspage.cpp +++ b/src/settings/collectionsettingspage.cpp @@ -34,6 +34,7 @@ #include #include +#include "core/application.h" #include "core/iconloader.h" #include "collection/collectiondirectorymodel.h" #include "collectionsettingspage.h" @@ -43,6 +44,13 @@ #include "ui_collectionsettingspage.h" const char *CollectionSettingsPage::kSettingsGroup = "Collection"; +const char *CollectionSettingsPage::kSettingsCacheSize = "cache_size"; +const char *CollectionSettingsPage::kSettingsCacheSizeUnit = "cache_size_unit"; +const char *CollectionSettingsPage::kSettingsDiskCacheEnable = "disk_cache_enable"; +const char *CollectionSettingsPage::kSettingsDiskCacheSize = "disk_cache_size"; +const char *CollectionSettingsPage::kSettingsDiskCacheSizeUnit = "disk_cache_size_unit"; + +const QStringList CollectionSettingsPage::cacheUnitNames = { "KB", "MB", "GB", "TB" }; CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog) : SettingsPage(dialog), @@ -91,6 +99,14 @@ void CollectionSettingsPage::CurrentRowChanged(const QModelIndex& index) { ui_->remove->setEnabled(index.isValid()); } +void CollectionSettingsPage::DiskCacheEnable(int state) { + bool checked = state == Qt::Checked; + ui_->button_disk_cache->setEnabled(checked); + ui_->label_disk_cache_size->setEnabled(checked); + ui_->spinbox_disk_cache_size->setEnabled(checked); + ui_->combobox_disk_cache_size->setEnabled(checked); +} + void CollectionSettingsPage::Load() { if (!initialised_model_) { @@ -130,6 +146,20 @@ void CollectionSettingsPage::Load() { ui_->checkbox_cover_lowercase->setChecked(s.value("cover_lowercase", true).toBool()); ui_->checkbox_cover_replace_spaces->setChecked(s.value("cover_replace_spaces", true).toBool()); + ui_->spinbox_cache_size->setValue(s.value(kSettingsCacheSize, 80).toInt()); + ui_->combobox_cache_size->addItems(cacheUnitNames); + ui_->combobox_cache_size->setCurrentIndex(s.value(kSettingsCacheSizeUnit, (int) CacheSizeUnit_MB).toInt()); + ui_->checkbox_disk_cache->setChecked(s.value(kSettingsDiskCacheEnable, false).toBool()); + ui_->label_disk_cache_size->setEnabled(ui_->checkbox_disk_cache->isChecked()); + ui_->spinbox_disk_cache_size->setEnabled(ui_->checkbox_disk_cache->isChecked()); + ui_->spinbox_disk_cache_size->setValue(s.value(kSettingsDiskCacheSize, 80).toInt()); + ui_->combobox_disk_cache_size->setEnabled(ui_->checkbox_disk_cache->isChecked()); + ui_->combobox_disk_cache_size->addItems(cacheUnitNames); + ui_->combobox_disk_cache_size->setCurrentIndex(s.value(kSettingsDiskCacheSizeUnit, (int) CacheSizeUnit_MB).toInt()); + + connect(ui_->checkbox_disk_cache, SIGNAL(stateChanged(int)), SLOT(DiskCacheEnable(int))); + connect(ui_->button_disk_cache, SIGNAL(clicked()), dialog()->app(), SIGNAL(ClearPixmapDiskCache())); + s.endGroup(); } @@ -161,6 +191,12 @@ void CollectionSettingsPage::Save() { s.setValue("cover_lowercase", ui_->checkbox_cover_lowercase->isChecked()); s.setValue("cover_replace_spaces", ui_->checkbox_cover_replace_spaces->isChecked()); + s.setValue(kSettingsCacheSize, ui_->spinbox_cache_size->value()); + s.setValue(kSettingsCacheSizeUnit, ui_->combobox_cache_size->currentIndex()); + s.setValue(kSettingsDiskCacheEnable, ui_->checkbox_disk_cache->isChecked()); + s.setValue(kSettingsDiskCacheSize, ui_->spinbox_disk_cache_size->value()); + s.setValue(kSettingsDiskCacheSizeUnit, ui_->combobox_disk_cache_size->currentIndex()); + s.endGroup(); } diff --git a/src/settings/collectionsettingspage.h b/src/settings/collectionsettingspage.h index 82b63ab4f..6d58420b6 100644 --- a/src/settings/collectionsettingspage.h +++ b/src/settings/collectionsettingspage.h @@ -39,6 +39,18 @@ public: ~CollectionSettingsPage(); static const char *kSettingsGroup; + static const char *kSettingsCacheSize; + static const char *kSettingsCacheSizeUnit; + static const char *kSettingsDiskCacheEnable; + static const char *kSettingsDiskCacheSize; + static const char *kSettingsDiskCacheSizeUnit; + + enum CacheSizeUnit { + CacheSizeUnit_KB, + CacheSizeUnit_MB, + CacheSizeUnit_GB, + CacheSizeUnit_TB, + }; enum SaveCover { SaveCover_Hash = 1, @@ -53,11 +65,14 @@ private slots: void Remove(); void CurrentRowChanged(const QModelIndex &index); + void DiskCacheEnable(int state); void CoverSaveInAlbumDirChanged(); private: Ui_CollectionSettingsPage *ui_; bool initialised_model_; + + static const QStringList cacheUnitNames; }; #endif // COLLECTIONSETTINGSPAGE_H diff --git a/src/settings/collectionsettingspage.ui b/src/settings/collectionsettingspage.ui index b4a337ba9..d70ab41f5 100644 --- a/src/settings/collectionsettingspage.ui +++ b/src/settings/collectionsettingspage.ui @@ -7,7 +7,7 @@ 0 0 509 - 746 + 913 @@ -249,6 +249,99 @@ If there are no matches then it will use the largest image in the directory. + + + + Album art cache + + + + + + + + + + + 0 + 0 + + + + Size + + + + + + + 1048576 + + + + + + + + 0 + 0 + + + + + + + + + + + + Enable Disk Cache + + + + + + + Clear Disk Cache + + + + + + + + + + + Disk Cache Size + + + + + + + 1048576 + + + + + + + + 0 + 0 + + + + + + + + + + +