From 2d087dfe15c5fa70ab79fa16cc947c3f5053909d Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sat, 15 Dec 2018 00:43:50 +0100 Subject: [PATCH] Add new last.fm album cover provider --- CMakeLists.txt | 12 - Dockerfile | 2 +- dist/debian/control | 3 +- dist/fedora/strawberry.spec.in | 1 - dist/opensuse/strawberry.spec.in | 1 - dist/pacman/PKGBUILD.in | 2 - dist/windows/strawberry-debug-x64.nsi.in | 2 - dist/windows/strawberry-debug-x86.nsi.in | 2 - dist/windows/strawberry-x64.nsi.in | 2 - dist/windows/strawberry-x86.nsi.in | 2 - src/CMakeLists.txt | 19 +- src/config.h.in | 2 - src/core/application.cpp | 7 +- src/core/application.h | 1 + src/covermanager/albumcoverfetchersearch.cpp | 2 +- src/covermanager/lastfmcompat.cpp | 126 --------- src/covermanager/lastfmcompat.h | 60 ----- src/covermanager/lastfmcoverprovider.cpp | 265 +++++++++++++++++-- src/covermanager/lastfmcoverprovider.h | 26 +- 19 files changed, 266 insertions(+), 271 deletions(-) delete mode 100644 src/covermanager/lastfmcompat.cpp delete mode 100644 src/covermanager/lastfmcompat.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 03075686..0cc12b0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,14 +188,6 @@ else() set(HAVE_TAGLIB_DSFFILE ON) endif() -# LASTFM -find_library(LASTFM5_LIBRARIES lastfm5) -find_path(LASTFM5_INCLUDE_DIRS lastfm5/ws.h) -find_path(LASTFM51_INCLUDE_DIRS lastfm5/Track.h) -if(LASTFM5_INCLUDE_DIRS AND LASTFM51_INCLUDE_DIRS) - set(HAVE_LIBLASTFM1 ON) -endif() - # Use system QtSingleApplication only if explicitly enabled. option(USE_SYSTEM_QTSINGLEAPPLICATION "Use system QtSingleApplication library" OFF) if(USE_SYSTEM_QTSINGLEAPPLICATION) @@ -296,10 +288,6 @@ else () ) endif() -optional_component(LIBLASTFM ON "Last.fm album cover provider" - DEPENDS "liblastfm" LASTFM5_LIBRARIES LASTFM5_INCLUDE_DIRS -) - optional_component(CHROMAPRINT ON "Chromaprint support / Tag fetching from Musicbrainz" DEPENDS "chromaprint" CHROMAPRINT_FOUND ) diff --git a/Dockerfile b/Dockerfile index 8b3af3e7..ac29b781 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ run zypper --non-interactive --gpg-auto-import-keys install \ boost-devel protobuf-devel sqlite3-devel taglib-devel \ gstreamer-devel gstreamer-plugins-base-devel libxine-devel vlc-devel \ libQt5Core-devel libQt5Gui-devel libQt5Widgets-devel libQt5Concurrent-devel libQt5Network-devel libQt5Sql-devel \ - libqt5-qtx11extras-devel libQt5Gui-private-headers-devel libqt5-qtbase-common-devel liblastfm-qt5-devel \ + libqt5-qtx11extras-devel libQt5Gui-private-headers-devel libqt5-qtbase-common-devel \ libcdio-devel libgpod-devel libplist-devel libmtp-devel libusbmuxd-devel libchromaprint-devel run mkdir -p /usr/src/app diff --git a/dist/debian/control b/dist/debian/control index cc6b133a..41eab663 100644 --- a/dist/debian/control +++ b/dist/debian/control @@ -32,8 +32,7 @@ Build-Depends: debhelper (>= 7), libmtp-dev, libplist-dev, libusbmuxd-dev, - libchromaprint-dev, - liblastfm5-dev + libchromaprint-dev Standards-Version: 3.9.8 Package: strawberry diff --git a/dist/fedora/strawberry.spec.in b/dist/fedora/strawberry.spec.in index 21f86364..60bc8366 100644 --- a/dist/fedora/strawberry.spec.in +++ b/dist/fedora/strawberry.spec.in @@ -13,7 +13,6 @@ BuildRequires: desktop-file-utils BuildRequires: libappstream-glib BuildRequires: gcc-c++ BuildRequires: hicolor-icon-theme -BuildRequires: liblastfm-qt5-devel BuildRequires: make BuildRequires: git BuildRequires: pkgconfig diff --git a/dist/opensuse/strawberry.spec.in b/dist/opensuse/strawberry.spec.in index b9059bc1..20f07bf3 100644 --- a/dist/opensuse/strawberry.spec.in +++ b/dist/opensuse/strawberry.spec.in @@ -19,7 +19,6 @@ BuildRequires: appstream-glib BuildRequires: gcc-c++ BuildRequires: hicolor-icon-theme BuildRequires: libQt5Gui-private-headers-devel -BuildRequires: liblastfm-qt5-devel BuildRequires: make BuildRequires: git BuildRequires: pkgconfig diff --git a/dist/pacman/PKGBUILD.in b/dist/pacman/PKGBUILD.in index 1d41d9bd..f025f3e4 100644 --- a/dist/pacman/PKGBUILD.in +++ b/dist/pacman/PKGBUILD.in @@ -26,11 +26,9 @@ depends=( vlc phonon-qt5 chromaprint - liblastfm-qt5 ) optdepends=( 'libgpod: iPod classic support' - 'liblastfm-qt5: LastFM cover provider' 'libcdio: Audio CD playback' 'libmtp: MTP device support' 'libusbmuxd: iPod Touch, iPhone, iPad support' diff --git a/dist/windows/strawberry-debug-x64.nsi.in b/dist/windows/strawberry-debug-x64.nsi.in index 49fbd931..730e618c 100644 --- a/dist/windows/strawberry-debug-x64.nsi.in +++ b/dist/windows/strawberry-debug-x64.nsi.in @@ -131,7 +131,6 @@ Section "Strawberry" Strawberry File "libiconv-2.dll" File "libintl-8.dll" File "libjpeg-9.dll" - File "liblastfm5.dll" File "libmp3lame-0.dll" File "libogg-0.dll" File "libopus-0.dll" @@ -374,7 +373,6 @@ Section "Uninstall" Delete "$INSTDIR\libiconv-2.dll" Delete "$INSTDIR\libintl-8.dll" Delete "$INSTDIR\libjpeg-9.dll" - Delete "$INSTDIR\liblastfm5.dll" Delete "$INSTDIR\libmp3lame-0.dll" Delete "$INSTDIR\libogg-0.dll" Delete "$INSTDIR\libopus-0.dll" diff --git a/dist/windows/strawberry-debug-x86.nsi.in b/dist/windows/strawberry-debug-x86.nsi.in index 16f4579c..dbc3ecc2 100644 --- a/dist/windows/strawberry-debug-x86.nsi.in +++ b/dist/windows/strawberry-debug-x86.nsi.in @@ -131,7 +131,6 @@ Section "Strawberry" Strawberry File "libiconv-2.dll" File "libintl-8.dll" File "libjpeg-9.dll" - File "liblastfm5.dll" File "libmp3lame-0.dll" File "libogg-0.dll" File "libopus-0.dll" @@ -374,7 +373,6 @@ Section "Uninstall" Delete "$INSTDIR\libiconv-2.dll" Delete "$INSTDIR\libintl-8.dll" Delete "$INSTDIR\libjpeg-9.dll" - Delete "$INSTDIR\liblastfm5.dll" Delete "$INSTDIR\libmp3lame-0.dll" Delete "$INSTDIR\libogg-0.dll" Delete "$INSTDIR\libopus-0.dll" diff --git a/dist/windows/strawberry-x64.nsi.in b/dist/windows/strawberry-x64.nsi.in index 92b60b0a..b7d2ac3f 100644 --- a/dist/windows/strawberry-x64.nsi.in +++ b/dist/windows/strawberry-x64.nsi.in @@ -131,7 +131,6 @@ Section "Strawberry" Strawberry File "libiconv-2.dll" File "libintl-8.dll" File "libjpeg-9.dll" - File "liblastfm5.dll" File "libmp3lame-0.dll" File "libogg-0.dll" File "libopus-0.dll" @@ -342,7 +341,6 @@ Section "Uninstall" Delete "$INSTDIR\libiconv-2.dll" Delete "$INSTDIR\libintl-8.dll" Delete "$INSTDIR\libjpeg-9.dll" - Delete "$INSTDIR\liblastfm5.dll" Delete "$INSTDIR\libmp3lame-0.dll" Delete "$INSTDIR\libogg-0.dll" Delete "$INSTDIR\libopus-0.dll" diff --git a/dist/windows/strawberry-x86.nsi.in b/dist/windows/strawberry-x86.nsi.in index b11288b7..ccb732f5 100644 --- a/dist/windows/strawberry-x86.nsi.in +++ b/dist/windows/strawberry-x86.nsi.in @@ -131,7 +131,6 @@ Section "Strawberry" Strawberry File "libiconv-2.dll" File "libintl-8.dll" File "libjpeg-9.dll" - File "liblastfm5.dll" File "libmp3lame-0.dll" File "libogg-0.dll" File "libopus-0.dll" @@ -342,7 +341,6 @@ Section "Uninstall" Delete "$INSTDIR\libiconv-2.dll" Delete "$INSTDIR\libintl-8.dll" Delete "$INSTDIR\libjpeg-9.dll" - Delete "$INSTDIR\liblastfm5.dll" Delete "$INSTDIR\libmp3lame-0.dll" Delete "$INSTDIR\libogg-0.dll" Delete "$INSTDIR\libopus-0.dll" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 621c3ec1..462f9027 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -70,10 +70,6 @@ include_directories(${TAGLIB_INCLUDE_DIRS}) include_directories(${QTSINGLEAPPLICATION_INCLUDE_DIRS}) include_directories(${QXT_INCLUDE_DIRS}) -if(HAVE_LIBLASTFM) - include_directories(${LASTFM5_INCLUDE_DIRS}) -endif(HAVE_LIBLASTFM) - include_directories(${CMAKE_SOURCE_DIR}/ext/libstrawberry-common) include_directories(${CMAKE_SOURCE_DIR}/ext/libstrawberry-tagreader) include_directories(${CMAKE_BINARY_DIR}/ext/libstrawberry-tagreader) @@ -195,6 +191,7 @@ set(SOURCES covermanager/coverexportrunnable.cpp covermanager/currentartloader.cpp covermanager/coverfromurldialog.cpp + covermanager/lastfmcoverprovider.cpp covermanager/musicbrainzcoverprovider.cpp covermanager/discogscoverprovider.cpp @@ -360,6 +357,7 @@ set(HEADERS covermanager/coverexportrunnable.h covermanager/currentartloader.h covermanager/coverfromurldialog.h + covermanager/lastfmcoverprovider.h covermanager/musicbrainzcoverprovider.h covermanager/discogscoverprovider.h @@ -529,15 +527,6 @@ optional_source(HAVE_DEEZER HEADERS engine/deezerengine.h ) -# Lastfm -optional_source(HAVE_LIBLASTFM - SOURCES - covermanager/lastfmcoverprovider.cpp - covermanager/lastfmcompat.cpp - HEADERS - covermanager/lastfmcoverprovider.h -) - # DBUS and MPRIS - Unix specific if(UNIX AND HAVE_DBUS) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dbus) @@ -945,10 +934,6 @@ if(HAVE_DZMEDIA) target_link_libraries(strawberry_lib ${LIBDZMEDIA_LIBRARIES}) endif() -if(HAVE_LIBLASTFM) - target_link_libraries(strawberry_lib ${LASTFM5_LIBRARIES}) -endif(HAVE_LIBLASTFM) - if(HAVE_LIBGPOD) target_link_libraries(strawberry_lib ${LIBGPOD_LIBRARIES}) endif(HAVE_LIBGPOD) diff --git a/src/config.h.in b/src/config.h.in index c2158295..aecd23ab 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -35,8 +35,6 @@ #cmakedefine HAVE_LIBARCHIVE #cmakedefine HAVE_AUDIOCD #cmakedefine HAVE_LIBGPOD -#cmakedefine HAVE_LIBLASTFM -#cmakedefine HAVE_LIBLASTFM1 #cmakedefine HAVE_LIBMTP #cmakedefine HAVE_LIBPULSE #cmakedefine HAVE_QCA diff --git a/src/core/application.cpp b/src/core/application.cpp index 6e78366f..10e30c23 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2,6 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2012, David Sansome + * 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 @@ -49,9 +50,7 @@ #include "covermanager/albumcoverloader.h" #include "covermanager/coverproviders.h" #include "covermanager/currentartloader.h" -#ifdef HAVE_LIBLASTFM - #include "covermanager/lastfmcoverprovider.h" -#endif +#include "covermanager/lastfmcoverprovider.h" #include "covermanager/discogscoverprovider.h" #include "covermanager/musicbrainzcoverprovider.h" @@ -104,9 +103,7 @@ class ApplicationImpl { cover_providers_([=]() { CoverProviders *cover_providers = new CoverProviders(app); // Initialize the repository of cover providers. -#ifdef HAVE_LIBLASTFM cover_providers->AddProvider(new LastFmCoverProvider(app)); -#endif cover_providers->AddProvider(new DiscogsCoverProvider(app)); cover_providers->AddProvider(new MusicbrainzCoverProvider(app)); return cover_providers; diff --git a/src/core/application.h b/src/core/application.h index 61ab027f..54aaa63f 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -2,6 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2012, David Sansome + * 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 diff --git a/src/covermanager/albumcoverfetchersearch.cpp b/src/covermanager/albumcoverfetchersearch.cpp index 2ebd9fab..178e1661 100644 --- a/src/covermanager/albumcoverfetchersearch.cpp +++ b/src/covermanager/albumcoverfetchersearch.cpp @@ -45,7 +45,7 @@ using std::stable_sort; -const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 12000; +const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 25000; const int AlbumCoverFetcherSearch::kImageLoadTimeoutMs = 3000; const int AlbumCoverFetcherSearch::kTargetSize = 500; const float AlbumCoverFetcherSearch::kGoodScore = 1.85; diff --git a/src/covermanager/lastfmcompat.cpp b/src/covermanager/lastfmcompat.cpp deleted file mode 100644 index 9adb9ffe..00000000 --- a/src/covermanager/lastfmcompat.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2012, David Sansome - * - * 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 "lastfmcompat.h" - -namespace lastfm { -namespace compat { - -#ifdef HAVE_LIBLASTFM1 - -XmlQuery EmptyXmlQuery() { return XmlQuery(); } - -bool ParseQuery(const QByteArray &data, XmlQuery *query, bool *connection_problems) { - - const bool ret = query->parse(data); - - if (connection_problems) { - *connection_problems = !ret && query->parseError().enumValue() == lastfm::ws::MalformedResponse; - } - - return ret; -} - -bool ParseUserList(QNetworkReply *reply, QList *users) { - - lastfm::XmlQuery lfm; - if (!lfm.parse(reply->readAll())) { - return false; - } - - *users = lastfm::UserList(lfm).users(); - return true; - -} - -#else // HAVE_LIBLASTFM1 - -XmlQuery EmptyXmlQuery() { - - QByteArray dummy; - return XmlQuery(dummy); - -} - -bool ParseQuery(const QByteArray &data, XmlQuery *query, bool *connection_problems) { - - try { - *query = lastfm::XmlQuery(data); -#ifdef Q_OS_WIN32 - if (lastfm::ws::last_parse_error != lastfm::ws::NoError) { - return false; - } -#endif // Q_OS_WIN32 - } - catch (lastfm::ws::ParseError e) { - qLog(Error) << "Last.fm parse error: " << e.enumValue(); - if (connection_problems) { - *connection_problems = e.enumValue() == lastfm::ws::MalformedResponse; - } - return false; - } - catch (std::runtime_error &e) { - qLog(Error) << __FUNCTION__ << e.what(); - return false; - } - - if (connection_problems) { - *connection_problems = false; - } - - // Check for app errors. - if (QDomElement(*query).attribute("status") == "failed") { - return false; - } - - return true; - -} - -bool ParseUserList(QNetworkReply *reply, QList *users) { - - try { - *users = lastfm::User::list(reply); -#ifdef Q_OS_WIN32 - if (lastfm::ws::last_parse_error != lastfm::ws::NoError) { - return false; - } -#endif // Q_OS_WIN32 - } - catch (std::runtime_error &e) { - qLog(Error) << __FUNCTION__ << e.what(); - return false; - } - return true; - -} - -#endif // HAVE_LIBLASTFM1 -} -} - diff --git a/src/covermanager/lastfmcompat.h b/src/covermanager/lastfmcompat.h deleted file mode 100644 index 80463458..00000000 --- a/src/covermanager/lastfmcompat.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2012, David Sansome - * - * 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 LASTFMCOMPAT_H -#define LASTFMCOMPAT_H - -#include "config.h" - -#include - -#include -#include -#include - -#ifdef HAVE_LIBLASTFM1 -# include -# include -# include -# include -#else -# include -# include -# include -# include -#endif - -namespace lastfm { -namespace compat { - -lastfm::XmlQuery EmptyXmlQuery(); -bool ParseQuery(const QByteArray &data, lastfm::XmlQuery *query, bool *connection_problems = nullptr); -bool ParseUserList(QNetworkReply *reply, QList *users); - -#ifdef HAVE_LIBLASTFM1 -typedef lastfm::User AuthenticatedUser; -#else -typedef lastfm::AuthenticatedUser AuthenticatedUser; -#endif -} -} - -#endif // LASTFMCOMPAT_H - diff --git a/src/covermanager/lastfmcoverprovider.cpp b/src/covermanager/lastfmcoverprovider.cpp index 9448b159..ce38ca47 100644 --- a/src/covermanager/lastfmcoverprovider.cpp +++ b/src/covermanager/lastfmcoverprovider.cpp @@ -1,7 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2010, David Sansome + * 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 @@ -21,39 +20,70 @@ #include "config.h" #include -#include #include +#include +#include +#include #include #include #include +#include #include +#include +#include +#include +#include +#include #include "core/closure.h" #include "core/network.h" +#include "core/logging.h" + #include "coverprovider.h" #include "albumcoverfetcher.h" -#include "lastfmcompat.h" #include "lastfmcoverprovider.h" +const char *LastFmCoverProvider::kUrl = "https://ws.audioscrobbler.com/2.0/"; const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e"; const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8"; -LastFmCoverProvider::LastFmCoverProvider(QObject *parent) : CoverProvider("last.fm", true, parent), network_(new NetworkAccessManager(this)) { - lastfm::ws::ApiKey = kApiKey; - lastfm::ws::SharedSecret = kSecret; - lastfm::setNetworkAccessManager(network_); -} +LastFmCoverProvider::LastFmCoverProvider(QObject *parent) : CoverProvider("last.fm", true, parent), network_(new NetworkAccessManager(this)) {} bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { - QMap params; - params["method"] = "album.search"; - params["album"] = album + " " + artist; + typedef QPair Param; + typedef QPair EncodedParam; + typedef QList ParamList; - QNetworkReply *reply = lastfm::ws::post(params); + ParamList params = ParamList() + << Param("album", QString(album + " " + artist)) + << Param("api_key", kApiKey) + << Param("lang", QLocale().name().left(2).toLower()) + << Param("method", "album.search"); + + QUrlQuery url_query; + QString data_to_sign; + for (const Param ¶m : params) { + EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + url_query.addQueryItem(encoded_param.first, encoded_param.second); + data_to_sign += param.first + param.second; + } + data_to_sign += kSecret; + + QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5); + QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower(); + + url_query.addQueryItem(QUrl::toPercentEncoding("api_sig"), QUrl::toPercentEncoding(signature)); + url_query.addQueryItem(QUrl::toPercentEncoding("format"), QUrl::toPercentEncoding("json")); + + QUrl url(kUrl); + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8()); NewClosure(reply, SIGNAL(finished()), this, SLOT(QueryFinished(QNetworkReply*, int)), reply, id); return true; + } void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, int id) { @@ -62,22 +92,207 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, int id) { CoverSearchResults results; - lastfm::XmlQuery query(lastfm::compat::EmptyXmlQuery()); - if (lastfm::compat::ParseQuery(reply->readAll(), &query)) { - // parse the list of search results - QList elements = query["results"]["albummatches"].children("album"); + QByteArray data = GetReplyData(reply); + if (data.isEmpty()) { + emit SearchFinished(id, results); + return; + } - for (const lastfm::XmlQuery &element : elements) { - CoverSearchResult result; - result.description = element["artist"].text() + " - " + element["name"].text(); - result.image_url = QUrl(element["image size=extralarge"].text()); - if (result.image_url.isEmpty()) continue; - results << result; + QJsonValue json_value = ExtractResults(data); + if (!json_value.isObject()) { + emit SearchFinished(id, results); + return; + } + + QJsonObject json_results = json_value.toObject(); + if (json_results.isEmpty()) { + Error("Json object is empty.", json_value); + emit SearchFinished(id, results); + return; + } + + if (!json_results.contains("albummatches")) { + Error("Json results is missing albummatches.", json_results); + emit SearchFinished(id, results); + return; + } + + QJsonValue json_matches = json_results["albummatches"]; + if (!json_matches.isObject()) { + Error("Json albummatches is not an object.", json_matches); + emit SearchFinished(id, results); + return; + } + + QJsonObject json_obj_matches = json_matches.toObject(); + if (json_obj_matches.isEmpty()) { + Error("Json albummatches object is empty.", json_matches); + emit SearchFinished(id, results); + return; + } + if (!json_obj_matches.contains("album")) { + Error("Json albummatches is missing album.", json_obj_matches); + emit SearchFinished(id, results); + return; + } + + QJsonValue json_album = json_obj_matches["album"]; + if (!json_album.isArray()) { + Error("Json album is not an array.", json_album); + emit SearchFinished(id, results); + return; + } + QJsonArray json_array = json_album.toArray(); + + for (const QJsonValue &value : json_array) { + if (!value.isObject()) { + Error("Invalid Json reply, album value is not an object.", value); + continue; } + QJsonObject json_obj = value.toObject(); + if (!json_obj.contains("artist") || !json_obj.contains("image") || !json_obj.contains("name")) { + Error("Invalid Json reply, album is missing artist, image or name.", json_obj); + continue; + } + QString artist = json_obj["artist"].toString(); + QString name = json_obj["name"].toString(); + + QJsonValue json_image = json_obj["image"]; + if (!json_image.isArray()) { + Error("Invalid Json reply, album image is not an array.", json_image); + continue; + } + QJsonArray json_array_image = json_image.toArray(); + QUrl url; + LastFmImageSize size; + for (QJsonValue json_value_image : json_array_image) { + if (!json_value_image.isObject()) { + Error("Invalid Json reply, album image value is not an object.", json_value_image); + continue; + } + QJsonObject json_object_image = json_value_image.toObject(); + if (!json_object_image.contains("#text") || !json_object_image.contains("size")) { + Error("Invalid Json reply, album image value is missing #text or size.", json_object_image); + continue; + } + QString image_url = json_object_image["#text"].toString(); + LastFmImageSize image_size = ImageSizeFromString(json_object_image["size"].toString().toLower()); + if (url.isEmpty() || image_size > size) { + url.setUrl(image_url); + size = image_size; + } + } + + if (url.isEmpty()) continue; + + CoverSearchResult cover_result; + cover_result.description = artist + " " + name; + cover_result.image_url = url; + results << cover_result; + } + emit SearchFinished(id, results); + +} + +QByteArray LastFmCoverProvider::GetReplyData(QNetworkReply *reply) { + + QByteArray data; + + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); } else { - // Drop through and emit an empty list of results. + if (reply->error() < 200) { + // This is a network error, there is nothing more to do. + QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + Error(failure_reason); + } + else { + // See if there is Json data containing "error" and "message" - then use that instead. + data = reply->readAll(); + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + QString failure_reason; + if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QJsonObject json_obj = json_doc.object(); + if (json_obj.contains("error") && json_obj.contains("message")) { + int error = json_obj["error"].toInt(); + QString message = json_obj["message"].toString(); + failure_reason = "Error: " + QString::number(error) + ": " + message; + } + else { + failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + } + else { + failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + Error(failure_reason); + } + return QByteArray(); } - emit SearchFinished(id, results); + return data; + +} + +QJsonObject LastFmCoverProvider::ExtractJsonObj(const QByteArray &data) { + + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + + if (error.error != QJsonParseError::NoError) { + Error("Reply from server missing Json data.", data); + return QJsonObject(); + } + if (json_doc.isNull() || json_doc.isEmpty()) { + Error("Received empty Json document.", json_doc); + 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; + +} + +QJsonValue LastFmCoverProvider::ExtractResults(const QByteArray &data) { + + QJsonObject json_obj = ExtractJsonObj(data); + if (json_obj.isEmpty()) return QJsonObject(); + + if (json_obj.contains("results")) { + QJsonValue json_results = json_obj["results"]; + return json_results; + } + else if (json_obj.contains("error") && json_obj.contains("message")) { + int error = json_obj["error"].toInt(); + QString message = json_obj["message"].toString(); + Error(QString("Error: %1: %2").arg(QString::number(error)).arg(message)); + } + else { + Error(QString("Json reply is missing results."), json_obj); + } + return QJsonValue(); + +} + +void LastFmCoverProvider::Error(QString error, QVariant debug) { + qLog(Error) << "LastFm:" << error; + if (debug.isValid()) qLog(Debug) << debug; +} + +LastFmCoverProvider::LastFmImageSize LastFmCoverProvider::ImageSizeFromString(const QString size) { + if (size == "small") return LastFmImageSize::Small; + else if (size == "medium") return LastFmImageSize::Medium; + else if (size == "large") return LastFmImageSize::Large; + else if (size == "extralarge") return LastFmImageSize::ExtraLarge; + else return LastFmImageSize::Unknown; } diff --git a/src/covermanager/lastfmcoverprovider.h b/src/covermanager/lastfmcoverprovider.h index b195f229..fa110b7f 100644 --- a/src/covermanager/lastfmcoverprovider.h +++ b/src/covermanager/lastfmcoverprovider.h @@ -1,7 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2010, David Sansome + * 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 @@ -33,26 +32,37 @@ #include "coverprovider.h" -// A built-in cover provider which fetches covers from last.fm. class LastFmCoverProvider : public CoverProvider { Q_OBJECT public: explicit LastFmCoverProvider(QObject *parent = nullptr); - bool StartSearch(const QString &artist, const QString &album, int id); - static const char *kApiKey; - static const char *kSecret; - private slots: void QueryFinished(QNetworkReply *reply, int id); private: + static const char *kUrl; + static const char *kApiKey; + static const char *kSecret; + enum LastFmImageSize { + Unknown, + Small, + Medium, + Large, + ExtraLarge + }; + QByteArray GetReplyData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(const QByteArray &data); + QJsonValue ExtractResults(const QByteArray &data); + LastFmImageSize ImageSizeFromString(const QString size); + void Error(QString error, QVariant debug = QVariant()); + QNetworkAccessManager *network_; QMap pending_queries_; }; -#endif // LASTFMCOVERPROVIDER_H +#endif // LASTFMCOVERPROVIDER_H