From 7bccc218785b3765e1244926d46ac648e8762481 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sat, 9 May 2020 01:48:08 +0200 Subject: [PATCH] Add setting for cover providers --- README.md | 2 +- debian/control | 2 +- ...rawberrymusicplayer.strawberry.appdata.xml | 2 +- dist/unix/strawberry.1 | 2 +- dist/unix/strawberry.spec.in | 2 +- src/CMakeLists.txt | 21 +- src/core/application.cpp | 4 + src/core/application.h | 1 + src/core/mainwindow.cpp | 2 + src/covermanager/albumcoverfetchersearch.cpp | 20 +- src/covermanager/albumcoverfetchersearch.h | 5 +- src/covermanager/coverprovider.cpp | 4 +- src/covermanager/coverprovider.h | 25 +- src/covermanager/coverproviders.cpp | 55 ++ src/covermanager/coverproviders.h | 7 + src/covermanager/deezercoverprovider.cpp | 2 +- src/covermanager/discogscoverprovider.cpp | 2 +- src/covermanager/lastfmcoverprovider.cpp | 2 +- src/covermanager/musicbrainzcoverprovider.cpp | 2 +- src/covermanager/musixmatchcoverprovider.cpp | 2 +- src/covermanager/qobuzcoverprovider.cpp | 2 +- src/covermanager/spotifycoverprovider.cpp | 529 ++++++++++++++++++ src/covermanager/spotifycoverprovider.h | 90 +++ src/covermanager/tidalcoverprovider.cpp | 2 +- src/covermanager/tidalcoverprovider.h | 5 +- src/lyrics/lyricsproviders.cpp | 11 +- src/lyrics/lyricsproviders.h | 2 + src/settings/coverssettingspage.cpp | 263 +++++++++ src/settings/coverssettingspage.h | 72 +++ src/settings/coverssettingspage.ui | 166 ++++++ src/settings/settingsdialog.cpp | 2 + src/settings/settingsdialog.h | 1 + 32 files changed, 1276 insertions(+), 33 deletions(-) create mode 100644 src/covermanager/spotifycoverprovider.cpp create mode 100644 src/covermanager/spotifycoverprovider.h create mode 100644 src/settings/coverssettingspage.cpp create mode 100644 src/settings/coverssettingspage.h create mode 100644 src/settings/coverssettingspage.ui 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,