From 1ad13cd3b02a504e4db59a297e2d4e60e7b6bc30 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Tue, 9 Dec 2025 18:45:57 +0100 Subject: [PATCH] Add lyrics from lrclib.net --- CMakeLists.txt | 2 + README.md | 2 +- debian/control | 2 +- ...rawberrymusicplayer.strawberry.appdata.xml | 2 +- dist/unix/strawberry.1 | 4 +- dist/unix/strawberry.spec.in | 3 +- src/core/application.cpp | 2 + src/lyrics/lrcliblyricsprovider.cpp | 168 ++++++++++++++++++ src/lyrics/lrcliblyricsprovider.h | 51 ++++++ 9 files changed, 228 insertions(+), 8 deletions(-) create mode 100644 src/lyrics/lrcliblyricsprovider.cpp create mode 100644 src/lyrics/lrcliblyricsprovider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d0c99c8b..4fbef4873 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -701,6 +701,7 @@ set(SOURCES src/lyrics/elyricsnetlyricsprovider.cpp src/lyrics/letraslyricsprovider.cpp src/lyrics/lyricfindlyricsprovider.cpp + src/lyrics/lrcliblyricsprovider.cpp src/settings/settingsdialog.cpp src/settings/settingspage.cpp @@ -997,6 +998,7 @@ set(HEADERS src/lyrics/elyricsnetlyricsprovider.h src/lyrics/letraslyricsprovider.h src/lyrics/lyricfindlyricsprovider.h + src/lyrics/lrcliblyricsprovider.h src/settings/settingsdialog.h src/settings/settingspage.h diff --git a/README.md b/README.md index 22b9b5cfa..20cbec3dc 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Supporting open-source developers helps ensure continued maintenance and improve - Loudness analysis and EBU R128 normalization - Editing tags and fetching missing tags via [MusicBrainz](https://musicbrainz.org/) - Album 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/), [Spotify](https://www.spotify.com/) -- Lyrics from: [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics](https://www.lololyrics.com/), [songlyrics](https://www.songlyrics.com/), [azlyrics](https://www.azlyrics.com/), [elyrics](https://www.elyrics.net/), [letras](https://www.letras.mus.br), [LyricFind](https://lyrics.lyricfind.com) +- Lyrics from: [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics](https://www.lololyrics.com/), [songlyrics](https://www.songlyrics.com/), [azlyrics](https://www.azlyrics.com/), [elyrics](https://www.elyrics.net/), [letras](https://www.letras.mus.br), [LyricFind](https://lyrics.lyricfind.com) and [lrclib.net](https://lrclib.net/) - Audio analyzer and equalizer - Transfer music to USB, MTP and iPod devices - Scrobbling to [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/) diff --git a/debian/control b/debian/control index 05035d18c..4b0c6d925 100644 --- a/debian/control +++ b/debian/control @@ -60,7 +60,7 @@ Description: music player and music collection organizer - Edit tags on audio files - Automatically retrieve tags from MusicBrainz - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - - Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind + - Lyrics from multiple sources - Audio analyzer - Audio equalizer - Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic diff --git a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml index 7a4c91436..5e9b1f1f1 100644 --- a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml +++ b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml @@ -31,7 +31,7 @@
  • Edit tags on audio files
  • Automatically retrieve tags from MusicBrainz
  • Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
  • -
  • Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
  • +
  • Lyrics from multiple sources
  • Audio analyzer and equalizer
  • Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
  • Scrobbler with support for Last.fm and ListenBrainz
  • diff --git a/dist/unix/strawberry.1 b/dist/unix/strawberry.1 index 17ccf79cf..5fc7a585b 100644 --- a/dist/unix/strawberry.1 +++ b/dist/unix/strawberry.1 @@ -29,9 +29,7 @@ Features: .br - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify .br -- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind -.br -- Support for multiple backends +- Lyrics from multiple sources .br - Audio analyzer .br diff --git a/dist/unix/strawberry.spec.in b/dist/unix/strawberry.spec.in index ab2218127..9c9406200 100644 --- a/dist/unix/strawberry.spec.in +++ b/dist/unix/strawberry.spec.in @@ -93,8 +93,7 @@ Features: - Edit tags on audio files - Automatically retrieve tags from MusicBrainz - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - - Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind - - Support for multiple backends + - Lyrics from multiple sources - Audio analyzer - Audio equalizer - Scrobbler with support for Last.fm and ListenBrainz diff --git a/src/core/application.cpp b/src/core/application.cpp index 86108be2b..c08eed78d 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -74,6 +74,7 @@ #include "lyrics/elyricsnetlyricsprovider.h" #include "lyrics/letraslyricsprovider.h" #include "lyrics/lyricfindlyricsprovider.h" +#include "lyrics/lrcliblyricsprovider.h" #include "scrobbler/audioscrobbler.h" #include "scrobbler/lastfmscrobbler.h" @@ -182,6 +183,7 @@ class ApplicationImpl { lyrics_providers->AddProvider(new ElyricsNetLyricsProvider(lyrics_providers->network())); lyrics_providers->AddProvider(new LetrasLyricsProvider(lyrics_providers->network())); lyrics_providers->AddProvider(new LyricFindLyricsProvider(lyrics_providers->network())); + lyrics_providers->AddProvider(new LrcLibLyricsProvider(lyrics_providers->network())); lyrics_providers->ReloadSettings(); return lyrics_providers; }), diff --git a/src/lyrics/lrcliblyricsprovider.cpp b/src/lyrics/lrcliblyricsprovider.cpp new file mode 100644 index 000000000..fdccf084b --- /dev/null +++ b/src/lyrics/lrcliblyricsprovider.cpp @@ -0,0 +1,168 @@ +/* + * Strawberry Music Player + * Copyright 2025, 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 "core/logging.h" +#include "core/networkaccessmanager.h" +#include "jsonlyricsprovider.h" +#include "lyricssearchrequest.h" +#include "lyricssearchresult.h" +#include "lrcliblyricsprovider.h" + +using namespace Qt::Literals::StringLiterals; +using std::make_shared; + +namespace { +constexpr char kApiUrl[] = "https://lrclib.net/api/get"; +} // namespace + +LrcLibLyricsProvider::LrcLibLyricsProvider(const SharedPtr network, QObject *parent) : JsonLyricsProvider(u"LrcLib"_s, true, false, network, parent) {} + +void LrcLibLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &request) { + + Q_ASSERT(QThread::currentThread() != qApp->thread()); + + const QUrl url(QString::fromUtf8(kApiUrl)); + QUrlQuery url_query; + url_query.addQueryItem(u"track_name"_s, QString::fromLatin1(QUrl::toPercentEncoding(request.title))); + url_query.addQueryItem(u"artist_name"_s, QString::fromLatin1(QUrl::toPercentEncoding(request.artist))); + url_query.addQueryItem(u"album_name"_s, QString::fromLatin1(QUrl::toPercentEncoding(request.album))); + url_query.addQueryItem(u"duration"_s, QString::number(request.duration)); + QNetworkReply *reply = CreateGetRequest(url, url_query); + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, request]() { HandleSearchReply(reply, id, request); }); + + qLog(Debug) << "LrcLibLyrics: Sending request for" << url << url_query.toString(); + +} + +LrcLibLyricsProvider::JsonObjectResult LrcLibLyricsProvider::ParseJsonObject(QNetworkReply *reply) { + + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return JsonObjectResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + } + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("statusCode"_L1) && json_object.contains("name"_L1) && json_object.contains("message"_L1)) { + const int code = json_object["statusCode"_L1].toInt(); + const QString name = json_object["name"_L1].toString(); + const QString message = json_object["message"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1 (%2) (%3)").arg(code).arg(name, message); + result.api_error = code; + } + else { + result.json_object = json_object; + } + } + else { + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); + } + } + + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + return result; + +} + +void LrcLibLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id, const LyricsSearchRequest &request) { + + Q_ASSERT(QThread::currentThread() != qApp->thread()); + + LyricsSearchResults results; + const QScopeGuard end_search = qScopeGuard([this, id, request, &results]() { EndSearch(id, request, results); }); + + if (!replies_.contains(reply)) return; + replies_.removeAll(reply); + QObject::disconnect(reply, nullptr, this, nullptr); + reply->deleteLater(); + + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + if (json_object_result.api_error != 404) { + Error(json_object_result.error_message); + } + return; + } + + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty() || + !json_object.contains("trackName"_L1) || + !json_object.contains("artistName"_L1) || + !json_object.contains("albumName"_L1) || + !json_object.contains("plainLyrics"_L1)) { + return; + } + + LyricsSearchResult result; + result.artist = json_object["artistName"_L1].toString(); + result.album = json_object["albumName"_L1].toString(); + result.title = json_object["trackName"_L1].toString(); + result.lyrics = json_object["plainLyrics"_L1].toString(); + results << result; + +} + +void LrcLibLyricsProvider::EndSearch(const int id, const LyricsSearchRequest &request, const LyricsSearchResults &results) { + + if (results.isEmpty()) { + qLog(Debug) << name_ << "No lyrics for" << request.artist << request.album << request.title; + } + else { + qLog(Debug) << name_ << "Got lyrics for" << request.artist << request.album << request.title; + } + + Q_EMIT SearchFinished(id, results); + +} diff --git a/src/lyrics/lrcliblyricsprovider.h b/src/lyrics/lrcliblyricsprovider.h new file mode 100644 index 000000000..633367bb1 --- /dev/null +++ b/src/lyrics/lrcliblyricsprovider.h @@ -0,0 +1,51 @@ +/* + * Strawberry Music Player + * Copyright 2025, 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 LRCLIBLYRICSPROVIDER_H +#define LRCLIBLYRICSPROVIDER_H + +#include "config.h" + +#include + +#include "jsonlyricsprovider.h" +#include "lyricssearchrequest.h" +#include "lyricssearchresult.h" + +class QNetworkReply; +class NetworkAccessManager; + +class LrcLibLyricsProvider : public JsonLyricsProvider { + Q_OBJECT + + public: + explicit LrcLibLyricsProvider(const SharedPtr network, QObject *parent = nullptr); + + private: + JsonObjectResult ParseJsonObject(QNetworkReply *reply); + void EndSearch(const int id, const LyricsSearchRequest &request, const LyricsSearchResults &results); + + protected Q_SLOTS: + void StartSearch(const int id, const LyricsSearchRequest &request) override; + + private Q_SLOTS: + void HandleSearchReply(QNetworkReply *reply, const int id, const LyricsSearchRequest &request); +}; + +#endif // LRCLIBLYRICSPROVIDER_H