From 65780e1672c5d534113a1469352f51ed945eccf6 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 7 Jul 2019 21:14:24 +0200 Subject: [PATCH] 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 --- src/CMakeLists.txt | 4 +- src/collection/collectionbackend.cpp | 26 +- src/collection/collectionbackend.h | 12 +- src/collection/collectionmodel.cpp | 99 +++---- src/collection/collectionmodel.h | 15 +- src/collection/collectionwatcher.cpp | 29 ++- src/collection/collectionwatcher.h | 8 +- src/context/contextalbumsmodel.cpp | 11 +- src/context/contextalbumsmodel.h | 2 +- src/context/contextview.cpp | 38 +-- src/context/contextview.h | 4 +- src/core/application.cpp | 8 +- src/core/application.h | 4 +- src/core/macsystemtrayicon.h | 5 +- src/core/macsystemtrayicon.mm | 3 +- src/core/mainwindow.cpp | 99 ++++--- src/core/mainwindow.h | 6 +- src/core/mpris2.cpp | 12 +- src/core/mpris2.h | 2 +- src/core/qtsystemtrayicon.cpp | 7 +- src/core/qtsystemtrayicon.h | 3 +- src/core/song.cpp | 56 ++-- src/core/song.h | 10 +- src/core/standarditemiconloader.cpp | 6 +- src/core/standarditemiconloader.h | 4 +- src/core/systemtrayicon.h | 2 +- .../albumcoverchoicecontroller.cpp | 237 +++++++++-------- src/covermanager/albumcoverchoicecontroller.h | 22 +- src/covermanager/albumcoverfetcher.cpp | 10 +- src/covermanager/albumcoverfetcher.h | 15 +- src/covermanager/albumcoverfetchersearch.cpp | 12 +- src/covermanager/albumcoverfetchersearch.h | 7 +- src/covermanager/albumcoverloader.cpp | 245 +++++++++++++++--- src/covermanager/albumcoverloader.h | 37 ++- src/covermanager/albumcoverloaderoptions.h | 1 - src/covermanager/albumcovermanager.cpp | 84 +++--- src/covermanager/albumcovermanager.h | 8 +- src/covermanager/albumcoversearcher.cpp | 10 +- src/covermanager/albumcoversearcher.h | 4 +- src/covermanager/coverexportrunnable.cpp | 11 +- src/covermanager/coverprovider.h | 5 +- ...loader.cpp => currentalbumcoverloader.cpp} | 54 ++-- ...tartloader.h => currentalbumcoverloader.h} | 27 +- src/covermanager/deezercoverprovider.cpp | 37 +-- src/covermanager/discogscoverprovider.cpp | 28 +- src/covermanager/discogscoverprovider.h | 3 +- src/covermanager/lastfmcoverprovider.cpp | 29 ++- src/covermanager/musicbrainzcoverprovider.cpp | 24 +- src/covermanager/tidalcoverprovider.cpp | 50 ++-- src/covermanager/tidalcoverprovider.h | 9 +- src/dialogs/edittagdialog.cpp | 36 +-- src/dialogs/edittagdialog.h | 4 +- src/engine/enginebase.h | 3 +- src/engine/gstenginepipeline.cpp | 2 +- src/engine/xineengine.cpp | 4 +- src/internet/internetsearch.cpp | 8 +- src/internet/internetsearch.h | 6 +- src/internet/internetsearchitemdelegate.cpp | 2 +- src/internet/internetsearchview.cpp | 10 +- src/internet/internetsearchview.h | 4 +- src/lyrics/lyricsfetcher.h | 6 +- src/musicbrainz/acoustidclient.cpp | 7 + src/musicbrainz/musicbrainzclient.cpp | 2 +- src/organise/organise.cpp | 19 +- src/playlist/playlistview.cpp | 9 +- src/playlist/playlistview.h | 2 +- src/playlistparsers/xspfparser.cpp | 28 +- src/qobuz/qobuzbaserequest.cpp | 71 ++--- src/qobuz/qobuzbaserequest.h | 14 +- src/qobuz/qobuzfavoriterequest.cpp | 13 +- src/qobuz/qobuzfavoriterequest.h | 1 + src/qobuz/qobuzrequest.cpp | 106 +++----- src/qobuz/qobuzrequest.h | 11 +- src/qobuz/qobuzservice.cpp | 64 +++-- src/qobuz/qobuzservice.h | 7 +- src/qobuz/qobuzstreamurlrequest.cpp | 39 +-- src/qobuz/qobuzstreamurlrequest.h | 5 + src/scrobbler/listenbrainzscrobbler.cpp | 61 +++-- src/scrobbler/scrobblingapi20.cpp | 63 +++-- src/subsonic/subsonicbaserequest.cpp | 65 +++-- src/subsonic/subsonicbaserequest.h | 11 +- src/subsonic/subsonicrequest.cpp | 121 +++------ src/subsonic/subsonicrequest.h | 16 +- src/subsonic/subsonicservice.cpp | 58 +++-- src/subsonic/subsonicservice.h | 10 +- src/subsonic/subsonicurlhandler.cpp | 1 + src/subsonic/subsonicurlhandler.h | 2 + src/tidal/tidalbaserequest.cpp | 89 ++++--- src/tidal/tidalbaserequest.h | 15 +- src/tidal/tidalfavoriterequest.cpp | 11 +- src/tidal/tidalfavoriterequest.h | 4 + src/tidal/tidalrequest.cpp | 79 +++--- src/tidal/tidalrequest.h | 11 +- src/tidal/tidalservice.cpp | 90 ++++--- src/tidal/tidalservice.h | 9 +- src/tidal/tidalstreamurlrequest.cpp | 53 ++-- src/tidal/tidalstreamurlrequest.h | 5 + src/widgets/osd.cpp | 17 +- src/widgets/osd.h | 4 +- src/widgets/playingwidget.cpp | 42 +-- src/widgets/playingwidget.h | 6 +- 101 files changed, 1531 insertions(+), 1239 deletions(-) rename src/covermanager/{currentartloader.cpp => currentalbumcoverloader.cpp} (57%) rename src/covermanager/{currentartloader.h => currentalbumcoverloader.h} (65%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6bee17d3..ca28eb96 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -196,7 +196,7 @@ set(SOURCES covermanager/coversearchstatistics.cpp covermanager/coversearchstatisticsdialog.cpp covermanager/coverexportrunnable.cpp - covermanager/currentartloader.cpp + covermanager/currentalbumcoverloader.cpp covermanager/coverfromurldialog.cpp covermanager/lastfmcoverprovider.cpp covermanager/musicbrainzcoverprovider.cpp @@ -378,7 +378,7 @@ set(HEADERS covermanager/coverproviders.h covermanager/coversearchstatisticsdialog.h covermanager/coverexportrunnable.h - covermanager/currentartloader.h + covermanager/currentalbumcoverloader.h covermanager/coverfromurldialog.h covermanager/lastfmcoverprovider.h covermanager/musicbrainzcoverprovider.h diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index 057aa5b3..39da069a 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -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); } diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index dbb541a6..58c35d1f 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -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 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 &artists, bool on); void IncrementPlayCount(int id); void IncrementSkipCount(int id, float progress); diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp index 159931d3..73fb32d3 100644 --- a/src/collection/collectionmodel.cpp +++ b/src/collection/collectionmodel.cpp @@ -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 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 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(this)->AlbumIcon(index); + return const_cast(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 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) { diff --git a/src/collection/collectionmodel.h b/src/collection/collectionmodel.h index afb81393..58bf4707 100644 --- a/src/collection/collectionmodel.h +++ b/src/collection/collectionmodel.h @@ -43,7 +43,6 @@ #include #include #include -#include #include #include "core/simpletreemodel.h" @@ -135,7 +134,7 @@ class CollectionModel : public SimpleTreeModel { // Get information about the collection void GetChildSongs(CollectionItem *item, QList *urls, SongList *songs, QSet *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 { 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 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_; diff --git a/src/collection/collectionwatcher.cpp b/src/collection/collectionwatcher.cpp index 784d461f..8df3b77c 100644 --- a/src/collection/collectionwatcher.cpp +++ b/src/collection/collectionwatcher.cpp @@ -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 &album_art) { +QUrl CollectionWatcher::ImageForSong(const QString &path, QMap &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(); } diff --git a/src/collection/collectionwatcher.h b/src/collection/collectionwatcher.h index 4c09895f..da399c5a 100644 --- a/src/collection/collectionwatcher.h +++ b/src/collection/collectionwatcher.h @@ -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 &album_art); + QUrl ImageForSong(const QString &path, QMap &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 *cues_processed); diff --git a/src/context/contextalbumsmodel.cpp b/src/context/contextalbumsmodel.cpp index 056d4210..9ec25e08 100644 --- a/src/context/contextalbumsmodel.cpp +++ b/src/context/contextalbumsmodel.cpp @@ -81,7 +81,7 @@ ContextAlbumsModel::ContextAlbumsModel(CollectionBackend *backend, Application * cover_loader_options_.pad_output_image_ = true; cover_loader_options_.scale_output_image_ = true; - connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(quint64, QImage))); + 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); @@ -165,14 +165,16 @@ QVariant ContextAlbumsModel::AlbumIcon(const QModelIndex &index) { } -void ContextAlbumsModel::AlbumArtLoaded(quint64 id, const QImage &image) { +void ContextAlbumsModel::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; + CollectionItem *item = item_and_cache_key.first; if (!item) return; + const QString &cache_key = item_and_cache_key.second; pending_cache_keys_.remove(cache_key); // Insert this image in the cache. @@ -181,7 +183,6 @@ void ContextAlbumsModel::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); diff --git a/src/context/contextalbumsmodel.h b/src/context/contextalbumsmodel.h index 4faece1f..41e7c2d3 100644 --- a/src/context/contextalbumsmodel.h +++ b/src/context/contextalbumsmodel.h @@ -109,7 +109,7 @@ class ContextAlbumsModel : public SimpleTreeModel { void LazyPopulate(CollectionItem *item, bool signal); private slots: - void AlbumArtLoaded(quint64 id, const QImage &image); + void AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); private: QueryResult RunQuery(CollectionItem *parent); diff --git a/src/context/contextview.cpp b/src/context/contextview.cpp index 1ee21f44..ca5a5ac9 100644 --- a/src/context/contextview.cpp +++ b/src/context/contextview.cpp @@ -60,7 +60,7 @@ #include "collection/collectionview.h" #include "covermanager/albumcoverchoicecontroller.h" #include "covermanager/albumcoverloader.h" -#include "covermanager/currentartloader.h" +#include "covermanager/currentalbumcoverloader.h" #include "lyrics/lyricsfetcher.h" #include "contextview.h" @@ -117,9 +117,7 @@ void ContextView::Init(Application *app, CollectionView *collectionview, AlbumCo connect(collectionview_, SIGNAL(TotalArtistCountUpdated_()), this, SLOT(UpdateNoSong())); connect(collectionview_, SIGNAL(TotalAlbumCountUpdated_()), this, SLOT(UpdateNoSong())); connect(lyrics_fetcher_, SIGNAL(LyricsFetched(const quint64, const QString&, const QString&)), this, SLOT(UpdateLyrics(const quint64, const QString&, const QString&))); - connect(app_->current_art_loader(), SIGNAL(ArtLoaded(Song, QString, QImage)), SLOT(AlbumArtLoaded(Song, QString, QImage))); connect(album_cover_choice_controller_, SIGNAL(AutomaticCoverSearchDone()), this, SLOT(AutomaticCoverSearchDone())); - connect(album_cover_choice_controller_->search_cover_auto_action(), SIGNAL(triggered()), this, SLOT(SearchCoverAutomatically())); AddActions(); @@ -595,7 +593,7 @@ void ContextView::ScaleCover() { } -void ContextView::AlbumArtLoaded(const Song &song, const QString&, const QImage &image) { +void ContextView::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { if (song.id() != song_playing_.id() || song.url() != song_playing_.url()) return; if (song.effective_albumartist() != song_playing_.effective_albumartist() || song.effective_album() != song_playing_.effective_album() || song.title() != song_playing_.title()) return; @@ -605,7 +603,6 @@ void ContextView::AlbumArtLoaded(const Song &song, const QString&, const QImage downloading_covers_ = false; song_ = song; SetImage(image); - GetCoverAutomatically(); } @@ -634,28 +631,15 @@ void ContextView::SetImage(const QImage &image) { } -void ContextView::GetCoverAutomatically() { +void ContextView::SearchCoverInProgress() { - // Search for cover automatically? - bool search = - album_cover_choice_controller_->search_cover_auto_action()->isChecked() && - !song_.has_manually_unset_cover() && - song_.art_automatic().isEmpty() && - song_.art_manual().isEmpty() && - !song_.effective_albumartist().isEmpty() && - !song_.effective_album().isEmpty(); + downloading_covers_ = true; - if (search) { - downloading_covers_ = true; - // This is done in mainwindow instead to avoid searching multiple times (ContextView & PlayingWidget) - //album_cover_choice_controller_->SearchCoverAutomatically(song_); - - // Show a spinner animation - spinner_animation_.reset(new QMovie(":/pictures/spinner.gif", QByteArray(), this)); - connect(spinner_animation_.get(), SIGNAL(updated(const QRect&)), SLOT(update())); - spinner_animation_->start(); - update(); - } + // Show a spinner animation + spinner_animation_.reset(new QMovie(":/pictures/spinner.gif", QByteArray(), this)); + connect(spinner_animation_.get(), SIGNAL(updated(const QRect&)), SLOT(update())); + spinner_animation_->start(); + update(); } @@ -703,7 +687,3 @@ void ContextView::ActionShowLyrics() { lyrics_id_ = lyrics_fetcher_->Search(song_.artist(), song_.album(), song_.title()); } } - -void ContextView::SearchCoverAutomatically() { - GetCoverAutomatically(); -} diff --git a/src/context/contextview.h b/src/context/contextview.h index 9f2d201c..58daaa68 100644 --- a/src/context/contextview.h +++ b/src/context/contextview.h @@ -131,9 +131,9 @@ class ContextView : public QWidget { void ActionShowAlbums(); void ActionShowLyrics(); void UpdateLyrics(const quint64 id, const QString &provider, const QString &lyrics); - void SearchCoverAutomatically(); + void SearchCoverInProgress(); void AutomaticCoverSearchDone(); - void AlbumArtLoaded(const Song &song, const QString &uri, const QImage &image); + void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void FadePreviousTrack(qreal value); }; diff --git a/src/core/application.cpp b/src/core/application.cpp index cbd143a2..a6799231 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -50,7 +50,7 @@ #include "playlist/playlistmanager.h" #include "covermanager/albumcoverloader.h" #include "covermanager/coverproviders.h" -#include "covermanager/currentartloader.h" +#include "covermanager/currentalbumcoverloader.h" #include "covermanager/lastfmcoverprovider.h" #include "covermanager/discogscoverprovider.h" #include "covermanager/musicbrainzcoverprovider.h" @@ -132,7 +132,7 @@ class ApplicationImpl { app->MoveToNewThread(loader); return loader; }), - current_art_loader_([=]() { return new CurrentArtLoader(app, app); }), + current_albumcover_loader_([=]() { return new CurrentAlbumCoverLoader(app, app); }), lyrics_providers_([=]() { LyricsProviders *lyrics_providers = new LyricsProviders(app); lyrics_providers->AddProvider(new AuddLyricsProvider(app)); @@ -182,7 +182,7 @@ class ApplicationImpl { Lazy playlist_manager_; Lazy cover_providers_; Lazy album_cover_loader_; - Lazy current_art_loader_; + Lazy current_albumcover_loader_; Lazy lyrics_providers_; Lazy internet_services_; #ifdef HAVE_TIDAL @@ -259,7 +259,7 @@ CollectionBackend *Application::collection_backend() const { return collection() CollectionModel *Application::collection_model() const { return collection()->model(); } AlbumCoverLoader *Application::album_cover_loader() const { return p_->album_cover_loader_.get(); } CoverProviders *Application::cover_providers() const { return p_->cover_providers_.get(); } -CurrentArtLoader *Application::current_art_loader() const { return p_->current_art_loader_.get(); } +CurrentAlbumCoverLoader *Application::current_albumcover_loader() const { return p_->current_albumcover_loader_.get(); } LyricsProviders *Application::lyrics_providers() const { return p_->lyrics_providers_.get(); } PlaylistBackend *Application::playlist_backend() const { return p_->playlist_backend_.get(); } PlaylistManager *Application::playlist_manager() const { return p_->playlist_manager_.get(); } diff --git a/src/core/application.h b/src/core/application.h index 2d39446c..7bbb0ad1 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -54,7 +54,7 @@ class DeviceManager; #endif class CoverProviders; class AlbumCoverLoader; -class CurrentArtLoader; +class CurrentAlbumCoverLoader; class LyricsProviders; class AudioScrobbler; class InternetServices; @@ -92,7 +92,7 @@ class Application : public QObject { CoverProviders *cover_providers() const; AlbumCoverLoader *album_cover_loader() const; - CurrentArtLoader *current_art_loader() const; + CurrentAlbumCoverLoader *current_albumcover_loader() const; LyricsProviders *lyrics_providers() const; diff --git a/src/core/macsystemtrayicon.h b/src/core/macsystemtrayicon.h index 17172fea..0992746a 100644 --- a/src/core/macsystemtrayicon.h +++ b/src/core/macsystemtrayicon.h @@ -26,8 +26,9 @@ #include #include -#include +#include #include +#include #include "systemtrayicon.h" @@ -42,7 +43,7 @@ class MacSystemTrayIcon : public SystemTrayIcon { void SetupMenu(QAction *previous, QAction *play, QAction *stop, QAction *stop_after, QAction *next, QAction *mute, QAction *love, QAction *quit); - void SetNowPlaying(const Song& song, const QString& image_path); + void SetNowPlaying(const Song& song, const QUrl &cover_url); void ClearNowPlaying(); private: diff --git a/src/core/macsystemtrayicon.mm b/src/core/macsystemtrayicon.mm index 2a0172a5..7dceaa81 100644 --- a/src/core/macsystemtrayicon.mm +++ b/src/core/macsystemtrayicon.mm @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -205,6 +206,6 @@ void MacSystemTrayIcon::ClearNowPlaying() { p_->ClearNowPlaying(); } -void MacSystemTrayIcon::SetNowPlaying(const Song& song, const QString& image_path) { +void MacSystemTrayIcon::SetNowPlaying(const Song& song, const QUrl& cover_url) { p_->ShowNowPlaying(song.artist(), song.PrettyTitle()); } diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 587a5f46..d0d87762 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -122,7 +122,7 @@ #include "covermanager/albumcovermanager.h" #include "covermanager/albumcoverchoicecontroller.h" #include "covermanager/albumcoverloader.h" -#include "covermanager/currentartloader.h" +#include "covermanager/currentalbumcoverloader.h" #ifndef Q_OS_WIN # include "device/devicemanager.h" # include "device/devicestatefiltermodel.h" @@ -251,8 +251,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co ui_->menu_help->menuAction()->setVisible(false); #endif - connect(app_->current_art_loader(), SIGNAL(ArtLoaded(Song, QString, QImage)), SLOT(AlbumArtLoaded(Song, QString, QImage))); - album_cover_choice_controller_->SetApplication(app); + connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, QUrl, QImage)), SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); + album_cover_choice_controller_->Init(app); connect(album_cover_choice_controller_->cover_from_file_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromFile())); connect(album_cover_choice_controller_->cover_to_file_action(), SIGNAL(triggered()), this, SLOT(SaveCoverToFile())); connect(album_cover_choice_controller_->cover_from_url_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromURL())); @@ -263,7 +263,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co ui_->multi_loading_indicator->SetTaskManager(app_->task_manager()); context_view_->Init(app_, collection_view_->view(), album_cover_choice_controller_); - ui_->widget_playing->SetApplication(app_, album_cover_choice_controller_); + ui_->widget_playing->Init(app_, album_cover_choice_controller_); // Initialise the search widget StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker()); @@ -521,10 +521,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co connect(ui_->track_slider, SIGNAL(Previous()), app_->player(), SLOT(Previous())); connect(ui_->track_slider, SIGNAL(Next()), app_->player(), SLOT(Next())); - // Context connections - - connect(context_view_->albums(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - // Collection connections connect(collection_view_->view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); connect(collection_view_->view(), SIGNAL(ShowConfigDialog()), SLOT(ShowCollectionConfig())); @@ -576,8 +572,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co connect(tidal_view_->albums_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); connect(tidal_view_->songs_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); connect(tidal_view_->search_view(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - TidalService *tidalservice = qobject_cast (app_->internet_services()->ServiceBySource(Song::Source_Tidal)); - if (tidalservice) + if (TidalService *tidalservice = qobject_cast (app_->internet_services()->ServiceBySource(Song::Source_Tidal))) connect(this, SIGNAL(AuthorisationUrlReceived(const QUrl&)), tidalservice, SLOT(AuthorisationUrlReceived(const QUrl&))); #endif @@ -710,6 +705,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co connect(app_->player(), SIGNAL(Playing()), context_view_, SLOT(Playing())); connect(app_->player(), SIGNAL(Stopped()), context_view_, SLOT(Stopped())); connect(app_->player(), SIGNAL(Error()), context_view_, SLOT(Error())); + connect(this, SIGNAL(AlbumCoverReady(Song, QUrl, QImage)), context_view_, SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); + connect(this, SIGNAL(SearchCoverInProgress()), context_view_, SLOT(SearchCoverInProgress())); + connect(context_view_->albums(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); // Analyzer connect(ui_->analyzer, SIGNAL(WheelEvent(int)), SLOT(VolumeWheelEvent(int))); @@ -735,6 +733,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co connect(app_->player(), SIGNAL(Stopped()), ui_->widget_playing, SLOT(Stopped())); connect(app_->player(), SIGNAL(Error()), ui_->widget_playing, SLOT(Error())); connect(ui_->widget_playing, SIGNAL(ShowAboveStatusBarChanged(bool)), SLOT(PlayingWidgetPositionChanged(bool))); + connect(this, SIGNAL(AlbumCoverReady(Song, QUrl, QImage)), ui_->widget_playing, SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); + connect(this, SIGNAL(SearchCoverInProgress()), ui_->widget_playing, SLOT(SearchCoverInProgress())); connect(ui_->action_console, SIGNAL(triggered()), SLOT(ShowConsole())); PlayingWidgetPositionChanged(ui_->widget_playing->show_above_status_bar()); @@ -942,6 +942,7 @@ void MainWindow::ReloadAllSettings() { osd_->ReloadSettings(); collection_view_->ReloadSettings(); ui_->playlist->view()->ReloadSettings(); + app_->album_cover_loader()->ReloadSettings(); album_cover_choice_controller_->ReloadSettings(); if (cover_manager_.get()) cover_manager_->ReloadSettings(); #ifdef HAVE_TIDAL @@ -962,6 +963,41 @@ void MainWindow::RefreshStyleSheet() { setStyleSheet(contents); } +void MainWindow::SaveSettings() { + + SaveGeometry(); + SavePlaybackStatus(); + ui_->tabs->SaveSettings(kSettingsGroup); + ui_->playlist->view()->SaveGeometry(); + ui_->playlist->view()->SaveSettings(); + app_->scrobbler()->WriteCache(); + + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("search_for_cover_auto", album_cover_choice_controller_->search_cover_auto_action()->isChecked()); + s.endGroup(); + +} + +void MainWindow::Exit() { + + SaveSettings(); + + if (app_->player()->engine()->is_fadeout_enabled()) { + // To shut down the application when fadeout will be finished + connect(app_->player()->engine(), SIGNAL(FadeoutFinishedSignal()), qApp, SLOT(quit())); + if (app_->player()->GetState() == Engine::Playing) { + app_->player()->Stop(); + hide(); + if (tray_icon_) tray_icon_->SetVisible(false); + return; // Don't quit the application now: wait for the fadeout finished signal + } + } + + qApp->quit(); + +} + void MainWindow::EngineChanged(Engine::EngineType enginetype) { ui_->action_equalizer->setEnabled(enginetype == Engine::EngineType::GStreamer || enginetype == Engine::EngineType::Xine); @@ -2331,30 +2367,6 @@ bool MainWindow::winEvent(MSG *msg, long*) { } #endif // Q_OS_WIN32 -void MainWindow::Exit() { - - SaveGeometry(); - SavePlaybackStatus(); - ui_->tabs->SaveSettings(kSettingsGroup); - ui_->playlist->view()->SaveGeometry(); - ui_->playlist->view()->SaveSettings(); - app_->scrobbler()->WriteCache(); - - if (app_->player()->engine()->is_fadeout_enabled()) { - // To shut down the application when fadeout will be finished - connect(app_->player()->engine(), SIGNAL(FadeoutFinishedSignal()), qApp, SLOT(quit())); - if (app_->player()->GetState() == Engine::Playing) { - app_->player()->Stop(); - hide(); - if (tray_icon_) tray_icon_->SetVisible(false); - return; // Don't quit the application now: wait for the fadeout finished signal - } - } - - qApp->quit(); - -} - #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) void MainWindow::AutoCompleteTags() { @@ -2475,20 +2487,19 @@ void MainWindow::ShowCover() { void MainWindow::SearchCoverAutomatically() { - QSettings s; - s.beginGroup(kSettingsGroup); - s.setValue("search_for_cover_auto", album_cover_choice_controller_->search_cover_auto_action()->isChecked()); - s.endGroup(); GetCoverAutomatically(); } -void MainWindow::AlbumArtLoaded(const Song &song, const QString&, const QImage &image) { +void MainWindow::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { if (song.effective_albumartist() != song_playing_.effective_albumartist() || song.effective_album() != song_playing_.effective_album() || song.title() != song_playing_.title()) return; song_ = song; image_original_ = image; + + emit AlbumCoverReady(song, cover_url, image); + GetCoverAutomatically(); } @@ -2497,14 +2508,18 @@ void MainWindow::GetCoverAutomatically() { // Search for cover automatically? bool search = + (song_.source() == Song::Source_LocalFile || song_.source() == Song::Source_Collection || song_.source() == Song::Source_CDDA) && album_cover_choice_controller_->search_cover_auto_action()->isChecked() && !song_.has_manually_unset_cover() && - song_.art_automatic().isEmpty() && - song_.art_manual().isEmpty() && + !song_.art_automatic_is_valid() && + !song_.art_manual_is_valid() && !song_.effective_albumartist().isEmpty() && !song_.effective_album().isEmpty(); - if (search) album_cover_choice_controller_->SearchCoverAutomatically(song_); + if (search) { + album_cover_choice_controller_->SearchCoverAutomatically(song_); + emit SearchCoverInProgress(); + } } diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index fd4929f5..2cf72255 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -127,6 +127,8 @@ class MainWindow : public QMainWindow, public PlatformInterface { bool LoadUrl(const QString& url); signals: + void AlbumCoverReady(const Song &song, const QUrl &cover_url, const QImage &image); + void SearchCoverInProgress(); // Signals that stop playing after track was toggled. void StopAfterToggled(bool stop); @@ -253,7 +255,7 @@ signals: void UnsetCover(); void ShowCover(); void SearchCoverAutomatically(); - void AlbumArtLoaded(const Song &song, const QString &uri, const QImage &image); + void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void ScrobblingEnabledChanged(bool value); void ScrobbleButtonVisibilityChanged(bool value); @@ -262,6 +264,8 @@ signals: private: + void SaveSettings(); + void ApplyAddBehaviour(BehaviourSettingsPage::AddBehaviour b, MimeData *data) const; void ApplyPlayBehaviour(BehaviourSettingsPage::PlayBehaviour b, MimeData *data) const; diff --git a/src/core/mpris2.cpp b/src/core/mpris2.cpp index 0d4bbb1d..2a4e22b4 100644 --- a/src/core/mpris2.cpp +++ b/src/core/mpris2.cpp @@ -55,7 +55,7 @@ #include "playlist/playlistitem.h" #include "playlist/playlistmanager.h" #include "playlist/playlistsequence.h" -#include "covermanager/currentartloader.h" +#include "covermanager/currentalbumcoverloader.h" #include #include @@ -120,7 +120,7 @@ Mpris2::Mpris2(Application *app, QObject *parent) return; } - connect(app_->current_art_loader(), SIGNAL(ArtLoaded(Song,QString,QImage)), SLOT(ArtLoaded(Song,QString))); + connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, QUrl, QImage)), SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); connect(app_->player()->engine(), SIGNAL(StateChanged(Engine::State)), SLOT(EngineStateChanged(Engine::State))); connect(app_->player(), SIGNAL(VolumeChanged(int)), SLOT(VolumeChanged())); @@ -376,7 +376,7 @@ QString Mpris2::current_track_id() const { // We send Metadata change notification as soon as the process of changing song starts... void Mpris2::CurrentSongChanged(const Song &song) { - ArtLoaded(song, ""); + AlbumCoverLoaded(song, QUrl(), QImage()); EmitNotification("CanPlay"); EmitNotification("CanPause"); EmitNotification("CanGoNext", CanGoNext()); @@ -386,7 +386,7 @@ void Mpris2::CurrentSongChanged(const Song &song) { } // ... and we add the cover information later, when it's available. -void Mpris2::ArtLoaded(const Song &song, const QString &art_uri) { +void Mpris2::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { last_metadata_ = QVariantMap(); song.ToXesam(&last_metadata_); @@ -394,8 +394,8 @@ void Mpris2::ArtLoaded(const Song &song, const QString &art_uri) { using mpris::AddMetadata; AddMetadata("mpris:trackid", current_track_id(), &last_metadata_); - if (!art_uri.isEmpty()) { - AddMetadata("mpris:artUrl", art_uri, &last_metadata_); + if (!cover_url.isValid()) { + AddMetadata("mpris:artUrl", cover_url.toLocalFile(), &last_metadata_); } AddMetadata("year", song.year(), &last_metadata_); diff --git a/src/core/mpris2.h b/src/core/mpris2.h index af337bbc..1f8be572 100644 --- a/src/core/mpris2.h +++ b/src/core/mpris2.h @@ -207,7 +207,7 @@ signals: void PlaylistChanged(const MprisPlaylist &playlist); private slots: - void ArtLoaded(const Song &song, const QString &art_uri); + void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void EngineStateChanged(Engine::State newState); void VolumeChanged(); diff --git a/src/core/qtsystemtrayicon.cpp b/src/core/qtsystemtrayicon.cpp index cd71b52f..050c6784 100644 --- a/src/core/qtsystemtrayicon.cpp +++ b/src/core/qtsystemtrayicon.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -235,14 +236,14 @@ void QtSystemTrayIcon::SetVisible(bool visible) { tray_->setVisible(visible); } -void QtSystemTrayIcon::SetNowPlaying(const Song &song, const QString &image_path) { +void QtSystemTrayIcon::SetNowPlaying(const Song &song, const QUrl &cover_url) { #ifdef Q_OS_WIN // Windows doesn't support HTML in tooltips, so just show something basic tray_->setToolTip(song.PrettyTitleWithArtist()); #else - int columns = image_path == nullptr ? 1 : 2; + int columns = cover_url.isEmpty() ? 1 : 2; QString tooltip(pattern_); @@ -260,7 +261,7 @@ void QtSystemTrayIcon::SetNowPlaying(const Song &song, const QString &image_path tooltip.replace("%lengthValue", song.PrettyLength().toHtmlEscaped()); if (columns == 2) { - QString final_path = image_path.startsWith("file://") ? image_path.mid(7) : image_path; + QString final_path = cover_url.isLocalFile() ? cover_url.path() : cover_url.toString(); if (de_ == "kde") { tooltip.replace("%image", ""); } diff --git a/src/core/qtsystemtrayicon.h b/src/core/qtsystemtrayicon.h index c6ab4ce5..43f8bde4 100644 --- a/src/core/qtsystemtrayicon.h +++ b/src/core/qtsystemtrayicon.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -52,7 +53,7 @@ class QtSystemTrayIcon : public SystemTrayIcon { void ShowPopup(const QString &summary, const QString &message, int timeout); - void SetNowPlaying(const Song &song, const QString &image_path); + void SetNowPlaying(const Song &song, const QUrl &cover_url); void ClearNowPlaying(); bool MuteEnabled() { return action_mute_->isVisible(); } diff --git a/src/core/song.cpp b/src/core/song.cpp index 4fada6b0..95855bb2 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -210,8 +210,8 @@ struct Song::Private : public QSharedData { bool compilation_off_; // Set by the user // Filenames to album art for this song. - QString art_automatic_; // Guessed by CollectionWatcher - QString art_manual_; // Set by the user - should take priority + QUrl art_automatic_; // Guessed by CollectionWatcher + QUrl art_manual_; // Set by the user - should take priority QString cue_path_; // If the song has a CUE, this contains it's path. @@ -325,12 +325,12 @@ int Song::playcount() const { return d->playcount_; } int Song::skipcount() const { return d->skipcount_; } int Song::lastplayed() const { return d->lastplayed_; } -const QString &Song::art_automatic() const { return d->art_automatic_; } -const QString &Song::art_manual() const { return d->art_manual_; } -bool Song::has_manually_unset_cover() const { return d->art_manual_ == kManuallyUnsetCover; } -void Song::manually_unset_cover() { d->art_manual_ = kManuallyUnsetCover; } -bool Song::has_embedded_cover() const { return d->art_automatic_ == kEmbeddedCover; } -void Song::set_embedded_cover() { d->art_automatic_ = kEmbeddedCover; } +const QUrl &Song::art_automatic() const { return d->art_automatic_; } +const QUrl &Song::art_manual() const { return d->art_manual_; } +bool Song::has_manually_unset_cover() const { return d->art_manual_.path() == kManuallyUnsetCover; } +void Song::manually_unset_cover() { d->art_manual_.clear(); d->art_manual_.setPath(kManuallyUnsetCover); } +bool Song::has_embedded_cover() const { return d->art_automatic_.path() == kEmbeddedCover; } +void Song::set_embedded_cover() { d->art_automatic_.clear(); d->art_automatic_.setPath(kEmbeddedCover); } const QImage &Song::image() const { return d->image_; } const QString &Song::cue_path() const { return d->cue_path_; } @@ -341,6 +341,26 @@ bool Song::is_metadata_good() const { return !d->title_.isEmpty() && !d->album_. bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal || d->source_ == Source_Subsonic || d->source_ == Source_Qobuz; } bool Song::is_cdda() const { return d->source_ == Source_CDDA; } +bool Song::art_automatic_is_valid() const { + return ( + (d->art_automatic_.path() == kManuallyUnsetCover) || + (d->art_automatic_.path() == kEmbeddedCover) || + (d->art_automatic_.isValid() && !d->art_automatic_.isLocalFile()) || + (d->art_automatic_.isLocalFile() && QFile::exists(d->art_automatic_.toLocalFile())) || + (d->art_automatic_.scheme().isEmpty() && !d->art_automatic_.path().isEmpty() && QFile::exists(d->art_automatic_.path())) + ); +} + +bool Song::art_manual_is_valid() const { + return ( + (d->art_manual_.path() == kManuallyUnsetCover) || + (d->art_manual_.path() == kEmbeddedCover) || + (d->art_manual_.isValid() && !d->art_manual_.isLocalFile()) || + (d->art_manual_.isLocalFile() && QFile::exists(d->art_manual_.toLocalFile())) || + (d->art_manual_.scheme().isEmpty() && !d->art_manual_.path().isEmpty() && QFile::exists(d->art_manual_.path())) + ); +} + const QString &Song::error() const { return d->error_; } void Song::set_id(int id) { d->id_ = id; } @@ -415,8 +435,8 @@ void Song::set_compilation_detected(bool v) { d->compilation_detected_ = v; } void Song::set_compilation_on(bool v) { d->compilation_on_ = v; } void Song::set_compilation_off(bool v) { d->compilation_off_ = v; } -void Song::set_art_automatic(const QString &v) { d->art_automatic_ = v; } -void Song::set_art_manual(const QString &v) { d->art_manual_ = v; } +void Song::set_art_automatic(const QUrl &v) { d->art_automatic_ = v; } +void Song::set_art_manual(const QUrl &v) { d->art_manual_ = v; } void Song::set_cue_path(const QString &v) { d->cue_path_ = v; } void Song::set_image(const QImage &i) { d->image_ = i; } @@ -677,7 +697,7 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata &pb) { } if (pb.has_art_automatic()) { - d->art_automatic_ = QStringFromStdString(pb.art_automatic()); + set_art_automatic(QUrl::fromEncoded(QByteArray(pb.art_automatic().data(), pb.art_automatic().size()))); } InitArtManual(); @@ -687,6 +707,7 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata &pb) { void Song::ToProtobuf(pb::tagreader::SongMetadata *pb) const { const QByteArray url(d->url_.toEncoded()); + const QByteArray art_automatic(d->art_automatic_.toEncoded()); pb->set_valid(d->valid_); pb->set_title(DataCommaSizeFromQString(d->title_)); @@ -717,7 +738,7 @@ void Song::ToProtobuf(pb::tagreader::SongMetadata *pb) const { pb->set_ctime(d->ctime_); pb->set_filesize(d->filesize_); pb->set_suspicious_tags(d->suspicious_tags_); - pb->set_art_automatic(DataCommaSizeFromQString(d->art_automatic_)); + pb->set_art_automatic(art_automatic.constData(), art_automatic.size()); pb->set_filetype(static_cast(d->filetype_)); } @@ -866,10 +887,10 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) { } else if (Song::kColumns.value(i) == "art_automatic") { - d->art_automatic_ = q.value(x).toString(); + set_art_automatic(QUrl::fromEncoded(tostr(x).toUtf8())); } else if (Song::kColumns.value(i) == "art_manual") { - d->art_manual_ = q.value(x).toString(); + set_art_manual(QUrl::fromEncoded(tostr(x).toUtf8())); } else if (Song::kColumns.value(i) == "effective_albumartist") { @@ -927,9 +948,10 @@ void Song::InitArtManual() { // If we don't have an art, check if we have one in the cache if (d->art_manual_.isEmpty() && d->art_automatic_.isEmpty()) { QString filename(Utilities::Sha1CoverHash(d->artist_, album2).toHex() + ".jpg"); - QString path(AlbumCoverLoader::ImageCacheDir() + "/" + filename); + QString path(AlbumCoverLoader::ImageCacheDir(d->source_) + "/" + filename); if (QFile::exists(path)) { - d->art_manual_ = path; + d->art_manual_.setScheme("file"); + d->art_manual_.setPath(path); } } @@ -1117,7 +1139,7 @@ void Song::MergeFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle) { if (!bundle.genre.isEmpty()) d->genre_ = bundle.genre; if (bundle.length > 0) set_length_nanosec(bundle.length); if (bundle.year > 0) d->year_ = bundle.year; - if (bundle.tracknr > 0) d->track_ = bundle.tracknr; + if (bundle.track > 0) d->track_ = bundle.track; if (bundle.filetype != FileType_Unknown) d->filetype_ = bundle.filetype; if (bundle.samplerate > 0) d->samplerate_ = bundle.samplerate; if (bundle.bitdepth > 0) d->samplerate_ = bundle.bitdepth; diff --git a/src/core/song.h b/src/core/song.h index 9605fe87..f0604eb7 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -228,8 +228,8 @@ class Song { int skipcount() const; int lastplayed() const; - const QString &art_automatic() const; - const QString &art_manual() const; + const QUrl &art_automatic() const; + const QUrl &art_manual() const; const QString &cue_path() const; bool has_cue() const; @@ -242,6 +242,8 @@ class Song { bool is_stream() const; bool is_cdda() const; bool is_metadata_good() const; + bool art_automatic_is_valid() const; + bool art_manual_is_valid() const; // Playlist views are special because you don't want to fill in album artists automatically for compilations, but you do for normal albums: const QString &playlist_albumartist() const; @@ -324,8 +326,8 @@ class Song { void set_compilation_on(bool v); void set_compilation_off(bool v); - void set_art_automatic(const QString &v); - void set_art_manual(const QString &v); + void set_art_automatic(const QUrl &v); + void set_art_manual(const QUrl &v); void set_cue_path(const QString &v); diff --git a/src/core/standarditemiconloader.cpp b/src/core/standarditemiconloader.cpp index 3b6fcd2c..b17bf51e 100644 --- a/src/core/standarditemiconloader.cpp +++ b/src/core/standarditemiconloader.cpp @@ -40,7 +40,7 @@ StandardItemIconLoader::StandardItemIconLoader(AlbumCoverLoader *cover_loader, Q cover_options_.desired_height_ = 16; - connect(cover_loader_, SIGNAL(ImageLoaded(quint64, QImage)), SLOT(ImageLoaded(quint64, QImage))); + connect(cover_loader_, SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(ImageLoaded(quint64, QUrl, QImage))); } void StandardItemIconLoader::SetModel(QAbstractItemModel *model) { @@ -56,7 +56,7 @@ void StandardItemIconLoader::SetModel(QAbstractItemModel *model) { } -void StandardItemIconLoader::LoadIcon(const QString &art_automatic, const QString &art_manual, QStandardItem *for_item) { +void StandardItemIconLoader::LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item) { const quint64 id = cover_loader_->LoadImageAsync(cover_options_, art_automatic, art_manual); pending_covers_[id] = for_item; @@ -92,7 +92,7 @@ void StandardItemIconLoader::ModelReset() { } -void StandardItemIconLoader::ImageLoaded(quint64 id, const QImage &image) { +void StandardItemIconLoader::ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { QStandardItem *item = pending_covers_.take(id); if (!item) return; diff --git a/src/core/standarditemiconloader.h b/src/core/standarditemiconloader.h index 26de7987..a0f2d7b3 100644 --- a/src/core/standarditemiconloader.h +++ b/src/core/standarditemiconloader.h @@ -49,11 +49,11 @@ class StandardItemIconLoader : public QObject { void SetModel(QAbstractItemModel *model); - void LoadIcon(const QString &art_automatic, const QString &art_manual, QStandardItem *for_item); + void LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item); void LoadIcon(const Song &song, QStandardItem *for_item); private slots: - void ImageLoaded(quint64 id, const QImage &image); + void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void RowsAboutToBeRemoved(const QModelIndex &parent, int begin, int end); void ModelReset(); diff --git a/src/core/systemtrayicon.h b/src/core/systemtrayicon.h index a53a452a..ba0f5794 100644 --- a/src/core/systemtrayicon.h +++ b/src/core/systemtrayicon.h @@ -47,7 +47,7 @@ class SystemTrayIcon : public QObject { // Called by the OSD virtual void ShowPopup(const QString &summary, const QString &message, int timeout) {} // If this get's invoked with image_path equal to nullptr, the tooltip should still be shown - just without the cover art. - virtual void SetNowPlaying(const Song &song, const QString &image_path) {} + virtual void SetNowPlaying(const Song &song, const QUrl &cover_url) {} virtual void ClearNowPlaying() {} virtual bool MuteEnabled() { return false; } diff --git a/src/covermanager/albumcoverchoicecontroller.cpp b/src/covermanager/albumcoverchoicecontroller.cpp index bc7fc8c7..fa9f3ab4 100644 --- a/src/covermanager/albumcoverchoicecontroller.cpp +++ b/src/covermanager/albumcoverchoicecontroller.cpp @@ -2,6 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome + * Copyright 2019, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -53,12 +54,14 @@ #include "collection/collectionbackend.h" #include "settings/collectionsettingspage.h" #include "organise/organiseformat.h" +#include "internet/internetservices.h" +#include "internet/internetservice.h" #include "albumcoverchoicecontroller.h" #include "albumcoverfetcher.h" #include "albumcoverloader.h" #include "albumcoversearcher.h" #include "coverfromurldialog.h" -#include "currentartloader.h" +#include "currentalbumcoverloader.h" const char *AlbumCoverChoiceController::kLoadImageFileFilter = QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)"); const char *AlbumCoverChoiceController::kSaveImageFileFilter = QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)"); @@ -100,6 +103,18 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent) : AlbumCoverChoiceController::~AlbumCoverChoiceController() {} +void AlbumCoverChoiceController::Init(Application *app) { + + app_ = app; + + cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this); + cover_searcher_ = new AlbumCoverSearcher(QIcon(":/pictures/cdcase.png"), app, this); + cover_searcher_->Init(cover_fetcher_); + + connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(const quint64, const QUrl&, const QImage&, CoverSearchStatistics)), this, SLOT(AlbumCoverFetched(const quint64, const QUrl&, const QImage&, CoverSearchStatistics))); + +} + void AlbumCoverChoiceController::ReloadSettings() { QSettings s; @@ -114,37 +129,28 @@ void AlbumCoverChoiceController::ReloadSettings() { } -void AlbumCoverChoiceController::SetApplication(Application *app) { - - app_ = app; - - cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this); - cover_searcher_ = new AlbumCoverSearcher(QIcon(":/pictures/cdcase.png"), app, this); - cover_searcher_->Init(cover_fetcher_); - - connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)), this, SLOT(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics))); - -} - QList AlbumCoverChoiceController::GetAllActions() { return QList() << cover_from_file_ << cover_to_file_ << separator_ << cover_from_url_ << search_for_cover_ << unset_cover_ << show_cover_; } -QString AlbumCoverChoiceController::LoadCoverFromFile(Song *song) { +QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) { - QString cover = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter)); + QString cover_file = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter)); - if (cover.isNull()) return QString(); + if (cover_file.isNull()) return QUrl(); // Can we load the image? - QImage image(cover); + QImage image(cover_file); - if (!image.isNull()) { - SaveCover(song, cover); - return cover; + if (image.isNull()) { + return QUrl(); } else { - return QString(); + QUrl cover_url; + cover_url.setScheme("file"); + cover_url.setPath(cover_file); + SaveCoverToSong(song, cover_url); + return cover_url; } } @@ -178,38 +184,43 @@ QString AlbumCoverChoiceController::GetInitialPathForFileDialog(const Song &song // Art automatic is first to show user which cover the album may be using now; // The song is using it if there's no manual path but we cannot use manual path here because it can contain cached paths - if (!song.art_automatic().isEmpty() && !song.has_embedded_cover()) { - return song.art_automatic(); - + if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() && !song.has_embedded_cover()) { + if (song.art_automatic().scheme().isEmpty() && QFile::exists(QFileInfo(song.art_automatic().path()).path())) { + return song.art_automatic().path(); + } + else if (song.art_automatic().scheme() == "file" && QFile::exists(QFileInfo(song.art_automatic().toLocalFile()).path())) { + return song.art_automatic().toLocalFile(); + } // If no automatic art, start in the song's folder } else if (!song.url().isEmpty() && song.url().toLocalFile().contains('/')) { return song.url().toLocalFile().section('/', 0, -2) + filename; // Fallback - start in home } - else { - return QDir::home().absolutePath() + filename; - } + + return QDir::home().absolutePath() + filename; } -QString AlbumCoverChoiceController::LoadCoverFromURL(Song *song) { +QUrl AlbumCoverChoiceController::LoadCoverFromURL(Song *song) { if (!cover_from_url_dialog_) { cover_from_url_dialog_ = new CoverFromURLDialog(this); } QImage image = cover_from_url_dialog_->Exec(); - if (!image.isNull()) { - QString cover = SaveCoverToFileAutomatic(song, image); - if (cover.isEmpty()) return QString(); - SaveCover(song, cover); - return cover; + if (image.isNull()) { + return QUrl(); + } + else { + QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true); + if (cover_url.isEmpty()) return QUrl(); + SaveCoverToSong(song, cover_url); + return cover_url; } - else { return QString(); } } -QString AlbumCoverChoiceController::SearchForCover(Song *song) { +QUrl AlbumCoverChoiceController::SearchForCover(Song *song) { QString album = song->effective_album(); album.remove(Song::kAlbumRemoveDisc); @@ -218,23 +229,26 @@ QString AlbumCoverChoiceController::SearchForCover(Song *song) { // Get something sensible to stick in the search box QImage image = cover_searcher_->Exec(song->effective_albumartist(), album); - if (!image.isNull()) { - QString cover = SaveCoverToFileAutomatic(song, image); - if (cover.isEmpty()) return QString(); - SaveCover(song, cover); - - return cover; + if (image.isNull()) { + return QUrl(); + } + else { + QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true); + if (cover_url.isEmpty()) return QUrl(); + SaveCoverToSong(song, cover_url); + return cover_url; } - else { return QString(); } } -QString AlbumCoverChoiceController::UnsetCover(Song *song) { +QUrl AlbumCoverChoiceController::UnsetCover(Song *song) { - QString cover = Song::kManuallyUnsetCover; - SaveCover(song, cover); + QUrl cover_url; + cover_url.setScheme("file"); + cover_url.setPath(Song::kManuallyUnsetCover); + SaveCoverToSong(song, cover_url); - return cover; + return cover_url; } @@ -307,7 +321,7 @@ void AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) { } -void AlbumCoverChoiceController::AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics) { +void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) { Song song; if (cover_fetching_tasks_.contains(id)) { @@ -315,93 +329,76 @@ void AlbumCoverChoiceController::AlbumCoverFetched(quint64 id, const QImage &ima } if (!image.isNull()) { - QString cover = SaveCoverToFileAutomatic(&song, image); - if (cover.isEmpty()) return; - SaveCover(&song, cover); + QUrl new_cover_url = SaveCoverToFileAutomatic(&song, cover_url, image, false); + if (!new_cover_url.isEmpty()) SaveCoverToSong(&song, new_cover_url); } emit AutomaticCoverSearchDone(); } -void AlbumCoverChoiceController::SaveCover(Song *song, const QString &cover) { +void AlbumCoverChoiceController::SaveCoverToSong(Song *song, const QUrl &cover_url) { - if (song->is_valid() && song->id() != -1) { - song->set_art_manual(cover); - app_->collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover); + if (!song->is_valid()) return; - if (song->url() == app_->current_art_loader()->last_song().url()) { - app_->current_art_loader()->LoadArt(*song); + song->set_art_manual(cover_url); + + if (song->id() != -1) { // Update the backends. + switch (song->source()) { + case Song::Source_Collection: + app_->collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url); + break; + case Song::Source_LocalFile: + case Song::Source_CDDA: + case Song::Source_Device: + case Song::Source_Stream: + case Song::Source_Unknown: + break; + case Song::Source_Tidal: + case Song::Source_Qobuz: + case Song::Source_Subsonic: + InternetService *service = app_->internet_services()->ServiceBySource(song->source()); + if (!service) break; + if (service->artists_collection_backend()) + service->artists_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url); + if (service->albums_collection_backend()) + service->albums_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url); + if (service->songs_collection_backend()) + service->songs_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url); + break; } + + } + + if (song->url() == app_->current_albumcover_loader()->last_song().url()) { + app_->current_albumcover_loader()->LoadAlbumCover(*song); } } -QString AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const QImage &image) { +QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const QUrl &cover_url, const QImage &image, const bool overwrite) { - QString albumartist(song->effective_albumartist()); - QString artist(song->artist()); - QString album(song->effective_album()); - album.remove(Song::kAlbumRemoveDisc); - - return SaveCoverToFileAutomatic(albumartist, artist, album, song->url().adjusted(QUrl::RemoveFilename).path(), image); + return SaveCoverToFileAutomatic(song->source(), song->effective_albumartist(), song->effective_album(), song->album_id(), song->url().adjusted(QUrl::RemoveFilename).path(), cover_url, image, overwrite); } -QString AlbumCoverChoiceController::SaveCoverToFileAutomatic(const QString &albumartist, const QString &artist, const QString &album, const QString &album_dir, const QImage &image) { +QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QImage &image, const bool overwrite) { - QString album_new(album); - album_new.remove(Song::kAlbumRemoveDisc); + QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, cover_url); + if (filepath.isEmpty()) return QUrl(); - QString path; - QString filename; - if (cover_album_dir_) { - path = album_dir; - } - else { - path = AlbumCoverLoader::ImageCacheDir(); + QUrl new_cover_url; + new_cover_url.setScheme("file"); + new_cover_url.setPath(filepath); + + // Don't overwrite when saving in album dir if the filename is set to pattern unless the "overwrite" is set. + if (source == Song::Source_Collection && QFile::exists(filepath) && !cover_overwrite_ && !overwrite && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern) { + return new_cover_url; } - if (path.right(1) == QDir::separator()) { - path.chop(1); - } + if (!image.save(filepath, "JPG") && !QFile::exists(filepath)) return QUrl(); - QDir dir; - if (!dir.mkpath(path)) { - qLog(Error) << "Unable to create directory" << path; - return QString(); - } - - if (cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) { - filename = CreateCoverFilename(albumartist, artist, album_new) + ".jpg"; - filename.remove(OrganiseFormat::kValidFatCharacters); - if (cover_lowercase_) filename = filename.toLower(); - if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-"); - } - else { - filename = Utilities::Sha1CoverHash(albumartist, album_new).toHex() + ".jpg"; - } - - QString filepath(path + "/" + filename); - - // Don't overwrite when saving in album dir if the filename is set to pattern unless the "cover_overwrite" is set. - if (QFile::exists(filepath) && !cover_overwrite_ && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern) { - return filepath; - } - - image.save(filepath, "JPG"); - - return filepath; - -} - -QString AlbumCoverChoiceController::CreateCoverFilename(const QString &albumartist, const QString &artist, const QString &album) { - - QString filename(cover_pattern_); - filename.replace("%albumartist", albumartist); - filename.replace("%artist", artist); - filename.replace("%album", album); - return filename; + return new_cover_url; } @@ -426,7 +423,7 @@ bool AlbumCoverChoiceController::CanAcceptDrag(const QDragEnterEvent *e) { } -QString AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) { +QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) { for (const QUrl &url : e->mimeData()->urls()) { @@ -434,21 +431,21 @@ QString AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) { const QString suffix = QFileInfo(filename).suffix().toLower(); if (IsKnownImageExtension(suffix)) { - SaveCover(song, filename); - return filename; + SaveCoverToSong(song, url); + return url; } } if (e->mimeData()->hasImage()) { QImage image = qvariant_cast(e->mimeData()->imageData()); if (!image.isNull()) { - QString cover_path = SaveCoverToFileAutomatic(song, image); - if (cover_path.isEmpty()) return QString(); - SaveCover(song, cover_path); - return cover_path; + QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true); + if (cover_url.isEmpty()) return QUrl(); + SaveCoverToSong(song, cover_url); + return cover_url; } } - return QString(); + return QUrl(); } diff --git a/src/covermanager/albumcoverchoicecontroller.h b/src/covermanager/albumcoverchoicecontroller.h index de0dc116..44dc178a 100644 --- a/src/covermanager/albumcoverchoicecontroller.h +++ b/src/covermanager/albumcoverchoicecontroller.h @@ -2,6 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome + * Copyright 2019, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,7 +59,7 @@ class AlbumCoverChoiceController : public QWidget { AlbumCoverChoiceController(QWidget *parent = nullptr); ~AlbumCoverChoiceController(); - void SetApplication(Application *app); + void Init(Application *app); void ReloadSettings(); // Getters for all QActions implemented by this controller. @@ -85,7 +86,7 @@ class AlbumCoverChoiceController : public QWidget { // Lets the user choose a cover from disk. If no cover will be chosen or the chosen cover will not be a proper image, this returns an empty string. // Otherwise, the path to the chosen cover will be returned. - QString LoadCoverFromFile(Song *song); + QUrl LoadCoverFromFile(Song *song); // Shows a dialog that allows user to save the given image on disk. // The image is supposed to be the cover of the given song's album. @@ -93,14 +94,14 @@ class AlbumCoverChoiceController : public QWidget { // Downloads the cover from an URL given by user. // This returns the downloaded image or null image if something went wrong for example when user cancelled the dialog. - QString LoadCoverFromURL(Song *song); + QUrl LoadCoverFromURL(Song *song); // Lets the user choose a cover among all that have been found on last.fm. // Returns the chosen cover or null cover if user didn't choose anything. - QString SearchForCover(Song *song); + QUrl SearchForCover(Song *song); // Returns a path which indicates that the cover has been unset manually. - QString UnsetCover(Song *song); + QUrl UnsetCover(Song *song); // Shows the cover of given song in it's original size. void ShowCover(const Song &song); @@ -111,15 +112,14 @@ class AlbumCoverChoiceController : public QWidget { void SearchCoverAutomatically(const Song &song); // Saves the chosen cover as manual cover path of this song in collection. - void SaveCover(Song *song, const QString &cover); + void SaveCoverToSong(Song *song, const QUrl &cover_url); // Saves the cover that the user picked through a drag and drop operation. - QString SaveCover(Song *song, const QDropEvent *e); + QUrl SaveCover(Song *song, const QDropEvent *e); // Saves the given image in album directory or cache as a cover for 'album artist' - 'album'. The method returns path of the image. - QString SaveCoverToFileAutomatic(const QString &albumartist, const QString &artist, const QString &album, const QString &album_dir, const QImage &image); - QString SaveCoverToFileAutomatic(const Song *song, const QImage &image); - QString CreateCoverFilename(const QString &albumartist, const QString &artist, const QString &album); + QUrl SaveCoverToFileAutomatic(const Song *song, const QUrl &cover_url, const QImage &image, const bool overwrite = false); + QUrl SaveCoverToFileAutomatic(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QImage &image, const bool overwrite = false); static bool CanAcceptDrag(const QDragEnterEvent *e); @@ -127,7 +127,7 @@ signals: void AutomaticCoverSearchDone(); private slots: - void AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics); + void AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics); private: QString GetInitialPathForFileDialog(const Song &song, const QString &filename); diff --git a/src/covermanager/albumcoverfetcher.cpp b/src/covermanager/albumcoverfetcher.cpp index e79a39b1..1c357680 100644 --- a/src/covermanager/albumcoverfetcher.cpp +++ b/src/covermanager/albumcoverfetcher.cpp @@ -114,15 +114,15 @@ void AlbumCoverFetcher::StartRequests() { AlbumCoverFetcherSearch *search = new AlbumCoverFetcherSearch(request, network_, this); active_requests_.insert(request.id, search); - connect(search, SIGNAL(SearchFinished(quint64, CoverSearchResults)), SLOT(SingleSearchFinished(quint64, CoverSearchResults))); - connect(search, SIGNAL(AlbumCoverFetched(quint64, const QImage&)), SLOT(SingleCoverFetched(quint64, const QImage&))); + connect(search, SIGNAL(SearchFinished(const quint64, const CoverSearchResults)), SLOT(SingleSearchFinished(const quint64, const CoverSearchResults))); + connect(search, SIGNAL(AlbumCoverFetched(const quint64, const QUrl&, const QImage&)), SLOT(SingleCoverFetched(const quint64, const QUrl&, const QImage&))); search->Start(cover_providers_); } } -void AlbumCoverFetcher::SingleSearchFinished(quint64 request_id, CoverSearchResults results) { +void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const CoverSearchResults results) { AlbumCoverFetcherSearch *search = active_requests_.take(request_id); if (!search) return; @@ -132,13 +132,13 @@ void AlbumCoverFetcher::SingleSearchFinished(quint64 request_id, CoverSearchResu } -void AlbumCoverFetcher::SingleCoverFetched(quint64 request_id, const QImage &image) { +void AlbumCoverFetcher::SingleCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &image) { AlbumCoverFetcherSearch *search = active_requests_.take(request_id); if (!search) return; search->deleteLater(); - emit AlbumCoverFetched(request_id, image, search->statistics()); + emit AlbumCoverFetched(request_id, cover_url, image, search->statistics()); } diff --git a/src/covermanager/albumcoverfetcher.h b/src/covermanager/albumcoverfetcher.h index 08cf190f..2fe0fbd2 100644 --- a/src/covermanager/albumcoverfetcher.h +++ b/src/covermanager/albumcoverfetcher.h @@ -36,7 +36,6 @@ #include #include #include -#include #include class CoverProviders; @@ -45,6 +44,8 @@ struct CoverSearchStatistics; // This class represents a single search-for-cover request. It identifies and describes the request. struct CoverSearchRequest { + CoverSearchRequest() : id(-1), search(false), fetchall(false) {} + // An unique (for one AlbumCoverFetcher) request identifier quint64 id; @@ -61,6 +62,8 @@ struct CoverSearchRequest { // This structure represents a single result of some album's cover search request. struct CoverSearchResult { + CoverSearchResult() : score(0.0) {} + // Used for grouping in the user interface. QString provider; @@ -92,17 +95,17 @@ class AlbumCoverFetcher : public QObject { static const int kMaxConcurrentRequests; quint64 SearchForCovers(const QString &artist, const QString &album); - quint64 FetchAlbumCover(const QString &artist, const QString &album, bool fetchall); + quint64 FetchAlbumCover(const QString &artist, const QString &album, const bool fetchall); void Clear(); signals: - void AlbumCoverFetched(quint64, const QImage &cover, const CoverSearchStatistics &statistics); - void SearchFinished(quint64, const CoverSearchResults &results, const CoverSearchStatistics &statistics); + void AlbumCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &cover, const CoverSearchStatistics &statistics); + void SearchFinished(const quint64 request_id, const CoverSearchResults &results, const CoverSearchStatistics &statistics); private slots: - void SingleSearchFinished(quint64, CoverSearchResults results); - void SingleCoverFetched(quint64, const QImage &cover); + void SingleSearchFinished(const quint64, const CoverSearchResults results); + void SingleCoverFetched(const quint64, const QUrl &cover_url, const QImage &cover); void StartRequests(); private: diff --git a/src/covermanager/albumcoverfetchersearch.cpp b/src/covermanager/albumcoverfetchersearch.cpp index 36af5409..654252b0 100644 --- a/src/covermanager/albumcoverfetchersearch.cpp +++ b/src/covermanager/albumcoverfetchersearch.cpp @@ -68,7 +68,7 @@ AlbumCoverFetcherSearch::AlbumCoverFetcherSearch( void AlbumCoverFetcherSearch::TerminateSearch() { - for (int id : pending_requests_.keys()) { + for (quint64 id : pending_requests_.keys()) { pending_requests_.take(id)->CancelSearch(id); } @@ -86,7 +86,7 @@ void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) { continue; } - connect(provider, SIGNAL(SearchFinished(int, QList)), SLOT(ProviderSearchFinished(int, QList))); + connect(provider, SIGNAL(SearchFinished(int, CoverSearchResults)), SLOT(ProviderSearchFinished(int, CoverSearchResults))); const int id = cover_providers->NextId(); const bool success = provider->StartSearch(request_.artist, request_.album, id); @@ -107,7 +107,7 @@ static bool CompareProviders(const CoverSearchResult &a, const CoverSearchResult return a.provider < b.provider; } -void AlbumCoverFetcherSearch::ProviderSearchFinished(int id, const QList &results) { +void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverSearchResults &results) { if (!pending_requests_.contains(id)) return; CoverProvider *provider = pending_requests_.take(id); @@ -155,7 +155,7 @@ void AlbumCoverFetcherSearch::AllProvidersFinished() { // No results? if (results_.isEmpty()) { statistics_.missing_images_++; - emit AlbumCoverFetched(request_.id, QImage()); + emit AlbumCoverFetched(request_.id, QUrl(), QImage()); return; } @@ -265,10 +265,12 @@ float AlbumCoverFetcherSearch::ScoreImage(const QImage &image) const { void AlbumCoverFetcherSearch::SendBestImage() { + QUrl cover_url; QImage image; if (!candidate_images_.isEmpty()) { const CandidateImage best_image = candidate_images_.values().back(); + cover_url = best_image.first.image_url; image = best_image.second; qLog(Info) << "Using " << best_image.first.image_url << "from" << best_image.first.provider << "with score" << best_image.first.score; @@ -282,7 +284,7 @@ void AlbumCoverFetcherSearch::SendBestImage() { statistics_.missing_images_++; } - emit AlbumCoverFetched(request_.id, image); + emit AlbumCoverFetched(request_.id, cover_url, image); } diff --git a/src/covermanager/albumcoverfetchersearch.h b/src/covermanager/albumcoverfetchersearch.h index e11923fc..eed4e290 100644 --- a/src/covermanager/albumcoverfetchersearch.h +++ b/src/covermanager/albumcoverfetchersearch.h @@ -60,13 +60,13 @@ class AlbumCoverFetcherSearch : public QObject { signals: // It's the end of search (when there was no fetch-me-a-cover request). - void SearchFinished(quint64, const CoverSearchResults &results); + void SearchFinished(const quint64, const CoverSearchResults &results); // It's the end of search and we've fetched a cover. - void AlbumCoverFetched(quint64, const QImage &cover); + void AlbumCoverFetched(const quint64, const QUrl &cover_url, const QImage &cover); private slots: - void ProviderSearchFinished(int id, const QList &results); + void ProviderSearchFinished(const int id, const CoverSearchResults &results); void ProviderCoverFetchFinished(RedirectFollower *reply); void TerminateSearch(); @@ -106,4 +106,3 @@ class AlbumCoverFetcherSearch : public QObject { }; #endif // ALBUMCOVERFETCHERSEARCH_H - diff --git a/src/covermanager/albumcoverloader.cpp b/src/covermanager/albumcoverloader.cpp index 4a1ec4db..1750b485 100644 --- a/src/covermanager/albumcoverloader.cpp +++ b/src/covermanager/albumcoverloader.cpp @@ -2,6 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome + * Copyright 2019, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,6 +23,7 @@ #include #include +#include #include #include #include @@ -37,11 +39,15 @@ #include #include #include +#include #include "core/closure.h" #include "core/network.h" #include "core/song.h" #include "core/tagreaderclient.h" +#include "core/utilities.h" +#include "settings/collectionsettingspage.h" +#include "organise/organiseformat.h" #include "albumcoverloader.h" #include "albumcoverloaderoptions.h" @@ -49,13 +55,147 @@ AlbumCoverLoader::AlbumCoverLoader(QObject *parent) : QObject(parent), stop_requested_(false), next_id_(1), - network_(new NetworkAccessManager(this)){} + network_(new NetworkAccessManager(this)), + cover_album_dir_(false), + cover_filename_(CollectionSettingsPage::SaveCover_Hash), + cover_overwrite_(false), + cover_lowercase_(true), + cover_replace_spaces_(true) + { + + ReloadSettings(); -QString AlbumCoverLoader::ImageCacheDir() { - return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/albumcovers"; } -void AlbumCoverLoader::CancelTask(quint64 id) { +void AlbumCoverLoader::ReloadSettings() { + + QSettings s; + s.beginGroup(CollectionSettingsPage::kSettingsGroup); + cover_album_dir_ = s.value("cover_album_dir", false).toBool(); + cover_filename_ = CollectionSettingsPage::SaveCover(s.value("cover_filename", CollectionSettingsPage::SaveCover_Hash).toInt()); + cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString(); + cover_overwrite_ = s.value("cover_overwrite", false).toBool(); + cover_lowercase_ = s.value("cover_lowercase", false).toBool(); + cover_replace_spaces_ = s.value("cover_replace_spaces", false).toBool(); + s.endGroup(); + +} + +QString AlbumCoverLoader::ImageCacheDir(const Song::Source source) { + + switch (source) { + case Song::Source_LocalFile: + case Song::Source_Collection: + case Song::Source_CDDA: + case Song::Source_Device: + case Song::Source_Stream: + case Song::Source_Unknown: + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/albumcovers"; + case Song::Source_Tidal: + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/tidalalbumcovers"; + case Song::Source_Qobuz: + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/qobuzalbumcovers"; + case Song::Source_Subsonic: + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/subsonicalbumcovers"; + } + + return QString(); + +} + +QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url) { + + album.remove(Song::kAlbumRemoveDisc); + + QString path; + if (source == Song::Source_Collection && cover_album_dir_ && !album_dir.isEmpty()) { + path = album_dir; + } + else { + path = AlbumCoverLoader::ImageCacheDir(source); + } + + if (path.right(1) == QDir::separator()) { + path.chop(1); + } + + QDir dir; + if (!dir.mkpath(path)) { + qLog(Error) << "Unable to create directory" << path; + return QString(); + } + + QString filename; + if (source == Song::Source_Collection && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) { + filename = CreateCoverFilename(artist, album) + ".jpg"; + filename.remove(OrganiseFormat::kValidFatCharacters); + if (cover_lowercase_) filename = filename.toLower(); + if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-"); + } + else { + switch (source) { + case Song::Source_Collection: + case Song::Source_LocalFile: + case Song::Source_CDDA: + case Song::Source_Device: + case Song::Source_Stream: + case Song::Source_Unknown: + filename = Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg"; + break; + case Song::Source_Tidal: + filename = album_id + "-" + cover_url.fileName(); + break; + case Song::Source_Qobuz: + case Song::Source_Subsonic: + filename = AlbumCoverFileName(artist, album); + break; + } + } + + if (filename.isEmpty()) return QString(); + + QString filepath(path + "/" + filename); + + return filepath; + +} + +QString AlbumCoverLoader::AlbumCoverFileName(QString artist, QString album) { + + artist.remove('/'); + album.remove('/'); + + QString filename = artist + "-" + album + ".jpg"; + filename = filename.toLower(); + filename.replace(' ', '-'); + filename.replace("--", "-"); + filename.replace(230, "ae"); + filename.replace(198, "AE"); + filename.replace(246, 'o'); + filename.replace(248, 'o'); + filename.replace(214, 'O'); + filename.replace(216, 'O'); + filename.replace(228, 'a'); + filename.replace(229, 'a'); + filename.replace(196, 'A'); + filename.replace(197, 'A'); + filename.remove(OrganiseFormat::kValidFatCharacters); + + return filename; + +} + +QString AlbumCoverLoader::CreateCoverFilename(const QString &artist, const QString &album) { + + QString filename(cover_pattern_); + filename.replace("%albumartist", artist); + filename.replace("%artist", artist); + filename.replace("%album", album); + return filename; + +} + +void AlbumCoverLoader::CancelTask(const quint64 id) { QMutexLocker l(&mutex_); for (QQueue::iterator it = tasks_.begin(); it != tasks_.end(); ++it) { @@ -83,7 +223,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options, return LoadImageAsync(options, song.art_automatic(), song.art_manual(), song.url().toLocalFile(), song.image()); } -quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QString &art_automatic, const QString &art_manual, const QString &song_filename, const QImage &embedded_image) { +quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QString &song_filename, const QImage &embedded_image) { Task task; task.options = options; @@ -102,6 +242,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, metaObject()->invokeMethod(this, "ProcessTasks", Qt::QueuedConnection); return task.id; + } void AlbumCoverLoader::ProcessTasks() { @@ -129,12 +270,13 @@ void AlbumCoverLoader::ProcessTask(Task *task) { if (result.loaded_success) { QImage scaled = ScaleAndPad(task->options, result.image); - emit ImageLoaded(task->id, scaled); - emit ImageLoaded(task->id, scaled, result.image); + emit ImageLoaded(task->id, result.cover_url, scaled); + emit ImageLoaded(task->id, result.cover_url, scaled, result.image); return; } NextState(task); + } void AlbumCoverLoader::NextState(Task *task) { @@ -146,8 +288,8 @@ void AlbumCoverLoader::NextState(Task *task) { } else { // Give up - emit ImageLoaded(task->id, task->options.default_output_image_); - emit ImageLoaded(task->id, task->options.default_output_image_, task->options.default_output_image_); + emit ImageLoaded(task->id, QUrl(), task->options.default_output_image_); + emit ImageLoaded(task->id, QUrl(), task->options.default_output_image_, task->options.default_output_image_); } } @@ -156,47 +298,56 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(const Task &task) // An image embedded in the song itself takes priority if (!task.embedded_image.isNull()) - return TryLoadResult(false, true, ScaleAndPad(task.options, task.embedded_image)); + return TryLoadResult(false, true, QUrl(), ScaleAndPad(task.options, task.embedded_image)); + + QUrl cover_url; - QString filename; switch (task.state) { - case State_TryingAuto: filename = task.art_automatic; break; - case State_TryingManual: filename = task.art_manual; break; + case State_TryingAuto: cover_url = task.art_automatic; break; + case State_TryingManual: cover_url = task.art_manual; break; } - if (filename == Song::kManuallyUnsetCover) - return TryLoadResult(false, true, task.options.default_output_image_); + if (cover_url.path() == Song::kManuallyUnsetCover) + return TryLoadResult(false, true, QUrl(), task.options.default_output_image_); - if (filename == Song::kEmbeddedCover && !task.song_filename.isEmpty()) { + else if (cover_url.path() == Song::kEmbeddedCover && !task.song_filename.isEmpty()) { const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task.song_filename); if (!taglib_image.isNull()) - return TryLoadResult(false, true, ScaleAndPad(task.options, taglib_image)); + return TryLoadResult(false, true, QUrl(), ScaleAndPad(task.options, taglib_image)); } - if (filename.toLower().startsWith("http://") || filename.toLower().startsWith("https://")) { - - QUrl url(filename); - QNetworkReply *reply = network_->get(QNetworkRequest(url)); - NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*)), reply); - - remote_tasks_.insert(reply, task); - return TryLoadResult(true, false, QImage()); + if (cover_url.path().isEmpty()) { + return TryLoadResult(false, false, cover_url, task.options.default_output_image_); } - else if (filename.isEmpty()) { - // Avoid "QFSFileEngine::open: No file name specified" messages if we know that the filename is empty - return TryLoadResult(false, false, task.options.default_output_image_); + else { + if (cover_url.scheme() == "file") { + QImage image(cover_url.toLocalFile()); + return TryLoadResult(false, !image.isNull(), cover_url, image.isNull() ? task.options.default_output_image_ : image); + } + else if (cover_url.scheme().isEmpty()) { // Assume a local file with no scheme. + QImage image(cover_url.path()); + return TryLoadResult(false, !image.isNull(), cover_url, image.isNull() ? task.options.default_output_image_ : image); + } + else if (!cover_url.scheme().isEmpty()) { // Assume remote URL + + QNetworkReply *reply = network_->get(QNetworkRequest(cover_url)); + NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, const QUrl&)), reply, cover_url); + + remote_tasks_.insert(reply, task); + return TryLoadResult(true, false, cover_url, QImage()); + } } - QImage image(filename); - return TryLoadResult(false, !image.isNull(), image.isNull() ? task.options.default_output_image_ : image); + return TryLoadResult(false, false, cover_url, task.options.default_output_image_); } -void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply) { +void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url) { reply->deleteLater(); + if (!remote_tasks_.contains(reply)) return; Task task = remote_tasks_.take(reply); // Handle redirects. @@ -208,7 +359,7 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply) { QNetworkRequest request = reply->request(); request.setUrl(redirect.toUrl()); QNetworkReply *redirected_reply = network_->get(request); - NewClosure(redirected_reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*)), redirected_reply); + NewClosure(redirected_reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, const QUrl&)), redirected_reply, redirect.toUrl()); remote_tasks_.insert(redirected_reply, task); return; @@ -219,8 +370,8 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply) { QImage image; if (image.load(reply, 0)) { QImage scaled = ScaleAndPad(task.options, image); - emit ImageLoaded(task.id, scaled); - emit ImageLoaded(task.id, scaled, image); + emit ImageLoaded(task.id, cover_url, scaled); + emit ImageLoaded(task.id, cover_url, scaled, image); return; } } @@ -256,16 +407,30 @@ QImage AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions &options, con } -QPixmap AlbumCoverLoader::TryLoadPixmap(const QString &automatic, const QString &manual, const QString &filename) { +QPixmap AlbumCoverLoader::TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QString &filename) { QPixmap ret; - if (manual == Song::kManuallyUnsetCover) return ret; - if (!manual.isEmpty()) ret.load(manual); + if (manual.path() == Song::kManuallyUnsetCover) return ret; + if (!manual.path().isEmpty()) { + if (manual.scheme().isEmpty()) { + ret.load(manual.path()); + } + else if (manual.scheme() == "file") { + ret.load(manual.toLocalFile()); + } + } if (ret.isNull()) { - if (automatic == Song::kEmbeddedCover && !filename.isNull()) + if (automatic.path() == Song::kEmbeddedCover && !filename.isEmpty()) { ret = QPixmap::fromImage(TagReaderClient::Instance()->LoadEmbeddedArtBlocking(filename)); - else if (!automatic.isEmpty()) - ret.load(automatic); + } + else if (!automatic.path().isEmpty()) { + if (automatic.scheme().isEmpty()) { + ret.load(automatic.path()); + } + else if (manual.scheme() == "file") { + ret.load(automatic.toLocalFile()); + } + } } return ret; diff --git a/src/covermanager/albumcoverloader.h b/src/covermanager/albumcoverloader.h index 408eecbb..63386dce 100644 --- a/src/covermanager/albumcoverloader.h +++ b/src/covermanager/albumcoverloader.h @@ -2,6 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome + * Copyright 2019, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,6 +38,7 @@ #include #include "core/song.h" +#include "settings/collectionsettingspage.h" #include "albumcoverloaderoptions.h" class Song; @@ -48,26 +50,31 @@ class AlbumCoverLoader : public QObject { public: explicit AlbumCoverLoader(QObject *parent = nullptr); + void ReloadSettings(); + void Stop() { stop_requested_ = true; } - static QString ImageCacheDir(); + static QString ImageCacheDir(const Song::Source source); + QString CreateCoverFilename(const QString &artist, const QString &album); + QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url); + QString AlbumCoverFileName(QString artist, QString album); quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song); - virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QString &art_automatic, const QString &art_manual, const QString &song_filename = QString(), const QImage &embedded_image = QImage()); + virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QString &song_filename = QString(), const QImage &embedded_image = QImage()); - void CancelTask(quint64 id); + void CancelTask(const quint64 id); void CancelTasks(const QSet &ids); - static QPixmap TryLoadPixmap(const QString &automatic, const QString &manual, const QString &filename = QString()); + static QPixmap TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QString &filename = QString()); static QImage ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image); signals: - void ImageLoaded(quint64 id, const QImage &image); - void ImageLoaded(quint64 id, const QImage &scaled, const QImage &original); + void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); + void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &scaled, const QImage &original); protected slots: void ProcessTasks(); - void RemoteFetchFinished(QNetworkReply *reply); + void RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url); protected: enum State { @@ -81,8 +88,8 @@ signals: AlbumCoverLoaderOptions options; quint64 id; - QString art_automatic; - QString art_manual; + QUrl art_automatic; + QUrl art_manual; QString song_filename; QImage embedded_image; State state; @@ -90,10 +97,12 @@ signals: }; struct TryLoadResult { - TryLoadResult(bool async, bool success, const QImage &i) : started_async(async), loaded_success(success), image(i) {} + TryLoadResult(bool async, bool success, const QUrl &_cover_url, const QImage &_image) : started_async(async), loaded_success(success), cover_url(_cover_url), image(_image) {} bool started_async; bool loaded_success; + + QUrl cover_url; QImage image; }; @@ -111,6 +120,14 @@ signals: NetworkAccessManager *network_; static const int kMaxRedirects = 3; + + bool cover_album_dir_; + CollectionSettingsPage::SaveCover cover_filename_; + QString cover_pattern_; + bool cover_overwrite_; + bool cover_lowercase_; + bool cover_replace_spaces_; + }; #endif // ALBUMCOVERLOADER_H diff --git a/src/covermanager/albumcoverloaderoptions.h b/src/covermanager/albumcoverloaderoptions.h index cfe46c4c..2cbc7e8c 100644 --- a/src/covermanager/albumcoverloaderoptions.h +++ b/src/covermanager/albumcoverloaderoptions.h @@ -39,4 +39,3 @@ struct AlbumCoverLoaderOptions { }; #endif // ALBUMCOVERLOADEROPTIONS_H - diff --git a/src/covermanager/albumcovermanager.cpp b/src/covermanager/albumcovermanager.cpp index b4791098..caaa1294 100644 --- a/src/covermanager/albumcovermanager.cpp +++ b/src/covermanager/albumcovermanager.cpp @@ -117,7 +117,7 @@ AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collec ui_->action_add_to_playlist->setIcon(IconLoader::Load("media-play" )); ui_->action_load->setIcon(IconLoader::Load("media-play" )); - album_cover_choice_controller_->SetApplication(app_); + album_cover_choice_controller_->Init(app_); cover_searcher_ = new AlbumCoverSearcher(no_cover_item_icon_, app_, this); cover_export_ = new AlbumCoverExport(this); @@ -146,7 +146,7 @@ AlbumCoverManager::~AlbumCoverManager() { } void AlbumCoverManager::ReloadSettings() { - album_cover_choice_controller_->ReloadSettings(); + app_->album_cover_loader()->ReloadSettings(); } CollectionBackend *AlbumCoverManager::backend() const { @@ -198,7 +198,7 @@ void AlbumCoverManager::Init() { connect(ui_->view, SIGNAL(clicked()), ui_->view, SLOT(showMenu())); connect(ui_->button_fetch, SIGNAL(clicked()), SLOT(FetchAlbumCovers())); connect(ui_->export_covers, SIGNAL(clicked()), SLOT(ExportCovers())); - connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)), SLOT(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics))); + connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(const quint64, const QUrl&, const QImage&, const CoverSearchStatistics&)), SLOT(AlbumCoverFetched(const quint64, const QUrl&, const QImage&, const CoverSearchStatistics&))); connect(ui_->action_fetch, SIGNAL(triggered()), SLOT(FetchSingleCover())); connect(ui_->albums, SIGNAL(doubleClicked(QModelIndex)), SLOT(AlbumDoubleClicked(QModelIndex))); connect(ui_->action_add_to_playlist, SIGNAL(triggered()), SLOT(AddSelectedToPlaylist())); @@ -214,7 +214,7 @@ void AlbumCoverManager::Init() { ui_->splitter->setSizes(QList() << 200 << width() - 200); } - connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(CoverImageLoaded(quint64, QImage))); + connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(const quint64, const QUrl&, const QImage&)), SLOT(CoverImageLoaded(const quint64, const QUrl&, const QImage&))); cover_searcher_->Init(cover_fetcher_); @@ -357,7 +357,7 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) { } -void AlbumCoverManager::CoverImageLoaded(quint64 id, const QImage &image) { +void AlbumCoverManager::CoverImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { if (!cover_loading_tasks_.contains(id)) return; @@ -455,14 +455,14 @@ void AlbumCoverManager::FetchAlbumCovers() { } -void AlbumCoverManager::AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics) { +void AlbumCoverManager::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) { if (!cover_fetching_tasks_.contains(id)) return; QListWidgetItem *item = cover_fetching_tasks_.take(id); if (!image.isNull()) { - SaveAndSetCover(item, image); + SaveAndSetCover(item, cover_url, image); } if (cover_fetching_tasks_.isEmpty()) { @@ -537,7 +537,7 @@ Song AlbumCoverManager::GetFirstSelectedAsSong() { Song AlbumCoverManager::ItemAsSong(QListWidgetItem *item) { - Song result; + Song result(Song::Source_Collection); QString title = item->data(Role_AlbumName).toString(); QString artist_name = EffectiveAlbumArtistName(*item); @@ -554,8 +554,8 @@ Song AlbumCoverManager::ItemAsSong(QListWidgetItem *item) { result.set_url(item->data(Role_FirstUrl).toUrl()); - result.set_art_automatic(item->data(Role_PathAutomatic).toString()); - result.set_art_manual(item->data(Role_PathManual).toString()); + result.set_art_automatic(item->data(Role_PathAutomatic).toUrl()); + result.set_art_manual(item->data(Role_PathManual).toUrl()); // force validity result.set_valid(true); @@ -587,10 +587,10 @@ void AlbumCoverManager::FetchSingleCover() { } -void AlbumCoverManager::UpdateCoverInList(QListWidgetItem *item, const QString &cover) { +void AlbumCoverManager::UpdateCoverInList(QListWidgetItem *item, const QUrl &cover_url) { - quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QString(), cover); - item->setData(Role_PathManual, cover); + quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QUrl(), cover_url); + item->setData(Role_PathManual, cover_url); cover_loading_tasks_[id] = item; } @@ -602,10 +602,10 @@ void AlbumCoverManager::LoadCoverFromFile() { QListWidgetItem *item = context_menu_items_[0]; - QString cover = album_cover_choice_controller_->LoadCoverFromFile(&song); + QUrl cover_url = album_cover_choice_controller_->LoadCoverFromFile(&song); - if (!cover.isEmpty()) { - UpdateCoverInList(item, cover); + if (!cover_url.isEmpty()) { + UpdateCoverInList(item, cover_url); } } @@ -622,11 +622,23 @@ void AlbumCoverManager::SaveCoverToFile() { image = no_cover_image_; } else { - if (!song.art_manual().isEmpty() && QFile::exists(song.art_manual())) { - image = QImage(song.art_manual()); + if (!song.art_manual().isEmpty() && !song.art_manual().path().isEmpty() && + ( + (song.art_manual().scheme().isEmpty() && QFile::exists(song.art_manual().path())) + || + (song.art_manual().scheme() == "file" && QFile::exists(song.art_manual().toLocalFile())) + ) + ) { + image = QImage(song.art_manual().toLocalFile()); } - else if(!song.art_automatic().isEmpty() && QFile::exists(song.art_automatic())) { - image = QImage(song.art_automatic()); + if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() && + ( + (song.art_automatic().scheme().isEmpty() && QFile::exists(song.art_automatic().path())) + || + (song.art_automatic().scheme() == "file" && QFile::exists(song.art_automatic().toLocalFile())) + ) + ) { + image = QImage(song.art_automatic().toLocalFile()); } else { image = no_cover_image_; @@ -644,10 +656,10 @@ void AlbumCoverManager::LoadCoverFromURL() { QListWidgetItem *item = context_menu_items_[0]; - QString cover = album_cover_choice_controller_->LoadCoverFromURL(&song); + QUrl cover_url = album_cover_choice_controller_->LoadCoverFromURL(&song); - if (!cover.isEmpty()) { - UpdateCoverInList(item, cover); + if (!cover_url.isEmpty()) { + UpdateCoverInList(item, cover_url); } } @@ -659,18 +671,18 @@ void AlbumCoverManager::SearchForCover() { QListWidgetItem *item = context_menu_items_[0]; - QString cover = album_cover_choice_controller_->SearchForCover(&song); - if (cover.isEmpty()) return; + QUrl cover_url = album_cover_choice_controller_->SearchForCover(&song); + if (cover_url.isEmpty()) return; // Force the found cover on all of the selected items for (QListWidgetItem *current : context_menu_items_) { // Don't save the first one twice if (current != item) { Song current_song = ItemAsSong(current); - album_cover_choice_controller_->SaveCover(¤t_song, cover); + album_cover_choice_controller_->SaveCoverToSong(¤t_song, cover_url); } - UpdateCoverInList(current, cover); + UpdateCoverInList(current, cover_url); } } @@ -682,17 +694,17 @@ void AlbumCoverManager::UnsetCover() { QListWidgetItem *item = context_menu_items_[0]; - QString cover = album_cover_choice_controller_->UnsetCover(&song); + QUrl cover_url = album_cover_choice_controller_->UnsetCover(&song); // Force the 'none' cover on all of the selected items for (QListWidgetItem *current : context_menu_items_) { current->setIcon(no_cover_item_icon_); - current->setData(Role_PathManual, cover); + current->setData(Role_PathManual, cover_url); // Don't save the first one twice if (current != item) { Song current_song = ItemAsSong(current); - album_cover_choice_controller_->SaveCover(¤t_song, cover); + album_cover_choice_controller_->SaveCoverToSong(¤t_song, cover_url); } } @@ -776,22 +788,22 @@ void AlbumCoverManager::LoadSelectedToPlaylist() { } -void AlbumCoverManager::SaveAndSetCover(QListWidgetItem *item, const QImage &image) { +void AlbumCoverManager::SaveAndSetCover(QListWidgetItem *item, const QUrl &cover_url, const QImage &image) { const QString artist = item->data(Role_ArtistName).toString(); const QString albumartist = item->data(Role_AlbumArtistName).toString(); const QString album = item->data(Role_AlbumName).toString(); const QUrl url = item->data(Role_FirstUrl).toUrl(); - QString path = album_cover_choice_controller_->SaveCoverToFileAutomatic((!albumartist.isEmpty() ? albumartist : artist), artist, album, url.adjusted(QUrl::RemoveFilename).path(), image); - if (path.isEmpty()) return; + QUrl new_cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(Song::Source_Collection, (!albumartist.isEmpty() ? albumartist : artist), album, QString(), url.adjusted(QUrl::RemoveFilename).path(), cover_url, image, false); + if (new_cover_url.isEmpty()) return; // Save the image in the database - collection_backend_->UpdateManualAlbumArtAsync(artist, albumartist, album, path); + collection_backend_->UpdateManualAlbumArtAsync(artist, albumartist, album, new_cover_url); // Update the icon in our list - quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QString(), path); - item->setData(Role_PathManual, path); + quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QUrl(), new_cover_url); + item->setData(Role_PathManual, new_cover_url); cover_loading_tasks_[id] = item; } diff --git a/src/covermanager/albumcovermanager.h b/src/covermanager/albumcovermanager.h index 9b51793d..d79b6f0b 100644 --- a/src/covermanager/albumcovermanager.h +++ b/src/covermanager/albumcovermanager.h @@ -93,11 +93,11 @@ class AlbumCoverManager : public QMainWindow { private slots: void ArtistChanged(QListWidgetItem *current); - void CoverImageLoaded(quint64 id, const QImage &image); + void CoverImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void UpdateFilter(); void FetchAlbumCovers(); void ExportCovers(); - void AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics); + void AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics); void CancelRequests(); // On the context menu @@ -115,7 +115,7 @@ class AlbumCoverManager : public QMainWindow { void AddSelectedToPlaylist(); void LoadSelectedToPlaylist(); - void UpdateCoverInList(QListWidgetItem *item, const QString &cover); + void UpdateCoverInList(QListWidgetItem *item, const QUrl &cover); void UpdateExportStatus(int exported, int bad, int count); private: @@ -152,7 +152,7 @@ class AlbumCoverManager : public QMainWindow { void UpdateStatusText(); bool ShouldHide(const QListWidgetItem &item, const QString &filter, HideCovers hide) const; - void SaveAndSetCover(QListWidgetItem *item, const QImage &image); + void SaveAndSetCover(QListWidgetItem *item, const QUrl &cover_url, const QImage &image); private: Ui_CoverManager *ui_; diff --git a/src/covermanager/albumcoversearcher.cpp b/src/covermanager/albumcoversearcher.cpp index f6c488a0..90a9b5b4 100644 --- a/src/covermanager/albumcoversearcher.cpp +++ b/src/covermanager/albumcoversearcher.cpp @@ -127,7 +127,7 @@ AlbumCoverSearcher::AlbumCoverSearcher(const QIcon &no_cover_icon, Application * options_.scale_output_image_ = false; options_.pad_output_image_ = false; - connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(ImageLoaded(quint64, QImage))); + connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(ImageLoaded(quint64, QUrl, QImage))); connect(ui_->search, SIGNAL(clicked()), SLOT(Search())); connect(ui_->covers, SIGNAL(doubleClicked(QModelIndex)), SLOT(CoverDoubleClicked(QModelIndex))); @@ -145,7 +145,7 @@ AlbumCoverSearcher::~AlbumCoverSearcher() { void AlbumCoverSearcher::Init(AlbumCoverFetcher *fetcher) { fetcher_ = fetcher; - connect(fetcher_, SIGNAL(SearchFinished(quint64,CoverSearchResults,CoverSearchStatistics)), SLOT(SearchFinished(quint64, CoverSearchResults))); + connect(fetcher_, SIGNAL(SearchFinished(quint64, CoverSearchResults, CoverSearchStatistics)), SLOT(SearchFinished(quint64, CoverSearchResults))); } @@ -197,7 +197,7 @@ void AlbumCoverSearcher::Search() { } -void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults &results) { +void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverSearchResults &results) { if (id != id_) return; @@ -212,7 +212,7 @@ void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults &re for (const CoverSearchResult &result : results) { if (result.image_url.isEmpty()) continue; - quint64 id = app_->album_cover_loader()->LoadImageAsync(options_, result.image_url.toString(), QString()); + quint64 id = app_->album_cover_loader()->LoadImageAsync(options_, result.image_url, QUrl()); QStandardItem *item = new QStandardItem; item->setIcon(no_cover_icon_); @@ -232,7 +232,7 @@ void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults &re } -void AlbumCoverSearcher::ImageLoaded(quint64 id, const QImage &image) { +void AlbumCoverSearcher::ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { if (!cover_loading_tasks_.contains(id)) return; QStandardItem *item = cover_loading_tasks_.take(id); diff --git a/src/covermanager/albumcoversearcher.h b/src/covermanager/albumcoversearcher.h index 169eb283..b2aae56e 100644 --- a/src/covermanager/albumcoversearcher.h +++ b/src/covermanager/albumcoversearcher.h @@ -87,8 +87,8 @@ protected: private slots: void Search(); - void SearchFinished(quint64 id, const CoverSearchResults &results); - void ImageLoaded(quint64 id, const QImage &image); + void SearchFinished(const quint64 id, const CoverSearchResults &results); + void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void CoverDoubleClicked(const QModelIndex &index); diff --git a/src/covermanager/coverexportrunnable.cpp b/src/covermanager/coverexportrunnable.cpp index a2146540..a6b776d5 100644 --- a/src/covermanager/coverexportrunnable.cpp +++ b/src/covermanager/coverexportrunnable.cpp @@ -59,11 +59,11 @@ QString CoverExportRunnable::GetCoverPath() { // Export downloaded covers? } else if (!song_.art_manual().isEmpty() && dialog_result_.export_downloaded_) { - return song_.art_manual(); + return song_.art_manual().toLocalFile(); // Export embedded covers? } - else if (!song_.art_automatic().isEmpty() && song_.art_automatic() == Song::kEmbeddedCover && dialog_result_.export_embedded_) { - return song_.art_automatic(); + else if (!song_.art_automatic().isEmpty() && song_.art_automatic().path() == Song::kEmbeddedCover && dialog_result_.export_embedded_) { + return song_.art_automatic().toLocalFile(); } else { return QString(); @@ -168,8 +168,7 @@ void CoverExportRunnable::ExportCover() { return; } - // we're handling overwrite as remove + copy so we need to delete the old file - // first + // We're handling overwrite as remove + copy so we need to delete the old file first if (dialog_result_.overwrite_ != AlbumCoverExport::OverwriteMode_None && QFile::exists(new_file)) { if (!QFile::remove(new_file)) { EmitCoverSkipped(); @@ -186,7 +185,7 @@ void CoverExportRunnable::ExportCover() { } } else { - // automatic or manual cover, available in an image file + // Automatic or manual cover, available in an image file if (!QFile::copy(cover_path, new_file)) { EmitCoverSkipped(); return; diff --git a/src/covermanager/coverprovider.h b/src/covermanager/coverprovider.h index f87c56cb..03e3a01f 100644 --- a/src/covermanager/coverprovider.h +++ b/src/covermanager/coverprovider.h @@ -29,8 +29,9 @@ #include #include +#include "albumcoverfetcher.h" + class Application; -struct CoverSearchResult; // Each implementation of this interface downloads covers from one online service. // There are no limitations on what this service might be - last.fm, Amazon, Google Images - you name it. @@ -53,7 +54,7 @@ class CoverProvider : public QObject { virtual void CancelSearch(int id) {} signals: - void SearchFinished(int id, const QList& results); + void SearchFinished(int id, const CoverSearchResults& results); private: Application *app_; diff --git a/src/covermanager/currentartloader.cpp b/src/covermanager/currentalbumcoverloader.cpp similarity index 57% rename from src/covermanager/currentartloader.cpp rename to src/covermanager/currentalbumcoverloader.cpp index 0329a8d2..baf142e8 100644 --- a/src/covermanager/currentartloader.cpp +++ b/src/covermanager/currentalbumcoverloader.cpp @@ -28,63 +28,71 @@ #include #include #include +#include #include #include "core/application.h" #include "playlist/playlistmanager.h" #include "albumcoverloader.h" -#include "currentartloader.h" +#include "currentalbumcoverloader.h" -CurrentArtLoader::CurrentArtLoader(Application *app, QObject *parent) +CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *parent) : QObject(parent), app_(app), - temp_file_pattern_(QDir::tempPath() + "/strawberry-art-XXXXXX.jpg"), - id_(0) + temp_file_pattern_(QDir::tempPath() + "/strawberry-cover-XXXXXX.jpg"), + id_(0) { options_.scale_output_image_ = false; options_.pad_output_image_ = false; options_.default_output_image_ = QImage(":/pictures/cdcase.png"); - connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(TempArtLoaded(quint64, QImage))); - connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(LoadArt(Song))); + connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(TempAlbumCoverLoaded(quint64, QUrl, QImage))); + connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(LoadAlbumCover(Song))); } -CurrentArtLoader::~CurrentArtLoader() {} +CurrentAlbumCoverLoader::~CurrentAlbumCoverLoader() {} -void CurrentArtLoader::LoadArt(const Song &song) { +void CurrentAlbumCoverLoader::LoadAlbumCover(const Song &song) { last_song_ = song; id_ = app_->album_cover_loader()->LoadImageAsync(options_, last_song_); } -void CurrentArtLoader::TempArtLoaded(quint64 id, const QImage &image) { +void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, const QUrl &remote_url, const QImage &image) { if (id != id_) return; id_ = 0; - QString uri; - QString thumbnail_uri; + QUrl cover_url; + QUrl thumbnail_url; QImage thumbnail; if (!image.isNull()) { - temp_art_.reset(new QTemporaryFile(temp_file_pattern_)); - temp_art_->setAutoRemove(true); - temp_art_->open(); - image.save(temp_art_->fileName(), "JPEG"); + + QString filename; + + temp_cover_.reset(new QTemporaryFile(temp_file_pattern_)); + temp_cover_->setAutoRemove(true); + temp_cover_->open(); + + image.save(temp_cover_->fileName(), "JPEG"); // Scale the image down to make a thumbnail. It's a bit crap doing it here since it's the GUI thread, but the alternative is hard. - temp_art_thumbnail_.reset(new QTemporaryFile(temp_file_pattern_)); - temp_art_thumbnail_->open(); - temp_art_thumbnail_->setAutoRemove(true); + temp_cover_thumbnail_.reset(new QTemporaryFile(temp_file_pattern_)); + temp_cover_thumbnail_->open(); + temp_cover_thumbnail_->setAutoRemove(true); thumbnail = image.scaledToHeight(120, Qt::SmoothTransformation); - thumbnail.save(temp_art_thumbnail_->fileName(), "JPEG"); + thumbnail.save(temp_cover_thumbnail_->fileName(), "JPEG"); - uri = "file://" + temp_art_->fileName(); - thumbnail_uri = "file://" + temp_art_thumbnail_->fileName(); + cover_url.setScheme("file"); + cover_url.setPath(temp_cover_->fileName()); + + thumbnail_url.setScheme("file"); + thumbnail_url.setPath(temp_cover_thumbnail_->fileName()); } - emit ArtLoaded(last_song_, uri, image); - emit ThumbnailLoaded(last_song_, thumbnail_uri, thumbnail); + emit AlbumCoverLoaded(last_song_, cover_url, image); + emit ThumbnailLoaded(last_song_, thumbnail_url, thumbnail); } diff --git a/src/covermanager/currentartloader.h b/src/covermanager/currentalbumcoverloader.h similarity index 65% rename from src/covermanager/currentartloader.h rename to src/covermanager/currentalbumcoverloader.h index a32b32d7..69156eb8 100644 --- a/src/covermanager/currentartloader.h +++ b/src/covermanager/currentalbumcoverloader.h @@ -18,8 +18,8 @@ * */ -#ifndef CURRENTARTLOADER_H -#define CURRENTARTLOADER_H +#ifndef CURRENTALBUMCOVERLOADER_H +#define CURRENTALBUMCOVERLOADER_H #include "config.h" @@ -36,25 +36,25 @@ class Application; -class CurrentArtLoader : public QObject { +class CurrentAlbumCoverLoader : public QObject { Q_OBJECT public: - explicit CurrentArtLoader(Application *app, QObject *parent = nullptr); - ~CurrentArtLoader(); + explicit CurrentAlbumCoverLoader(Application *app, QObject *parent = nullptr); + ~CurrentAlbumCoverLoader(); const AlbumCoverLoaderOptions &options() const { return options_; } const Song &last_song() const { return last_song_; } public slots: - void LoadArt(const Song &song); + void LoadAlbumCover(const Song &song); -signals: - void ArtLoaded(const Song &song, const QString &uri, const QImage &image); - void ThumbnailLoaded(const Song &song, const QString &uri, const QImage &image); + signals: + void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); + void ThumbnailLoaded(const Song &song, const QUrl &thumbnail_uri, const QImage &image); private slots: - void TempArtLoaded(quint64 id, const QImage &image); + void TempAlbumCoverLoaded(const quint64 id, const QUrl &remote_url, const QImage &image); private: Application *app_; @@ -62,11 +62,12 @@ signals: QString temp_file_pattern_; - std::unique_ptr temp_art_; - std::unique_ptr temp_art_thumbnail_; + std::unique_ptr temp_cover_; + std::unique_ptr temp_cover_thumbnail_; quint64 id_; Song last_song_; + }; -#endif // CURRENTARTLOADER_H +#endif // CURRENTALBUMCOVERLOADER_H diff --git a/src/covermanager/deezercoverprovider.cpp b/src/covermanager/deezercoverprovider.cpp index 13cbb7a7..886afe98 100644 --- a/src/covermanager/deezercoverprovider.cpp +++ b/src/covermanager/deezercoverprovider.cpp @@ -53,13 +53,13 @@ DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): Cov bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { typedef QPair Param; - typedef QList Parameters; + typedef QList Params; typedef QPair EncodedParam; typedef QList EncodedParamList; - Parameters params = Parameters() << Param("output", "json") - << Param("q", QString(artist + " " + album)) - << Param("limit", QString::number(kLimit)); + const Params params = Params() << Param("output", "json") + << Param("q", QString(artist + " " + album)) + << Param("limit", QString::number(kLimit)); QUrlQuery url_query; for (const Param ¶m : params) { @@ -88,18 +88,18 @@ QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) { data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. - QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(failure_reason); + QString error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + Error(error); } else { - // See if there is Json data containing "error" - then use that instead. + // See if there is Json data containing "error" object - then use that instead. data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - QString failure_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + QString error; + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (json_obj.contains("error")) { QJsonValue json_value_error = json_obj["error"]; @@ -108,12 +108,19 @@ QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) { int code = json_error["code"].toInt(); QString message = json_error["message"].toString(); QString type = json_error["type"].toString(); - failure_reason = QString("%1 (%2)").arg(message).arg(code); + error = QString("%1 (%2)").arg(message).arg(code); } } } - if (failure_reason.isEmpty()) failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(failure_reason); + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } + } + Error(error); } return QByteArray(); } diff --git a/src/covermanager/discogscoverprovider.cpp b/src/covermanager/discogscoverprovider.cpp index 257ac003..4e73d8a1 100644 --- a/src/covermanager/discogscoverprovider.cpp +++ b/src/covermanager/discogscoverprovider.cpp @@ -184,30 +184,32 @@ QByteArray DiscogsCoverProvider::GetReplyData(QNetworkReply *reply) { data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. - QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(failure_reason); + QString error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + Error(error); } else { // See if there is Json data containing "message" - then use that instead. data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - QString failure_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QString error; + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (json_obj.contains("message")) { - failure_reason = json_obj["message"].toString(); + error = json_obj["message"].toString(); + } + } + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); } } - else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - Error(failure_reason); + Error(error); } return QByteArray(); } diff --git a/src/covermanager/discogscoverprovider.h b/src/covermanager/discogscoverprovider.h index b063b1a7..fdfc41f7 100644 --- a/src/covermanager/discogscoverprovider.h +++ b/src/covermanager/discogscoverprovider.h @@ -40,6 +40,7 @@ class Application; // This struct represents a single search-for-cover request. It identifies and describes the request. struct DiscogsCoverSearchContext { + DiscogsCoverSearchContext() : id(-1), r_count(0) {} // The unique request identifier int id; @@ -55,6 +56,7 @@ Q_DECLARE_METATYPE(DiscogsCoverSearchContext) // This struct represents a single release request. It identifies and describes the request. struct DiscogsCoverReleaseContext { + DiscogsCoverReleaseContext() : id(-1) {} int id; // The unique request identifier int s_id; // The search request identifier @@ -102,4 +104,3 @@ class DiscogsCoverProvider : public CoverProvider { }; #endif // DISCOGSCOVERPROVIDER_H - diff --git a/src/covermanager/lastfmcoverprovider.cpp b/src/covermanager/lastfmcoverprovider.cpp index 0d1b6581..fa80c2f5 100644 --- a/src/covermanager/lastfmcoverprovider.cpp +++ b/src/covermanager/lastfmcoverprovider.cpp @@ -204,32 +204,33 @@ QByteArray LastFmCoverProvider::GetReplyData(QNetworkReply *reply) { data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. - QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(failure_reason); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { // See if there is Json data containing "error" and "message" - then use that instead. data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - QString failure_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QString error; + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (json_obj.contains("error") && json_obj.contains("message")) { - int error = json_obj["error"].toInt(); + int code = json_obj["error"].toInt(); QString message = json_obj["message"].toString(); - failure_reason = "Error: " + QString::number(error) + ": " + message; + error = "Error: " + QString::number(code) + ": " + message; + } + } + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); } } - else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - Error(failure_reason); + Error(error); } return QByteArray(); } diff --git a/src/covermanager/musicbrainzcoverprovider.cpp b/src/covermanager/musicbrainzcoverprovider.cpp index 43d5f485..9d8c056f 100644 --- a/src/covermanager/musicbrainzcoverprovider.cpp +++ b/src/covermanager/musicbrainzcoverprovider.cpp @@ -184,7 +184,7 @@ QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) { data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); Error(failure_reason); @@ -192,22 +192,24 @@ QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) { else { // See if there is Json data containing "error" - then use that instead. data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - QString failure_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QString error; + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (json_obj.contains("error")) { - failure_reason = json_obj["error"].toString(); + error = json_obj["error"].toString(); + } + } + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); } } - else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - Error(failure_reason); + Error(error); } return QByteArray(); } diff --git a/src/covermanager/tidalcoverprovider.cpp b/src/covermanager/tidalcoverprovider.cpp index 6d2f907b..67312b35 100644 --- a/src/covermanager/tidalcoverprovider.cpp +++ b/src/covermanager/tidalcoverprovider.cpp @@ -57,14 +57,14 @@ TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) : } -bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { +bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) { if (!service_ || !service_->authenticated()) return false; - QList parameters; - parameters << Param("query", QString(artist + " " + album)); - parameters << Param("limit", QString::number(kLimit)); - QNetworkReply *reply = CreateRequest("search/albums", parameters); + ParamList params = ParamList() << Param("query", QString(artist + " " + album)) + << Param("limit", QString::number(kLimit)); + + QNetworkReply *reply = CreateRequest("search/albums", params); NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, int)), reply, id); return true; @@ -73,20 +73,13 @@ bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album void TidalCoverProvider::CancelSearch(int id) {} -QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, const QList ¶ms_supplied) { +QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, const ParamList ¶ms_supplied) { - typedef QPair Param; - typedef QList ParamList; - typedef QPair EncodedParam; - typedef QList EncodedParamList; - - ParamList parameters = ParamList() - << params_supplied - << Param("sessionId", service_->session_id()) - << Param("countryCode", service_->country_code()); + const ParamList params = ParamList() << params_supplied + << Param("countryCode", service_->country_code()); QUrlQuery url_query; - for (const Param& param : parameters) { + for (const Param ¶m : params) { EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); url_query.addQueryItem(encoded_param.first, encoded_param.second); } @@ -95,7 +88,8 @@ QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, url.setQuery(url_query); QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - req.setRawHeader("X-Tidal-SessionId", service_->session_id().toUtf8()); + if (!service_->access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + service_->access_token().toUtf8()); + if (!service_->session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", service_->session_id().toUtf8()); QNetworkReply *reply = network_->get(req); return reply; @@ -106,38 +100,42 @@ QByteArray TidalCoverProvider::GetReplyData(QNetworkReply *reply, QString &error QByteArray data; - if (reply->error() == QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { - // See if there is Json data containing "userMessage" - then use that instead. + // See if there is Json data containing "status" and "userMessage" - then use that instead. data = reply->readAll(); QJsonParseError parse_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); int status = 0; int sub_status = 0; - QString failure_reason; if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { status = json_obj["status"].toInt(); sub_status = json_obj["subStatus"].toInt(); QString user_message = json_obj["userMessage"].toString(); - failure_reason = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); + error = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); } } - if (failure_reason.isEmpty()) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } if (status == 401 && sub_status == 6001) { // User does not have a valid session service_->Logout(); } - error = Error(failure_reason); + error = Error(error); } return QByteArray(); } @@ -195,7 +193,7 @@ QJsonValue TidalCoverProvider::ExtractItems(QJsonObject &json_obj, QString &erro } -void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, int id) { +void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { reply->deleteLater(); diff --git a/src/covermanager/tidalcoverprovider.h b/src/covermanager/tidalcoverprovider.h index f5c2d0bc..53c825ce 100644 --- a/src/covermanager/tidalcoverprovider.h +++ b/src/covermanager/tidalcoverprovider.h @@ -43,20 +43,21 @@ class TidalCoverProvider : public CoverProvider { public: explicit TidalCoverProvider(Application *app, QObject *parent = nullptr); - bool StartSearch(const QString &artist, const QString &album, int id); + bool StartSearch(const QString &artist, const QString &album, const int id); void CancelSearch(int id); private slots: - void HandleSearchReply(QNetworkReply *reply, int id); + void HandleSearchReply(QNetworkReply *reply, const int id); private: typedef QPair Param; + typedef QList ParamList; + typedef QPair EncodedParam; static const char *kApiUrl; static const char *kResourcesUrl; - static const char *kApiTokenB64; static const int kLimit; - QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_supplied); + QNetworkReply *CreateRequest(const QString &ressource_name, const ParamList ¶ms_supplied); QByteArray GetReplyData(QNetworkReply *reply, QString &error); QJsonObject ExtractJsonObj(QByteArray &data, QString &error); QJsonValue ExtractItems(QByteArray &data, QString &error); diff --git a/src/dialogs/edittagdialog.cpp b/src/dialogs/edittagdialog.cpp index cfc43eda..557fbc5a 100644 --- a/src/dialogs/edittagdialog.cpp +++ b/src/dialogs/edittagdialog.cpp @@ -107,7 +107,7 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) cover_options_.default_output_image_ = AlbumCoverLoader::ScaleAndPad(cover_options_, QImage(":/pictures/cdcase.png")); - connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64,QImage,QImage)), SLOT(ArtLoaded(quint64,QImage,QImage))); + connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage, QImage)), SLOT(AlbumCoverLoaded(quint64, QUrl, QImage, QImage))); #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) connect(tag_fetcher_, SIGNAL(ResultAvailable(Song, SongList)), results_dialog_, SLOT(FetchTagFinished(Song, SongList)), Qt::QueuedConnection); @@ -116,7 +116,7 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) connect(results_dialog_, SIGNAL(finished(int)), tag_fetcher_, SLOT(Cancel())); #endif - album_cover_choice_controller_->SetApplication(app_); + album_cover_choice_controller_->Init(app_); ui_->setupUi(this); ui_->splitter->setSizes(QList() << 200 << width() - 200); @@ -505,13 +505,13 @@ void EditTagDialog::UpdateSummaryTab(const Song &song) { art_is_set = false; } else if (!song.art_manual().isEmpty()) { - summary += tr("Cover art set from %1").arg(song.art_manual()).toHtmlEscaped(); + summary += tr("Cover art set from %1").arg(song.art_manual().toString()).toHtmlEscaped(); } else if (song.has_embedded_cover()) { summary += tr("Cover art from embedded image"); } else if (!song.art_automatic().isEmpty()) { - summary += tr("Cover art loaded automatically from %1").arg(song.art_automatic()).toHtmlEscaped(); + summary += tr("Cover art loaded automatically from %1").arg(song.art_automatic().toString()).toHtmlEscaped(); } else { summary += tr("Cover art not set").toHtmlEscaped(); @@ -559,7 +559,7 @@ void EditTagDialog::UpdateStatisticsTab(const Song &song) { } -void EditTagDialog::ArtLoaded(quint64 id, const QImage &scaled, const QImage &original) { +void EditTagDialog::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &scaled, const QImage &original) { if (id == cover_art_id_) { ui_->art->setPixmap(QPixmap::fromImage(scaled)); @@ -623,9 +623,9 @@ void EditTagDialog::LoadCoverFromFile() { const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - QString cover = album_cover_choice_controller_->LoadCoverFromFile(song); + QUrl cover_url = album_cover_choice_controller_->LoadCoverFromFile(song); - if (!cover.isEmpty()) UpdateCoverOf(*song, sel, cover); + if (!cover_url.isEmpty()) UpdateCoverOf(*song, sel, cover_url); } @@ -645,9 +645,9 @@ void EditTagDialog::LoadCoverFromURL() { const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - QString cover = album_cover_choice_controller_->LoadCoverFromURL(song); + QUrl cover_url = album_cover_choice_controller_->LoadCoverFromURL(song); - if (!cover.isEmpty()) UpdateCoverOf(*song, sel, cover); + if (!cover_url.isEmpty()) UpdateCoverOf(*song, sel, cover_url); } void EditTagDialog::SearchForCover() { @@ -657,9 +657,9 @@ void EditTagDialog::SearchForCover() { const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - QString cover = album_cover_choice_controller_->SearchForCover(song); + QUrl cover_url = album_cover_choice_controller_->SearchForCover(song); - if (!cover.isEmpty()) UpdateCoverOf(*song, sel, cover); + if (!cover_url.isEmpty()) UpdateCoverOf(*song, sel, cover_url); } void EditTagDialog::UnsetCover() { @@ -669,8 +669,8 @@ void EditTagDialog::UnsetCover() { const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - QString cover = album_cover_choice_controller_->UnsetCover(song); - UpdateCoverOf(*song, sel, cover); + QUrl cover_url = album_cover_choice_controller_->UnsetCover(song); + UpdateCoverOf(*song, sel, cover_url); } void EditTagDialog::ShowCover() { @@ -683,7 +683,7 @@ void EditTagDialog::ShowCover() { album_cover_choice_controller_->ShowCover(*song); } -void EditTagDialog::UpdateCoverOf(const Song &selected, const QModelIndexList &sel, const QString &cover) { +void EditTagDialog::UpdateCoverOf(const Song &selected, const QModelIndexList &sel, const QUrl &cover_url) { if (!selected.is_valid() || selected.id() == -1) return; @@ -696,7 +696,7 @@ void EditTagDialog::UpdateCoverOf(const Song &selected, const QModelIndexList &s Song *other_song = &data_[i].original_; if (selected.artist() == other_song->artist() && selected.album() == other_song->album()) { - other_song->set_art_manual(cover); + other_song->set_art_manual(cover_url); } } @@ -779,9 +779,9 @@ bool EditTagDialog::eventFilter(QObject *o, QEvent *e) { const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); Song *song = GetFirstSelected(); - const QString cover = album_cover_choice_controller_->SaveCover(song, event); - if (!cover.isEmpty()) { - UpdateCoverOf(*song, sel, cover); + const QUrl cover_url = album_cover_choice_controller_->SaveCover(song, event); + if (!cover_url.isEmpty()) { + UpdateCoverOf(*song, sel, cover_url); } break; diff --git a/src/dialogs/edittagdialog.h b/src/dialogs/edittagdialog.h index ebe68395..bbe13674 100644 --- a/src/dialogs/edittagdialog.h +++ b/src/dialogs/edittagdialog.h @@ -114,7 +114,7 @@ class EditTagDialog : public QDialog { void FetchTagSongChosen(const Song &original_song, const Song &new_metadata); #endif - void ArtLoaded(quint64 id, const QImage &scaled, const QImage &original); + void AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &scaled, const QImage &original); void LoadCoverFromFile(); void SaveCoverToFile(); @@ -139,7 +139,7 @@ class EditTagDialog : public QDialog { }; Song *GetFirstSelected(); - void UpdateCoverOf(const Song &selected, const QModelIndexList &sel, const QString &cover); + void UpdateCoverOf(const Song &selected, const QModelIndexList &sel, const QUrl &cover_url); bool DoesValueVary(const QModelIndexList &sel, const QString &id) const; bool IsValueModified(const QModelIndexList &sel, const QString &id) const; diff --git a/src/engine/enginebase.h b/src/engine/enginebase.h index 3c9c117d..34462254 100644 --- a/src/engine/enginebase.h +++ b/src/engine/enginebase.h @@ -206,6 +206,7 @@ private: }; struct SimpleMetaBundle { + SimpleMetaBundle() : length(-1), year(-1), track(-1), samplerate(-1), bitdepth(-1) {} QUrl url; QString title; QString artist; @@ -214,7 +215,7 @@ struct SimpleMetaBundle { QString genre; qlonglong length; int year; - int tracknr; + int track; Song::FileType filetype; int samplerate; int bitdepth; diff --git a/src/engine/gstenginepipeline.cpp b/src/engine/gstenginepipeline.cpp index cbf5db5e..a161b272 100644 --- a/src/engine/gstenginepipeline.cpp +++ b/src/engine/gstenginepipeline.cpp @@ -624,7 +624,7 @@ void GstEnginePipeline::TagMessageReceived(GstMessage *msg) { bundle.album = ParseStrTag(taglist, GST_TAG_ALBUM); bundle.length = 0; bundle.year = 0; - bundle.tracknr = 0; + bundle.track = 0; bundle.filetype = Song::FileType_Unknown; bundle.samplerate = 0; bundle.bitdepth = 0; diff --git a/src/engine/xineengine.cpp b/src/engine/xineengine.cpp index c80694bd..8484ed85 100644 --- a/src/engine/xineengine.cpp +++ b/src/engine/xineengine.cpp @@ -821,7 +821,7 @@ Engine::SimpleMetaBundle XineEngine::FetchMetaData() const { bundle.length = 0; bundle.year = QString::fromUtf8(xine_get_meta_info(stream_, XINE_META_INFO_YEAR)).toInt(); - bundle.tracknr = QString::fromUtf8(xine_get_meta_info(stream_, XINE_META_INFO_TRACK_NUMBER)).toInt(); + bundle.track = QString::fromUtf8(xine_get_meta_info(stream_, XINE_META_INFO_TRACK_NUMBER)).toInt(); bundle.samplerate = xine_get_stream_info(stream_, XINE_STREAM_INFO_AUDIO_SAMPLERATE); bundle.bitdepth = xine_get_stream_info(stream_, XINE_STREAM_INFO_AUDIO_BITS); @@ -834,7 +834,7 @@ Engine::SimpleMetaBundle XineEngine::FetchMetaData() const { << bundle.genre << bundle.length << bundle.year - << bundle.tracknr + << bundle.track << bundle.samplerate << bundle.bitdepth << bundle.bitrate; diff --git a/src/internet/internetsearch.cpp b/src/internet/internetsearch.cpp index 830d28dc..d099b05d 100644 --- a/src/internet/internetsearch.cpp +++ b/src/internet/internetsearch.cpp @@ -61,7 +61,7 @@ InternetSearch::InternetSearch(Application *app, Song::Source source, QObject *p cover_loader_options_.pad_output_image_ = true; cover_loader_options_.scale_output_image_ = true; - connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(const quint64, const QImage&))); + connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(AlbumCoverLoaded(quint64, QUrl, QImage))); connect(this, SIGNAL(SearchAsyncSig(const int, const QString&, const SearchType)), this, SLOT(DoSearchAsync(const int, const QString&, const SearchType))); connect(service_, SIGNAL(SearchUpdateStatus(const int, const QString&)), SLOT(UpdateStatusSlot(const int, const QString&))); @@ -206,7 +206,7 @@ bool InternetSearch::FindCachedPixmap(const InternetSearch::Result &result, QPix return pixmap_cache_.find(result.pixmap_cache_key_, pixmap); } -int InternetSearch::LoadArtAsync(const InternetSearch::Result &result) { +int InternetSearch::LoadAlbumCoverAsync(const InternetSearch::Result &result) { const int id = art_searches_next_id_++; @@ -219,7 +219,7 @@ int InternetSearch::LoadArtAsync(const InternetSearch::Result &result) { } -void InternetSearch::AlbumArtLoaded(const quint64 id, const QImage &image) { +void InternetSearch::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { if (!cover_loader_tasks_.contains(id)) return; int orig_id = cover_loader_tasks_.take(id); @@ -229,7 +229,7 @@ void InternetSearch::AlbumArtLoaded(const quint64 id, const QImage &image) { QPixmap pixmap = QPixmap::fromImage(image); pixmap_cache_.insert(key, pixmap); - emit ArtLoaded(orig_id, pixmap); + emit AlbumCoverLoaded(orig_id, pixmap); } diff --git a/src/internet/internetsearch.h b/src/internet/internetsearch.h index 8f611d07..271755ce 100644 --- a/src/internet/internetsearch.h +++ b/src/internet/internetsearch.h @@ -67,7 +67,7 @@ class InternetSearch : public QObject { InternetService *service() const { return service_; } int SearchAsync(const QString &query, SearchType type); - int LoadArtAsync(const InternetSearch::Result &result); + int LoadAlbumCoverAsync(const InternetSearch::Result &result); void CancelSearch(const int id); void CancelArt(const int id); @@ -86,7 +86,7 @@ class InternetSearch : public QObject { void ProgressSetMaximum(const int id, const int progress); void UpdateProgress(const int id, const int max); - void ArtLoaded(const int id, const QPixmap &pixmap); + void AlbumCoverLoaded(const int id, const QPixmap &pixmap); protected: @@ -117,7 +117,7 @@ class InternetSearch : public QObject { void DoSearchAsync(const int id, const QString &query, const SearchType type); void SearchDone(const int service_id, const SongList &songs, const QString &error); - void AlbumArtLoaded(const quint64 id, const QImage &image); + void AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void UpdateStatusSlot(const int id, const QString &text); void ProgressSetMaximumSlot(const int id, const int progress); diff --git a/src/internet/internetsearchitemdelegate.cpp b/src/internet/internetsearchitemdelegate.cpp index 2916b212..5165b8ef 100644 --- a/src/internet/internetsearchitemdelegate.cpp +++ b/src/internet/internetsearchitemdelegate.cpp @@ -30,7 +30,7 @@ InternetSearchItemDelegate::InternetSearchItemDelegate(InternetSearchView *view) void InternetSearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { // Tell the view we painted this item so it can lazy load some art. - const_cast(view_)->LazyLoadArt(index); + const_cast(view_)->LazyLoadAlbumCover(index); CollectionItemDelegate::paint(painter, option, index); diff --git a/src/internet/internetsearchview.cpp b/src/internet/internetsearchview.cpp index 27fcef22..60efd521 100644 --- a/src/internet/internetsearchview.cpp +++ b/src/internet/internetsearchview.cpp @@ -173,7 +173,7 @@ void InternetSearchView::Init(Application *app, InternetSearch *engine, const QS connect(engine_, SIGNAL(AddResults(const int, InternetSearch::ResultList)), SLOT(AddResults(const int, const InternetSearch::ResultList)), Qt::QueuedConnection); connect(engine_, SIGNAL(SearchError(const int, const QString&)), SLOT(SearchError(const int, const QString&)), Qt::QueuedConnection); - connect(engine_, SIGNAL(ArtLoaded(const int, const QPixmap&)), SLOT(ArtLoaded(const int, const QPixmap&)), Qt::QueuedConnection); + connect(engine_, SIGNAL(AlbumCoverLoaded(const int, const QPixmap&)), SLOT(AlbumCoverLoaded(const int, const QPixmap&)), Qt::QueuedConnection); ReloadSettings(); @@ -294,7 +294,7 @@ void InternetSearchView::SwapModels() { } -void InternetSearchView::LazyLoadArt(const QModelIndex &proxy_index) { +void InternetSearchView::LazyLoadAlbumCover(const QModelIndex &proxy_index) { if (!proxy_index.isValid() || proxy_index.model() != front_proxy_) { return; @@ -332,12 +332,12 @@ void InternetSearchView::LazyLoadArt(const QModelIndex &proxy_index) { const InternetSearch::Result result = item->data(InternetSearchModel::Role_Result).value(); // Load the art. - int id = engine_->LoadArtAsync(result); + int id = engine_->LoadAlbumCoverAsync(result); art_requests_[id] = source_index; } -void InternetSearchView::ArtLoaded(const int id, const QPixmap &pixmap) { +void InternetSearchView::AlbumCoverLoaded(const int id, const QPixmap &pixmap) { if (!art_requests_.contains(id)) return; QModelIndex index = art_requests_.take(id); @@ -545,7 +545,7 @@ void InternetSearchView::SetGroupBy(const CollectionModel::Grouping &g) { // Clear requests: changing "group by" on the models will cause all the items to be removed/added again, // so all the QModelIndex here will become invalid. New requests will be created for those - // songs when they will be displayed again anyway (when InternetSearchItemDelegate::paint will call LazyLoadArt) + // songs when they will be displayed again anyway (when InternetSearchItemDelegate::paint will call LazyLoadAlbumCover) art_requests_.clear(); // Update the models front_model_->SetGroupBy(g, true); diff --git a/src/internet/internetsearchview.h b/src/internet/internetsearchview.h index f026d8df..130a0ce4 100644 --- a/src/internet/internetsearchview.h +++ b/src/internet/internetsearchview.h @@ -63,7 +63,7 @@ class InternetSearchView : public QWidget { static const int kSwapModelsTimeoutMsec; - void LazyLoadArt(const QModelIndex &index); + void LazyLoadAlbumCover(const QModelIndex &index); void showEvent(QShowEvent *e); void hideEvent(QHideEvent *e); @@ -89,7 +89,7 @@ class InternetSearchView : public QWidget { void UpdateProgress(const int id, const int max); void AddResults(const int id, const InternetSearch::ResultList &results); void SearchError(const int id, const QString &error); - void ArtLoaded(const int id, const QPixmap &pixmap); + void AlbumCoverLoaded(const int id, const QPixmap &pixmap); void FocusOnFilter(QKeyEvent *event); diff --git a/src/lyrics/lyricsfetcher.h b/src/lyrics/lyricsfetcher.h index 860a5e2e..6fa00cc1 100644 --- a/src/lyrics/lyricsfetcher.h +++ b/src/lyrics/lyricsfetcher.h @@ -37,19 +37,21 @@ class LyricsProviders; class LyricsFetcherSearch; struct LyricsSearchRequest { - quint64 id = -1; + LyricsSearchRequest() : id(-1) {} + quint64 id; QString artist; QString album; QString title; }; struct LyricsSearchResult { + LyricsSearchResult() : score(0.0) {} QString provider; QString artist; QString album; QString title; QString lyrics; - float score = 0.0; + float score; }; Q_DECLARE_METATYPE(LyricsSearchResult); diff --git a/src/musicbrainz/acoustidclient.cpp b/src/musicbrainz/acoustidclient.cpp index 3bd65a0f..55a46888 100644 --- a/src/musicbrainz/acoustidclient.cpp +++ b/src/musicbrainz/acoustidclient.cpp @@ -46,6 +46,7 @@ #include "core/closure.h" #include "core/network.h" #include "core/timeconstants.h" +#include "core/logging.h" using std::stable_sort; @@ -117,6 +118,12 @@ void AcoustidClient::RequestFinished(QNetworkReply *reply, const int request_id) requests_.remove(request_id); if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + if (reply->error() != QNetworkReply::NoError) { + qLog(Error) << QString("Acoustid: %1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + qLog(Error) << QString("Acoustid: Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } emit Finished(request_id, QStringList()); return; } diff --git a/src/musicbrainz/musicbrainzclient.cpp b/src/musicbrainz/musicbrainzclient.cpp index 1969ab6d..a194648e 100644 --- a/src/musicbrainz/musicbrainzclient.cpp +++ b/src/musicbrainz/musicbrainzclient.cpp @@ -79,7 +79,7 @@ QByteArray MusicBrainzClient::GetReplyData(QNetworkReply *reply, QString &error) data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } diff --git a/src/organise/organise.cpp b/src/organise/organise.cpp index 566d31a6..b30ce4af 100644 --- a/src/organise/organise.cpp +++ b/src/organise/organise.cpp @@ -37,6 +37,7 @@ #include "core/taskmanager.h" #include "core/musicstorage.h" #include "core/tagreaderclient.h" +#include "core/song.h" #include "organise.h" #ifdef HAVE_GSTREAMER # include "transcoder/transcoder.h" @@ -200,11 +201,21 @@ void Organise::ProcessSomeFiles() { job.albumcover_ = albumcover_; job.remove_original_ = !copy_; - if (!task.song_info_.song_.art_manual().isEmpty()) { - job.cover_source_ = task.song_info_.song_.art_manual(); + if (task.song_info_.song_.art_manual_is_valid() && task.song_info_.song_.art_manual().path() != Song::kManuallyUnsetCover) { + if (task.song_info_.song_.art_manual().scheme() == "file" && QFile::exists(task.song_info_.song_.art_manual().toLocalFile())) { + job.cover_source_ = task.song_info_.song_.art_manual().toLocalFile(); + } + else if (task.song_info_.song_.art_manual().scheme().isEmpty() && QFile::exists(task.song_info_.song_.art_manual().path())) { + job.cover_source_ = task.song_info_.song_.art_manual().path(); + } } - else if (!task.song_info_.song_.art_automatic().isEmpty()) { - job.cover_source_ = task.song_info_.song_.art_automatic(); + else if (task.song_info_.song_.art_automatic_is_valid() && task.song_info_.song_.art_automatic().path() != Song::kEmbeddedCover) { + if (task.song_info_.song_.art_automatic().scheme() == "file" && QFile::exists(task.song_info_.song_.art_automatic().toLocalFile())) { + job.cover_source_ = task.song_info_.song_.art_automatic().toLocalFile(); + } + else if (task.song_info_.song_.art_automatic().scheme().isEmpty() && QFile::exists(task.song_info_.song_.art_automatic().path())) { + job.cover_source_ = task.song_info_.song_.art_automatic().path(); + } } if (!job.cover_source_.isEmpty()) { job.cover_dest_ = QFileInfo(job.destination_).path() + "/" + QFileInfo(job.cover_source_).fileName(); diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp index dae73e0f..7111cae8 100644 --- a/src/playlist/playlistview.cpp +++ b/src/playlist/playlistview.cpp @@ -73,7 +73,7 @@ #include "playlistdelegates.h" #include "playlistheader.h" #include "playlistview.h" -#include "covermanager/currentartloader.h" +#include "covermanager/currentalbumcoverloader.h" #include "settings/appearancesettingspage.h" #include "settings/playlistsettingspage.h" @@ -212,7 +212,7 @@ void PlaylistView::SetApplication(Application *app) { Q_ASSERT(app); app_ = app; - connect(app_->current_art_loader(), SIGNAL(ArtLoaded(const Song&, const QString&, const QImage&)), SLOT(CurrentSongChanged(const Song&, const QString&, const QImage&))); + connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(const Song&, const QUrl&, const QImage&)), SLOT(CurrentSongChanged(const Song&, const QUrl&, const QImage&))); connect(app_->player(), SIGNAL(Paused()), SLOT(StopGlowing())); connect(app_->player(), SIGNAL(Playing()), SLOT(StartGlowing())); connect(app_->player(), SIGNAL(Stopped()), SLOT(StopGlowing())); @@ -1210,7 +1210,7 @@ void PlaylistView::CopyCurrentSongToClipboard() const { } -void PlaylistView::CurrentSongChanged(const Song &song, const QString &uri, const QImage &song_art) { +void PlaylistView::CurrentSongChanged(const Song &song, const QUrl &cover_url, const QImage &song_art) { if (current_song_cover_art_ == song_art) return; @@ -1262,6 +1262,7 @@ void PlaylistView::set_background_image(const QImage &image) { if (isVisible()) { previous_background_image_opacity_ = 1.0; + if (fade_animation_->state() == QTimeLine::Running) fade_animation_->stop(); fade_animation_->start(); } @@ -1280,7 +1281,7 @@ void PlaylistView::FadePreviousBackgroundImage(qreal value) { } void PlaylistView::PlayerStopped() { - CurrentSongChanged(Song(), QString(), QImage()); + CurrentSongChanged(Song(), QUrl(), QImage()); } void PlaylistView::focusInEvent(QFocusEvent *event) { diff --git a/src/playlist/playlistview.h b/src/playlist/playlistview.h index 4ba33e05..f3b72d1e 100644 --- a/src/playlist/playlistview.h +++ b/src/playlist/playlistview.h @@ -126,7 +126,7 @@ class PlaylistView : public QTreeView { void SetColumnAlignment(int section, Qt::Alignment alignment); void CopyCurrentSongToClipboard() const; - void CurrentSongChanged(const Song &new_song, const QString &uri, const QImage &cover_art); + void CurrentSongChanged(const Song &new_song, const QUrl &cover_url, const QImage &song_art); void PlayerStopped(); signals: diff --git a/src/playlistparsers/xspfparser.cpp b/src/playlistparsers/xspfparser.cpp index ac88ea51..5b84fcaa 100644 --- a/src/playlistparsers/xspfparser.cpp +++ b/src/playlistparsers/xspfparser.cpp @@ -168,32 +168,18 @@ void XSPFParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, writer.writeTextElement("trackNum", QString::number(song.track())); } - QString art = song.art_manual().isEmpty() ? song.art_automatic() : song.art_manual(); + QUrl cover_url = song.art_manual().isEmpty() || song.art_manual().path().isEmpty() ? song.art_automatic() : song.art_manual(); // Ignore images that are in our resource bundle. - if (!art.startsWith(":") && !art.isEmpty()) { - QString art_filename; - if (!art.contains("://")) { - art_filename = art; + if (!cover_url.isEmpty() && !cover_url.path().isEmpty() && cover_url.path() != Song::kManuallyUnsetCover && cover_url.path() != Song::kEmbeddedCover) { + if (cover_url.scheme().isEmpty()) { + cover_url.setScheme("file"); } - else if (QUrl(art).scheme() == "file") { - art_filename = QUrl(art).toLocalFile(); - } - - if (!art_filename.isEmpty() && !(art_filename == "(embedded)")) { - // Make this filename relative to the directory we're saving the playlist. - QUrl url = QUrl(art_filename); - url.setScheme("file"); // Need to explicitly set this. - art_filename = URLOrFilename(url, dir, path_type).toUtf8(); - } - else { - // Just use whatever URL was in the Song. - art_filename = art; - } - - writer.writeTextElement("image", art_filename); + QString cover_filename = URLOrFilename(cover_url, dir, path_type).toUtf8(); + writer.writeTextElement("image", cover_filename); } } } + writer.writeEndDocument(); } diff --git a/src/qobuz/qobuzbaserequest.cpp b/src/qobuz/qobuzbaserequest.cpp index 29b45c58..9ea22491 100644 --- a/src/qobuz/qobuzbaserequest.cpp +++ b/src/qobuz/qobuzbaserequest.cpp @@ -77,6 +77,7 @@ QNetworkReply *QobuzBaseRequest::CreateRequest(const QString &ressource_name, co req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply *reply = network_->get(req); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleSSLErrors(QList))); replies_ << reply; //qLog(Debug) << "Qobuz: Sending request" << url; @@ -85,7 +86,15 @@ QNetworkReply *QobuzBaseRequest::CreateRequest(const QString &ressource_name, co } -QByteArray QobuzBaseRequest::GetReplyData(QNetworkReply *reply, QString &error) { +void QobuzBaseRequest::HandleSSLErrors(QList ssl_errors) { + + for (QSslError &ssl_error : ssl_errors) { + Error(ssl_error.errorString()); + } + +} + +QByteArray QobuzBaseRequest::GetReplyData(QNetworkReply *reply) { if (replies_.contains(reply)) { replies_.removeAll(reply); @@ -94,39 +103,38 @@ QByteArray QobuzBaseRequest::GetReplyData(QNetworkReply *reply, QString &error) QByteArray data; - if (reply->error() == QNetworkReply::NoError) { - int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (http_code == 200) { - data = reply->readAll(); - } - else { - error = Error(QString("Received HTTP code %1").arg(http_code)); - } + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. - error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { // See if there is Json data containing "status", "code" and "message" - then use that instead. data = reply->readAll(); + QString error; QJsonParseError parse_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - QString failure_reason; if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) { QString status = json_obj["status"].toString(); int code = json_obj["code"].toInt(); QString message = json_obj["message"].toString(); - failure_reason = QString("%1 (%2)").arg(message).arg(code); + error = QString("%1 (%2)").arg(message).arg(code); } } - if (failure_reason.isEmpty()) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } - error = Error(failure_reason); + Error(error); } return QByteArray(); } @@ -135,29 +143,29 @@ QByteArray QobuzBaseRequest::GetReplyData(QNetworkReply *reply, QString &error) } -QJsonObject QobuzBaseRequest::ExtractJsonObj(QByteArray &data, QString &error) { +QJsonObject QobuzBaseRequest::ExtractJsonObj(QByteArray &data) { QJsonParseError json_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); if (json_error.error != QJsonParseError::NoError) { - error = Error("Reply from server missing Json data.", data); + Error("Reply from server missing Json data.", data); return QJsonObject(); } if (json_doc.isNull() || json_doc.isEmpty()) { - error = Error("Received empty Json document.", data); + Error("Received empty Json document.", data); return QJsonObject(); } if (!json_doc.isObject()) { - error = Error("Json document is not an object.", json_doc); + Error("Json document is not an object.", json_doc); return QJsonObject(); } QJsonObject json_obj = json_doc.object(); if (json_obj.isEmpty()) { - error = Error("Received empty Json object.", json_doc); + Error("Received empty Json object.", json_doc); return QJsonObject(); } @@ -165,18 +173,18 @@ QJsonObject QobuzBaseRequest::ExtractJsonObj(QByteArray &data, QString &error) { } -QJsonValue QobuzBaseRequest::ExtractItems(QByteArray &data, QString &error) { +QJsonValue QobuzBaseRequest::ExtractItems(QByteArray &data) { - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj, error); + return ExtractItems(json_obj); } -QJsonValue QobuzBaseRequest::ExtractItems(QJsonObject &json_obj, QString &error) { +QJsonValue QobuzBaseRequest::ExtractItems(QJsonObject &json_obj) { if (!json_obj.contains("items")) { - error = Error("Json reply is missing items.", json_obj); + Error("Json reply is missing items.", json_obj); return QJsonArray(); } QJsonValue json_items = json_obj["items"]; @@ -184,11 +192,12 @@ QJsonValue QobuzBaseRequest::ExtractItems(QJsonObject &json_obj, QString &error) } -QString QobuzBaseRequest::Error(QString error, QVariant debug) { +QString QobuzBaseRequest::ErrorsToHTML(const QStringList &errors) { - qLog(Error) << "Qobuz:" << error; - if (debug.isValid()) qLog(Debug) << debug; - - return error; + QString error_html; + for (const QString &error : errors) { + error_html += error + "
"; + } + return error_html; } diff --git a/src/qobuz/qobuzbaserequest.h b/src/qobuz/qobuzbaserequest.h index 9086f5e2..fd6978ea 100644 --- a/src/qobuz/qobuzbaserequest.h +++ b/src/qobuz/qobuzbaserequest.h @@ -71,12 +71,13 @@ class QobuzBaseRequest : public QObject { typedef QList EncodedParamList; QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_provided); - QByteArray GetReplyData(QNetworkReply *reply, QString &error); - QJsonObject ExtractJsonObj(QByteArray &data, QString &error); - QJsonValue ExtractItems(QByteArray &data, QString &error); - QJsonValue ExtractItems(QJsonObject &json_obj, QString &error); + QByteArray GetReplyData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(QByteArray &data); + QJsonValue ExtractItems(QByteArray &data); + QJsonValue ExtractItems(QJsonObject &json_obj); - virtual QString Error(QString error, QVariant debug = QVariant()); + virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; + QString ErrorsToHTML(const QStringList &errors); QString api_url() { return QString(kApiUrl); } QString app_id() { return service_->app_id(); } @@ -95,6 +96,9 @@ class QobuzBaseRequest : public QObject { int max_login_attempts() { return service_->max_login_attempts(); } int login_attempts() { return service_->login_attempts(); } + private slots: + void HandleSSLErrors(QList ssl_errors); + private: static const char *kApiUrl; diff --git a/src/qobuz/qobuzfavoriterequest.cpp b/src/qobuz/qobuzfavoriterequest.cpp index f0ba061e..ced37108 100644 --- a/src/qobuz/qobuzfavoriterequest.cpp +++ b/src/qobuz/qobuzfavoriterequest.cpp @@ -154,8 +154,7 @@ void QobuzFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const Favorit return; } - QString error; - QByteArray data = GetReplyData(reply, error); + QByteArray data = GetReplyData(reply); if (reply->error() != QNetworkReply::NoError) { return; @@ -258,8 +257,7 @@ void QobuzFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const Favo return; } - QString error; - QByteArray data = GetReplyData(reply, error); + QByteArray data = GetReplyData(reply); if (reply->error() != QNetworkReply::NoError) { return; } @@ -279,3 +277,10 @@ void QobuzFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const Favo } } + +void QobuzFavoriteRequest::Error(const QString &error, const QVariant &debug) { + + qLog(Error) << "Qobuz:" << error; + if (debug.isValid()) qLog(Debug) << debug; + +} diff --git a/src/qobuz/qobuzfavoriterequest.h b/src/qobuz/qobuzfavoriterequest.h index a2d182a5..03bbb39b 100644 --- a/src/qobuz/qobuzfavoriterequest.h +++ b/src/qobuz/qobuzfavoriterequest.h @@ -65,6 +65,7 @@ class QobuzFavoriteRequest : public QobuzBaseRequest { void RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs); private: + void Error(const QString &error, const QVariant &debug = QVariant()); QString FavoriteText(const FavoriteType type); void AddFavorites(const FavoriteType type, const SongList &songs); void RemoveFavorites(const FavoriteType type, const SongList &songs); diff --git a/src/qobuz/qobuzrequest.cpp b/src/qobuz/qobuzrequest.cpp index f99b573e..920d6ca2 100644 --- a/src/qobuz/qobuzrequest.cpp +++ b/src/qobuz/qobuzrequest.cpp @@ -35,7 +35,8 @@ #include "core/network.h" #include "core/song.h" #include "core/timeconstants.h" -#include "organise/organiseformat.h" +#include "core/application.h" +#include "covermanager/albumcoverloader.h" #include "qobuzservice.h" #include "qobuzurlhandler.h" #include "qobuzrequest.h" @@ -47,10 +48,11 @@ const int QobuzRequest::kMaxConcurrentArtistAlbumsRequests = 3; const int QobuzRequest::kMaxConcurrentAlbumSongsRequests = 3; const int QobuzRequest::kMaxConcurrentAlbumCoverRequests = 1; -QobuzRequest::QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, NetworkAccessManager *network, QueryType type, QObject *parent) +QobuzRequest::QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent) : QobuzBaseRequest(service, network, parent), service_(service), url_handler_(url_handler), + app_(app), network_(network), type_(type), query_id_(-1), @@ -300,8 +302,7 @@ void QobuzRequest::AddSongsSearchRequest(const int offset) { void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { - QString error; - QByteArray data = GetReplyData(reply, error); + QByteArray data = GetReplyData(reply); --artists_requests_active_; @@ -312,7 +313,7 @@ void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re return; } - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { ArtistsFinishCheck(); return; @@ -363,7 +364,7 @@ void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re emit UpdateProgress(query_id_, artists_received_); } - QJsonValue json_value = ExtractItems(json_obj_artists, error); + QJsonValue json_value = ExtractItems(json_obj_artists); if (!json_value.isArray()) { ArtistsFinishCheck(); return; @@ -496,8 +497,7 @@ void QobuzRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const qint64 void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const int limit_requested, const int offset_requested) { - QString error; - QByteArray data = GetReplyData(reply, error); + QByteArray data = GetReplyData(reply); if (finished_) return; @@ -506,7 +506,7 @@ void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_r return; } - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { AlbumsFinishCheck(artist_id_requested); return; @@ -559,7 +559,7 @@ void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_r return; } - QJsonValue json_value = ExtractItems(json_obj_albums, error); + QJsonValue json_value = ExtractItems(json_obj_albums); if (!json_value.isArray()) { AlbumsFinishCheck(artist_id_requested); return; @@ -726,8 +726,7 @@ void QobuzRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 ar void QobuzRequest::SongsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const QString &album_id_requested, const int limit_requested, const int offset_requested, const QString &album_artist_requested, const QString &album_requested) { - QString error; - QByteArray data = GetReplyData(reply, error); + QByteArray data = GetReplyData(reply); if (finished_) return; @@ -736,7 +735,7 @@ void QobuzRequest::SongsReceived(QNetworkReply *reply, const qint64 artist_id_re return; } - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested); return; @@ -825,7 +824,7 @@ void QobuzRequest::SongsReceived(QNetworkReply *reply, const qint64 artist_id_re return; } - QJsonValue json_value = ExtractItems(json_obj_tracks, error); + QJsonValue json_value = ExtractItems(json_obj_tracks); if (!json_value.isArray()) { SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album); return; @@ -1045,7 +1044,7 @@ int QobuzRequest::ParseSong(Song &song, const QJsonObject &json_obj, qint64 arti song.set_track(track); song.set_url(url); song.set_length_nanosec(duration); - song.set_art_automatic(cover_url.toEncoded()); + song.set_art_automatic(cover_url); song.set_comment(copyright); song.set_directory_id(0); song.set_filetype(Song::FileType_Stream); @@ -1082,12 +1081,13 @@ void QobuzRequest::AddAlbumCoverRequest(Song &song) { return; } - album_covers_requests_sent_.insertMulti(cover_url, &song); - ++album_covers_requested_; - AlbumCoverRequest request; request.url = cover_url; - request.filename = AlbumCoverFileName(song); + request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url); + if (request.filename.isEmpty()) return; + + album_covers_requests_sent_.insertMulti(cover_url, &song); + ++album_covers_requested_; album_cover_requests_queue_.enqueue(request); @@ -1132,9 +1132,8 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur return; } - QString error; if (reply->error() != QNetworkReply::NoError) { - error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); album_covers_requests_sent_.remove(cover_url); AlbumCoverFinishCheck(); return; @@ -1142,7 +1141,7 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur QByteArray data = reply->readAll(); if (data.isEmpty()) { - error = Error(QString("Received empty image data for %1").arg(cover_url.toString())); + Error(QString("Received empty image data for %1").arg(cover_url.toString())); album_covers_requests_sent_.remove(cover_url); AlbumCoverFinishCheck(); return; @@ -1151,57 +1150,26 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur QImage image; if (image.loadFromData(data)) { - QDir dir; - if (dir.mkpath(service_->CoverCacheDir())) { - QString filepath(service_->CoverCacheDir() + "/" + filename); - if (image.save(filepath, "JPG")) { - while (album_covers_requests_sent_.contains(cover_url)) { - Song *song = album_covers_requests_sent_.take(cover_url); - song->set_art_automatic(filepath); - } + if (image.save(filename, "JPG")) { + while (album_covers_requests_sent_.contains(cover_url)) { + Song *song = album_covers_requests_sent_.take(cover_url); + QUrl cover_url; + cover_url.setScheme("file"); + cover_url.setPath(filename); + song->set_art_automatic(cover_url); } } } else { album_covers_requests_sent_.remove(cover_url); - error = Error(QString("Error decoding image data from %1").arg(cover_url.toString())); + Error(QString("Error decoding image data from %1").arg(cover_url.toString())); } AlbumCoverFinishCheck(); } -QString QobuzRequest::AlbumCoverFileName(const Song &song) { - - QString artist = song.effective_albumartist(); - QString album = song.effective_album(); - QString title = song.title(); - - artist.remove('/'); - album.remove('/'); - title.remove('/'); - - QString filename = artist + "-" + album + ".jpg"; - filename = filename.toLower(); - filename.replace(' ', '-'); - filename.replace("--", "-"); - filename.replace(230, "ae"); - filename.replace(198, "AE"); - filename.replace(246, 'o'); - filename.replace(248, 'o'); - filename.replace(214, 'O'); - filename.replace(216, 'O'); - filename.replace(228, 'a'); - filename.replace(229, 'a'); - filename.replace(196, 'A'); - filename.replace(197, 'A'); - filename.remove(OrganiseFormat::kValidFatCharacters); - - return filename; - -} - void QobuzRequest::AlbumCoverFinishCheck() { if (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) @@ -1245,28 +1213,24 @@ void QobuzRequest::FinishCheck() { if (songs_.isEmpty() && errors_.isEmpty()) emit Results(query_id_, songs_, tr("Unknown error")); else - emit Results(query_id_, songs_, errors_); + emit Results(query_id_, songs_, ErrorsToHTML(errors_)); } } } -QString QobuzRequest::Error(QString error, QVariant debug) { - - qLog(Error) << "Qobuz:" << error; - if (debug.isValid()) qLog(Debug) << debug; +void QobuzRequest::Error(const QString &error, const QVariant &debug) { if (!error.isEmpty()) { - errors_ += error; - errors_ += "
"; + errors_ << error; + qLog(Error) << "Qobuz:" << error; } + if (debug.isValid()) qLog(Debug) << debug; FinishCheck(); - return error; - } -void QobuzRequest::Warn(QString error, QVariant debug) { +void QobuzRequest::Warn(const QString &error, const QVariant &debug) { qLog(Error) << "Qobuz:" << error; if (debug.isValid()) qLog(Debug) << debug; diff --git a/src/qobuz/qobuzrequest.h b/src/qobuz/qobuzrequest.h index 1d9146c3..a73d13bc 100644 --- a/src/qobuz/qobuzrequest.h +++ b/src/qobuz/qobuzrequest.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include "core/song.h" #include "qobuzbaserequest.h" +class Application; class NetworkAccessManager; class QobuzService; class QobuzUrlHandler; @@ -49,7 +51,7 @@ class QobuzRequest : public QobuzBaseRequest { public: - QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, NetworkAccessManager *network, QueryType type, QObject *parent); + QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent); ~QobuzRequest(); void ReloadSettings(); @@ -141,8 +143,8 @@ class QobuzRequest : public QobuzBaseRequest { void AlbumCoverFinishCheck(); void FinishCheck(); - void Warn(QString error, QVariant debug = QVariant()); - QString Error(QString error, QVariant debug = QVariant()); + void Warn(const QString &error, const QVariant &debug = QVariant()); + void Error(const QString &error, const QVariant &debug = QVariant()); static const int kMaxConcurrentArtistsRequests; static const int kMaxConcurrentAlbumsRequests; @@ -153,6 +155,7 @@ class QobuzRequest : public QobuzBaseRequest { QobuzService *service_; QobuzUrlHandler *url_handler_; + Application *app_; NetworkAccessManager *network_; QueryType type_; @@ -193,7 +196,7 @@ class QobuzRequest : public QobuzBaseRequest { int album_covers_received_; SongList songs_; - QString errors_; + QStringList errors_; bool no_results_; QList album_cover_replies_; diff --git a/src/qobuz/qobuzservice.cpp b/src/qobuz/qobuzservice.cpp index 660901b5..46565a25 100644 --- a/src/qobuz/qobuzservice.cpp +++ b/src/qobuz/qobuzservice.cpp @@ -23,7 +23,6 @@ #include #include -#include #include #include #include @@ -213,10 +212,6 @@ void QobuzService::ReloadSettings() { } -QString QobuzService::CoverCacheDir() { - return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/qobuzalbumcovers"; -} - void QobuzService::SendLogin() { SendLogin(app_id_, username_, password_); } @@ -224,6 +219,7 @@ void QobuzService::SendLogin() { void QobuzService::SendLogin(const QString &app_id, const QString &username, const QString &password) { emit UpdateStatus(tr("Authenticating...")); + login_errors_.clear(); login_sent_ = true; ++login_attempts_; @@ -248,20 +244,30 @@ void QobuzService::SendLogin(const QString &app_id, const QString &username, con QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); QNetworkReply *reply = network_->post(req, query); + + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList))); NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply); qLog(Debug) << "Qobuz: Sending request" << url << query; } +void QobuzService::HandleLoginSSLErrors(QList ssl_errors) { + + for (QSslError &ssl_error : ssl_errors) { + login_errors_ += ssl_error.errorString(); + } + +} + void QobuzService::HandleAuthReply(QNetworkReply *reply) { reply->deleteLater(); login_sent_ = false; - if (reply->error() != QNetworkReply::NoError) { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); return; @@ -271,29 +277,29 @@ void QobuzService::HandleAuthReply(QNetworkReply *reply) { QByteArray data(reply->readAll()); QJsonParseError json_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - QString failure_reason; if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) { QString status = json_obj["status"].toString(); int code = json_obj["code"].toInt(); QString message = json_obj["message"].toString(); - failure_reason = QString("%1 (%2)").arg(message).arg(code); + login_errors_ << QString("%1 (%2)").arg(message).arg(code); } } - if (failure_reason.isEmpty()) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (login_errors_.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } - LoginError(failure_reason); + LoginError(); return; } } - int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (http_code != 200) { - LoginError(QString("Received HTTP code %1").arg(http_code)); - return; - } + login_errors_.clear(); QByteArray data(reply->readAll()); QJsonParseError json_error; @@ -406,7 +412,7 @@ void QobuzService::GetArtists() { ResetArtistsRequest(); - artists_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::QueryType_Artists, this)); + artists_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Artists, this)); connect(artists_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(ArtistsResultsReceived(const int, const SongList&, const QString&))); connect(artists_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(ArtistsUpdateStatusReceived(const int, const QString&))); @@ -456,7 +462,7 @@ void QobuzService::GetAlbums() { } ResetAlbumsRequest(); - albums_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::QueryType_Albums, this)); + albums_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Albums, this)); connect(albums_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(AlbumsResultsReceived(const int, const SongList&, const QString&))); connect(albums_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(AlbumsUpdateStatusReceived(const int, const QString&))); connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(AlbumsProgressSetMaximumReceived(const int, const int))); @@ -505,7 +511,7 @@ void QobuzService::GetSongs() { } ResetSongsRequest(); - songs_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::QueryType_Songs, this)); + songs_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Songs, this)); connect(songs_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(SongsResultsReceived(const int, const SongList&, const QString&))); connect(songs_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(SongsUpdateStatusReceived(const int, const QString&))); connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(SongsProgressSetMaximumReceived(const int, const int))); @@ -586,7 +592,7 @@ void QobuzService::SendSearch() { return; } - search_request_.reset(new QobuzRequest(this, url_handler_, network_, type, this)); + search_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, type, this)); connect(search_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(SearchResultsReceived(const int, const SongList&, const QString&))); connect(search_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SIGNAL(SearchUpdateStatus(const int, const QString&))); @@ -631,14 +637,20 @@ void QobuzService::HandleStreamURLFinished(const QUrl &original_url, const QUrl } -QString QobuzService::LoginError(QString error, QVariant debug) { +void QobuzService::LoginError(const QString &error, const QVariant &debug) { - qLog(Error) << "Qobuz:" << error; + if (!error.isEmpty()) login_errors_ << error; + + QString error_html; + for (const QString &error : login_errors_) { + qLog(Error) << "Qobuz:" << error; + error_html += error + "
"; + } if (debug.isValid()) qLog(Debug) << debug; - emit LoginFailure(error); - emit LoginComplete(false, error); + emit LoginFailure(error_html); + emit LoginComplete(false, error_html); - return error; + login_errors_.clear(); } diff --git a/src/qobuz/qobuzservice.h b/src/qobuz/qobuzservice.h index ce76d3bb..3ad29628 100644 --- a/src/qobuz/qobuzservice.h +++ b/src/qobuz/qobuzservice.h @@ -61,7 +61,6 @@ class QobuzService : public InternetService { static const Song::Source kSource; void ReloadSettings(); - QString CoverCacheDir(); void Logout(); int Search(const QString &query, InternetSearch::SearchType type); @@ -69,6 +68,7 @@ class QobuzService : public InternetService { const int max_login_attempts() { return kLoginAttempts; } + Application *app() { return app_; } QString app_id() { return app_id_; } QString app_secret() { return app_secret_; } QString username() { return username_; } @@ -124,6 +124,7 @@ class QobuzService : public InternetService { private slots: void SendLogin(); + void HandleLoginSSLErrors(QList ssl_errors); void HandleAuthReply(QNetworkReply *reply); void ResetLoginAttempts(); void StartSearch(); @@ -150,7 +151,7 @@ class QobuzService : public InternetService { typedef QList EncodedParamList; void SendSearch(); - QString LoginError(QString error, QVariant debug = QVariant()); + void LoginError(const QString &error = QString(), const QVariant &debug = QVariant()); static const char *kAuthUrl; static const int kLoginAttempts; @@ -214,6 +215,8 @@ class QobuzService : public InternetService { QList stream_url_requests_; + QStringList login_errors_; + }; #endif // QOBUZSERVICE_H diff --git a/src/qobuz/qobuzstreamurlrequest.cpp b/src/qobuz/qobuzstreamurlrequest.cpp index 88d68afb..0e25f59d 100644 --- a/src/qobuz/qobuzstreamurlrequest.cpp +++ b/src/qobuz/qobuzstreamurlrequest.cpp @@ -67,7 +67,7 @@ void QobuzStreamURLRequest::LoginComplete(bool success, QString error) { need_login_ = false; if (!success) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } @@ -150,42 +150,40 @@ void QobuzStreamURLRequest::StreamURLReceived() { disconnect(reply_, 0, nullptr, 0); reply_->deleteLater(); - QString error; - - QByteArray data = GetReplyData(reply_, error); + QByteArray data = GetReplyData(reply_); if (data.isEmpty()) { reply_ = nullptr; if (!authenticated() && login_sent() && tries_ <= 1) { need_login_ = true; return; } - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } reply_ = nullptr; - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } if (!json_obj.contains("track_id")) { - error = Error("Invalid Json reply, stream url is missing track_id.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error("Invalid Json reply, stream url is missing track_id.", json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } int track_id = json_obj["track_id"].toInt(); if (track_id != song_id_) { - error = Error("Incorrect track ID returned.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error("Incorrect track ID returned.", json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } if (!json_obj.contains("mime_type") || !json_obj.contains("url")) { - error = Error("Invalid Json reply, stream url is missing url or mime_type.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error("Invalid Json reply, stream url is missing url or mime_type.", json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } @@ -204,8 +202,8 @@ void QobuzStreamURLRequest::StreamURLReceived() { } if (!url.isValid()) { - error = Error("Returned stream url is invalid.", json_obj); - emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, error); + Error("Returned stream url is invalid.", json_obj); + emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, ErrorsToHTML(errors_)); return; } @@ -225,3 +223,14 @@ void QobuzStreamURLRequest::StreamURLReceived() { emit StreamURLFinished(original_url_, url, filetype, samplerate, bit_depth, duration); } + +void QobuzStreamURLRequest::Error(const QString &error, const QVariant &debug) { + + if (!error.isEmpty()) { + qLog(Error) << "Qobuz:" << error; + errors_ << error; + } + if (debug.isValid()) qLog(Debug) << debug; + +} + diff --git a/src/qobuz/qobuzstreamurlrequest.h b/src/qobuz/qobuzstreamurlrequest.h index 936c32d1..1258a645 100644 --- a/src/qobuz/qobuzstreamurlrequest.h +++ b/src/qobuz/qobuzstreamurlrequest.h @@ -22,7 +22,9 @@ #include "config.h" +#include #include +#include #include #include "core/song.h" @@ -57,12 +59,15 @@ class QobuzStreamURLRequest : public QobuzBaseRequest { void StreamURLReceived(); private: + void Error(const QString &error, const QVariant &debug = QVariant()); + QobuzService *service_; QNetworkReply *reply_; QUrl original_url_; int song_id_; int tries_; bool need_login_; + QStringList errors_; }; diff --git a/src/scrobbler/listenbrainzscrobbler.cpp b/src/scrobbler/listenbrainzscrobbler.cpp index 3868174a..04cf71b4 100644 --- a/src/scrobbler/listenbrainzscrobbler.cpp +++ b/src/scrobbler/listenbrainzscrobbler.cpp @@ -197,31 +197,32 @@ void ListenBrainzScrobbler::AuthenticateReplyFinished(QNetworkReply *reply) { data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. - QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - AuthError(failure_reason); + AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { // See if there is Json data containing "error" and "error_description" - then use that instead. data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - QString failure_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QString error; + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (json_obj.contains("error") && json_obj.contains("error_description")) { - QString error = json_obj["error"].toString(); - failure_reason = json_obj["error_description"].toString(); + QString error_code = json_obj["error"].toString(); + error = json_obj["error_description"].toString(); + } + } + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); } } - else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - AuthError(failure_reason); + AuthError(error); } return; } @@ -286,40 +287,38 @@ QByteArray ListenBrainzScrobbler::GetReplyData(QNetworkReply *reply) { else { if (reply->error() < 200) { // This is a network error, there is nothing more to do. - QString error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(error_reason); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { // See if there is Json data containing "code" and "error" - then use that instead. data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - QString error_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QString error; + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (json_obj.contains("code") && json_obj.contains("error")) { int error_code = json_obj["code"].toInt(); QString error_message = json_obj["error"].toString(); - error_reason = QString("%1 (%2)").arg(error_message).arg(error_code); + error = QString("%1 (%2)").arg(error_message).arg(error_code); } else { - error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } } - else { - error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::AuthenticationRequiredError) { // Session is probably expired Logout(); - Error(error_reason); - } - else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error - Error(error_reason); - } - else { // Fail - Error(error_reason); } + Error(error); } return QByteArray(); } diff --git a/src/scrobbler/scrobblingapi20.cpp b/src/scrobbler/scrobblingapi20.cpp index 1795215e..eb31630a 100644 --- a/src/scrobbler/scrobblingapi20.cpp +++ b/src/scrobbler/scrobblingapi20.cpp @@ -224,30 +224,34 @@ void ScrobblingAPI20::AuthenticateReplyFinished(QNetworkReply *reply) { else { if (reply->error() < 200) { // This is a network error, there is nothing more to do. - QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - AuthError(failure_reason); + AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { // See if there is Json data containing "error" and "message" - then use that instead. data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - QString failure_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QString error; + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (json_obj.contains("error") && json_obj.contains("message")) { - int error = json_obj["error"].toInt(); + int code = json_obj["error"].toInt(); QString message = json_obj["message"].toString(); - failure_reason = "Error: " + QString::number(error) + ": " + message; + error = "Error: " + QString::number(code) + ": " + message; } else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } } - else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } - AuthError(failure_reason); + AuthError(error); } return; } @@ -348,31 +352,32 @@ QByteArray ScrobblingAPI20::GetReplyData(QNetworkReply *reply) { data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. - QString error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(error_reason); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { + QString error; // See if there is Json data containing "error" and "message" - then use that instead. data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); int error_code = -1; - QString error_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (json_obj.contains("error") && json_obj.contains("message")) { error_code = json_obj["error"].toInt(); QString error_message = json_obj["message"].toString(); - error_reason = QString("%1 (%2)").arg(error_message).arg(error_code); - } - else { - error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + error = QString("%1 (%2)").arg(error_message).arg(error_code); } } - else { - error_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || @@ -382,14 +387,8 @@ QByteArray ScrobblingAPI20::GetReplyData(QNetworkReply *reply) { ){ // Session is probably expired Logout(); - Error(error_reason); - } - else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error - Error(error_reason); - } - else { // Fail - Error(error_reason); } + Error(error); } return QByteArray(); } diff --git a/src/subsonic/subsonicbaserequest.cpp b/src/subsonic/subsonicbaserequest.cpp index cf6c7609..1d750ef8 100644 --- a/src/subsonic/subsonicbaserequest.cpp +++ b/src/subsonic/subsonicbaserequest.cpp @@ -100,6 +100,7 @@ QNetworkReply *SubsonicBaseRequest::CreateGetRequest(const QString &ressource_na req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply *reply = network_->get(req); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleSSLErrors(QList))); replies_ << reply; //qLog(Debug) << "Subsonic: Sending request" << url; @@ -108,7 +109,15 @@ QNetworkReply *SubsonicBaseRequest::CreateGetRequest(const QString &ressource_na } -QByteArray SubsonicBaseRequest::GetReplyData(QNetworkReply *reply, QString &error) { +void SubsonicBaseRequest::HandleSSLErrors(QList ssl_errors) { + + for (QSslError &ssl_error : ssl_errors) { + Error(ssl_error.errorString()); + } + +} + +QByteArray SubsonicBaseRequest::GetReplyData(QNetworkReply *reply) { if (replies_.contains(reply)) { replies_.removeAll(reply); @@ -117,26 +126,20 @@ QByteArray SubsonicBaseRequest::GetReplyData(QNetworkReply *reply, QString &erro QByteArray data; - if (reply->error() == QNetworkReply::NoError) { - int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (http_code == 200) { - data = reply->readAll(); - } - else { - error = Error(QString("Received HTTP code %1").arg(http_code)); - } + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. - error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { // See if there is Json data containing "error" - then use that instead. data = reply->readAll(); + QString error; QJsonParseError parse_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - QString failure_reason; if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (!json_obj.isEmpty() && json_obj.contains("error")) { @@ -146,15 +149,20 @@ QByteArray SubsonicBaseRequest::GetReplyData(QNetworkReply *reply, QString &erro if (!json_obj.isEmpty() && json_obj.contains("code") && json_obj.contains("message")) { int code = json_obj["code"].toInt(); QString message = json_obj["message"].toString(); - failure_reason = QString("%1 (%2)").arg(message).arg(code); + error = QString("%1 (%2)").arg(message).arg(code); } } } } - if (failure_reason.isEmpty()) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } - error = Error(failure_reason); + Error(error); } } @@ -162,40 +170,40 @@ QByteArray SubsonicBaseRequest::GetReplyData(QNetworkReply *reply, QString &erro } -QJsonObject SubsonicBaseRequest::ExtractJsonObj(QByteArray &data, QString &error) { +QJsonObject SubsonicBaseRequest::ExtractJsonObj(QByteArray &data) { QJsonParseError json_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); if (json_error.error != QJsonParseError::NoError) { - error = Error("Reply from server missing Json data.", data); + Error("Reply from server missing Json data.", data); return QJsonObject(); } if (json_doc.isNull() || json_doc.isEmpty()) { - error = Error("Received empty Json document.", data); + Error("Received empty Json document.", data); return QJsonObject(); } if (!json_doc.isObject()) { - error = Error("Json document is not an object.", json_doc); + Error("Json document is not an object.", json_doc); return QJsonObject(); } QJsonObject json_obj = json_doc.object(); if (json_obj.isEmpty()) { - error = Error("Received empty Json object.", json_doc); + Error("Received empty Json object.", json_doc); return QJsonObject(); } if (!json_obj.contains("subsonic-response")) { - error = Error("Json reply is missing subsonic-response.", json_obj); + Error("Json reply is missing subsonic-response.", json_obj); return QJsonObject(); } QJsonValue json_response = json_obj["subsonic-response"]; if (!json_response.isObject()) { - error = Error("Json response is not an object.", json_response); + Error("Json response is not an object.", json_response); return QJsonObject(); } json_obj = json_response.toObject(); @@ -204,11 +212,12 @@ QJsonObject SubsonicBaseRequest::ExtractJsonObj(QByteArray &data, QString &error } -QString SubsonicBaseRequest::Error(QString error, QVariant debug) { +QString SubsonicBaseRequest::ErrorsToHTML(const QStringList &errors) { - qLog(Error) << "Subsonic:" << error; - if (debug.isValid()) qLog(Debug) << debug; - - return error; + QString error_html; + for (const QString &error : errors) { + error_html += error + "
"; + } + return error_html; } diff --git a/src/subsonic/subsonicbaserequest.h b/src/subsonic/subsonicbaserequest.h index 15915aef..d0a243b3 100644 --- a/src/subsonic/subsonicbaserequest.h +++ b/src/subsonic/subsonicbaserequest.h @@ -39,7 +39,6 @@ #include "internet/internetsearch.h" #include "subsonicservice.h" -class Application; class NetworkAccessManager; class SubsonicUrlHandler; class CollectionBackend; @@ -61,10 +60,11 @@ class SubsonicBaseRequest : public QObject { QUrl CreateUrl(const QString &ressource_name, const QList ¶ms_provided); QNetworkReply *CreateGetRequest(const QString &ressource_name, const QList ¶ms_provided); - QByteArray GetReplyData(QNetworkReply *reply, QString &error); - QJsonObject ExtractJsonObj(QByteArray &data, QString &error); + QByteArray GetReplyData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(QByteArray &data); - virtual QString Error(QString error, QVariant debug = QVariant()); + virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; + QString ErrorsToHTML(const QStringList &errors); QString client_name() { return service_->client_name(); } QString api_version() { return service_->api_version(); } @@ -74,6 +74,9 @@ class SubsonicBaseRequest : public QObject { bool verify_certificate() { return service_->verify_certificate(); } bool download_album_covers() { return service_->download_album_covers(); } + private slots: + void HandleSSLErrors(QList ssl_errors); + private: SubsonicService *service_; diff --git a/src/subsonic/subsonicrequest.cpp b/src/subsonic/subsonicrequest.cpp index 3d55c563..25c7cb33 100644 --- a/src/subsonic/subsonicrequest.cpp +++ b/src/subsonic/subsonicrequest.cpp @@ -28,18 +28,20 @@ #include #include #include +#include #include #include #include #include #include +#include "core/application.h" #include "core/closure.h" #include "core/logging.h" #include "core/network.h" #include "core/song.h" #include "core/timeconstants.h" -#include "organise/organiseformat.h" +#include "covermanager/albumcoverloader.h" #include "subsonicservice.h" #include "subsonicurlhandler.h" #include "subsonicrequest.h" @@ -48,10 +50,11 @@ const int SubsonicRequest::kMaxConcurrentAlbumsRequests = 3; const int SubsonicRequest::kMaxConcurrentAlbumSongsRequests = 3; const int SubsonicRequest::kMaxConcurrentAlbumCoverRequests = 1; -SubsonicRequest::SubsonicRequest(SubsonicService *service, SubsonicUrlHandler *url_handler, NetworkAccessManager *network, QObject *parent) +SubsonicRequest::SubsonicRequest(SubsonicService *service, SubsonicUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QObject *parent) : SubsonicBaseRequest(service, network, parent), service_(service), url_handler_(url_handler), + app_(app), network_(network), finished_(false), albums_requests_active_(0), @@ -141,8 +144,7 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset --albums_requests_active_; - QString error; - QByteArray data = GetReplyData(reply, error); + QByteArray data = GetReplyData(reply); if (finished_) return; @@ -151,7 +153,7 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset return; } - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { AlbumsFinishCheck(offset_requested); return; @@ -180,7 +182,7 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset } if (!json_obj.contains("albumList") && !json_obj.contains("albumList2")) { - error = Error("Json reply is missing albumList.", json_obj); + Error("Json reply is missing albumList.", json_obj); AlbumsFinishCheck(offset_requested); return; } @@ -189,7 +191,7 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset else if (json_obj.contains("albumList2")) json_albumlist = json_obj["albumList2"]; if (!json_albumlist.isObject()) { - error = Error("Json album list is not an object.", json_albumlist); + Error("Json album list is not an object.", json_albumlist); AlbumsFinishCheck(offset_requested); } json_obj = json_albumlist.toObject(); @@ -200,7 +202,7 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset } if (!json_obj.contains("album")) { - error = Error("Json album list does not contain album array.", json_obj); + Error("Json album list does not contain album array.", json_obj); AlbumsFinishCheck(offset_requested); } QJsonValue json_album = json_obj["album"]; @@ -210,7 +212,7 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset return; } if (!json_album.isArray()) { - error = Error("Json album is not an array.", json_album); + Error("Json album is not an array.", json_album); AlbumsFinishCheck(offset_requested); } QJsonArray json_albums = json_album.toArray(); @@ -329,8 +331,7 @@ void SubsonicRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 emit UpdateProgress(album_songs_received_); - QString error; - QByteArray data = GetReplyData(reply, error); + QByteArray data = GetReplyData(reply); if (finished_) return; @@ -339,7 +340,7 @@ void SubsonicRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 return; } - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { SongsFinishCheck(); return; @@ -367,27 +368,27 @@ void SubsonicRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 } if (!json_obj.contains("album")) { - error = Error("Json reply is missing albumList.", json_obj); + Error("Json reply is missing albumList.", json_obj); SongsFinishCheck(); return; } QJsonValue json_album = json_obj["album"]; if (!json_album.isObject()) { - error = Error("Json album is not an object.", json_album); + Error("Json album is not an object.", json_album); SongsFinishCheck(); return; } QJsonObject json_album_obj = json_album.toObject(); if (!json_album_obj.contains("song")) { - error = Error("Json album object does not contain song array.", json_obj); + Error("Json album object does not contain song array.", json_obj); SongsFinishCheck(); return; } QJsonValue json_song = json_album_obj["song"]; if (!json_song.isArray()) { - error = Error("Json song is not an array.", json_album_obj); + Error("Json song is not an array.", json_album_obj); SongsFinishCheck(); return; } @@ -531,7 +532,7 @@ int SubsonicRequest::ParseSong(Song &song, const QJsonObject &json_obj, const qi if (year > 0) song.set_year(year); song.set_url(url); song.set_length_nanosec(duration); - if (cover_url.isValid()) song.set_art_automatic(cover_url.toEncoded()); + if (cover_url.isValid()) song.set_art_automatic(cover_url); song.set_genre(genre); song.set_directory_id(0); song.set_filetype(filetype); @@ -561,56 +562,27 @@ void SubsonicRequest::GetAlbumCovers() { void SubsonicRequest::AddAlbumCoverRequest(Song &song) { - QUrl url(song.art_automatic()); - if (!url.isValid()) return; + QUrl cover_url(song.art_automatic()); + if (!cover_url.isValid()) return; if (album_covers_requests_sent_.contains(song.album_id())) { album_covers_requests_sent_.insertMulti(song.album_id(), &song); return; } + AlbumCoverRequest request; + request.album_id = song.album_id(); + request.url = cover_url; + request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url); + if (request.filename.isEmpty()) return; + album_covers_requests_sent_.insertMulti(song.album_id(), &song); ++album_covers_requested_; - AlbumCoverRequest request; - request.album_id = song.album_id(); - request.url = url; - request.filename = AlbumCoverFileName(song); - album_cover_requests_queue_.enqueue(request); } -QString SubsonicRequest::AlbumCoverFileName(const Song &song) { - - QString artist = song.effective_albumartist(); - QString album = song.effective_album(); - QString title = song.title(); - - artist.remove('/'); - album.remove('/'); - title.remove('/'); - - QString filename = artist + "-" + album + ".jpg"; - filename = filename.toLower(); - filename.replace(' ', '-'); - filename.replace("--", "-"); - filename.replace(230, "ae"); - filename.replace(198, "AE"); - filename.replace(246, 'o'); - filename.replace(248, 'o'); - filename.replace(214, 'O'); - filename.replace(216, 'O'); - filename.replace(228, 'a'); - filename.replace(229, 'a'); - filename.replace(196, 'A'); - filename.replace(197, 'A'); - filename.remove(OrganiseFormat::kValidFatCharacters); - - return filename; - -} - void SubsonicRequest::FlushAlbumCoverRequests() { while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) { @@ -657,9 +629,8 @@ void SubsonicRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &al return; } - QString error; if (reply->error() != QNetworkReply::NoError) { - error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); album_covers_requests_sent_.remove(album_id); AlbumCoverFinishCheck(); return; @@ -667,7 +638,7 @@ void SubsonicRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &al QByteArray data = reply->readAll(); if (data.isEmpty()) { - error = Error(QString("Received empty image data for %1").arg(url.toString())); + Error(QString("Received empty image data for %1").arg(url.toString())); album_covers_requests_sent_.remove(album_id); AlbumCoverFinishCheck(); return; @@ -675,22 +646,19 @@ void SubsonicRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &al QImage image; if (image.loadFromData(data)) { - - QDir dir; - if (dir.mkpath(service_->CoverCacheDir())) { - QString filepath(service_->CoverCacheDir() + "/" + filename); - if (image.save(filepath, "JPG")) { - while (album_covers_requests_sent_.contains(album_id)) { - Song *song = album_covers_requests_sent_.take(album_id); - song->set_art_automatic(filepath); - } + if (image.save(filename, "JPG")) { + while (album_covers_requests_sent_.contains(album_id)) { + Song *song = album_covers_requests_sent_.take(album_id); + QUrl cover_url; + cover_url.setScheme("file"); + cover_url.setPath(filename); + song->set_art_automatic(cover_url); } } - } else { album_covers_requests_sent_.remove(album_id); - error = Error(QString("Error decoding image data from %1").arg(url.toString())); + Error(QString("Error decoding image data from %1").arg(url.toString())); } AlbumCoverFinishCheck(); @@ -729,29 +697,26 @@ void SubsonicRequest::FinishCheck() { if (songs_.isEmpty() && errors_.isEmpty()) emit Results(songs_, tr("Unknown error")); else - emit Results(songs_, errors_); + emit Results(songs_, ErrorsToHTML(errors_)); } } } -QString SubsonicRequest::Error(QString error, QVariant debug) { - - qLog(Error) << "Subsonic:" << error; - if (debug.isValid()) qLog(Debug) << debug; +void SubsonicRequest::Error(const QString &error, const QVariant &debug) { if (!error.isEmpty()) { - errors_ += error; - errors_ += "
"; + qLog(Error) << "Subsonic:" << error; + errors_ << error; } - FinishCheck(); + if (debug.isValid()) qLog(Debug) << debug; - return error; + FinishCheck(); } -void SubsonicRequest::Warn(QString error, QVariant debug) { +void SubsonicRequest::Warn(const QString &error, const QVariant &debug) { qLog(Error) << "Subsonic:" << error; if (debug.isValid()) qLog(Debug) << debug; diff --git a/src/subsonic/subsonicrequest.h b/src/subsonic/subsonicrequest.h index 6f09d24e..3f12cbab 100644 --- a/src/subsonic/subsonicrequest.h +++ b/src/subsonic/subsonicrequest.h @@ -27,19 +27,18 @@ #include #include #include -#include #include #include #include +#include #include #include #include -#include -#include #include "core/song.h" #include "subsonicbaserequest.h" +class Application; class NetworkAccessManager; class SubsonicService; class SubsonicUrlHandler; @@ -49,7 +48,7 @@ class SubsonicRequest : public SubsonicBaseRequest { public: - SubsonicRequest(SubsonicService *service, SubsonicUrlHandler *url_handler, NetworkAccessManager *network, QObject *parent); + SubsonicRequest(SubsonicService *service, SubsonicUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QObject *parent); ~SubsonicRequest(); void ReloadSettings(); @@ -64,7 +63,6 @@ class SubsonicRequest : public SubsonicBaseRequest { void UpdateProgress(const int max); private slots: - void AlbumsReplyReceived(QNetworkReply *reply, const int offset_requested); void AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const qint64 album_id, const QString &album_artist); void AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename); @@ -95,7 +93,6 @@ class SubsonicRequest : public SubsonicBaseRequest { void SongsFinishCheck(); void AddAlbumSongsRequest(const qint64 artist_id, const qint64 album_id, const QString &album_artist, const int offset = 0); - QString AlbumCoverFileName(const Song &song); void FlushAlbumSongsRequests(); int ParseSong(Song &song, const QJsonObject &json_obj, const qint64 artist_id_requested = 0, const qint64 album_id_requested = 0, const QString &album_artist = QString()); @@ -106,8 +103,8 @@ class SubsonicRequest : public SubsonicBaseRequest { void AlbumCoverFinishCheck(); void FinishCheck(); - void Warn(QString error, QVariant debug = QVariant()); - QString Error(QString error, QVariant debug = QVariant()); + void Warn(const QString &error, const QVariant &debug = QVariant()); + void Error(const QString &error, const QVariant &debug = QVariant()); static const int kMaxConcurrentAlbumsRequests; static const int kMaxConcurrentArtistAlbumsRequests; @@ -116,6 +113,7 @@ class SubsonicRequest : public SubsonicBaseRequest { SubsonicService *service_; SubsonicUrlHandler *url_handler_; + Application *app_; NetworkAccessManager *network_; bool finished_; @@ -138,7 +136,7 @@ class SubsonicRequest : public SubsonicBaseRequest { int album_covers_received_; SongList songs_; - QString errors_; + QStringList errors_; bool no_results_; QList album_cover_replies_; diff --git a/src/subsonic/subsonicservice.cpp b/src/subsonic/subsonicservice.cpp index a1ed0b2d..e25085f6 100644 --- a/src/subsonic/subsonicservice.cpp +++ b/src/subsonic/subsonicservice.cpp @@ -23,7 +23,6 @@ #include #include -#include #include #include #include @@ -32,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -118,10 +118,6 @@ void SubsonicService::ReloadSettings() { } -QString SubsonicService::CoverCacheDir() { - return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/subsonicalbumcovers"; -} - void SubsonicService::SendPing() { SendPing(server_url_, username_, password_); } @@ -158,19 +154,29 @@ void SubsonicService::SendPing(QUrl url, const QString &username, const QString req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + errors_.clear(); QNetworkReply *reply = network_->get(req); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandlePingSSLErrors(QList))); NewClosure(reply, SIGNAL(finished()), this, SLOT(HandlePingReply(QNetworkReply*)), reply); //qLog(Debug) << "Subsonic: Sending request" << url << query; } +void SubsonicService::HandlePingSSLErrors(QList ssl_errors) { + + for (QSslError &ssl_error : ssl_errors) { + errors_ += ssl_error.errorString(); + } + +} + void SubsonicService::HandlePingReply(QNetworkReply *reply) { reply->deleteLater(); - if (reply->error() != QNetworkReply::NoError) { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. PingError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); return; @@ -180,7 +186,6 @@ void SubsonicService::HandlePingReply(QNetworkReply *reply) { QByteArray data = reply->readAll(); QJsonParseError parse_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - QString failure_reason; if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (!json_obj.isEmpty() && json_obj.contains("error")) { @@ -190,24 +195,25 @@ void SubsonicService::HandlePingReply(QNetworkReply *reply) { if (!json_obj.isEmpty() && json_obj.contains("code") && json_obj.contains("message")) { int code = json_obj["code"].toInt(); QString message = json_obj["message"].toString(); - failure_reason = QString("%1 (%2)").arg(message).arg(code); + errors_ << QString("%1 (%2)").arg(message).arg(code); } } } } - if (failure_reason.isEmpty()) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (errors_.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } - PingError(failure_reason); + PingError(); return; } } - int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (http_code != 200) { - PingError(QString("Received HTTP code %1").arg(http_code)); - return; - } + errors_.clear(); QByteArray data(reply->readAll()); @@ -330,7 +336,7 @@ void SubsonicService::GetSongs() { } ResetSongsRequest(); - songs_request_.reset(new SubsonicRequest(this, url_handler_, network_, this)); + songs_request_.reset(new SubsonicRequest(this, url_handler_, app_, network_, this)); connect(songs_request_.get(), SIGNAL(Results(const SongList&, const QString&)), SLOT(SongsResultsReceived(const SongList&, const QString&))); connect(songs_request_.get(), SIGNAL(UpdateStatus(const QString&)), SIGNAL(SongsUpdateStatus(const QString&))); connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(const int)), SIGNAL(SongsProgressSetMaximum(const int))); @@ -346,14 +352,20 @@ void SubsonicService::SongsResultsReceived(const SongList &songs, const QString } -QString SubsonicService::PingError(QString error, QVariant debug) { +void SubsonicService::PingError(const QString &error, const QVariant &debug) { - qLog(Error) << "Subsonic:" << error; + if (!error.isEmpty()) errors_ << error; + + QString error_html; + for (const QString &error : errors_) { + qLog(Error) << "Subsonic:" << error; + error_html += error + "
"; + } if (debug.isValid()) qLog(Debug) << debug; - emit TestFailure(error); - emit TestComplete(false, error); + emit TestFailure(error_html); + emit TestComplete(false, error_html); - return error; + errors_.clear(); } diff --git a/src/subsonic/subsonicservice.h b/src/subsonic/subsonicservice.h index 1d7cf33b..755fef6f 100644 --- a/src/subsonic/subsonicservice.h +++ b/src/subsonic/subsonicservice.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -59,7 +60,8 @@ class SubsonicService : public InternetService { static const Song::Source kSource; void ReloadSettings(); - QString CoverCacheDir(); + + Application *app() { return app_; } QString client_name() { return kClientName; } QString api_version() { return kApiVersion; } @@ -89,6 +91,8 @@ class SubsonicService : public InternetService { void ResetSongsRequest(); private slots: + //void HandlePingSSLErrors(QNetworkReply *reply, QList ssl_errors); + void HandlePingSSLErrors(QList ssl_errors); void HandlePingReply(QNetworkReply *reply); void SongsResultsReceived(const SongList &songs, const QString &error); @@ -99,7 +103,7 @@ class SubsonicService : public InternetService { typedef QPair EncodedParam; typedef QList EncodedParamList; - QString PingError(QString error, QVariant debug = QVariant()); + void PingError(const QString &error = QString(), const QVariant &debug = QVariant()); static const char *kClientName; static const char *kApiVersion; @@ -122,6 +126,8 @@ class SubsonicService : public InternetService { bool verify_certificate_; bool download_album_covers_; + QStringList errors_; + }; #endif // SUBSONICSERVICE_H diff --git a/src/subsonic/subsonicurlhandler.cpp b/src/subsonic/subsonicurlhandler.cpp index 500cd1a0..f5b7e791 100644 --- a/src/subsonic/subsonicurlhandler.cpp +++ b/src/subsonic/subsonicurlhandler.cpp @@ -65,3 +65,4 @@ UrlHandler::LoadResult SubsonicUrlHandler::StartLoading(const QUrl &url) { return LoadResult(url, LoadResult::TrackAvailable, media_url); } + diff --git a/src/subsonic/subsonicurlhandler.h b/src/subsonic/subsonicurlhandler.h index e95268ba..2842d277 100644 --- a/src/subsonic/subsonicurlhandler.h +++ b/src/subsonic/subsonicurlhandler.h @@ -54,6 +54,8 @@ class SubsonicUrlHandler : public UrlHandler { typedef QPair EncodedParam; typedef QList EncodedParamList; + void Error(const QString &error, const QVariant &debug) {} + SubsonicService *service_; }; diff --git a/src/tidal/tidalbaserequest.cpp b/src/tidal/tidalbaserequest.cpp index c8c9b26c..c49a96a7 100644 --- a/src/tidal/tidalbaserequest.cpp +++ b/src/tidal/tidalbaserequest.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,8 @@ #include "tidalservice.h" #include "tidalbaserequest.h" -const char *TidalBaseRequest::kApiUrl = "https://api.tidalhifi.com/v1"; +//const char *TidalBaseRequest::kApiUrl = "https://api.tidalhifi.com/v1"; +const char *TidalBaseRequest::kApiUrl = "https://192.168.1.112"; TidalBaseRequest::TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent) : QObject(parent), @@ -77,6 +79,7 @@ QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, co if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8()); QNetworkReply *reply = network_->get(req); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleSSLErrors(QList))); replies_ << reply; //qLog(Debug) << "Tidal: Sending request" << url; @@ -85,7 +88,15 @@ QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, co } -QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, QString &error, const bool send_login) { +void TidalBaseRequest::HandleSSLErrors(QList ssl_errors) { + + for (QSslError &ssl_error : ssl_errors) { + Error(ssl_error.errorString()); + } + +} + +QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, const bool send_login) { if (replies_.contains(reply)) { replies_.removeAll(reply); @@ -94,54 +105,51 @@ QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, QString &error, QByteArray data; - if (reply->error() == QNetworkReply::NoError) { - int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (http_code == 200) { - data = reply->readAll(); - } - else { - error = Error(QString("Received HTTP code %1").arg(http_code)); - } + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); } else { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. - error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } else { - // See if there is Json data containing "userMessage" - then use that instead. + // See if there is Json data containing "status" and "userMessage" - then use that instead. data = reply->readAll(); - QJsonParseError parse_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); + QString error; + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); int status = 0; int sub_status = 0; - QString failure_reason; - if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { status = json_obj["status"].toInt(); sub_status = json_obj["subStatus"].toInt(); QString user_message = json_obj["userMessage"].toString(); - failure_reason = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); + error = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); } } - if (failure_reason.isEmpty()) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); } if (status == 401 && sub_status == 6001) { // User does not have a valid session emit service_->Logout(); if (!oauth() && send_login && login_attempts() < max_login_attempts() && !api_token().isEmpty() && !username().isEmpty() && !password().isEmpty()) { - qLog(Error) << "Tidal:" << failure_reason; + qLog(Error) << "Tidal:" << error; qLog(Info) << "Tidal:" << "Attempting to login."; NeedLogin(); emit service_->Login(); } else { - error = Error(failure_reason); + Error(error); } } - else { // Fail - error = Error(failure_reason); + else { + Error(error); } } return QByteArray(); @@ -151,29 +159,29 @@ QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, QString &error, } -QJsonObject TidalBaseRequest::ExtractJsonObj(QByteArray &data, QString &error) { +QJsonObject TidalBaseRequest::ExtractJsonObj(QByteArray &data) { QJsonParseError json_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); if (json_error.error != QJsonParseError::NoError) { - error = Error("Reply from server missing Json data.", data); + Error("Reply from server missing Json data.", data); return QJsonObject(); } if (json_doc.isNull() || json_doc.isEmpty()) { - error = Error("Received empty Json document.", data); + Error("Received empty Json document.", data); return QJsonObject(); } if (!json_doc.isObject()) { - error = Error("Json document is not an object.", json_doc); + Error("Json document is not an object.", json_doc); return QJsonObject(); } QJsonObject json_obj = json_doc.object(); if (json_obj.isEmpty()) { - error = Error("Received empty Json object.", json_doc); + Error("Received empty Json object.", json_doc); return QJsonObject(); } @@ -181,18 +189,18 @@ QJsonObject TidalBaseRequest::ExtractJsonObj(QByteArray &data, QString &error) { } -QJsonValue TidalBaseRequest::ExtractItems(QByteArray &data, QString &error) { +QJsonValue TidalBaseRequest::ExtractItems(QByteArray &data) { - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj, error); + return ExtractItems(json_obj); } -QJsonValue TidalBaseRequest::ExtractItems(QJsonObject &json_obj, QString &error) { +QJsonValue TidalBaseRequest::ExtractItems(QJsonObject &json_obj) { - if (!json_obj.contains("items")) { - error = Error("Json reply is missing items.", json_obj); + if (!json_obj.contains("items_")) { + Error("Json reply is missing items.", json_obj); return QJsonArray(); } QJsonValue json_items = json_obj["items"]; @@ -200,11 +208,12 @@ QJsonValue TidalBaseRequest::ExtractItems(QJsonObject &json_obj, QString &error) } -QString TidalBaseRequest::Error(QString error, QVariant debug) { +QString TidalBaseRequest::ErrorsToHTML(const QStringList &errors) { - qLog(Error) << "Tidal:" << error; - if (debug.isValid()) qLog(Debug) << debug; - - return error; + QString error_html; + for (const QString &error : errors) { + error_html += error + "
"; + } + return error_html; } diff --git a/src/tidal/tidalbaserequest.h b/src/tidal/tidalbaserequest.h index 802adf9c..1667474c 100644 --- a/src/tidal/tidalbaserequest.h +++ b/src/tidal/tidalbaserequest.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -71,12 +72,13 @@ class TidalBaseRequest : public QObject { typedef QList EncodedParamList; QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_provided); - QByteArray GetReplyData(QNetworkReply *reply, QString &error, const bool send_login); - QJsonObject ExtractJsonObj(QByteArray &data, QString &error); - QJsonValue ExtractItems(QByteArray &data, QString &error); - QJsonValue ExtractItems(QJsonObject &json_obj, QString &error); + QByteArray GetReplyData(QNetworkReply *reply, const bool send_login); + QJsonObject ExtractJsonObj(QByteArray &data); + QJsonValue ExtractItems(QByteArray &data); + QJsonValue ExtractItems(QJsonObject &json_obj); - virtual QString Error(QString error, QVariant debug = QVariant()); + virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; + QString ErrorsToHTML(const QStringList &errors); QString api_url() { return QString(kApiUrl); } const bool oauth() { return service_->oauth(); } @@ -100,6 +102,9 @@ class TidalBaseRequest : public QObject { int login_attempts() { return service_->login_attempts(); } virtual void NeedLogin() = 0; + + private slots: + void HandleSSLErrors(QList ssl_errors); private: diff --git a/src/tidal/tidalfavoriterequest.cpp b/src/tidal/tidalfavoriterequest.cpp index 72431db0..6ee26673 100644 --- a/src/tidal/tidalfavoriterequest.cpp +++ b/src/tidal/tidalfavoriterequest.cpp @@ -163,7 +163,7 @@ void TidalFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const Favorit } QString error; - QByteArray data = GetReplyData(reply, error, false); + QByteArray data = GetReplyData(reply, false); if (reply->error() != QNetworkReply::NoError) { return; @@ -265,7 +265,7 @@ void TidalFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const Favo } QString error; - QByteArray data = GetReplyData(reply, error, false); + QByteArray data = GetReplyData(reply, false); if (reply->error() != QNetworkReply::NoError) { return; } @@ -285,3 +285,10 @@ void TidalFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const Favo } } + +void TidalFavoriteRequest::Error(const QString &error, const QVariant &debug) { + + qLog(Error) << "Tidal:" << error; + if (debug.isValid()) qLog(Debug) << debug; + +} diff --git a/src/tidal/tidalfavoriterequest.h b/src/tidal/tidalfavoriterequest.h index 45e5b16d..16e7ae43 100644 --- a/src/tidal/tidalfavoriterequest.h +++ b/src/tidal/tidalfavoriterequest.h @@ -22,7 +22,10 @@ #include "config.h" +#include #include +#include +#include #include "tidalbaserequest.h" #include "core/song.h" @@ -69,6 +72,7 @@ class TidalFavoriteRequest : public TidalBaseRequest { void RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs); private: + void Error(const QString &error, const QVariant &debug = QVariant()); QString FavoriteText(const FavoriteType type); void AddFavorites(const FavoriteType type, const SongList &songs); void RemoveFavorites(const FavoriteType type, const SongList songs); diff --git a/src/tidal/tidalrequest.cpp b/src/tidal/tidalrequest.cpp index e1f98a02..c32291c0 100644 --- a/src/tidal/tidalrequest.cpp +++ b/src/tidal/tidalrequest.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include #include @@ -35,6 +34,8 @@ #include "core/network.h" #include "core/song.h" #include "core/timeconstants.h" +#include "core/application.h" +#include "covermanager/albumcoverloader.h" #include "tidalservice.h" #include "tidalurlhandler.h" #include "tidalrequest.h" @@ -47,10 +48,11 @@ const int TidalRequest::kMaxConcurrentArtistAlbumsRequests = 3; const int TidalRequest::kMaxConcurrentAlbumSongsRequests = 3; const int TidalRequest::kMaxConcurrentAlbumCoverRequests = 1; -TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler, NetworkAccessManager *network, QueryType type, QObject *parent) +TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent) : TidalBaseRequest(service, network, parent), service_(service), url_handler_(url_handler), + app_(app), network_(network), type_(type), fetchalbums_(service->fetchalbums()), @@ -309,8 +311,7 @@ void TidalRequest::AddSongsSearchRequest(const int offset) { void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { - QString error; - QByteArray data = GetReplyData(reply, error, (offset_requested == 0)); + QByteArray data = GetReplyData(reply, (offset_requested == 0)); --artists_requests_active_; @@ -321,7 +322,7 @@ void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re return; } - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { ArtistsFinishCheck(); return; @@ -359,7 +360,7 @@ void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re emit UpdateProgress(query_id_, artists_received_); } - QJsonValue json_value = ExtractItems(json_obj, error); + QJsonValue json_value = ExtractItems(json_obj); if (!json_value.isArray()) { ArtistsFinishCheck(); return; @@ -490,8 +491,7 @@ void TidalRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const qint64 void TidalRequest::AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const int limit_requested, const int offset_requested, const bool auto_login) { - QString error; - QByteArray data = GetReplyData(reply, error, auto_login); + QByteArray data = GetReplyData(reply, auto_login); if (finished_) return; @@ -500,7 +500,7 @@ void TidalRequest::AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_r return; } - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { AlbumsFinishCheck(artist_id_requested); return; @@ -525,7 +525,7 @@ void TidalRequest::AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_r return; } - QJsonValue json_value = ExtractItems(json_obj, error); + QJsonValue json_value = ExtractItems(json_obj); if (!json_value.isArray()) { AlbumsFinishCheck(artist_id_requested); return; @@ -736,8 +736,7 @@ void TidalRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 ar void TidalRequest::SongsReceived(QNetworkReply *reply, const qint64 artist_id, const qint64 album_id, const int limit_requested, const int offset_requested, const bool auto_login, const QString &album_artist) { - QString error; - QByteArray data = GetReplyData(reply, error, auto_login); + QByteArray data = GetReplyData(reply, auto_login); if (finished_) return; @@ -746,7 +745,7 @@ void TidalRequest::SongsReceived(QNetworkReply *reply, const qint64 artist_id, c return; } - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, 0, 0, album_artist); return; @@ -771,7 +770,7 @@ void TidalRequest::SongsReceived(QNetworkReply *reply, const qint64 artist_id, c return; } - QJsonValue json_value = ExtractItems(json_obj, error); + QJsonValue json_value = ExtractItems(json_obj); if (!json_value.isArray()) { SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist); return; @@ -986,7 +985,7 @@ int TidalRequest::ParseSong(Song &song, const QJsonObject &json_obj, const qint6 song.set_disc(disc); song.set_url(url); song.set_length_nanosec(duration); - song.set_art_automatic(cover_url.toEncoded()); + song.set_art_automatic(cover_url); song.set_comment(copyright); song.set_directory_id(0); song.set_filetype(Song::FileType_Stream); @@ -1020,12 +1019,14 @@ void TidalRequest::AddAlbumCoverRequest(Song &song) { return; } - album_covers_requests_sent_.insertMulti(song.album_id(), &song); - ++album_covers_requested_; - AlbumCoverRequest request; request.album_id = song.album_id(); request.url = QUrl(song.art_automatic()); + request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), request.url); + if (request.filename.isEmpty()) return; + + album_covers_requests_sent_.insertMulti(song.album_id(), &song); + ++album_covers_requested_; album_cover_requests_queue_.enqueue(request); @@ -1041,13 +1042,13 @@ void TidalRequest::FlushAlbumCoverRequests() { QNetworkRequest req(request.url); QNetworkReply *reply = network_->get(req); album_cover_replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, const QString&, const QUrl&)), reply, request.album_id, request.url); + NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, const QString&, const QUrl&, const QString&)), reply, request.album_id, request.url, request.filename); } } -void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url) { +void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename) { if (album_cover_replies_.contains(reply)) { album_cover_replies_.removeAll(reply); @@ -1070,9 +1071,8 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album return; } - QString error; if (reply->error() != QNetworkReply::NoError) { - error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); album_covers_requests_sent_.remove(album_id); AlbumCoverFinishCheck(); return; @@ -1080,7 +1080,7 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album QByteArray data = reply->readAll(); if (data.isEmpty()) { - error = Error(QString("Received empty image data for %1").arg(url.toString())); + Error(QString("Received empty image data for %1").arg(url.toString())); album_covers_requests_sent_.remove(album_id); AlbumCoverFinishCheck(); return; @@ -1089,21 +1089,20 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album QImage image; if (image.loadFromData(data)) { - QDir dir; - if (dir.mkpath(service_->CoverCacheDir())) { - QString filename(service_->CoverCacheDir() + "/" + album_id + "-" + url.fileName()); - if (image.save(filename, "JPG")) { - while (album_covers_requests_sent_.contains(album_id)) { - Song *song = album_covers_requests_sent_.take(album_id); - song->set_art_automatic(filename); - } + if (image.save(filename, "JPG")) { + while (album_covers_requests_sent_.contains(album_id)) { + Song *song = album_covers_requests_sent_.take(album_id); + QUrl cover_url; + cover_url.setScheme("file"); + cover_url.setPath(filename); + song->set_art_automatic(cover_url); } } } else { album_covers_requests_sent_.remove(album_id); - error = Error(QString("Error decoding image data from %1").arg(url.toString())); + Error(QString("Error decoding image data from %1").arg(url.toString())); } AlbumCoverFinishCheck(); @@ -1155,24 +1154,22 @@ void TidalRequest::FinishCheck() { if (songs_.isEmpty() && errors_.isEmpty()) emit Results(query_id_, songs_, tr("Unknown error")); else - emit Results(query_id_, songs_, errors_); + emit Results(query_id_, songs_, ErrorsToHTML(errors_)); } } } -QString TidalRequest::Error(QString error, QVariant debug) { - - qLog(Error) << "Tidal:" << error; - if (debug.isValid()) qLog(Debug) << debug; +void TidalRequest::Error(const QString &error, const QVariant &debug) { if (!error.isEmpty()) { - errors_ += error; - errors_ += "
"; + errors_ << error; + qLog(Error) << "Tidal:" << error; } - FinishCheck(); - return error; + if (debug.isValid()) qLog(Debug) << debug; + + FinishCheck(); } diff --git a/src/tidal/tidalrequest.h b/src/tidal/tidalrequest.h index ff0c1dc5..deb6e292 100644 --- a/src/tidal/tidalrequest.h +++ b/src/tidal/tidalrequest.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -49,7 +50,7 @@ class TidalRequest : public TidalBaseRequest { public: - TidalRequest(TidalService *service, TidalUrlHandler *url_handler, NetworkAccessManager *network, QueryType type, QObject *parent); + TidalRequest(TidalService *service, TidalUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent); ~TidalRequest(); void ReloadSettings(); @@ -82,7 +83,7 @@ class TidalRequest : public TidalBaseRequest { void ArtistAlbumsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const int offset_requested); void AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const qint64 album_id, const int offset_requested, const QString &album_artist); - void AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url); + void AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename); private: typedef QPair Param; @@ -100,6 +101,7 @@ class TidalRequest : public TidalBaseRequest { qint64 artist_id = 0; QString album_id = 0; QUrl url; + QString filename; }; const bool IsQuery() { return (type_ == QueryType_Artists || type_ == QueryType_Albums || type_ == QueryType_Songs); } @@ -142,7 +144,7 @@ class TidalRequest : public TidalBaseRequest { void FinishCheck(); void Warn(QString error, QVariant debug = QVariant()); - QString Error(QString error, QVariant debug = QVariant()); + void Error(const QString &error, const QVariant &debug = QVariant()); static const char *kResourcesUrl; static const int kMaxConcurrentArtistsRequests; @@ -154,6 +156,7 @@ class TidalRequest : public TidalBaseRequest { TidalService *service_; TidalUrlHandler *url_handler_; + Application *app_; NetworkAccessManager *network_; QueryType type_; @@ -197,7 +200,7 @@ class TidalRequest : public TidalBaseRequest { int album_covers_received_; SongList songs_; - QString errors_; + QStringList errors_; bool need_login_; bool no_results_; QList album_cover_replies_; diff --git a/src/tidal/tidalservice.cpp b/src/tidal/tidalservice.cpp index 99fd4ff5..d4d5849c 100644 --- a/src/tidal/tidalservice.cpp +++ b/src/tidal/tidalservice.cpp @@ -23,7 +23,6 @@ #include #include -#include #include #include #include @@ -34,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -228,10 +228,6 @@ void TidalService::ReloadSettings() { } -QString TidalService::CoverCacheDir() { - return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/tidalalbumcovers"; -} - void TidalService::StartAuthorisation() { login_sent_ = true; @@ -315,7 +311,10 @@ void TidalService::AuthorisationUrlReceived(const QUrl &url) { QUrl url(kOAuthAccessTokenUrl); QNetworkRequest request = QNetworkRequest(url); QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); + + login_errors_.clear(); QNetworkReply *reply = network_->post(request, query); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList))); NewClosure(reply, SIGNAL(finished()), this, SLOT(AccessTokenRequestFinished(QNetworkReply*)), reply); } @@ -328,47 +327,53 @@ void TidalService::AuthorisationUrlReceived(const QUrl &url) { } +void TidalService::HandleLoginSSLErrors(QList ssl_errors) { + + for (QSslError &ssl_error : ssl_errors) { + login_errors_ += ssl_error.errorString(); + } + +} + void TidalService::AccessTokenRequestFinished(QNetworkReply *reply) { reply->deleteLater(); login_sent_ = false; - if (reply->error() != QNetworkReply::NoError) { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); return; } else { - // See if there is Json data containing "redirectUri" then use that instead. + // See if there is Json data containing "status" and "userMessage" then use that instead. QByteArray data(reply->readAll()); QJsonParseError json_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - QString failure_reason; if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { int status = json_obj["status"].toInt(); int sub_status = json_obj["subStatus"].toInt(); QString user_message = json_obj["userMessage"].toString(); - failure_reason = QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); + login_errors_ << QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); } } - if (failure_reason.isEmpty()) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (login_errors_.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } - LoginError(failure_reason); + LoginError(); return; } } - int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (http_code != 200) { - LoginError(QString("Received HTTP code %1").arg(http_code)); - return; - } - QByteArray data(reply->readAll()); QJsonParseError json_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); @@ -474,6 +479,7 @@ void TidalService::SendLogin(const QString &api_token, const QString &username, QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); QNetworkReply *reply = network_->post(req, query); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList))); NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply); //qLog(Debug) << "Tidal: Sending request" << url << query; @@ -486,10 +492,11 @@ void TidalService::HandleAuthReply(QNetworkReply *reply) { login_sent_ = false; - if (reply->error() != QNetworkReply::NoError) { - if (reply->error() < 200) { + if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { // This is a network error, there is nothing more to do. LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + login_errors_.clear(); return; } else { @@ -497,24 +504,31 @@ void TidalService::HandleAuthReply(QNetworkReply *reply) { QByteArray data(reply->readAll()); QJsonParseError json_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - QString failure_reason; if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { QJsonObject json_obj = json_doc.object(); if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { int status = json_obj["status"].toInt(); int sub_status = json_obj["subStatus"].toInt(); QString user_message = json_obj["userMessage"].toString(); - failure_reason = QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); + login_errors_ << QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); } } - if (failure_reason.isEmpty()) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + if (login_errors_.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } } - LoginError(failure_reason); + LoginError(); + login_errors_.clear(); return; } } + login_errors_.clear(); + QByteArray data(reply->readAll()); QJsonParseError json_error; QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); @@ -645,7 +659,7 @@ void TidalService::GetArtists() { ResetArtistsRequest(); - artists_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::QueryType_Artists, this)); + artists_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Artists, this)); connect(artists_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(ArtistsResultsReceived(const int, const SongList&, const QString&))); connect(artists_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(ArtistsUpdateStatusReceived(const int, const QString&))); @@ -699,7 +713,7 @@ void TidalService::GetAlbums() { } ResetAlbumsRequest(); - albums_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::QueryType_Albums, this)); + albums_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Albums, this)); connect(albums_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(AlbumsResultsReceived(const int, const SongList&, const QString&))); connect(albums_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(AlbumsUpdateStatusReceived(const int, const QString&))); connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(AlbumsProgressSetMaximumReceived(const int, const int))); @@ -752,7 +766,7 @@ void TidalService::GetSongs() { } ResetSongsRequest(); - songs_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::QueryType_Songs, this)); + songs_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Songs, this)); connect(songs_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(SongsResultsReceived(const int, const SongList&, const QString&))); connect(songs_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(SongsUpdateStatusReceived(const int, const QString&))); connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(SongsProgressSetMaximumReceived(const int, const int))); @@ -842,7 +856,7 @@ void TidalService::SendSearch() { return; } - search_request_.reset(new TidalRequest(this, url_handler_, network_, type, this)); + search_request_.reset(new TidalRequest(this, url_handler_, app_, network_, type, this)); connect(search_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(SearchResultsReceived(const int, const SongList&, const QString&))); connect(search_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SIGNAL(SearchUpdateStatus(const int, const QString&))); @@ -894,14 +908,20 @@ void TidalService::HandleStreamURLFinished(const QUrl &original_url, const QUrl } -QString TidalService::LoginError(QString error, QVariant debug) { +void TidalService::LoginError(const QString &error, const QVariant &debug) { - qLog(Error) << "Tidal:" << error; + if (!error.isEmpty()) login_errors_ << error; + + QString error_html; + for (const QString &error : login_errors_) { + qLog(Error) << "Tidal:" << error; + error_html += error + "
"; + } if (debug.isValid()) qLog(Debug) << debug; - emit LoginFailure(error); - emit LoginComplete(false, error); + emit LoginFailure(error_html); + emit LoginComplete(false, error_html); - return error; + login_errors_.clear(); } diff --git a/src/tidal/tidalservice.h b/src/tidal/tidalservice.h index ad9c25e0..1028c0c2 100644 --- a/src/tidal/tidalservice.h +++ b/src/tidal/tidalservice.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +62,6 @@ class TidalService : public InternetService { static const Song::Source kSource; void ReloadSettings(); - QString CoverCacheDir(); void Logout(); int Search(const QString &query, InternetSearch::SearchType type); @@ -69,6 +69,8 @@ class TidalService : public InternetService { const int max_login_attempts() { return kLoginAttempts; } + Application *app() { return app_; } + const bool oauth() { return oauth_; } QString client_id() { return client_id_; } QString api_token() { return api_token_; } @@ -132,6 +134,7 @@ class TidalService : public InternetService { private slots: void StartAuthorisation(); void AuthorisationUrlReceived(const QUrl &url); + void HandleLoginSSLErrors(QList ssl_errors); void AccessTokenRequestFinished(QNetworkReply *reply); void SendLogin(); void HandleAuthReply(QNetworkReply *reply); @@ -160,7 +163,7 @@ class TidalService : public InternetService { typedef QList EncodedParamList; void SendSearch(); - QString LoginError(QString error, QVariant debug = QVariant()); + void LoginError(const QString &error = QString(), const QVariant &debug = QVariant()); static const char *kApiTokenB64; static const char *kOAuthUrl; @@ -240,6 +243,8 @@ class TidalService : public InternetService { QList stream_url_requests_; + QStringList login_errors_; + }; #endif // TIDALSERVICE_H diff --git a/src/tidal/tidalstreamurlrequest.cpp b/src/tidal/tidalstreamurlrequest.cpp index c5cf505c..22a1fb60 100644 --- a/src/tidal/tidalstreamurlrequest.cpp +++ b/src/tidal/tidalstreamurlrequest.cpp @@ -147,37 +147,35 @@ void TidalStreamURLRequest::StreamURLReceived() { disconnect(reply_, 0, nullptr, 0); reply_->deleteLater(); - QString error; - - QByteArray data = GetReplyData(reply_, error, true); + QByteArray data = GetReplyData(reply_, true); if (data.isEmpty()) { reply_ = nullptr; if (!authenticated() && login_sent() && tries_ <= 1) { need_login_ = true; return; } - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } reply_ = nullptr; //qLog(Debug) << "Tidal:" << data; - QJsonObject json_obj = ExtractJsonObj(data, error); + QJsonObject json_obj = ExtractJsonObj(data); if (json_obj.isEmpty()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } if (!json_obj.contains("trackId")) { - error = Error("Invalid Json reply, stream missing trackId.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error("Invalid Json reply, stream missing trackId.", json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } int track_id(json_obj["trackId"].toInt()); if (track_id != song_id_) { - error = Error("Incorrect track ID returned.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error("Incorrect track ID returned.", json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } @@ -209,8 +207,8 @@ void TidalStreamURLRequest::StreamURLReceived() { QString filepath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/tidalstreams"; QString filename = "tidal-" + QString::number(song_id_) + ".xml"; if (!QDir().mkpath(filepath)) { - error = Error(QString("Failed to create directory %1.").arg(filepath), json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error(QString("Failed to create directory %1.").arg(filepath), json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } QUrl url("file://" + filepath + "/" + filename); @@ -218,8 +216,8 @@ void TidalStreamURLRequest::StreamURLReceived() { if (file.exists()) file.remove(); if (!file.open(QIODevice::WriteOnly)) { - error = Error(QString("Failed to open file %1 for writing.").arg(url.toLocalFile()), json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error(QString("Failed to open file %1 for writing.").arg(url.toLocalFile()), json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } file.write(data_manifest); @@ -231,15 +229,15 @@ void TidalStreamURLRequest::StreamURLReceived() { else { - json_obj = ExtractJsonObj(data_manifest, error); + json_obj = ExtractJsonObj(data_manifest); if (json_obj.isEmpty()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } if (!json_obj.contains("mimeType")) { - error = Error("Invalid Json reply, stream url reply manifest is missing mimeType.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error("Invalid Json reply, stream url reply manifest is missing mimeType.", json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } @@ -260,8 +258,8 @@ void TidalStreamURLRequest::StreamURLReceived() { if (json_obj.contains("urls")) { QJsonValue json_urls = json_obj["urls"]; if (!json_urls.isArray()) { - error = Error("Invalid Json reply, urls is not an array.", json_urls); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); + Error("Invalid Json reply, urls is not an array.", json_urls); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, ErrorsToHTML(errors_)); return; } QJsonArray json_array_urls = json_urls.toArray(); @@ -275,11 +273,22 @@ void TidalStreamURLRequest::StreamURLReceived() { } if (urls.isEmpty()) { - error = Error("Missing stream urls.", json_obj); - emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, error); + Error("Missing stream urls.", json_obj); + emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, ErrorsToHTML(errors_)); return; } emit StreamURLFinished(original_url_, urls.first(), filetype, -1, -1, -1); } + +void TidalStreamURLRequest::Error(const QString &error, const QVariant &debug) { + + qLog(Error) << "Tidal:" << error; + if (debug.isValid()) qLog(Debug) << debug; + + if (!error.isEmpty()) { + errors_ << error; + } + +} diff --git a/src/tidal/tidalstreamurlrequest.h b/src/tidal/tidalstreamurlrequest.h index 1085cdd1..aa35a215 100644 --- a/src/tidal/tidalstreamurlrequest.h +++ b/src/tidal/tidalstreamurlrequest.h @@ -22,7 +22,9 @@ #include "config.h" +#include #include +#include #include #include "core/song.h" @@ -60,12 +62,15 @@ class TidalStreamURLRequest : public TidalBaseRequest { void StreamURLReceived(); private: + void Error(const QString &error, const QVariant &debug = QVariant()); + TidalService *service_; QNetworkReply *reply_; QUrl original_url_; int song_id_; int tries_; bool need_login_; + QStringList errors_; }; diff --git a/src/widgets/osd.cpp b/src/widgets/osd.cpp index 1007d826..f35faf42 100644 --- a/src/widgets/osd.cpp +++ b/src/widgets/osd.cpp @@ -43,7 +43,7 @@ #include "core/application.h" #include "core/logging.h" #include "core/systemtrayicon.h" -#include "covermanager/currentartloader.h" +#include "covermanager/currentalbumcoverloader.h" const char *OSD::kSettingsGroup = "OSD"; @@ -68,7 +68,7 @@ OSD::OSD(SystemTrayIcon *tray_icon, Application *app, QObject *parent) pretty_popup_(new OSDPretty(OSDPretty::Mode_Popup)) { - connect(app_->current_art_loader(), SIGNAL(ThumbnailLoaded(Song, QString, QImage)), SLOT(AlbumArtLoaded(Song, QString, QImage))); + connect(app_->current_albumcover_loader(), SIGNAL(ThumbnailLoaded(Song, QUrl, QImage)), SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); ReloadSettings(); Init(); @@ -114,18 +114,18 @@ void OSD::ReloadPrettyOSDSettings() { void OSD::ReshowCurrentSong() { force_show_next_ = true; - AlbumArtLoaded(last_song_, last_image_uri_, last_image_); + AlbumCoverLoaded(last_song_, last_image_uri_, last_image_); } -void OSD::AlbumArtLoaded(const Song &song, const QString &uri, const QImage &image) { +void OSD::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { // Don't change tray icon details if it's a preview if (!preview_mode_ && tray_icon_) - tray_icon_->SetNowPlaying(song, uri); + tray_icon_->SetNowPlaying(song, cover_url); last_song_ = song; last_image_ = image; - last_image_uri_ = uri; + last_image_uri_ = cover_url; QStringList message_parts; QString summary; @@ -187,7 +187,7 @@ void OSD::Paused() { void OSD::Resumed() { if (show_on_resume_) { - AlbumArtLoaded(last_song_, last_image_uri_, last_image_); + AlbumCoverLoaded(last_song_, last_image_uri_, last_image_); } } @@ -373,7 +373,8 @@ void OSD::ShowPreview(const Behaviour type, const QString &line1, const QString // We want to reload the settings, but we can't do this here because the cover art loading is asynch preview_mode_ = true; - AlbumArtLoaded(song, QString(), QImage()); + AlbumCoverLoaded(song, QUrl(), QImage()); + } void OSD::SetPrettyOSDToggleMode(bool toggle) { diff --git a/src/widgets/osd.h b/src/widgets/osd.h index 3b636250..fcbfcf11 100644 --- a/src/widgets/osd.h +++ b/src/widgets/osd.h @@ -104,7 +104,7 @@ class OSD : public QObject { #if defined(HAVE_DBUS) void CallFinished(QDBusPendingCallWatcher *watcher); #endif - void AlbumArtLoaded(const Song &song, const QString &uri, const QImage &image); + void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); private: Application *app_; @@ -128,7 +128,7 @@ class OSD : public QObject { OSDPretty *pretty_popup_; Song last_song_; - QString last_image_uri_; + QUrl last_image_uri_; QImage last_image_; #ifdef HAVE_DBUS diff --git a/src/widgets/playingwidget.cpp b/src/widgets/playingwidget.cpp index de282078..a7aae7c3 100644 --- a/src/widgets/playingwidget.cpp +++ b/src/widgets/playingwidget.cpp @@ -46,7 +46,7 @@ #include "core/application.h" #include "covermanager/albumcoverchoicecontroller.h" #include "covermanager/albumcoverloader.h" -#include "covermanager/currentartloader.h" +#include "covermanager/currentalbumcoverloader.h" #include "playingwidget.h" using std::unique_ptr; @@ -131,13 +131,12 @@ PlayingWidget::PlayingWidget(QWidget *parent) PlayingWidget::~PlayingWidget() {} -void PlayingWidget::SetApplication(Application *app, AlbumCoverChoiceController *album_cover_choice_controller) { +void PlayingWidget::Init(Application *app, AlbumCoverChoiceController *album_cover_choice_controller) { app_ = app; - connect(app_->current_art_loader(), SIGNAL(ArtLoaded(Song, QString, QImage)), SLOT(AlbumArtLoaded(Song, QString, QImage))); album_cover_choice_controller_ = album_cover_choice_controller; - album_cover_choice_controller_->SetApplication(app_); + album_cover_choice_controller_->Init(app_); QList cover_actions = album_cover_choice_controller_->GetAllActions(); cover_actions.append(album_cover_choice_controller_->search_cover_auto_action()); menu_->addActions(cover_actions); @@ -152,7 +151,6 @@ void PlayingWidget::SetApplication(Application *app, AlbumCoverChoiceController connect(above_statusbar_action_, SIGNAL(toggled(bool)), SLOT(ShowAboveStatusBar(bool))); connect(album_cover_choice_controller_, SIGNAL(AutomaticCoverSearchDone()), this, SLOT(AutomaticCoverSearchDone())); - connect(album_cover_choice_controller_->search_cover_auto_action(), SIGNAL(triggered()), this, SLOT(SearchCoverAutomatically())); } @@ -270,7 +268,7 @@ void PlayingWidget::SongChanged(const Song &song) { song_ = song; } -void PlayingWidget::AlbumArtLoaded(const Song &song, const QString &, const QImage &image) { +void PlayingWidget::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { if (!playing_ || song.id() != song_playing_.id() || song.url() != song_playing_.url() || song.effective_albumartist() != song_playing_.effective_albumartist() || song.effective_album() != song_playing_.effective_album() || song.title() != song_playing_.title()) return; if (timeline_fade_->state() == QTimeLine::Running && image == image_original_) return; @@ -279,7 +277,6 @@ void PlayingWidget::AlbumArtLoaded(const Song &song, const QString &, const QIma downloading_covers_ = false; song_ = song; SetImage(image); - GetCoverAutomatically(); } @@ -495,28 +492,15 @@ void PlayingWidget::dropEvent(QDropEvent *e) { } -void PlayingWidget::GetCoverAutomatically() { +void PlayingWidget::SearchCoverInProgress() { - // Search for cover automatically? - bool search = - album_cover_choice_controller_->search_cover_auto_action()->isChecked() && - !song_.has_manually_unset_cover() && - song_.art_automatic().isEmpty() && - song_.art_manual().isEmpty() && - !song_.effective_albumartist().isEmpty() && - !song_.effective_album().isEmpty(); + downloading_covers_ = true; - if (search) { - downloading_covers_ = true; - // This is done in mainwindow instead to avoid searching multiple times (ContextView & PlayingWidget) - // album_cover_choice_controller_->SearchCoverAutomatically(song_); - - // Show a spinner animation - spinner_animation_.reset(new QMovie(":/pictures/spinner.gif", QByteArray(), this)); - connect(spinner_animation_.get(), SIGNAL(updated(const QRect&)), SLOT(update())); - spinner_animation_->start(); - update(); - } + // Show a spinner animation + spinner_animation_.reset(new QMovie(":/pictures/spinner.gif", QByteArray(), this)); + connect(spinner_animation_.get(), SIGNAL(updated(const QRect&)), SLOT(update())); + spinner_animation_->start(); + update(); } @@ -527,7 +511,3 @@ void PlayingWidget::AutomaticCoverSearchDone() { update(); } - -void PlayingWidget::SearchCoverAutomatically() { - GetCoverAutomatically(); -} diff --git a/src/widgets/playingwidget.h b/src/widgets/playingwidget.h index d915fcbe..33a7b5be 100644 --- a/src/widgets/playingwidget.h +++ b/src/widgets/playingwidget.h @@ -66,7 +66,7 @@ class PlayingWidget : public QWidget { PlayingWidget(QWidget *parent = nullptr); ~PlayingWidget(); - void SetApplication(Application *app, AlbumCoverChoiceController *album_cover_choice_controller); + void Init(Application *app, AlbumCoverChoiceController *album_cover_choice_controller); bool IsEnabled() { return enabled_; } void SetEnabled(bool enabled); void SetEnabled(); @@ -83,6 +83,7 @@ class PlayingWidget : public QWidget { void Stopped(); void Error(); void SongChanged(const Song &song); + void SearchCoverInProgress(); protected: void paintEvent(QPaintEvent *e); @@ -97,10 +98,9 @@ class PlayingWidget : public QWidget { void ShowAboveStatusBar(bool above); void FitCoverWidth(bool fit); - void SearchCoverAutomatically(); void AutomaticCoverSearchDone(); - void AlbumArtLoaded(const Song &song, const QString &uri, const QImage &image); + void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void SetHeight(int height); void FadePreviousTrack(qreal value);