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
This commit is contained in:
Gavin D. Howard 2020-02-07 15:18:18 -07:00 committed by GitHub
parent ab7b65a30b
commit 691f5d99ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 254 additions and 16 deletions

View File

@ -158,6 +158,7 @@ void SCollection::ResumeWatcher() { watcher_->SetRescanPausedAsync(false); }
void SCollection::ReloadSettings() {
watcher_->ReloadSettingsAsync();
model_->ReloadSettings();
}

View File

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

View File

@ -188,6 +188,8 @@ class CollectionBackend : public CollectionBackendInterface {
SongList GetSongsBySongId(const QList<int> &song_ids);
SongList GetSongsBySongId(const QStringList &song_ids);
Song::Source Source() const;
public slots:
void Exit();
void LoadDirectories();

View File

@ -46,6 +46,7 @@
#include <QImage>
#include <QPixmapCache>
#include <QSettings>
#include <QStandardPaths>
#include <QtDebug>
#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<QIODevice> 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<QIODevice> 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<QUrl> *urls, SongList *songs, QSet<int> *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;

View File

@ -43,6 +43,7 @@
#include <QImage>
#include <QIcon>
#include <QPixmap>
#include <QNetworkDiskCache>
#include <QSettings>
#include "core/simpletreemodel.h"
@ -69,7 +70,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
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<CollectionItem> {
// 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<CollectionModel::QueryResult> 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_;

View File

@ -126,6 +126,7 @@ signals:
void SettingsChanged();
void SettingsDialogRequested(SettingsDialog::Page page);
void ExitFinished();
void ClearPixmapDiskCache();
private:
std::unique_ptr<ApplicationImpl> p_;

View File

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

View File

@ -34,6 +34,7 @@
#include <QPushButton>
#include <QSettings>
#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();
}

View File

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

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>509</width>
<height>746</height>
<height>913</height>
</rect>
</property>
<property name="windowTitle">
@ -249,6 +249,99 @@ If there are no matches then it will use the largest image in the directory.</st
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="albumArtGroupBox">
<property name="title">
<string>Album art cache</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_cache_size">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinbox_cache_size">
<property name="maximum">
<number>1048576</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combobox_cache_size">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="checkbox_disk_cache">
<property name="text">
<string>Enable Disk Cache</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_disk_cache">
<property name="text">
<string>Clear Disk Cache</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_disk_cache_size">
<property name="text">
<string>Disk Cache Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinbox_disk_cache_size">
<property name="maximum">
<number>1048576</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combobox_disk_cache_size">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>