diff --git a/README.md b/README.md
index 33896688c..bfc79a20c 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/), [Deezer](https://www.deezer.com/) and [Tidal](https://www.tidal.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/) and [Qobuz](https://www.qobuz.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 64ae6c07f..b89e1df62 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 Lastfm, Musicbrainz, Discogs and Deezer
+ - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal and Qobuz
- 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 bf6269bbc..4e69db65e 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, Deezer and Tidal
+ Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal and Qobuz
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 213cf3117..64ad1d70f 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 Lastfm, Musicbrainz, Discogs, Deezer and Tidal
+- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal and Qobuz
.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 4004edb79..faad7e509 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, Deezer and Tidal
+ - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal and Qobuz
- 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 a91656b82..b00bc5519 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -187,6 +187,7 @@ set(SOURCES
covermanager/discogscoverprovider.cpp
covermanager/deezercoverprovider.cpp
covermanager/qobuzcoverprovider.cpp
+ covermanager/musixmatchcoverprovider.cpp
lyrics/lyricsproviders.cpp
lyrics/lyricsprovider.cpp
@@ -379,6 +380,7 @@ set(HEADERS
covermanager/discogscoverprovider.h
covermanager/deezercoverprovider.h
covermanager/qobuzcoverprovider.h
+ covermanager/musixmatchcoverprovider.h
lyrics/lyricsproviders.h
lyrics/lyricsprovider.h
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 1cae4f1c3..cf358f763 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -57,6 +57,7 @@
#include "covermanager/musicbrainzcoverprovider.h"
#include "covermanager/deezercoverprovider.h"
#include "covermanager/qobuzcoverprovider.h"
+#include "covermanager/musixmatchcoverprovider.h"
#include "lyrics/lyricsproviders.h"
#include "lyrics/auddlyricsprovider.h"
@@ -121,6 +122,7 @@ class ApplicationImpl {
cover_providers->AddProvider(new DiscogsCoverProvider(app, app));
cover_providers->AddProvider(new DeezerCoverProvider(app, app));
cover_providers->AddProvider(new QobuzCoverProvider(app, app));
+ cover_providers->AddProvider(new MusixmatchCoverProvider(app, app));
#ifdef HAVE_TIDAL
cover_providers->AddProvider(new TidalCoverProvider(app, app));
#endif
diff --git a/src/covermanager/musixmatchcoverprovider.cpp b/src/covermanager/musixmatchcoverprovider.cpp
new file mode 100644
index 000000000..60e915a06
--- /dev/null
+++ b/src/covermanager/musixmatchcoverprovider.cpp
@@ -0,0 +1,225 @@
+/*
+ * 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 "core/logging.h"
+#include "core/network.h"
+#include "coverprovider.h"
+#include "albumcoverfetcher.h"
+#include "coverprovider.h"
+#include "musixmatchcoverprovider.h"
+
+MusixmatchCoverProvider::MusixmatchCoverProvider(Application *app, QObject *parent): CoverProvider("Musixmatch", 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) {
+
+ Q_UNUSED(title);
+
+ QString artist_stripped = artist;
+ QString album_stripped = album;
+
+ artist_stripped = artist_stripped.replace('/', '-');
+ artist_stripped = artist_stripped.remove(QRegExp("[^A-Za-z0-9\\- ]"));
+ artist_stripped = artist_stripped.simplified();
+ artist_stripped = artist_stripped.replace(' ', '-');
+ artist_stripped = artist_stripped.replace(QRegExp("(-)\\1+"), "-");
+ artist_stripped = artist_stripped.toLower();
+
+ album_stripped = album_stripped.replace('/', '-');
+ album_stripped = album_stripped.remove(QRegExp("[^a-zA-Z0-9\\- ]"));
+ album_stripped = album_stripped.simplified();
+ album_stripped = album_stripped.replace(' ', '-').toLower();
+ album_stripped = album_stripped.replace(QRegExp("(-)\\1+"), "-");
+ album_stripped = album_stripped.toLower();
+
+ if (artist_stripped.isEmpty() || album_stripped.isEmpty()) return false;
+
+ QUrl url(QString("https://www.musixmatch.com/album/%1/%2").arg(artist_stripped).arg(album_stripped));
+ QNetworkRequest req(url);
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ QNetworkReply *reply = network_->get(req);
+ connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, album); });
+
+ //qLog(Debug) << "Musixmatch: Sending request for" << artist_stripped << album_stripped << url;
+
+ return true;
+
+}
+
+void MusixmatchCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
+
+void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, const QString &artist, const QString &album) {
+
+ reply->deleteLater();
+
+ CoverSearchResults results;
+
+ if (reply->error() != QNetworkReply::NoError) {
+ Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ emit SearchFinished(id, results);
+ return;
+ }
+ else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
+ Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ QByteArray data = reply->readAll();
+ if (data.isEmpty()) {
+ Error("Empty reply received from server.");
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ QTextCodec *codec = QTextCodec::codecForName("utf-8");
+ if (!codec) {
+ emit SearchFinished(id, results);
+ return;
+ }
+ QString content = codec->toUnicode(data);
+
+ QString data_begin = "var __mxmState = ";
+ QString data_end = ";";
+ int begin_idx = content.indexOf(data_begin);
+ QString content_json;
+ if (begin_idx > 0) {
+ begin_idx += data_begin.length();
+ int end_idx = content.indexOf(data_end, begin_idx);
+ if (end_idx > begin_idx) {
+ content_json = content.mid(begin_idx, end_idx - begin_idx);
+ }
+ }
+
+ if (content_json.isEmpty()) {
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ if (content_json.contains(QRegExp("<[^>]*>"))) { // Make sure it's not HTML code.
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ QJsonParseError error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(content_json.toUtf8(), &error);
+
+ if (error.error != QJsonParseError::NoError) {
+ Error(QString("Failed to parse json data: %1").arg(error.errorString()));
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ if (json_doc.isEmpty()) {
+ Error("Received empty Json document.", data);
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ if (!json_doc.isObject()) {
+ Error("Json document is not an object.", json_doc);
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ QJsonObject json_obj = json_doc.object();
+ if (json_obj.isEmpty()) {
+ Error("Received empty Json object.", json_doc);
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ if (!json_obj.contains("page") || !json_obj["page"].isObject()) {
+ Error("Json reply is missing page object.", json_obj);
+ emit SearchFinished(id, results);
+ return;
+ }
+ json_obj = json_obj["page"].toObject();
+
+ if (!json_obj.contains("album") || !json_obj["album"].isObject()) {
+ Error("Json page object is missing album object.", json_obj);
+ emit SearchFinished(id, results);
+ return;
+ }
+ QJsonObject obj_album = json_obj["album"].toObject();
+
+ if (!obj_album.contains("artistName") || !obj_album.contains("name")) {
+ Error("Json album object is missing artistName or name.", obj_album);
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ QString cover;
+
+ if (obj_album.contains("coverart800x800")) {
+ cover = obj_album["coverart800x800"].toString();
+ }
+ else if (obj_album.contains("coverart500x500")) {
+ cover = obj_album["coverart500x500"].toString();
+ }
+ else if (obj_album.contains("coverart350x350")) {
+ cover = obj_album["coverart350x350"].toString();
+ }
+
+ if (cover.isEmpty()) {
+ emit SearchFinished(id, results);
+ return;
+ }
+ QUrl cover_url(cover);
+ if (!cover_url.isValid()) {
+ Error("Received cover url is not valid.", cover);
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ CoverSearchResult result;
+ result.artist = obj_album["artistName"].toString();
+ result.album = obj_album["name"].toString();
+ result.image_url = cover_url;
+
+ if (artist.toLower() == result.artist.toLower() || album.toLower() == result.album.toLower()) {
+ results.append(result);
+ }
+
+ emit SearchFinished(id, results);
+
+}
+
+void MusixmatchCoverProvider::Error(const QString &error, const QVariant &debug) {
+
+ qLog(Error) << "Musixmatch:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+}
diff --git a/src/covermanager/musixmatchcoverprovider.h b/src/covermanager/musixmatchcoverprovider.h
new file mode 100644
index 000000000..e34c85e55
--- /dev/null
+++ b/src/covermanager/musixmatchcoverprovider.h
@@ -0,0 +1,55 @@
+/*
+ * 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 MUSIXMATCHCOVERPROVIDER_H
+#define MUSIXMATCHCOVERPROVIDER_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include "coverprovider.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+
+class MusixmatchCoverProvider : public CoverProvider {
+ Q_OBJECT
+
+ public:
+ explicit MusixmatchCoverProvider(Application *app, QObject *parent = nullptr);
+
+ bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
+ void CancelSearch(const int id);
+
+ private:
+ void Error(const QString &error, const QVariant &debug = QVariant());
+
+ private slots:
+ void HandleSearchReply(QNetworkReply *reply, const int id, const QString &artist, const QString &album);
+
+ private:
+ QNetworkAccessManager *network_;
+
+};
+
+#endif // MUSIXMATCHCOVERPROVIDER_H