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