Add lyrics from lrclib.net

This commit is contained in:
Jonas Kvinge
2025-12-09 18:45:57 +01:00
parent 5c640e0e36
commit 1ad13cd3b0
9 changed files with 228 additions and 8 deletions

View File

@@ -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

View File

@@ -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/)

2
debian/control vendored
View File

@@ -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

View File

@@ -31,7 +31,7 @@
<li>Edit tags on audio files</li>
<li>Automatically retrieve tags from MusicBrainz</li>
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind</li>
<li>Lyrics from multiple sources</li>
<li>Audio analyzer and equalizer</li>
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
<li>Scrobbler with support for Last.fm and ListenBrainz</li>

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}),

View File

@@ -0,0 +1,168 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QApplication>
#include <QThread>
#include <QByteArray>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QRegularExpression>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonArray>
#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<NetworkAccessManager> 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);
}

View File

@@ -0,0 +1,51 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef LRCLIBLYRICSPROVIDER_H
#define LRCLIBLYRICSPROVIDER_H
#include "config.h"
#include <QString>
#include "jsonlyricsprovider.h"
#include "lyricssearchrequest.h"
#include "lyricssearchresult.h"
class QNetworkReply;
class NetworkAccessManager;
class LrcLibLyricsProvider : public JsonLyricsProvider {
Q_OBJECT
public:
explicit LrcLibLyricsProvider(const SharedPtr<NetworkAccessManager> 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