Improve album cover searching and cover manager, use HttpStatusCodeAttribute and QSslError for services

- Improve album cover manager
- Change art_automatic and art_manual to QUrl
- Refresh collection album covers when new album covers are fetched
- Fix automatic album cover searching for local files outside of the collection
- Make all Json services check HttpStatusCodeAttribute
- Show detailed SSL errors for Subsonic, Tidal and Qobuz
This commit is contained in:
Jonas Kvinge
2019-07-07 21:14:24 +02:00
parent c92a7967ea
commit 65780e1672
101 changed files with 1531 additions and 1239 deletions

View File

@@ -976,8 +976,8 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
info.artist = compilation ? QString() : query.Value(1).toString();
info.album_artist = compilation ? QString() : query.Value(2).toString();
info.album_name = query.Value(0).toString();
info.art_automatic = query.Value(5).toString();
info.art_manual = query.Value(6).toString();
info.art_automatic = query.Value(5).toUrl();
info.art_manual = query.Value(6).toUrl();
info.first_url = QUrl::fromEncoded(query.Value(7).toByteArray());
if ((info.artist == last_artist || info.album_artist == last_album_artist) && info.album_name == last_album)
@@ -1015,8 +1015,8 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &artist, c
if (!ExecQuery(&query)) return ret;
if (query.Next()) {
ret.art_automatic = query.Value(0).toString();
ret.art_manual = query.Value(1).toString();
ret.art_automatic = query.Value(0).toUrl();
ret.art_manual = query.Value(1).toUrl();
ret.first_url = QUrl::fromEncoded(query.Value(2).toByteArray());
}
@@ -1024,13 +1024,13 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &artist, c
}
void CollectionBackend::UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QString &art) {
void CollectionBackend::UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url) {
metaObject()->invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, artist), Q_ARG(QString, albumartist), Q_ARG(QString, album), Q_ARG(QString, art));
metaObject()->invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, artist), Q_ARG(QString, albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url));
}
void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QString &art) {
void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
@@ -1040,10 +1040,10 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QStrin
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("album", album);
if (!albumartist.isNull() && !albumartist.isEmpty()) {
if (!albumartist.isEmpty()) {
query.AddWhere("albumartist", albumartist);
}
else if (!artist.isNull()) {
else if (!artist.isEmpty()) {
query.AddWhere("artist", artist);
}
@@ -1057,7 +1057,7 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QStrin
}
// Update the songs
QString sql(QString("UPDATE %1 SET art_manual = :art WHERE album = :album AND unavailable = 0").arg(songs_table_));
QString sql(QString("UPDATE %1 SET art_manual = :cover WHERE album = :album AND unavailable = 0").arg(songs_table_));
if (!albumartist.isNull() && !albumartist.isEmpty()) {
sql += " AND albumartist = :albumartist";
@@ -1068,12 +1068,12 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QStrin
QSqlQuery q(db);
q.prepare(sql);
q.bindValue(":art", art);
q.bindValue(":cover", cover_url);
q.bindValue(":album", album);
if (!albumartist.isNull() && !albumartist.isEmpty()) {
if (!albumartist.isEmpty()) {
q.bindValue(":albumartist", albumartist);
}
else if (!artist.isNull()) {
else if (!artist.isEmpty()) {
q.bindValue(":artist", artist);
}

View File

@@ -52,7 +52,7 @@ class CollectionBackendInterface : public QObject {
struct Album {
Album() {}
Album(const QString &_artist, const QString &_album_artist, const QString &_album_name, const QString &_art_automatic, const QString &_art_manual, const QUrl &_first_url) :
Album(const QString &_artist, const QString &_album_artist, const QString &_album_name, const QUrl &_art_automatic, const QUrl &_art_manual, const QUrl &_first_url) :
artist(_artist),
album_artist(_album_artist),
album_name(_album_name),
@@ -68,8 +68,8 @@ class CollectionBackendInterface : public QObject {
QString album_artist;
QString album_name;
QString art_automatic;
QString art_manual;
QUrl art_automatic;
QUrl art_manual;
QUrl first_url;
};
typedef QList<Album> AlbumList;
@@ -99,7 +99,7 @@ class CollectionBackendInterface : public QObject {
virtual AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) = 0;
virtual AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) = 0;
virtual void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QString &art) = 0;
virtual void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url) = 0;
virtual Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album) = 0;
virtual Song GetSongById(int id) = 0;
@@ -155,7 +155,7 @@ class CollectionBackend : public CollectionBackendInterface {
AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions());
AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions());
void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QString &art);
void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url);
Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album);
Song GetSongById(int id);
@@ -193,7 +193,7 @@ class CollectionBackend : public CollectionBackendInterface {
void MarkSongsUnavailable(const SongList &songs, bool unavailable = true);
void AddOrUpdateSubdirs(const SubdirectoryList &subdirs);
void UpdateCompilations();
void UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QString &art);
void UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url);
void ForceCompilation(const QString &album, const QList<QString> &artists, bool on);
void IncrementPlayCount(int id);
void IncrementSkipCount(int id, float progress);

View File

@@ -107,10 +107,7 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
cover_loader_options_.scale_output_image_ = true;
if (app_)
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(quint64, QImage)));
//icon_cache_->setCacheDirectory(Utilities::GetConfigPath(Utilities::Path_CacheRoot) + "/pixmapcache");
//icon_cache_->setMaximumCacheSize(CollectionModel::kIconCacheSize);
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(AlbumCoverLoaded(quint64, QUrl, QImage)));
QIcon nocover = IconLoader::Load("cdcase");
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
@@ -422,7 +419,11 @@ void CollectionModel::SongsDeleted(const SongList &songs) {
if (node->parent != root_) parents << node->parent;
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
QModelIndex idx = ItemToIndex(node->parent);
const QString cache_key = AlbumIconPixmapCacheKey(idx);
QPixmapCache::remove(cache_key);
beginRemoveRows(idx, node->row, node->row);
node->parent->Delete(node->row);
song_nodes_.remove(song.id());
endRemoveRows();
@@ -491,51 +492,47 @@ void CollectionModel::SongsDeleted(const SongList &songs) {
}
QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &index) const {
QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const {
QStringList path;
QModelIndex index_copy(index);
while (index_copy.isValid()) {
path.prepend(index_copy.data().toString());
index_copy = index_copy.parent();
QModelIndex idx_copy(idx);
while (idx_copy.isValid()) {
//const CollectionItem *item = IndexToItem(idx_copy);
//if (item && group_by_[item->container_level] == GroupBy_Album) {
// QString album = idx_copy.data().toString();
// album.remove(Song::kAlbumRemoveDisc);
// path.prepend(album);
//}
//else {
path.prepend(idx_copy.data().toString());
//}
idx_copy = idx_copy.parent();
}
return "collectionart:" + path.join("/");
}
QVariant CollectionModel::AlbumIcon(const QModelIndex &index) {
QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
CollectionItem *item = IndexToItem(index);
CollectionItem *item = IndexToItem(idx);
if (!item) return no_cover_icon_;
// Check the cache for a pixmap we already loaded.
const QString cache_key = AlbumIconPixmapCacheKey(index);
const QString cache_key = AlbumIconPixmapCacheKey(idx);
QPixmap cached_pixmap;
if (QPixmapCache::find(cache_key, &cached_pixmap)) {
return cached_pixmap;
}
#if 0
// Try to load it from the disk cache
std::unique_ptr<QIODevice> cache(icon_cache_->data(QUrl(cache_key)));
if (cache) {
QImage cached_image;
if (cached_image.load(cache.get(), "XPM")) {
QPixmapCache::insert(cache_key, QPixmap::fromImage(cached_image));
return QPixmap::fromImage(cached_image);
}
}
#endif
// Maybe we're loading a pixmap already?
if (pending_cache_keys_.contains(cache_key)) {
return no_cover_icon_;
}
// No art is cached and we're not loading it already. Load art for the first song in the album.
SongList songs = GetChildSongs(index);
SongList songs = GetChildSongs(idx);
if (!songs.isEmpty()) {
const quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, songs.first());
pending_art_[id] = ItemAndCacheKey(item, cache_key);
@@ -546,14 +543,16 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &index) {
}
void CollectionModel::AlbumArtLoaded(quint64 id, const QImage &image) {
void CollectionModel::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) {
if (!pending_art_.contains(id)) return;
ItemAndCacheKey item_and_cache_key = pending_art_.take(id);
CollectionItem *item = item_and_cache_key.first;
const QString &cache_key = item_and_cache_key.second;
if (!item) return;
const QString &cache_key = item_and_cache_key.second;
pending_cache_keys_.remove(cache_key);
// Insert this image in the cache.
@@ -562,35 +561,19 @@ void CollectionModel::AlbumArtLoaded(quint64 id, const QImage &image) {
QPixmapCache::insert(cache_key, no_cover_icon_);
}
else {
//qLog(Debug) << cache_key;
QPixmap image_pixmap;
image_pixmap = QPixmap::fromImage(image);
QPixmapCache::insert(cache_key, image_pixmap);
}
#if 0
// if not already in the disk cache
std::unique_ptr<QIODevice> cached_img(icon_cache_->data(QUrl(cache_key)));
if (!cached_img && !image.isNull()) {
QNetworkCacheMetaData item_metadata;
item_metadata.setSaveToDisk(true);
item_metadata.setUrl(QUrl(cache_key));
QIODevice *cache = icon_cache_->prepare(item_metadata);
if (cache) {
image.save(cache, "XPM");
icon_cache_->insert(cache);
}
}
#endif
const QModelIndex index = ItemToIndex(item);
emit dataChanged(index, index);
const QModelIndex idx = ItemToIndex(item);
emit dataChanged(idx, idx);
}
QVariant CollectionModel::data(const QModelIndex &index, int role) const {
QVariant CollectionModel::data(const QModelIndex &idx, int role) const {
const CollectionItem *item = IndexToItem(index);
const CollectionItem *item = IndexToItem(idx);
// Handle a special case for returning album artwork instead of a generic CD icon.
// this is here instead of in the other data() function to let us use the
@@ -604,7 +587,7 @@ QVariant CollectionModel::data(const QModelIndex &index, int role) const {
}
if (is_album_node) {
// It has const behaviour some of the time - that's ok right?
return const_cast<CollectionModel*>(this)->AlbumIcon(index);
return const_cast<CollectionModel*>(this)->AlbumIcon(idx);
}
}
@@ -1326,9 +1309,9 @@ QString CollectionModel::SortTextForSong(const Song &song) {
}
Qt::ItemFlags CollectionModel::flags(const QModelIndex &index) const {
Qt::ItemFlags CollectionModel::flags(const QModelIndex &idx) const {
switch (IndexToItem(index)->type) {
switch (IndexToItem(idx)->type) {
case CollectionItem::Type_Song:
case CollectionItem::Type_Container:
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
@@ -1355,8 +1338,8 @@ QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const {
data->backend = backend_;
for (const QModelIndex &index : indexes) {
GetChildSongs(IndexToItem(index), &urls, &data->songs, &song_ids);
for (const QModelIndex &idx : indexes) {
GetChildSongs(IndexToItem(idx), &urls, &data->songs, &song_ids);
}
data->setUrls(urls);
@@ -1410,15 +1393,15 @@ SongList CollectionModel::GetChildSongs(const QModelIndexList &indexes) const {
SongList ret;
QSet<int> song_ids;
for (const QModelIndex &index : indexes) {
GetChildSongs(IndexToItem(index), &dontcare, &ret, &song_ids);
for (const QModelIndex &idx : indexes) {
GetChildSongs(IndexToItem(idx), &dontcare, &ret, &song_ids);
}
return ret;
}
SongList CollectionModel::GetChildSongs(const QModelIndex &index) const {
return GetChildSongs(QModelIndexList() << index);
SongList CollectionModel::GetChildSongs(const QModelIndex &idx) const {
return GetChildSongs(QModelIndexList() << idx);
}
void CollectionModel::SetFilterAge(int age) {

View File

@@ -43,7 +43,6 @@
#include <QImage>
#include <QIcon>
#include <QPixmap>
#include <QNetworkDiskCache>
#include <QSettings>
#include "core/simpletreemodel.h"
@@ -135,7 +134,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
// Get information about the collection
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
SongList GetChildSongs(const QModelIndex &index) const;
SongList GetChildSongs(const QModelIndex &idx) const;
SongList GetChildSongs(const QModelIndexList &indexes) const;
// Might be accurate
@@ -144,8 +143,8 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
int total_album_count() const { return total_album_count_; }
// QAbstractItemModel
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
Qt::ItemFlags flags(const QModelIndex &index) const;
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const;
Qt::ItemFlags flags(const QModelIndex &idx) const;
QStringList mimeTypes() const;
QMimeData *mimeData(const QModelIndexList &indexes) const;
bool canFetchMore(const QModelIndex &parent) const;
@@ -203,7 +202,7 @@ signals:
// Called after ResetAsync
void ResetAsyncQueryFinished(QFuture<CollectionModel::QueryResult> future);
void AlbumArtLoaded(quint64 id, const QImage &image);
void AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image);
private:
// Provides some optimisations for loading the list of items in the root.
@@ -236,8 +235,8 @@ signals:
QString DividerDisplayText(GroupBy type, const QString &key) const;
// Helpers
QString AlbumIconPixmapCacheKey(const QModelIndex &index) const;
QVariant AlbumIcon(const QModelIndex &index);
QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const;
QVariant AlbumIcon(const QModelIndex &idx);
QVariant data(const CollectionItem *item, int role) const;
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
@@ -270,8 +269,6 @@ signals:
QIcon playlists_dir_icon_;
QIcon playlist_icon_;
QNetworkDiskCache *icon_cache_;
int init_task_id_;
bool use_pretty_covers_;

View File

@@ -382,8 +382,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
bool changed = (matching_song.mtime() != qMax(file_info.lastModified().toTime_t(), song_cue_mtime)) || cue_deleted || cue_added;
// Also want to look to see whether the album art has changed
QString image = ImageForSong(file, album_art);
if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) || (!matching_song.art_automatic().isEmpty() && !matching_song.has_embedded_cover() && !QFile::exists(matching_song.art_automatic()))) {
QUrl image = ImageForSong(file, album_art);
if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) || (!matching_song.art_automatic().isEmpty() && !matching_song.has_embedded_cover() && !QFile::exists(matching_song.art_automatic().toLocalFile()))) {
changed = true;
}
@@ -415,7 +415,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
qLog(Debug) << file << "created";
// choose an image for the song(s)
QString image = ImageForSong(file, album_art);
QUrl image = ImageForSong(file, album_art);
for (Song song : song_list) {
song.set_directory_id(t->dir());
@@ -457,7 +457,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
}
void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QString &image, ScanTransaction *t) {
void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QUrl &image, ScanTransaction *t) {
QFile cue(matching_cue);
cue.open(QIODevice::ReadOnly);
@@ -497,7 +497,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, const QStr
}
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QString &image, bool cue_deleted, ScanTransaction *t) {
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QUrl &image, bool cue_deleted, ScanTransaction *t) {
// If a cue got deleted, we turn it's first section into the new 'raw' (cueless) song and we just remove the rest of the sections from the collection
if (cue_deleted) {
@@ -562,7 +562,7 @@ SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path
}
void CollectionWatcher::PreserveUserSetData(const QString &file, const QString &image, const Song &matching_song, Song *out, ScanTransaction *t) {
void CollectionWatcher::PreserveUserSetData(const QString &file, const QUrl &image, const Song &matching_song, Song *out, ScanTransaction *t) {
out->set_id(matching_song.id());
@@ -731,20 +731,27 @@ QString CollectionWatcher::PickBestImage(const QStringList &images) {
}
QString CollectionWatcher::ImageForSong(const QString &path, QMap<QString, QStringList> &album_art) {
QUrl CollectionWatcher::ImageForSong(const QString &path, QMap<QString, QStringList> &album_art) {
QString dir(DirectoryPart(path));
if (album_art.contains(dir)) {
if (album_art[dir].count() == 1)
return album_art[dir][0];
if (album_art[dir].count() == 1) {
QUrl url;
url.setScheme("file");
url.setPath(album_art[dir][0]);
return url;
}
else {
QString best_image = PickBestImage(album_art[dir]);
album_art[dir] = QStringList() << best_image;
return best_image;
QUrl url;
url.setScheme("file");
url.setPath(best_image);
return url;
}
}
return QString();
return QUrl();
}

View File

@@ -155,17 +155,17 @@ signals:
inline static QString ExtensionPart(const QString &fileName);
inline static QString DirectoryPart(const QString &fileName);
QString PickBestImage(const QStringList &images);
QString ImageForSong(const QString &path, QMap<QString, QStringList> &album_art);
QUrl ImageForSong(const QString &path, QMap<QString, QStringList> &album_art);
void AddWatch(const Directory &dir, const QString &path);
uint GetMtimeForCue(const QString &cue_path);
void PerformScan(bool incremental, bool ignore_mtimes);
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QString &image, ScanTransaction *t);
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QUrl &image, ScanTransaction *t);
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
void UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QString &image, bool cue_deleted, ScanTransaction *t);
void UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QUrl &image, bool cue_deleted, ScanTransaction *t);
// Updates a new song with some metadata taken from it's equivalent old song (for example rating and score).
void PreserveUserSetData(const QString &file, const QString &image, const Song &matching_song, Song *out, ScanTransaction *t);
void PreserveUserSetData(const QString &file, const QUrl &image, const Song &matching_song, Song *out, ScanTransaction *t);
// Scans a single media file that's present on the disk but not yet in the collection.
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
SongList ScanNewFile(const QString &file, const QString &path, const QString &matching_cue, QSet<QString> *cues_processed);