diff --git a/README.md b/README.md
index bfc79a20c..9a629b45c 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ You can also make a one-time payment through [paypal.me/jonaskvinge](https://pay
* Advanced audio output and device configuration for bit-perfect playback on Linux
* Edit tags on music files
* Fetch tags from MusicBrainz
- * Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/) and [Qobuz](https://www.qobuz.com/)
+ * Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
* Song lyrics from [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
* Support for multiple backends
* Audio analyzer
diff --git a/debian/control b/debian/control
index b89e1df62..b65b0619b 100644
--- a/debian/control
+++ b/debian/control
@@ -55,7 +55,7 @@ Description: Audio player and music collection organizer
- Advanced audio output and device configuration for bit-perfect playback on Linux
- Edit tags on music files
- Fetch tags from MusicBrainz
- - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal and Qobuz
+ - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Support for multiple backends
- Audio analyzer
diff --git a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml
index 4e69db65e..a5ad5e3e8 100644
--- a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml
+++ b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml
@@ -26,7 +26,7 @@
Advanced audio output and device configuration for bit-perfect playback on Linux
Edit tags on music files
Fetch tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal and Qobuz
+ Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
Support for multiple backends
Audio analyzer and equalizer
diff --git a/dist/unix/strawberry.1 b/dist/unix/strawberry.1
index 64ad1d70f..bf2c4fa3a 100644
--- a/dist/unix/strawberry.1
+++ b/dist/unix/strawberry.1
@@ -25,7 +25,7 @@ Features:
.br
- Fetch tags from MusicBrainz
.br
-- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal and Qobuz
+- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
.br
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
.br
diff --git a/dist/unix/strawberry.spec.in b/dist/unix/strawberry.spec.in
index faad7e509..fec377b80 100644
--- a/dist/unix/strawberry.spec.in
+++ b/dist/unix/strawberry.spec.in
@@ -104,7 +104,7 @@ Features:
- Advanced audio output and device configuration for bit-perfect playback on Linux
- Edit tags on music files
- Fetch tags from MusicBrainz
- - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal and Qobuz
+ - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Support for multiple backends
- Audio analyzer
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b00bc5519..f79d73af9 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -188,6 +188,7 @@ set(SOURCES
covermanager/deezercoverprovider.cpp
covermanager/qobuzcoverprovider.cpp
covermanager/musixmatchcoverprovider.cpp
+ covermanager/spotifycoverprovider.cpp
lyrics/lyricsproviders.cpp
lyrics/lyricsprovider.cpp
@@ -206,13 +207,14 @@ set(SOURCES
settings/behavioursettingspage.cpp
settings/collectionsettingspage.cpp
settings/backendsettingspage.cpp
- settings/contextsettingspage.cpp
settings/playlistsettingspage.cpp
+ settings/scrobblersettingspage.cpp
+ settings/coverssettingspage.cpp
+ settings/lyricssettingspage.cpp
settings/networkproxysettingspage.cpp
settings/appearancesettingspage.cpp
+ settings/contextsettingspage.cpp
settings/notificationssettingspage.cpp
- settings/scrobblersettingspage.cpp
- settings/lyricssettingspage.cpp
dialogs/about.cpp
dialogs/console.cpp
@@ -381,6 +383,7 @@ set(HEADERS
covermanager/deezercoverprovider.h
covermanager/qobuzcoverprovider.h
covermanager/musixmatchcoverprovider.h
+ covermanager/spotifycoverprovider.h
lyrics/lyricsproviders.h
lyrics/lyricsprovider.h
@@ -399,13 +402,14 @@ set(HEADERS
settings/behavioursettingspage.h
settings/collectionsettingspage.h
settings/backendsettingspage.h
- settings/contextsettingspage.h
settings/playlistsettingspage.h
+ settings/scrobblersettingspage.h
+ settings/coverssettingspage.h
+ settings/lyricssettingspage.h
settings/networkproxysettingspage.h
settings/appearancesettingspage.h
+ settings/contextsettingspage.h
settings/notificationssettingspage.h
- settings/scrobblersettingspage.h
- settings/lyricssettingspage.h
dialogs/about.h
dialogs/errordialog.h
@@ -498,11 +502,12 @@ set(UI
settings/backendsettingspage.ui
settings/contextsettingspage.ui
settings/playlistsettingspage.ui
+ settings/scrobblersettingspage.ui
+ settings/coverssettingspage.ui
+ settings/lyricssettingspage.ui
settings/networkproxysettingspage.ui
settings/appearancesettingspage.ui
settings/notificationssettingspage.ui
- settings/scrobblersettingspage.ui
- settings/lyricssettingspage.ui
equalizer/equalizer.ui
equalizer/equalizerslider.ui
diff --git a/src/core/application.cpp b/src/core/application.cpp
index cf358f763..46f0102e9 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -58,6 +58,7 @@
#include "covermanager/deezercoverprovider.h"
#include "covermanager/qobuzcoverprovider.h"
#include "covermanager/musixmatchcoverprovider.h"
+#include "covermanager/spotifycoverprovider.h"
#include "lyrics/lyricsproviders.h"
#include "lyrics/auddlyricsprovider.h"
@@ -123,9 +124,11 @@ class ApplicationImpl {
cover_providers->AddProvider(new DeezerCoverProvider(app, app));
cover_providers->AddProvider(new QobuzCoverProvider(app, app));
cover_providers->AddProvider(new MusixmatchCoverProvider(app, app));
+ cover_providers->AddProvider(new SpotifyCoverProvider(app, app));
#ifdef HAVE_TIDAL
cover_providers->AddProvider(new TidalCoverProvider(app, app));
#endif
+ cover_providers->ReloadSettings();
return cover_providers;
}),
album_cover_loader_([=]() {
@@ -136,6 +139,7 @@ class ApplicationImpl {
current_albumcover_loader_([=]() { return new CurrentAlbumCoverLoader(app, app); }),
lyrics_providers_([=]() {
LyricsProviders *lyrics_providers = new LyricsProviders(app);
+ // Initialize the repository of lyrics providers.
lyrics_providers->AddProvider(new AuddLyricsProvider(app));
lyrics_providers->AddProvider(new GeniusLyricsProvider(app));
lyrics_providers->AddProvider(new OVHLyricsProvider(app));
diff --git a/src/core/application.h b/src/core/application.h
index c2e8043a3..040504b6f 100644
--- a/src/core/application.h
+++ b/src/core/application.h
@@ -53,6 +53,7 @@ class DeviceManager;
class CoverProviders;
class AlbumCoverLoader;
class CurrentAlbumCoverLoader;
+class CoverProviders;
class LyricsProviders;
class AudioScrobbler;
class InternetServices;
diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp
index 6512c1a33..379116215 100644
--- a/src/core/mainwindow.cpp
+++ b/src/core/mainwindow.cpp
@@ -134,6 +134,7 @@
#include "covermanager/albumcoverchoicecontroller.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/currentalbumcoverloader.h"
+#include "covermanager/coverproviders.h"
#include "lyrics/lyricsproviders.h"
#ifndef Q_OS_WIN
# include "device/devicemanager.h"
@@ -951,6 +952,7 @@ void MainWindow::ReloadAllSettings() {
album_cover_choice_controller_->ReloadSettings();
if (cover_manager_.get()) cover_manager_->ReloadSettings();
context_view_->ReloadSettings();
+ app_->cover_providers()->ReloadSettings();
app_->lyrics_providers()->ReloadSettings();
#ifdef HAVE_SUBSONIC
subsonic_view_->ReloadSettings();
diff --git a/src/covermanager/albumcoverfetchersearch.cpp b/src/covermanager/albumcoverfetchersearch.cpp
index 3dc650a6f..c7158dc31 100644
--- a/src/covermanager/albumcoverfetchersearch.cpp
+++ b/src/covermanager/albumcoverfetchersearch.cpp
@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome
+ * Copyright 2018-2020, Jonas Kvinge
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -72,12 +73,23 @@ void AlbumCoverFetcherSearch::TerminateSearch() {
void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
- for (CoverProvider *provider : cover_providers->List()) {
+ QList cover_providers_sorted = cover_providers->List();
+ std::stable_sort(cover_providers_sorted.begin(), cover_providers_sorted.end(), ProviderCompareOrder);
- // Skip provider if it does not have fetchall set, and we are doing fetchall - "Fetch Missing Covers".
+ for (CoverProvider *provider : cover_providers_sorted) {
+
+ if (!provider->is_enabled()) continue;
+
+ // Skip any provider that requires authentication but is not authenticated.
+ if (provider->AuthenticationRequired() && !provider->IsAuthenticated()) {
+ continue;
+ }
+
+ // Skip provider if it does not have fetchall set and we are doing fetchall - "Fetch Missing Covers".
if (!provider->fetchall() && request_.fetchall) {
continue;
}
+
// If album is missing, check if we can still use this provider by searching using artist + title.
if (!provider->allow_missing_album() && request_.album.isEmpty()) {
continue;
@@ -299,6 +311,10 @@ void AlbumCoverFetcherSearch::Cancel() {
}
+bool AlbumCoverFetcherSearch::ProviderCompareOrder(CoverProvider *a, CoverProvider *b) {
+ return a->order() < b->order();
+}
+
bool AlbumCoverFetcherSearch::CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b) {
return a.score > b.score;
}
diff --git a/src/covermanager/albumcoverfetchersearch.h b/src/covermanager/albumcoverfetchersearch.h
index 711f3f209..c6d9a059c 100644
--- a/src/covermanager/albumcoverfetchersearch.h
+++ b/src/covermanager/albumcoverfetchersearch.h
@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome
+ * Copyright 2018-2020, Jonas Kvinge
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -69,13 +70,15 @@ class AlbumCoverFetcherSearch : public QObject {
void TerminateSearch();
private:
- static bool CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b);
void AllProvidersFinished();
void FetchMoreImages();
float ScoreImage(const QImage &image) const;
void SendBestImage();
+ static bool ProviderCompareOrder(CoverProvider *a, CoverProvider *b);
+ static bool CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b);
+
private:
static const int kSearchTimeoutMs;
static const int kImageLoadTimeoutMs;
diff --git a/src/covermanager/coverprovider.cpp b/src/covermanager/coverprovider.cpp
index debe03c6e..fae19440f 100644
--- a/src/covermanager/coverprovider.cpp
+++ b/src/covermanager/coverprovider.cpp
@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome
+ * Copyright 2018-2020, Jonas Kvinge
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,5 +27,4 @@
#include "core/application.h"
#include "coverprovider.h"
-CoverProvider::CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent)
- : QObject(parent), app_(app), name_(name), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}
+CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent) : QObject(parent), app_(app), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}
diff --git a/src/covermanager/coverprovider.h b/src/covermanager/coverprovider.h
index 7426d96e0..5eae85f98 100644
--- a/src/covermanager/coverprovider.h
+++ b/src/covermanager/coverprovider.h
@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome
+ * Copyright 2018-2020, Jonas Kvinge
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,6 +27,7 @@
#include
#include
#include
+#include
#include "albumcoverfetcher.h"
@@ -37,27 +39,42 @@ class CoverProvider : public QObject {
Q_OBJECT
public:
- explicit CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
+ explicit CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
// A name (very short description) of this provider, like "last.fm".
QString name() const { return name_; }
+ bool is_enabled() const { return enabled_; }
+ int order() const { return order_; }
bool quality() const { return quality_; }
bool fetchall() const { return fetchall_; }
bool allow_missing_album() const { return allow_missing_album_; }
+ void set_enabled(const bool enabled) { enabled_ = enabled; }
+ void set_order(const int order) { order_ = order; }
+
+ bool AuthenticationRequired() const { return authentication_required_; }
+ virtual bool IsAuthenticated() const { return true; }
+ virtual void Authenticate() {}
+ virtual void Deauthenticate() {}
+
// Starts searching for covers matching the given query text.
// Returns true if the query has been started, or false if an error occurred.
// The provider should remember the ID and emit it along with the result when it finishes.
virtual bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) = 0;
-
- virtual void CancelSearch(int id) { Q_UNUSED(id); }
+ virtual void CancelSearch(const int id) { Q_UNUSED(id); }
signals:
- void SearchFinished(int id, const CoverSearchResults& results);
+ void AuthenticationComplete(bool, QStringList = QStringList());
+ void AuthenticationSuccess();
+ void AuthenticationFailure(QStringList);
+ void SearchFinished(int, CoverSearchResults);
private:
Application *app_;
QString name_;
+ bool enabled_;
+ int order_;
+ bool authentication_required_;
float quality_;
bool fetchall_;
bool allow_missing_album_;
diff --git a/src/covermanager/coverproviders.cpp b/src/covermanager/coverproviders.cpp
index a4a8d29d2..a23e07f7c 100644
--- a/src/covermanager/coverproviders.cpp
+++ b/src/covermanager/coverproviders.cpp
@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome
+ * Copyright 2018-2020, Jonas Kvinge
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,13 +23,23 @@
#include
#include
+#include
+#include
+#include
+#include
#include
+#include
+#include
#include
#include "core/logging.h"
#include "coverprovider.h"
#include "coverproviders.h"
+#include "settings/coverssettingspage.h"
+
+int CoverProviders::NextOrderId = 0;
+
CoverProviders::CoverProviders(QObject *parent) : QObject(parent) {}
CoverProviders::~CoverProviders() {
@@ -39,6 +50,48 @@ CoverProviders::~CoverProviders() {
}
+void CoverProviders::ReloadSettings() {
+
+ QMap all_providers;
+ for (CoverProvider *provider : cover_providers_.keys()) {
+ if (!provider->is_enabled()) continue;
+ all_providers.insert(provider->order(), provider->name());
+ }
+
+ QSettings s;
+ s.beginGroup(CoversSettingsPage::kSettingsGroup);
+ QStringList providers_enabled = s.value("providers", QStringList() << all_providers.values()).toStringList();
+ s.endGroup();
+
+ int i = 0;
+ QList providers;
+ for (const QString &name : providers_enabled) {
+ CoverProvider *provider = ProviderByName(name);
+ if (provider) {
+ provider->set_enabled(true);
+ provider->set_order(++i);
+ providers << provider;
+ }
+ }
+
+ for (CoverProvider *provider : cover_providers_.keys()) {
+ if (!providers.contains(provider)) {
+ provider->set_enabled(false);
+ provider->set_order(++i);
+ }
+ }
+
+}
+
+CoverProvider *CoverProviders::ProviderByName(const QString &name) const {
+
+ for (CoverProvider *provider : cover_providers_.keys()) {
+ if (provider->name() == name) return provider;
+ }
+ return nullptr;
+
+}
+
void CoverProviders::AddProvider(CoverProvider *provider) {
{
@@ -47,6 +100,8 @@ void CoverProviders::AddProvider(CoverProvider *provider) {
connect(provider, SIGNAL(destroyed()), SLOT(ProviderDestroyed()));
}
+ provider->set_order(++NextOrderId);
+
qLog(Debug) << "Registered cover provider" << provider->name();
}
diff --git a/src/covermanager/coverproviders.h b/src/covermanager/coverproviders.h
index 01698e161..2edb70559 100644
--- a/src/covermanager/coverproviders.h
+++ b/src/covermanager/coverproviders.h
@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome
+ * Copyright 2018-2020, Jonas Kvinge
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,6 +43,10 @@ class CoverProviders : public QObject {
explicit CoverProviders(QObject *parent = nullptr);
~CoverProviders();
+ void ReloadSettings();
+
+ CoverProvider *ProviderByName(const QString &name) const;
+
// Lets a cover provider register itself in the repository.
void AddProvider(CoverProvider *provider);
void RemoveProvider(CoverProvider *provider);
@@ -60,6 +65,8 @@ class CoverProviders : public QObject {
private:
Q_DISABLE_COPY(CoverProviders)
+ static int NextOrderId;
+
QMap cover_providers_;
QMutex mutex_;
diff --git a/src/covermanager/deezercoverprovider.cpp b/src/covermanager/deezercoverprovider.cpp
index fcec0853e..22ba04d77 100644
--- a/src/covermanager/deezercoverprovider.cpp
+++ b/src/covermanager/deezercoverprovider.cpp
@@ -51,7 +51,7 @@
const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com";
const int DeezerCoverProvider::kLimit = 10;
-DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
+DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", true, false, 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
diff --git a/src/covermanager/discogscoverprovider.cpp b/src/covermanager/discogscoverprovider.cpp
index 6249c0c64..fd04af387 100644
--- a/src/covermanager/discogscoverprovider.cpp
+++ b/src/covermanager/discogscoverprovider.cpp
@@ -58,7 +58,7 @@ const char *DiscogsCoverProvider::kUrlSearch = "https://api.discogs.com/database
const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
-DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", 0.0, false, false, app, parent), network_(new NetworkAccessManager(this)) {}
+DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", false, false, 0.0, false, false, app, parent), network_(new NetworkAccessManager(this)) {}
DiscogsCoverProvider::~DiscogsCoverProvider() {
requests_search_.clear();
diff --git a/src/covermanager/lastfmcoverprovider.cpp b/src/covermanager/lastfmcoverprovider.cpp
index 21131d3b1..286c9410d 100644
--- a/src/covermanager/lastfmcoverprovider.cpp
+++ b/src/covermanager/lastfmcoverprovider.cpp
@@ -53,7 +53,7 @@ const char *LastFmCoverProvider::kUrl = "https://ws.audioscrobbler.com/2.0/";
const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e";
const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8";
-LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
+LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("Last.fm", true, false, 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
diff --git a/src/covermanager/musicbrainzcoverprovider.cpp b/src/covermanager/musicbrainzcoverprovider.cpp
index 723bb2dd8..29fc73554 100644
--- a/src/covermanager/musicbrainzcoverprovider.cpp
+++ b/src/covermanager/musicbrainzcoverprovider.cpp
@@ -48,7 +48,7 @@ const char *MusicbrainzCoverProvider::kReleaseSearchUrl = "https://musicbrainz.o
const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front";
const int MusicbrainzCoverProvider::kLimit = 8;
-MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", 1.5, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
+MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", true, false, 1.5, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
diff --git a/src/covermanager/musixmatchcoverprovider.cpp b/src/covermanager/musixmatchcoverprovider.cpp
index 817f9b71b..cb41cf38f 100644
--- a/src/covermanager/musixmatchcoverprovider.cpp
+++ b/src/covermanager/musixmatchcoverprovider.cpp
@@ -40,7 +40,7 @@
#include "coverprovider.h"
#include "musixmatchcoverprovider.h"
-MusixmatchCoverProvider::MusixmatchCoverProvider(Application *app, QObject *parent): CoverProvider("Musixmatch", 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
+MusixmatchCoverProvider::MusixmatchCoverProvider(Application *app, QObject *parent): CoverProvider("Musixmatch", true, false, 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
bool MusixmatchCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
diff --git a/src/covermanager/qobuzcoverprovider.cpp b/src/covermanager/qobuzcoverprovider.cpp
index 4206d92f0..23ffc6b09 100644
--- a/src/covermanager/qobuzcoverprovider.cpp
+++ b/src/covermanager/qobuzcoverprovider.cpp
@@ -50,7 +50,7 @@ const char *QobuzCoverProvider::kApiUrl = "https://www.qobuz.com/api.json/0.2";
const char *QobuzCoverProvider::kAppID = "OTQyODUyNTY3";
const int QobuzCoverProvider::kLimit = 10;
-QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : CoverProvider("Qobuz", 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
+QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : CoverProvider("Qobuz", true, false, 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
diff --git a/src/covermanager/spotifycoverprovider.cpp b/src/covermanager/spotifycoverprovider.cpp
new file mode 100644
index 000000000..e15790500
--- /dev/null
+++ b/src/covermanager/spotifycoverprovider.cpp
@@ -0,0 +1,529 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2020, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/application.h"
+#include "core/network.h"
+#include "core/logging.h"
+#include "core/song.h"
+#include "core/utilities.h"
+#include "internet/localredirectserver.h"
+#include "albumcoverfetcher.h"
+#include "coverprovider.h"
+#include "spotifycoverprovider.h"
+
+const char *SpotifyCoverProvider::kSettingsGroup = "Spotify";
+const char *SpotifyCoverProvider::kOAuthAuthorizeUrl = "https://accounts.spotify.com/authorize";
+const char *SpotifyCoverProvider::kOAuthAccessTokenUrl = "https://accounts.spotify.com/api/token";
+const char *SpotifyCoverProvider::kOAuthRedirectUrl = "http://localhost:63111/";
+const char *SpotifyCoverProvider::kClientIDB64 = "ZTZjY2Y2OTQ5NzY1NGE3NThjOTAxNWViYzdiMWQzMTc=";
+const char *SpotifyCoverProvider::kClientSecretB64 = "N2ZlMDMxODk1NTBlNDE3ZGI1ZWQ1MzE3ZGZlZmU2MTE=";
+const char *SpotifyCoverProvider::kApiUrl = "https://api.spotify.com/v1";
+const int SpotifyCoverProvider::kLimit = 10;
+
+SpotifyCoverProvider::SpotifyCoverProvider(Application *app, QObject *parent) : CoverProvider("Spotify", true, true, 2.5, true, true, app, parent), network_(new NetworkAccessManager(this)), server_(nullptr) {
+
+ QSettings s;
+ s.beginGroup(kSettingsGroup);
+ if (s.contains("access_token")) {
+ access_token_ = s.value("access_token").toString();
+ }
+ s.endGroup();
+
+}
+
+void SpotifyCoverProvider::Authenticate() {
+
+ QUrl redirect_url(kOAuthRedirectUrl);
+
+ if (!server_) {
+ server_ = new LocalRedirectServer(this);
+ server_->set_https(false);
+ int port = redirect_url.port();
+ int port_max = port + 10;
+ bool success = false;
+ forever {
+ server_->set_port(port);
+ if (server_->Listen()) { success = true; break; }
+ ++port;
+ if (port > port_max) break;
+ }
+ if (!success) {
+ AuthError(server_->error());
+ server_->deleteLater();
+ server_ = nullptr;
+ return;
+ }
+ connect(server_, SIGNAL(Finished()), this, SLOT(RedirectArrived()));
+ }
+
+ code_verifier_ = Utilities::CryptographicRandomString(44);
+ code_challenge_ = QString(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
+ if (code_challenge_.lastIndexOf(QChar('=')) == code_challenge_.length() - 1) {
+ code_challenge_.chop(1);
+ }
+
+ const ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64))
+ << Param("response_type", "code")
+ << Param("redirect_uri", redirect_url.toString())
+ << Param("state", code_challenge_);
+
+
+ QUrlQuery url_query;
+ for (const Param ¶m : params) {
+ url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ }
+
+ QUrl url(kOAuthAuthorizeUrl);
+ url.setQuery(url_query);
+
+ const bool result = QDesktopServices::openUrl(url);
+ if (!result) {
+ QMessageBox messagebox(QMessageBox::Information, tr("Spotify Authentication"), tr("Please open this URL in your browser") + QString(":
%1").arg(url.toString()), QMessageBox::Ok);
+ messagebox.setTextFormat(Qt::RichText);
+ messagebox.exec();
+ }
+
+}
+
+void SpotifyCoverProvider::Deauthenticate() {
+
+ access_token_.clear();
+
+ QSettings s;
+ s.beginGroup(kSettingsGroup);
+ s.remove("access_token");
+ s.endGroup();
+
+}
+
+void SpotifyCoverProvider::RedirectArrived() {
+
+ if (!server_) return;
+
+ if (server_->error().isEmpty()) {
+ QUrl url = server_->request_url();
+ if (url.isValid()) {
+ QUrlQuery url_query(url);
+ if (url_query.hasQueryItem("error")) {
+ AuthError(QUrlQuery(url).queryItemValue("error"));
+ }
+ else if (url_query.hasQueryItem("code")) {
+ QUrl redirect_url(kOAuthRedirectUrl);
+ redirect_url.setPort(server_->url().port());
+ RequestAccessToken(url, redirect_url);
+ }
+ else {
+ AuthError(tr("Redirect missing token code!"));
+ }
+ }
+ else {
+ AuthError(tr("Received invalid reply from web browser."));
+ }
+ }
+ else {
+ AuthError(server_->error());
+ }
+
+ server_->close();
+ server_->deleteLater();
+ server_ = nullptr;
+
+}
+
+void SpotifyCoverProvider::RequestAccessToken(const QUrl &url, const QUrl &redirect_url) {
+
+ qLog(Debug) << "Spotify: Authorization URL Received" << url;
+
+ QUrlQuery url_query(url);
+
+ if (url.hasQuery() && url_query.hasQueryItem("code") && url_query.hasQueryItem("state")) {
+
+ QString code = url_query.queryItemValue("code");
+ QString state = url_query.queryItemValue("state");
+
+ const ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64))
+ << Param("client_secret", QByteArray::fromBase64(kClientSecretB64))
+ << Param("grant_type", "authorization_code")
+ << Param("code", code)
+ << Param("redirect_uri", redirect_url.toString());
+
+ QUrlQuery new_url_query;
+ for (const Param ¶m : params) {
+ new_url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ }
+
+ QUrl new_url(kOAuthAccessTokenUrl);
+ QNetworkRequest req = QNetworkRequest(new_url);
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ QString auth_header_data = QByteArray::fromBase64(kClientIDB64) + QString(":") + QByteArray::fromBase64(kClientSecretB64);
+ req.setRawHeader("Authorization", "Basic " + auth_header_data.toUtf8().toBase64());
+
+ QByteArray query = new_url_query.toString(QUrl::FullyEncoded).toUtf8();
+
+ QNetworkReply *reply = network_->post(req, query);
+ connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList)));
+ connect(reply, &QNetworkReply::finished, [=] { AccessTokenRequestFinished(reply); });
+
+ }
+
+ else {
+ AuthError(tr("Redirect from Spotify is missing query items code or state."));
+ return;
+ }
+
+}
+
+void SpotifyCoverProvider::HandleLoginSSLErrors(QList ssl_errors) {
+
+ for (const QSslError &ssl_error : ssl_errors) {
+ login_errors_ += ssl_error.errorString();
+ }
+
+}
+
+void SpotifyCoverProvider::AccessTokenRequestFinished(QNetworkReply *reply) {
+
+ reply->deleteLater();
+
+ 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.
+ AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ return;
+ }
+ else {
+ // See if there is Json data containing "error" and "error_description" then use that instead.
+ QByteArray data = reply->readAll();
+ 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.isEmpty() && json_obj.contains("error") && json_obj.contains("error_description")) {
+ QString error = json_obj["error"].toString();
+ QString error_description = json_obj["error_description"].toString();
+ login_errors_ << QString("Authentication failure: %1 (%2)").arg(error).arg(error_description);
+ }
+ }
+ 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());
+ }
+ }
+ AuthError();
+ return;
+ }
+ }
+
+ QByteArray data = reply->readAll();
+ qLog(Debug) << data;
+
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ AuthError("Authentication reply from server missing Json data.");
+ return;
+ }
+
+ if (json_doc.isEmpty()) {
+ AuthError("Authentication reply from server has empty Json document.");
+ return;
+ }
+
+ if (!json_doc.isObject()) {
+ AuthError("Authentication reply from server has Json document that is not an object.", json_doc);
+ return;
+ }
+
+ QJsonObject json_obj = json_doc.object();
+ if (json_obj.isEmpty()) {
+ AuthError("Authentication reply from server has empty Json object.", json_doc);
+ return;
+ }
+
+ if (!json_obj.contains("access_token")) {
+ AuthError("Authentication reply from server is missing access token.", json_obj);
+ return;
+ }
+
+ access_token_ = json_obj["access_token"].toString();
+
+ QSettings s;
+ s.beginGroup(kSettingsGroup);
+ s.setValue("access_token", access_token_);
+ s.endGroup();
+
+ qLog(Debug) << "Spotify: Authentication was successful, got access token" << access_token_;
+
+ emit AuthenticationComplete(true);
+ emit AuthenticationSuccess();
+
+}
+
+bool SpotifyCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
+
+ if (access_token_.isEmpty()) return false;
+
+ QString type;
+ QString query;
+ QString extract;
+ if (album.isEmpty()) {
+ type = "track";
+ query = artist + " " + title;
+ extract = "tracks";
+ }
+ else {
+ type = "album";
+ query = artist + " " + album;
+ extract = "albums";
+ }
+
+ ParamList params = ParamList() << Param("q", query)
+ << Param("type", type)
+ << Param("limit", QString::number(kLimit));
+
+ QUrlQuery url_query;
+ for (const Param ¶m : params) {
+ url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ }
+
+ QUrl url(kApiUrl + QString("/search"));
+ url.setQuery(url_query);
+ QNetworkRequest req(url);
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ req.setRawHeader("Authorization", "Bearer " + access_token_.toUtf8());
+
+ QNetworkReply *reply = network_->get(req);
+ connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, extract); });
+
+ return true;
+
+}
+
+void SpotifyCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
+
+QByteArray SpotifyCoverProvider::GetReplyData(QNetworkReply *reply) {
+
+ QByteArray data;
+
+ if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
+ data = reply->readAll();
+ }
+ else {
+ if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
+ // This is a network error, there is nothing more to do.
+ Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ }
+ else {
+ data = reply->readAll();
+ QJsonParseError parse_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
+ QString error;
+ if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
+ QJsonObject json_obj = json_doc.object();
+ if (!json_obj.isEmpty() && json_obj.contains("error") && json_obj["error"].isObject()) {
+ QJsonObject obj_error = json_obj["error"].toObject();
+ if (obj_error.contains("status") && obj_error.contains("message")) {
+ int status = obj_error["status"].toInt();
+ QString message = obj_error["message"].toString();
+ error = QString("%1 (%2)").arg(message).arg(status);
+ }
+ }
+ }
+ if (error.isEmpty()) {
+ if (reply->error() != QNetworkReply::NoError) {
+ if (reply->error() == 204) access_token_.clear();
+ 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();
+ }
+
+ return data;
+
+}
+
+QJsonObject SpotifyCoverProvider::ExtractJsonObj(const QByteArray &data) {
+
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ Error(QString("Failed to parse json data: %1").arg(json_error.errorString()));
+ return QJsonObject();
+ }
+
+ if (json_doc.isEmpty()) {
+ Error("Received empty Json document.", data);
+ return QJsonObject();
+ }
+
+ if (!json_doc.isObject()) {
+ Error("Json document is not an object.", json_doc);
+ return QJsonObject();
+ }
+
+ QJsonObject json_obj = json_doc.object();
+ if (json_obj.isEmpty()) {
+ Error("Received empty Json object.", json_doc);
+ return QJsonObject();
+ }
+
+ return json_obj;
+
+}
+
+void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, const QString &extract) {
+
+ reply->deleteLater();
+
+ QByteArray data = GetReplyData(reply);
+ if (data.isEmpty()) {
+ emit SearchFinished(id, CoverSearchResults());
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ emit SearchFinished(id, CoverSearchResults());
+ return;
+ }
+
+ if (!json_obj.contains(extract) || !json_obj[extract].isObject()) {
+ Error(QString("Json object is missing %1 object.").arg(extract), json_obj);
+ emit SearchFinished(id, CoverSearchResults());
+ return;
+ }
+ json_obj = json_obj[extract].toObject();
+
+ if (!json_obj.contains("items") || !json_obj["items"].isArray()) {
+ Error(QString("%1 object is missing items array.").arg(extract), json_obj);
+ emit SearchFinished(id, CoverSearchResults());
+ return;
+ }
+
+ QJsonArray array_items = json_obj["items"].toArray();
+ if (array_items.isEmpty()) {
+ emit SearchFinished(id, CoverSearchResults());
+ return;
+ }
+
+ CoverSearchResults results;
+ for (const QJsonValue &value_item : array_items) {
+
+ if (!value_item.isObject()) {
+ continue;
+ }
+ QJsonObject obj_item = value_item.toObject();
+
+ QJsonObject obj_album = obj_item;
+ if (obj_item.contains("album") && obj_item["album"].isObject()) {
+ obj_album = obj_item["album"].toObject();
+ }
+
+ if (!obj_album.contains("artists") || !obj_album.contains("name") || !obj_album.contains("images") || !obj_album["artists"].isArray() || !obj_album["images"].isArray()) {
+ continue;
+ }
+ QJsonArray array_artists = obj_album["artists"].toArray();
+ QJsonArray array_images = obj_album["images"].toArray();
+ QString album = obj_album["name"].toString();
+
+ QStringList artists;
+ for (const QJsonValue &value_artist : array_artists) {
+ if (!value_artist.isObject()) continue;
+ QJsonObject obj_artist = value_artist.toObject();
+ if (!obj_artist.contains("name")) continue;
+ artists << obj_artist["name"].toString();
+ }
+
+ for (const QJsonValue &value_image : array_images) {
+ if (!value_image.isObject()) continue;
+ QJsonObject obj_image = value_image.toObject();
+ if (!obj_image.contains("url") || !obj_image.contains("width") || !obj_image.contains("height")) continue;
+ int width = obj_image["width"].toInt();
+ int height = obj_image["height"].toInt();
+ if (width < 300 || height < 300) continue;
+ QUrl url(obj_image["url"].toString());
+ CoverSearchResult result;
+ result.album = album;
+ result.image_url = url;
+ if (!artists.isEmpty()) result.artist = artists.first();
+ results << result;
+ }
+
+ }
+ emit SearchFinished(id, results);
+
+}
+
+void SpotifyCoverProvider::AuthError(const QString &error, const QVariant &debug) {
+
+ if (!error.isEmpty()) login_errors_ << error;
+
+ for (const QString &e : login_errors_) Error(e);
+ if (debug.isValid()) qLog(Debug) << debug;
+
+ emit AuthenticationFailure(login_errors_);
+ emit AuthenticationComplete(false, login_errors_);
+
+ login_errors_.clear();
+
+}
+
+void SpotifyCoverProvider::Error(const QString &error, const QVariant &debug) {
+
+ qLog(Error) << "Spotify:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+}
diff --git a/src/covermanager/spotifycoverprovider.h b/src/covermanager/spotifycoverprovider.h
new file mode 100644
index 000000000..3ae3bab12
--- /dev/null
+++ b/src/covermanager/spotifycoverprovider.h
@@ -0,0 +1,90 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef SPOTIFYCOVERPROVIDER_H
+#define SPOTIFYCOVERPROVIDER_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "coverprovider.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+class Application;
+class LocalRedirectServer;
+
+class SpotifyCoverProvider : public CoverProvider {
+ Q_OBJECT
+
+ public:
+ explicit SpotifyCoverProvider(Application *app, QObject *parent = nullptr);
+ bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
+ void CancelSearch(const int id);
+
+ void Authenticate();
+ void Deauthenticate();
+ bool IsAuthenticated() const { return !access_token_.isEmpty(); }
+
+ private slots:
+ void HandleLoginSSLErrors(QList ssl_errors);
+ void RedirectArrived();
+ void AccessTokenRequestFinished(QNetworkReply *reply);
+ void HandleSearchReply(QNetworkReply *reply, const int id, const QString &extract);
+
+ private:
+ void RequestAccessToken(const QUrl &url, const QUrl &redirect_url);
+ QByteArray GetReplyData(QNetworkReply *reply);
+ QJsonObject ExtractJsonObj(const QByteArray &data);
+ void AuthError(const QString &error = QString(), const QVariant &debug = QVariant());
+ void Error(const QString &error, const QVariant &debug = QVariant());
+
+ private:
+ typedef QPair Param;
+ typedef QList ParamList;
+
+ static const char *kSettingsGroup;
+ static const char *kClientIDB64;
+ static const char *kClientSecretB64;
+ static const char *kOAuthAuthorizeUrl;
+ static const char *kOAuthAccessTokenUrl;
+ static const char *kOAuthRedirectUrl;
+ static const char *kApiUrl;
+ static const int kLimit;
+
+ QNetworkAccessManager *network_;
+ LocalRedirectServer *server_;
+ QStringList login_errors_;
+ QString code_verifier_;
+ QString code_challenge_;
+ QString access_token_;
+
+};
+
+#endif // SPOTIFYCOVERPROVIDER_H
diff --git a/src/covermanager/tidalcoverprovider.cpp b/src/covermanager/tidalcoverprovider.cpp
index 2d21ca4cf..ccfd7c82f 100644
--- a/src/covermanager/tidalcoverprovider.cpp
+++ b/src/covermanager/tidalcoverprovider.cpp
@@ -53,7 +53,7 @@ const char *TidalCoverProvider::kResourcesUrl = "https://resources.tidal.com";
const int TidalCoverProvider::kLimit = 10;
TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) :
- CoverProvider("Tidal", 2.5, true, true, app, parent),
+ CoverProvider("Tidal", true, true, 2.5, true, true, app, parent),
service_(app->internet_services()->Service()),
network_(new NetworkAccessManager(this)) {
diff --git a/src/covermanager/tidalcoverprovider.h b/src/covermanager/tidalcoverprovider.h
index 55e502508..2b691b397 100644
--- a/src/covermanager/tidalcoverprovider.h
+++ b/src/covermanager/tidalcoverprovider.h
@@ -33,11 +33,11 @@
#include
#include "coverprovider.h"
+#include "tidal/tidalservice.h"
class QNetworkAccessManager;
class QNetworkReply;
class Application;
-class TidalService;
class TidalCoverProvider : public CoverProvider {
Q_OBJECT
@@ -47,6 +47,9 @@ class TidalCoverProvider : public CoverProvider {
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
void CancelSearch(const int id);
+ bool IsAuthenticated() const { return service_ && service_->authenticated(); }
+ void Deauthenticate() { if (service_) service_->Logout(); }
+
private slots:
void HandleSearchReply(QNetworkReply *reply, const int id);
diff --git a/src/lyrics/lyricsproviders.cpp b/src/lyrics/lyricsproviders.cpp
index b9aaecba2..2e560cc76 100644
--- a/src/lyrics/lyricsproviders.cpp
+++ b/src/lyrics/lyricsproviders.cpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -35,6 +36,8 @@
#include "settings/lyricssettingspage.h"
+int LyricsProviders::NextOrderId = 0;
+
LyricsProviders::LyricsProviders(QObject *parent) : QObject(parent) {}
LyricsProviders::~LyricsProviders() {
@@ -47,15 +50,15 @@ LyricsProviders::~LyricsProviders() {
void LyricsProviders::ReloadSettings() {
- QStringList all_providers;
+ QMap all_providers;
for (LyricsProvider *provider : lyrics_providers_.keys()) {
if (!provider->is_enabled()) continue;
- all_providers << provider->name();
+ all_providers.insert(provider->order(), provider->name());
}
QSettings s;
s.beginGroup(LyricsSettingsPage::kSettingsGroup);
- QStringList providers_enabled = s.value("providers", all_providers).toStringList();
+ QStringList providers_enabled = s.value("providers", QStringList() << all_providers.values()).toStringList();
s.endGroup();
int i = 0;
@@ -95,6 +98,8 @@ void LyricsProviders::AddProvider(LyricsProvider *provider) {
connect(provider, SIGNAL(destroyed()), SLOT(ProviderDestroyed()));
}
+ provider->set_order(++NextOrderId);
+
qLog(Debug) << "Registered lyrics provider" << provider->name();
}
diff --git a/src/lyrics/lyricsproviders.h b/src/lyrics/lyricsproviders.h
index de12ce3cc..39eff5bf3 100644
--- a/src/lyrics/lyricsproviders.h
+++ b/src/lyrics/lyricsproviders.h
@@ -54,6 +54,8 @@ class LyricsProviders : public QObject {
private:
Q_DISABLE_COPY(LyricsProviders)
+ static int NextOrderId;
+
QMap lyrics_providers_;
QList ordered_providers_;
QMutex mutex_;
diff --git a/src/settings/coverssettingspage.cpp b/src/settings/coverssettingspage.cpp
new file mode 100644
index 000000000..9284b607d
--- /dev/null
+++ b/src/settings/coverssettingspage.cpp
@@ -0,0 +1,263 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2020, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "settingsdialog.h"
+#include "coverssettingspage.h"
+#include "ui_coverssettingspage.h"
+#include "core/application.h"
+#include "core/iconloader.h"
+#include "core/logging.h"
+#include "covermanager/coverproviders.h"
+#include "covermanager/coverprovider.h"
+#include "widgets/loginstatewidget.h"
+
+const char *CoversSettingsPage::kSettingsGroup = "Covers";
+
+CoversSettingsPage::CoversSettingsPage(SettingsDialog *parent) : SettingsPage(parent), ui_(new Ui::CoversSettingsPage), provider_selected_(false) {
+
+ ui_->setupUi(this);
+ setWindowIcon(IconLoader::Load("cdcase"));
+
+ connect(ui_->providers_up, SIGNAL(clicked()), SLOT(ProvidersMoveUp()));
+ connect(ui_->providers_down, SIGNAL(clicked()), SLOT(ProvidersMoveDown()));
+ connect(ui_->providers, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)), SLOT(CurrentItemChanged(QListWidgetItem*, QListWidgetItem*)));
+ connect(ui_->providers, SIGNAL(itemSelectionChanged()), SLOT(ItemSelectionChanged()));
+ connect(ui_->providers, SIGNAL(itemChanged(QListWidgetItem*)), SLOT(ItemChanged(QListWidgetItem*)));
+
+ connect(ui_->button_authenticate, SIGNAL(clicked()), SLOT(AuthenticateClicked()));
+ connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
+
+ NoProviderSelected();
+ DisableAuthentication();
+
+ dialog()->installEventFilter(this);
+
+}
+
+CoversSettingsPage::~CoversSettingsPage() { delete ui_; }
+
+void CoversSettingsPage::Load() {
+
+ ui_->providers->clear();
+
+ QList cover_providers_sorted = dialog()->app()->cover_providers()->List();
+ std::stable_sort(cover_providers_sorted.begin(), cover_providers_sorted.end(), ProviderCompareOrder);
+
+ for (CoverProvider *provider : cover_providers_sorted) {
+ QListWidgetItem *item = new QListWidgetItem(ui_->providers);
+ item->setText(provider->name());
+ item->setCheckState(provider->is_enabled() ? Qt::Checked : Qt::Unchecked);
+ item->setForeground(provider->is_enabled() ? palette().color(QPalette::Active, QPalette::Text) : palette().color(QPalette::Disabled, QPalette::Text));
+ }
+
+}
+
+void CoversSettingsPage::Save() {
+
+ QStringList providers;
+ for (int i = 0 ; i < ui_->providers->count() ; ++i) {
+ const QListWidgetItem *item = ui_->providers->item(i);
+ if (item->checkState() == Qt::Checked) providers << item->text();
+ }
+
+ qLog(Debug) << providers;
+
+ QSettings s;
+ s.beginGroup(kSettingsGroup);
+ s.setValue("providers", providers);
+ s.endGroup();
+
+}
+
+void CoversSettingsPage::CurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous) {
+
+ if (item_previous) {
+ CoverProvider *provider = dialog()->app()->cover_providers()->ProviderByName(item_previous->text());
+ if (provider && provider->AuthenticationRequired()) DisconnectAuthentication(provider);
+ }
+
+ if (item_current) {
+ const int row = ui_->providers->row(item_current);
+ ui_->providers_up->setEnabled(row != 0);
+ ui_->providers_down->setEnabled(row != ui_->providers->count() - 1);
+ CoverProvider *provider = dialog()->app()->cover_providers()->ProviderByName(item_current->text());
+ if (provider && provider->AuthenticationRequired()) {
+ if (provider->name() == "Tidal" && !provider->IsAuthenticated()) {
+ DisableAuthentication();
+ ui_->label_auth_info->setText(tr("Use Tidal settings to authenticate."));
+ }
+ else {
+ ui_->login_state->SetLoggedIn(provider->IsAuthenticated() ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut);
+ ui_->button_authenticate->setEnabled(true);
+ ui_->button_authenticate->show();
+ ui_->login_state->show();
+ ui_->label_auth_info->setText(tr("%1 needs authentication.").arg(provider->name()));
+ }
+ }
+ else {
+ DisableAuthentication();
+ ui_->label_auth_info->setText(tr("%1 does not need authentication.").arg(provider->name()));
+ }
+ provider_selected_ = true;
+ }
+ else {
+ DisableAuthentication();
+ NoProviderSelected();
+ ui_->providers_up->setEnabled(false);
+ ui_->providers_down->setEnabled(false);
+ provider_selected_ = false;
+ }
+
+}
+
+void CoversSettingsPage::ItemSelectionChanged() {
+
+ if (ui_->providers->selectedItems().count() == 0) {
+ DisableAuthentication();
+ NoProviderSelected();
+ ui_->providers_up->setEnabled(false);
+ ui_->providers_down->setEnabled(false);
+ provider_selected_ = false;
+ }
+ else {
+ if (ui_->providers->currentItem() && !provider_selected_) {
+ CurrentItemChanged(ui_->providers->currentItem(), nullptr);
+ }
+ }
+
+}
+
+void CoversSettingsPage::ProvidersMoveUp() { ProvidersMove(-1); }
+
+void CoversSettingsPage::ProvidersMoveDown() { ProvidersMove(+1); }
+
+void CoversSettingsPage::ProvidersMove(const int d) {
+
+ const int row = ui_->providers->currentRow();
+ QListWidgetItem *item = ui_->providers->takeItem(row);
+ ui_->providers->insertItem(row + d, item);
+ ui_->providers->setCurrentRow(row + d);
+
+}
+
+void CoversSettingsPage::ItemChanged(QListWidgetItem *item) {
+
+ item->setForeground((item->checkState() == Qt::Checked) ? palette().color(QPalette::Active, QPalette::Text) : palette().color(QPalette::Disabled, QPalette::Text));
+
+}
+
+void CoversSettingsPage::NoProviderSelected() {
+ ui_->label_auth_info->setText(tr("No provider selected."));
+}
+
+void CoversSettingsPage::DisableAuthentication() {
+
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
+ ui_->button_authenticate->setEnabled(false);
+ ui_->login_state->hide();
+ ui_->button_authenticate->hide();
+
+}
+
+void CoversSettingsPage::DisconnectAuthentication(CoverProvider *provider) {
+
+ disconnect(provider, SIGNAL(AuthenticationFailure(QStringList)), this, SLOT(AuthenticationFailure(QStringList)));
+ disconnect(provider, SIGNAL(AuthenticationSuccess()), this, SLOT(AuthenticationSuccess()));
+
+}
+
+void CoversSettingsPage::AuthenticateClicked() {
+
+ if (!ui_->providers->currentItem()) return;
+ CoverProvider *provider = dialog()->app()->cover_providers()->ProviderByName(ui_->providers->currentItem()->text());
+ if (!provider) return;
+ ui_->button_authenticate->setEnabled(false);
+ connect(provider, SIGNAL(AuthenticationFailure(QStringList)), this, SLOT(AuthenticationFailure(QStringList)));
+ connect(provider, SIGNAL(AuthenticationSuccess()), this, SLOT(AuthenticationSuccess()));
+ provider->Authenticate();
+
+}
+
+void CoversSettingsPage::LogoutClicked() {
+
+ if (!ui_->providers->currentItem()) return;
+ CoverProvider *provider = dialog()->app()->cover_providers()->ProviderByName(ui_->providers->currentItem()->text());
+ if (!provider) return;
+ provider->Deauthenticate();
+
+ if (provider->name() == "Tidal") {
+ DisableAuthentication();
+ ui_->label_auth_info->setText(tr("Use Tidal settings to authenticate."));
+ }
+ else {
+ ui_->button_authenticate->setEnabled(true);
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
+ }
+
+}
+
+void CoversSettingsPage::AuthenticationSuccess() {
+
+ CoverProvider *provider = qobject_cast(sender());
+ if (!provider) return;
+ DisconnectAuthentication(provider);
+
+ if (!this->isVisible() || !ui_->providers->currentItem() || ui_->providers->currentItem()->text() != provider->name()) return;
+
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
+ ui_->button_authenticate->setEnabled(true);
+
+}
+
+void CoversSettingsPage::AuthenticationFailure(const QStringList &errors) {
+
+ CoverProvider *provider = qobject_cast(sender());
+ if (!provider) return;
+ DisconnectAuthentication(provider);
+
+ if (!this->isVisible() || !ui_->providers->currentItem() || ui_->providers->currentItem()->text() != provider->name()) return;
+
+ QMessageBox::warning(this, tr("Authentication failed"), errors.join("\n"));
+
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
+ ui_->button_authenticate->setEnabled(true);
+
+}
+
+bool CoversSettingsPage::ProviderCompareOrder(CoverProvider *a, CoverProvider *b) {
+ return a->order() < b->order();
+}
diff --git a/src/settings/coverssettingspage.h b/src/settings/coverssettingspage.h
new file mode 100644
index 000000000..0d7339e08
--- /dev/null
+++ b/src/settings/coverssettingspage.h
@@ -0,0 +1,72 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2020, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef COVERSSETTINGSPAGE_H
+#define COVERSSETTINGSPAGE_H
+
+#include "config.h"
+
+#include
+#include
+#include
+
+#include "settings/settingspage.h"
+
+class QListWidgetItem;
+
+class CoverProvider;
+class SettingsDialog;
+class Ui_CoversSettingsPage;
+
+class CoversSettingsPage : public SettingsPage {
+ Q_OBJECT
+
+ public:
+ explicit CoversSettingsPage(SettingsDialog *parent = nullptr);
+ ~CoversSettingsPage();
+
+ static const char *kSettingsGroup;
+
+ void Load();
+ void Save();
+
+ private:
+ void NoProviderSelected();
+ void ProvidersMove(const int d);
+ void DisableAuthentication();
+ void DisconnectAuthentication(CoverProvider *provider);
+ static bool ProviderCompareOrder(CoverProvider *a, CoverProvider *b);
+
+ private slots:
+ void CurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous);
+ void ItemSelectionChanged();
+ void ItemChanged(QListWidgetItem *item);
+ void ProvidersMoveUp();
+ void ProvidersMoveDown();
+ void AuthenticateClicked();
+ void LogoutClicked();
+ void AuthenticationSuccess();
+ void AuthenticationFailure(const QStringList &errors);
+
+ private:
+ Ui_CoversSettingsPage *ui_;
+ bool provider_selected_;
+};
+
+#endif // COVERSSETTINGSPAGE_H
diff --git a/src/settings/coverssettingspage.ui b/src/settings/coverssettingspage.ui
new file mode 100644
index 000000000..c6b84f4a7
--- /dev/null
+++ b/src/settings/coverssettingspage.ui
@@ -0,0 +1,166 @@
+
+
+ CoversSettingsPage
+
+
+
+ 0
+ 0
+ 460
+ 600
+
+
+
+ Covers
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Cover providers
+
+
+
-
+
+
+ Choose the providers you want to use when searching for covers.
+
+
+ true
+
+
+
+ -
+
+
-
+
+
+ -
+
+
-
+
+
+ false
+
+
+ Move up
+
+
+
+ -
+
+
+ false
+
+
+ Move down
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Authentication
+
+
+
-
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ -
+
+
-
+
+
+ Authenticate
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 30
+
+
+
+
+
+
+
+
+ LoginStateWidget
+ QWidget
+ widgets/loginstatewidget.h
+ 1
+
+
+
+
+
+
+
+
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp
index 328e2c948..2f8e4bff4 100644
--- a/src/settings/settingsdialog.cpp
+++ b/src/settings/settingsdialog.cpp
@@ -61,6 +61,7 @@
#include "backendsettingspage.h"
#include "playlistsettingspage.h"
#include "scrobblersettingspage.h"
+#include "coverssettingspage.h"
#include "lyricssettingspage.h"
#include "transcodersettingspage.h"
#include "networkproxysettingspage.h"
@@ -131,6 +132,7 @@ SettingsDialog::SettingsDialog(Application *app, QMainWindow *mainwindow, QWidge
AddPage(Page_Backend, new BackendSettingsPage(this), general);
AddPage(Page_Playlist, new PlaylistSettingsPage(this), general);
AddPage(Page_Scrobbler, new ScrobblerSettingsPage(this), general);
+ AddPage(Page_Covers, new CoversSettingsPage(this), general);
AddPage(Page_Lyrics, new LyricsSettingsPage(this), general);
#ifdef HAVE_GSTREAMER
AddPage(Page_Transcoding, new TranscoderSettingsPage(this), general);
diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h
index ea985c49d..f033ae499 100644
--- a/src/settings/settingsdialog.h
+++ b/src/settings/settingsdialog.h
@@ -79,6 +79,7 @@ class SettingsDialog : public QDialog {
Page_Playback,
Page_Playlist,
Page_Scrobbler,
+ Page_Covers,
Page_Lyrics,
Page_Transcoding,
Page_Proxy,