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