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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -109,7 +109,7 @@ class ContextAlbumsModel : public SimpleTreeModel<CollectionItem> {
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);

View File

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

View File

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

View File

@ -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<PlaylistManager> playlist_manager_;
Lazy<CoverProviders> cover_providers_;
Lazy<AlbumCoverLoader> album_cover_loader_;
Lazy<CurrentArtLoader> current_art_loader_;
Lazy<CurrentAlbumCoverLoader> current_albumcover_loader_;
Lazy<LyricsProviders> lyrics_providers_;
Lazy<InternetServices> 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(); }

View File

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

View File

@ -26,8 +26,9 @@
#include <memory>
#include <QObject>
#include <QAction>
#include <QUrl>
#include <QPixmap>
#include <QAction>
#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:

View File

@ -28,6 +28,7 @@
#include <QApplication>
#include <QAction>
#include <QIcon>
#include <QUrl>
#include <QtDebug>
#include <AppKit/NSMenu.h>
@ -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());
}

View File

@ -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<TidalService*> (app_->internet_services()->ServiceBySource(Song::Source_Tidal));
if (tidalservice)
if (TidalService *tidalservice = qobject_cast<TidalService*> (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();
}
}

View File

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

View File

@ -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 <core/mpris2_player.h>
#include <core/mpris2_playlists.h>
@ -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_);

View File

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

View File

@ -29,6 +29,7 @@
#include <QMenu>
#include <QIcon>
#include <QString>
#include <QUrl>
#include <QtEvents>
#include <QSettings>
@ -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", "<img src=\"" % final_path % "\" />");
}

View File

@ -28,6 +28,7 @@
#include <QObject>
#include <QSystemTrayIcon>
#include <QString>
#include <QUrl>
#include <QPixmap>
#include <QAction>
#include <QMenu>
@ -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(); }

View File

@ -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<pb::tagreader::SongMetadata_FileType>(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;

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* 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<QAction*> AlbumCoverChoiceController::GetAllActions() {
return QList<QAction*>() << 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<QImage>(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();
}

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* 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);

View File

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

View File

@ -36,7 +36,6 @@
#include <QString>
#include <QUrl>
#include <QImage>
#include <QVector>
#include <QNetworkAccessManager>
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:

View File

@ -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<CoverSearchResult>)), SLOT(ProviderSearchFinished(int, QList<CoverSearchResult>)));
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<CoverSearchResult> &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);
}

View File

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

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QtGlobal>
#include <QObject>
#include <QDir>
#include <QQueue>
#include <QMutex>
#include <QStandardPaths>
@ -37,11 +39,15 @@
#include <QPainter>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#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<Task>::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;

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <QNetworkReply>
#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<quint64> &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

View File

@ -39,4 +39,3 @@ struct AlbumCoverLoaderOptions {
};
#endif // ALBUMCOVERLOADEROPTIONS_H

View File

@ -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<int>() << 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(&current_song, cover);
album_cover_choice_controller_->SaveCoverToSong(&current_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(&current_song, cover);
album_cover_choice_controller_->SaveCoverToSong(&current_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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -29,8 +29,9 @@
#include <QList>
#include <QString>
#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<CoverSearchResult>& results);
void SearchFinished(int id, const CoverSearchResults& results);
private:
Application *app_;

View File

@ -28,63 +28,71 @@
#include <QString>
#include <QStringBuilder>
#include <QImage>
#include <QStandardPaths>
#include <QTemporaryFile>
#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);
}

View File

@ -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<QTemporaryFile> temp_art_;
std::unique_ptr<QTemporaryFile> temp_art_thumbnail_;
std::unique_ptr<QTemporaryFile> temp_cover_;
std::unique_ptr<QTemporaryFile> temp_cover_thumbnail_;
quint64 id_;
Song last_song_;
};
#endif // CURRENTARTLOADER_H
#endif // CURRENTALBUMCOVERLOADER_H

View File

@ -53,13 +53,13 @@ DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): Cov
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
typedef QPair<QString, QString> Param;
typedef QList<Param> Parameters;
typedef QList<Param> Params;
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> 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 &param : 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();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Param> 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<Param> &params_supplied) {
QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, const ParamList &params_supplied) {
typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList;
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> 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 &param : 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();

View File

@ -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<QString, QString> Param;
typedef QList<Param> ParamList;
typedef QPair<QByteArray, QByteArray> 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<Param> &params_supplied);
QNetworkReply *CreateRequest(const QString &ressource_name, const ParamList &params_supplied);
QByteArray GetReplyData(QNetworkReply *reply, QString &error);
QJsonObject ExtractJsonObj(QByteArray &data, QString &error);
QJsonValue ExtractItems(QByteArray &data, QString &error);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<InternetSearchView*>(view_)->LazyLoadArt(index);
const_cast<InternetSearchView*>(view_)->LazyLoadAlbumCover(index);
CollectionItemDelegate::paint(painter, option, index);

View File

@ -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<InternetSearch::Result>();
// 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<QSslError>)), this, SLOT(HandleSSLErrors(QList<QSslError>)));
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<QSslError> 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 + "<br />";
}
return error_html;
}

View File

@ -71,12 +71,13 @@ class QobuzBaseRequest : public QObject {
typedef QList<EncodedParam> EncodedParamList;
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<Param> &params_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<QSslError> ssl_errors);
private:
static const char *kApiUrl;

View File

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

View File

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

View File

@ -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_ += "<br />";
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;

View File

@ -31,6 +31,7 @@
#include <QMultiMap>
#include <QQueue>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QNetworkReply>
#include <QJsonObject>
@ -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<QNetworkReply*> album_cover_replies_;

View File

@ -23,7 +23,6 @@
#include <memory>
#include <QObject>
#include <QStandardPaths>
#include <QDesktopServices>
#include <QCryptographicHash>
#include <QByteArray>
@ -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<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply);
qLog(Debug) << "Qobuz: Sending request" << url << query;
}
void QobuzService::HandleLoginSSLErrors(QList<QSslError> 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 + "<br />";
}
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();
}

View File

@ -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<QSslError> ssl_errors);
void HandleAuthReply(QNetworkReply *reply);
void ResetLoginAttempts();
void StartSearch();
@ -150,7 +151,7 @@ class QobuzService : public InternetService {
typedef QList<EncodedParam> 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<QobuzStreamURLRequest*> stream_url_requests_;
QStringList login_errors_;
};
#endif // QOBUZSERVICE_H

View File

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

View File

@ -22,7 +22,9 @@
#include "config.h"
#include <QObject>
#include <QString>
#include <QStringList>
#include <QUrl>
#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_;
};

View File

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

View File

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

View File

@ -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<QSslError>)), this, SLOT(HandleSSLErrors(QList<QSslError>)));
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<QSslError> 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 + "<br />";
}
return error_html;
}

View File

@ -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<Param> &params_provided);
QNetworkReply *CreateGetRequest(const QString &ressource_name, const QList<Param> &params_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<QSslError> ssl_errors);
private:
SubsonicService *service_;

View File

@ -28,18 +28,20 @@
#include <QUrl>
#include <QImage>
#include <QNetworkReply>
#include <QSslError>
#include <QSslConfiguration>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QMimeDatabase>
#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_ += "<br />";
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;

View File

@ -27,19 +27,18 @@
#include <QPair>
#include <QList>
#include <QHash>
#include <QMap>
#include <QMultiMap>
#include <QQueue>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#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<QNetworkReply*> album_cover_replies_;

View File

@ -23,7 +23,6 @@
#include <memory>
#include <QObject>
#include <QStandardPaths>
#include <QByteArray>
#include <QPair>
#include <QList>
@ -32,6 +31,7 @@
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSslError>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
@ -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<QSslError>)), this, SLOT(HandlePingSSLErrors(QList<QSslError>)));
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandlePingReply(QNetworkReply*)), reply);
//qLog(Debug) << "Subsonic: Sending request" << url << query;
}
void SubsonicService::HandlePingSSLErrors(QList<QSslError> 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 + "<br />";
}
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();
}

View File

@ -30,6 +30,7 @@
#include <QPair>
#include <QList>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QNetworkReply>
#include <QTimer>
@ -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<QSslError> ssl_errors);
void HandlePingSSLErrors(QList<QSslError> ssl_errors);
void HandlePingReply(QNetworkReply *reply);
void SongsResultsReceived(const SongList &songs, const QString &error);
@ -99,7 +103,7 @@ class SubsonicService : public InternetService {
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> 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

View File

@ -65,3 +65,4 @@ UrlHandler::LoadResult SubsonicUrlHandler::StartLoading(const QUrl &url) {
return LoadResult(url, LoadResult::TrackAvailable, media_url);
}

View File

@ -54,6 +54,8 @@ class SubsonicUrlHandler : public UrlHandler {
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> EncodedParamList;
void Error(const QString &error, const QVariant &debug) {}
SubsonicService *service_;
};

View File

@ -28,6 +28,7 @@
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSslError>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
@ -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<QSslError>)), this, SLOT(HandleSSLErrors(QList<QSslError>)));
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<QSslError> 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 + "<br />";
}
return error_html;
}

View File

@ -29,6 +29,7 @@
#include <QString>
#include <QUrl>
#include <QNetworkReply>
#include <QSslError>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
@ -71,12 +72,13 @@ class TidalBaseRequest : public QObject {
typedef QList<EncodedParam> EncodedParamList;
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<Param> &params_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<QSslError> ssl_errors);
private:

View File

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

View File

@ -22,7 +22,10 @@
#include "config.h"
#include <QObject>
#include <QList>
#include <QString>
#include <QStringList>
#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);

View File

@ -21,7 +21,6 @@
#include <QObject>
#include <QByteArray>
#include <QDir>
#include <QString>
#include <QUrl>
#include <QImage>
@ -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_ += "<br />";
errors_ << error;
qLog(Error) << "Tidal:" << error;
}
FinishCheck();
return error;
if (debug.isValid()) qLog(Debug) << debug;
FinishCheck();
}

View File

@ -31,6 +31,7 @@
#include <QMultiMap>
#include <QQueue>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QNetworkReply>
#include <QJsonObject>
@ -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<QString, QString> 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<QNetworkReply*> album_cover_replies_;

View File

@ -23,7 +23,6 @@
#include <memory>
#include <QObject>
#include <QStandardPaths>
#include <QDesktopServices>
#include <QCryptographicHash>
#include <QByteArray>
@ -34,6 +33,7 @@
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSslError>
#include <QTimer>
#include <QJsonParseError>
#include <QJsonDocument>
@ -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<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
NewClosure(reply, SIGNAL(finished()), this, SLOT(AccessTokenRequestFinished(QNetworkReply*)), reply);
}
@ -328,47 +327,53 @@ void TidalService::AuthorisationUrlReceived(const QUrl &url) {
}
void TidalService::HandleLoginSSLErrors(QList<QSslError> 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<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
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 + "<br />";
}
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();
}

View File

@ -30,6 +30,7 @@
#include <QPair>
#include <QList>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QNetworkReply>
#include <QTimer>
@ -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<QSslError> ssl_errors);
void AccessTokenRequestFinished(QNetworkReply *reply);
void SendLogin();
void HandleAuthReply(QNetworkReply *reply);
@ -160,7 +163,7 @@ class TidalService : public InternetService {
typedef QList<EncodedParam> 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<TidalStreamURLRequest*> stream_url_requests_;
QStringList login_errors_;
};
#endif // TIDALSERVICE_H

View File

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

View File

@ -22,7 +22,9 @@
#include "config.h"
#include <QObject>
#include <QString>
#include <QStringList>
#include <QUrl>
#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_;
};

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More