diff --git a/CMakeLists.txt b/CMakeLists.txt
index 61e03fbd..e5f388d8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -453,6 +453,8 @@ target_sources(
widgets/volumecontrol.cpp
context/lyricsettings.cpp
context/ultimatelyricsprovider.cpp
+ context/ultimatelyricshttpprovider.cpp
+ context/ultimatelyricscommandprovider.cpp
context/ultimatelyrics.cpp
context/lyricsdialog.cpp
context/contextwidget.cpp
@@ -469,6 +471,7 @@ target_sources(
context/lastfmengine.cpp
context/metaengine.cpp
context/onlineview.cpp
+ context/lyrics_providers/lrcliblyricsprovider.cpp
streams/streamspage.cpp
streams/streamdialog.cpp
streams/streamfetcher.cpp
diff --git a/context/lyrics_providers.xml b/context/lyrics_providers.xml
index a8217193..48fc36db 100644
--- a/context/lyrics_providers.xml
+++ b/context/lyrics_providers.xml
@@ -1,6 +1,6 @@
-
+
@@ -15,7 +15,7 @@
-
+
@@ -23,14 +23,14 @@
-
+
-
+
@@ -42,7 +42,7 @@
-
+
@@ -59,7 +59,7 @@
-
+
@@ -77,7 +77,7 @@
-
+
@@ -89,7 +89,7 @@
-
+
@@ -109,7 +109,7 @@
-
+
@@ -170,7 +170,7 @@
-
+
-
+
@@ -191,7 +191,7 @@
-
+
@@ -202,7 +202,7 @@
-
+
@@ -212,7 +212,7 @@
-
+
@@ -225,7 +225,7 @@
-
+
@@ -234,7 +234,7 @@
-
+
@@ -271,7 +271,7 @@
-
+
@@ -282,7 +282,7 @@
-
+
@@ -297,7 +297,7 @@
-
+
@@ -309,7 +309,7 @@
-
+
@@ -318,14 +318,14 @@
-
+
-
+
@@ -334,7 +334,7 @@
-
+
@@ -344,4 +344,9 @@
+
+
+
+
+
diff --git a/context/lyrics_providers/lrcliblyricsprovider.cpp b/context/lyrics_providers/lrcliblyricsprovider.cpp
new file mode 100644
index 00000000..0361c4ce
--- /dev/null
+++ b/context/lyrics_providers/lrcliblyricsprovider.cpp
@@ -0,0 +1,66 @@
+//
+// Created by bartkk on 17.10.2025.
+//
+
+#include "lrcliblyricsprovider.h"
+
+#include "network/networkaccessmanager.h"
+
+#include
+#include
+#include
+#include
+
+void LRCLibLyricsProvider::fetchInfo(int id, Song metadata, bool removeThe)
+{
+ (void)removeThe;
+
+ QUrl url("https://lrclib.net/api/get");
+
+ QUrlQuery query;
+ query.addQueryItem("track_name", QUrl::toPercentEncoding(metadata.title));
+ query.addQueryItem("artist_name", QUrl::toPercentEncoding(metadata.artist));
+ query.addQueryItem("track_duration", QString::number(metadata.time));
+ url.setQuery(query);
+
+ NetworkJob* reply = NetworkAccessManager::self()->get(url);
+ requests[reply] = id;
+ connect(reply, &NetworkJob::finished, this, &LRCLibLyricsProvider::lyricsFetched);
+}
+
+void LRCLibLyricsProvider::abort()
+{
+ QHash::ConstIterator it(requests.constBegin());
+ QHash::ConstIterator end(requests.constEnd());
+
+ for (; it != end; ++it) {
+ it.key()->cancelAndDelete();
+ }
+ requests.clear();
+}
+
+LRCLibLyricsProvider::~LRCLibLyricsProvider()
+{
+ LRCLibLyricsProvider::abort();
+}
+
+void LRCLibLyricsProvider::lyricsFetched()
+{
+ NetworkJob* reply = qobject_cast(sender());
+ if (!reply) {
+ return;
+ }
+
+ int id = requests.take(reply);
+ reply->deleteLater();
+
+ auto replyData = reply->readAll();
+ auto json = QJsonDocument::fromJson(replyData);
+
+ if (reply->error() == QNetworkReply::NoError) {
+ auto lyrics = json["plainLyrics"].toString();
+ emit lyricsReady(id, lyrics);
+ } else {
+ emit lyricsReady(id, "");
+ }
+}
\ No newline at end of file
diff --git a/context/lyrics_providers/lrcliblyricsprovider.h b/context/lyrics_providers/lrcliblyricsprovider.h
new file mode 100644
index 00000000..a499dbb6
--- /dev/null
+++ b/context/lyrics_providers/lrcliblyricsprovider.h
@@ -0,0 +1,21 @@
+#ifndef CANTATA_LRCLIBLYRICSPROVIDER_H
+#define CANTATA_LRCLIBLYRICSPROVIDER_H
+#include "context/ultimatelyricsprovider.h"
+
+class LRCLibLyricsProvider : public UltimateLyricsProvider {
+public:
+ static void enableDebug();
+
+ QString displayName() const override { return "LRCLib.net"; }
+ void fetchInfo(int id, Song metadata, bool removeThe) override;
+ void abort() override;
+ ~LRCLibLyricsProvider() override;
+
+private Q_SLOTS:
+ void lyricsFetched();
+
+private:
+ QHash requests;
+};
+
+#endif//CANTATA_LRCLIBLYRICSPROVIDER_H
diff --git a/context/ultimatelyrics.cpp b/context/ultimatelyrics.cpp
index 8c98514d..8ad026f1 100644
--- a/context/ultimatelyrics.cpp
+++ b/context/ultimatelyrics.cpp
@@ -22,8 +22,12 @@
*/
#include "ultimatelyrics.h"
+
#include "gui/settings.h"
+#include "lyrics_providers/lrcliblyricsprovider.h"
#include "support/globalstatic.h"
+#include "ultimatelyricscommandprovider.h"
+#include "ultimatelyricshttpprovider.h"
#include "ultimatelyricsprovider.h"
#include
#include
@@ -31,6 +35,7 @@
#include
#include
#include
+#include
GLOBAL_STATIC(UltimateLyrics, instance)
@@ -39,78 +44,6 @@ static bool compareLyricProviders(const UltimateLyricsProvider* a, const Ultimat
return a->getRelevance() < b->getRelevance();
}
-static QString parseInvalidIndicator(QXmlStreamReader* reader)
-{
- QString ret = reader->attributes().value("value").toString();
- reader->skipCurrentElement();
- return ret;
-}
-
-static UltimateLyricsProvider::Rule parseRule(QXmlStreamReader* reader)
-{
- UltimateLyricsProvider::Rule ret;
-
- while (!reader->atEnd()) {
- reader->readNext();
-
- if (QXmlStreamReader::EndElement == reader->tokenType()) {
- break;
- }
-
- if (QXmlStreamReader::StartElement == reader->tokenType()) {
- if (QLatin1String("item") == reader->name()) {
- QXmlStreamAttributes attr = reader->attributes();
- if (attr.hasAttribute("tag")) {
- ret << UltimateLyricsProvider::RuleItem(attr.value("tag").toString(), QString());
- }
- else if (attr.hasAttribute("begin")) {
- ret << UltimateLyricsProvider::RuleItem(attr.value("begin").toString(), attr.value("end").toString());
- }
- }
- reader->skipCurrentElement();
- }
- }
- return ret;
-}
-
-static UltimateLyricsProvider* parseProvider(QXmlStreamReader* reader)
-{
- QXmlStreamAttributes attributes = reader->attributes();
-
- UltimateLyricsProvider* scraper = new UltimateLyricsProvider;
- scraper->setName(attributes.value("name").toString());
- scraper->setCharset(attributes.value("charset").toString());
- scraper->setUrl(attributes.value("url").toString());
-
- while (!reader->atEnd()) {
- reader->readNext();
-
- if (QXmlStreamReader::EndElement == reader->tokenType()) {
- break;
- }
-
- if (QXmlStreamReader::StartElement == reader->tokenType()) {
- if (QLatin1String("extract") == reader->name()) {
- scraper->addExtractRule(parseRule(reader));
- }
- else if (QLatin1String("exclude") == reader->name()) {
- scraper->addExcludeRule(parseRule(reader));
- }
- else if (QLatin1String("invalidIndicator") == reader->name()) {
- scraper->addInvalidIndicator(parseInvalidIndicator(reader));
- }
- else if (QLatin1String("urlFormat") == reader->name()) {
- scraper->addUrlFormat(reader->attributes().value("replace").toString(), reader->attributes().value("with").toString());
- reader->skipCurrentElement();
- }
- else {
- reader->skipCurrentElement();
- }
- }
- }
- return scraper;
-}
-
void UltimateLyrics::release()
{
for (UltimateLyricsProvider* provider : providers) {
@@ -150,6 +83,12 @@ UltimateLyricsProvider* UltimateLyrics::getNext(int& index)
return nullptr;
}
+void UltimateLyrics::registerExtra(UltimateLyricsProvider* provider)
+{
+ providers << provider;
+ connect(provider, &UltimateLyricsProvider::lyricsReady, this, &UltimateLyrics::lyricsReady);
+}
+
void UltimateLyrics::load()
{
if (!providers.isEmpty()) {
@@ -178,10 +117,22 @@ void UltimateLyrics::load()
reader.readNext();
if (QLatin1String("provider") == reader.name()) {
- QString name = reader.attributes().value("name").toString();
+ auto attributes = reader.attributes();
+ QString name = attributes.value("name").toString();
+ QString type = attributes.value("type").toString();
if (!providerNames.contains(name)) {
- UltimateLyricsProvider* provider = parseProvider(&reader);
+ UltimateLyricsProvider* provider;
+
+ if (type == QString("http")) {
+ provider = UltimateLyricsHttpProvider::parseProvider(&reader);
+ } else if (type == QString("command")) {
+ provider = UltimateLyricsCommandProvider::parseProvider(&reader);
+ } else {
+ // TODO: Throw exception
+ __builtin_unreachable();
+ }
+
if (provider) {
providers << provider;
connect(provider, SIGNAL(lyricsReady(int, QString)), this, SIGNAL(lyricsReady(int, QString)));
@@ -193,6 +144,9 @@ void UltimateLyrics::load()
}
}
+ // Register extra providers
+ registerExtra(new LRCLibLyricsProvider());
+
setEnabled(Settings::self()->lyricProviders());
}
diff --git a/context/ultimatelyrics.h b/context/ultimatelyrics.h
index 3c175105..3eafdcf7 100644
--- a/context/ultimatelyrics.h
+++ b/context/ultimatelyrics.h
@@ -36,6 +36,7 @@ class UltimateLyrics : public QObject {
UltimateLyrics() {}
UltimateLyricsProvider* getNext(int& index);
+ void registerExtra(UltimateLyricsProvider* provider);
const QList getProviders();
void release();
void setEnabled(const QStringList& enabled);
diff --git a/context/ultimatelyricscommandprovider.cpp b/context/ultimatelyricscommandprovider.cpp
new file mode 100644
index 00000000..32cb1402
--- /dev/null
+++ b/context/ultimatelyricscommandprovider.cpp
@@ -0,0 +1,128 @@
+#include "ultimatelyricscommandprovider.h"
+
+#include "network/networkaccessmanager.h"
+
+#include
+#include
+#include
+#include
+
+static bool debugEnabled = false;
+#define DBUG \
+if (debugEnabled) qWarning() << "CommandLyricProvider" << __FUNCTION__
+void UltimateLyricsCommandProvider::enableDebug()
+{
+ debugEnabled = true;
+}
+
+static QString noSpace(const QString& text)
+{
+ QString ret(text);
+ ret.remove(' ');
+ return ret;
+}
+
+static QString firstChar(const QString& text)
+{
+ return text.isEmpty() ? text : text[0].toLower();
+}
+
+static QString titleCase(const QString& text)
+{
+ if (0 == text.length()) {
+ return QString();
+ }
+ if (1 == text.length()) {
+ return text[0].toUpper();
+ }
+ return text[0].toUpper() + text.right(text.length() - 1).toLower();
+}
+
+QString UltimateLyricsCommandProvider::displayName() const
+{
+ return name;
+}
+
+void UltimateLyricsCommandProvider::fetchInfo(int id, Song metadata, bool removeThe)
+{
+ auto artistFixed = metadata.basicArtist();
+ auto titleFixed = metadata.basicTitle();
+
+ if (removeThe && artistFixed.startsWith(constThe)) {
+ artistFixed = artistFixed.mid(constThe.length());
+ }
+
+ QStringList replacedArguments = arguments;
+ for (auto &argument : replacedArguments) {
+ argument = argument.replace(constArtistArg, artistFixed);
+ argument = argument.replace(constArtistLowerArg, artistFixed.toLower());
+ argument = argument.replace(constArtistLowerNoSpaceArg, noSpace(artistFixed.toLower()));
+ argument = argument.replace(constArtistFirstCharArg, firstChar(artistFixed));
+ argument = argument.replace(constAlbumArg, metadata.album);
+ argument = argument.replace(constAlbumLowerArg, metadata.album.toLower());
+ argument = argument.replace(constAlbumLowerNoSpaceArg, noSpace(metadata.album.toLower()));
+ argument = argument.replace(constTitleArg, titleFixed);
+ argument = argument.replace(constTitleLowerArg, titleFixed.toLower());
+ argument = argument.replace(constTitleCaseArg, titleCase(titleFixed));
+ argument = argument.replace(constYearArg, QString::number(metadata.year));
+ argument = argument.replace(constTrackNoArg, QString::number(metadata.track));
+ argument = argument.replace(constDuration, QString::number(metadata.time));
+ }
+
+ QProcess *process = new QProcess(this);
+ processes.append(process);
+
+ connect(process, &QProcess::finished, this, [this, process, id](int exitCode, QProcess::ExitStatus exitStatus) {
+ DBUG << "Process " << executable << "exited with exitCode:" << exitCode << ", and exitStatus:" << exitStatus;
+
+ processes.removeAll(process);
+ process->deleteLater();
+
+ auto output = process->readAllStandardOutput();
+ emit lyricsReady(id, output);
+ });
+
+ connect(process, &QProcess::errorOccurred, this, [this, process](QProcess::ProcessError error) {
+ qCritical() << "UltimateLyricsCommandProvider: Starting command failed:" << error << "err:" << process->errorString();
+
+ processes.removeAll(process);
+ process->deleteLater();
+ });
+
+ DBUG << "Starting " << executable << replacedArguments;
+ process->start(executable, replacedArguments);
+}
+
+void UltimateLyricsCommandProvider::abort()
+{
+ for (const auto process : processes) {
+ process->deleteLater();
+ }
+}
+
+UltimateLyricsProvider* UltimateLyricsCommandProvider::parseProvider(QXmlStreamReader* reader)
+{
+ QXmlStreamAttributes attributes = reader->attributes();
+
+ auto* scraper = new UltimateLyricsCommandProvider;
+ scraper->setName(attributes.value("name").toString());
+ scraper->setExecutable(attributes.value("executable").toString());
+
+ while (!reader->atEnd()) {
+ reader->readNext();
+
+ if (QXmlStreamReader::EndElement == reader->tokenType()) {
+ break;
+ }
+
+ if (QXmlStreamReader::StartElement == reader->tokenType()) {
+ if (QLatin1String("argument") == reader->name()) {
+ scraper->addArgument(reader->attributes().value("value").toString());
+ reader->skipCurrentElement();
+ }
+ }
+ }
+ return scraper;
+}
+
+#include "moc_ultimatelyricscommandprovider.cpp"
diff --git a/context/ultimatelyricscommandprovider.h b/context/ultimatelyricscommandprovider.h
new file mode 100644
index 00000000..8dea8c71
--- /dev/null
+++ b/context/ultimatelyricscommandprovider.h
@@ -0,0 +1,32 @@
+#ifndef ULTIMATELYRICSCOMMANDPROVIDER_H
+#define ULTIMATELYRICSCOMMANDPROVIDER_H
+
+#include "ultimatelyricsprovider.h"
+#include
+#include
+#include
+
+class QXmlStreamReader;
+
+class UltimateLyricsCommandProvider : public UltimateLyricsProvider {
+ Q_OBJECT
+
+public:
+ static void enableDebug();
+
+ QString displayName() const override;
+ void fetchInfo(int id, Song metadata, bool removeThe = false) override;
+ void abort() override;
+
+ void setExecutable(const QString& c) { executable = c; }
+
+ void addArgument(QString value) { arguments.append(value); };
+ static UltimateLyricsProvider* parseProvider(QXmlStreamReader* reader);
+
+private:
+ QString executable;
+ QStringList arguments;
+ QList processes;
+};
+
+#endif//ULTIMATELYRICSCOMMANDPROVIDER_H
diff --git a/context/ultimatelyricshttpprovider.cpp b/context/ultimatelyricshttpprovider.cpp
new file mode 100644
index 00000000..c277df80
--- /dev/null
+++ b/context/ultimatelyricshttpprovider.cpp
@@ -0,0 +1,531 @@
+/*
+ * Cantata
+ *
+ * Copyright (c) 2011-2022 Craig Drummond
+ *
+ */
+/* This file is part of Clementine.
+ Copyright 2010, David Sansome
+
+ Clementine 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.
+
+ Clementine 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 Clementine. If not, see .
+*/
+
+#include "ultimatelyricshttpprovider.h"
+#include "network/networkaccessmanager.h"
+#include
+#include
+#include
+#include
+#include
+static bool debugEnabled = false;
+#define DBUG \
+ if (debugEnabled) qWarning() << "Lyrics" << __FUNCTION__
+void UltimateLyricsHttpProvider::enableDebug()
+{
+ debugEnabled = true;
+}
+
+static QString noSpace(const QString& text)
+{
+ QString ret(text);
+ ret.remove(' ');
+ return ret;
+}
+
+static QString firstChar(const QString& text)
+{
+ return text.isEmpty() ? text : text[0].toLower();
+}
+
+static QString titleCase(const QString& text)
+{
+ if (0 == text.length()) {
+ return QString();
+ }
+ if (1 == text.length()) {
+ return text[0].toUpper();
+ }
+ return text[0].toUpper() + text.right(text.length() - 1).toLower();
+}
+
+static QString doTagReplace(QString str, const Song& song)
+{
+ if (str.contains(QLatin1Char('{'))) {
+ QString artistFixed = song.basicArtist();
+ str.replace(constArtistArg, artistFixed);
+ str.replace(constArtistFirstCharArg, firstChar(artistFixed));
+ str.replace(constAlbumArg, song.album);
+ str.replace(constTitleArg, song.basicTitle());
+ str.replace(constYearArg, QString::number(song.year));
+ str.replace(constTrackNoArg, QString::number(song.track));
+ }
+ return str;
+}
+
+static QString extract(const QString& source, const QString& begin, const QString& end, bool isTag = false)
+{
+ DBUG << "Looking for" << begin << end;
+ int beginIdx = source.indexOf(begin, 0, Qt::CaseInsensitive);
+ bool skipTagClose = false;
+
+ if (-1 == beginIdx && isTag) {
+ beginIdx = source.indexOf(QString(begin).remove(">"), 0, Qt::CaseInsensitive);
+ skipTagClose = true;
+ }
+ if (-1 == beginIdx) {
+ DBUG << "Failed to find begin";
+ return QString();
+ }
+ if (skipTagClose) {
+ int closeIdx = source.indexOf(">", beginIdx);
+ if (-1 != closeIdx) {
+ beginIdx = closeIdx + 1;
+ }
+ else {
+ beginIdx += begin.length();
+ }
+ }
+ else {
+ beginIdx += begin.length();
+ }
+
+ int endIdx = source.indexOf(end, beginIdx, Qt::CaseInsensitive);
+ if (-1 == endIdx && QLatin1String("null") != end) {
+ DBUG << "Failed to find end";
+ return QString();
+ }
+
+ DBUG << "Found match";
+ return source.mid(beginIdx, endIdx - beginIdx - 1);
+}
+
+static QRegularExpression xmlTagRegex = QRegularExpression("<(\\w+).*>");
+static QString extractXmlTag(const QString& source, const QString& tag)
+{
+ DBUG << "Looking for" << tag;
+ auto match = xmlTagRegex.match(tag);
+ if (!match.hasMatch()) {
+ DBUG << "Failed to find tag";
+ return QString();
+ }
+
+ DBUG << "Found match";
+ return extract(source, tag, "" + match.captured(1) + ">", true);
+}
+
+static QString exclude(const QString& source, const QString& begin, const QString& end)
+{
+ int beginIdx = source.indexOf(begin, 0, Qt::CaseInsensitive);
+ if (-1 == beginIdx) {
+ return source;
+ }
+
+ int endIdx = source.indexOf(end, beginIdx + begin.length(), Qt::CaseInsensitive);
+ if (-1 == endIdx) {
+ return source;
+ }
+
+ return source.left(beginIdx) + source.right(source.length() - endIdx - end.length());
+}
+
+static QString excludeXmlTag(const QString& source, const QString& tag)
+{
+ auto match = xmlTagRegex.match(tag);
+ if (!match.hasMatch()) {
+ return source;
+ }
+
+ return exclude(source, tag, "" + match.captured(1) + ">");
+}
+
+static void applyExtractRule(const UltimateLyricsHttpProvider::Rule& rule, QString& content, const Song& song)
+{
+ for (const UltimateLyricsHttpProvider::RuleItem& item : rule) {
+ if (item.second.isNull()) {
+ content = extractXmlTag(content, doTagReplace(item.first, song));
+ }
+ else {
+ content = extract(content, doTagReplace(item.first, song), doTagReplace(item.second, song));
+ }
+ }
+}
+
+static void applyExcludeRule(const UltimateLyricsHttpProvider::Rule& rule, QString& content, const Song& song)
+{
+ for (const UltimateLyricsHttpProvider::RuleItem& item : rule) {
+ if (item.second.isNull()) {
+ content = excludeXmlTag(content, doTagReplace(item.first, song));
+ }
+ else {
+ content = exclude(content, doTagReplace(item.first, song), doTagReplace(item.second, song));
+ }
+ }
+}
+
+static QString urlEncode(QString str)
+{
+ str.replace(QLatin1Char('&'), QLatin1String("%26"));
+ str.replace(QLatin1Char('?'), QLatin1String("%3f"));
+ str.replace(QLatin1Char('+'), QLatin1String("%2b"));
+ return str;
+}
+
+static bool tryWithoutThe(const Song& s)
+{
+ return 0 == s.priority && s.basicArtist().startsWith(constThe);
+}
+
+UltimateLyricsHttpProvider::UltimateLyricsHttpProvider()
+{
+ setEnabled(true);
+ setRelevance(0);
+}
+
+UltimateLyricsHttpProvider::~UltimateLyricsHttpProvider()
+{
+ abort();
+}
+
+QString UltimateLyricsHttpProvider::displayName() const
+{
+ QString n(name);
+ n.replace("(POLISH)", tr("(Polish Translations)"));
+ n.replace("(PORTUGUESE)", tr("(Portuguese Translations)"));
+ return n;
+}
+
+void UltimateLyricsHttpProvider::fetchInfo(int id, Song metadata, bool removeThe)
+{
+ auto converter = QStringDecoder(charset.toLatin1().constData(), QStringConverter::Flag::Default);
+
+ if (!converter.isValid()) {
+ emit lyricsReady(id, QString());
+ return;
+ }
+
+ QString artistFixed = metadata.basicArtist();
+ QString titleFixed = metadata.basicTitle();
+ QString urlText(url);
+
+ if (removeThe && artistFixed.startsWith(constThe)) {
+ artistFixed = artistFixed.mid(constThe.length());
+ }
+
+ if (QLatin1String("lyrics.wikia.com") == name) {
+ QUrl url(urlText);
+ QUrlQuery query;
+
+ query.addQueryItem(QLatin1String("artist"), artistFixed);
+ query.addQueryItem(QLatin1String("song"), titleFixed);
+ query.addQueryItem(QLatin1String("func"), QLatin1String("getSong"));
+ query.addQueryItem(QLatin1String("fmt"), QLatin1String("xml"));
+ url.setQuery(query);
+
+ NetworkJob* reply = NetworkAccessManager::self()->get(url);
+ requests[reply] = id;
+ connect(reply, SIGNAL(finished()), this, SLOT(wikiMediaSearchResponse()));
+ return;
+ }
+
+ metadata.priority = removeThe ? 1 : 0;// HACK Use this to indicate if searching without 'The '
+ songs.insert(id, metadata);
+
+ // Fill in fields in the URL
+ bool urlContainsDetails = urlText.contains(QLatin1Char('{'));
+ if (urlContainsDetails) {
+ doUrlReplace(constArtistArg, artistFixed, urlText);
+ doUrlReplace(constArtistLowerArg, artistFixed.toLower(), urlText);
+ doUrlReplace(constArtistLowerNoSpaceArg, noSpace(artistFixed.toLower()), urlText);
+ doUrlReplace(constArtistFirstCharArg, firstChar(artistFixed), urlText);
+ doUrlReplace(constAlbumArg, metadata.album, urlText);
+ doUrlReplace(constAlbumLowerArg, metadata.album.toLower(), urlText);
+ doUrlReplace(constAlbumLowerNoSpaceArg, noSpace(metadata.album.toLower()), urlText);
+ doUrlReplace(constTitleArg, titleFixed, urlText);
+ doUrlReplace(constTitleLowerArg, titleFixed.toLower(), urlText);
+ doUrlReplace(constTitleCaseArg, titleCase(titleFixed), urlText);
+ doUrlReplace(constYearArg, QString::number(metadata.year), urlText);
+ doUrlReplace(constTrackNoArg, QString::number(metadata.track), urlText);
+ doUrlReplace(constDuration, QString::number(metadata.time), urlText);
+ }
+
+ // For some reason Qt messes up the ? -> %3F and & -> %26 conversions - by placing 25 after the %
+ // So, try and revert this...
+ QUrl url(urlText);
+
+ if (urlContainsDetails) {
+ QByteArray data = url.toEncoded();
+ data.replace("%253F", "%3F");
+ data.replace("%253f", "%3f");
+ data.replace("%2526", "%26");
+ url = QUrl::fromEncoded(data, QUrl::StrictMode);
+ }
+
+ QNetworkRequest req(url);
+ req.setRawHeader("User-Agent", "Mozilla/5.0 (X11; Linux i686; rv:6.0) Gecko/20100101 Firefox/6.0");
+ NetworkJob* reply = NetworkAccessManager::self()->get(req);
+ requests[reply] = id;
+ connect(reply, SIGNAL(finished()), this, SLOT(lyricsFetched()));
+}
+
+void UltimateLyricsHttpProvider::abort()
+{
+ QHash::ConstIterator it(requests.constBegin());
+ QHash::ConstIterator end(requests.constEnd());
+
+ for (; it != end; ++it) {
+ it.key()->cancelAndDelete();
+ }
+ requests.clear();
+ songs.clear();
+}
+
+void UltimateLyricsHttpProvider::wikiMediaSearchResponse()
+{
+ NetworkJob* reply = qobject_cast(sender());
+ if (!reply) {
+ return;
+ }
+
+ int id = requests.take(reply);
+ reply->deleteLater();
+
+ if (!reply->ok()) {
+ Song song = songs.take(id);
+ if (tryWithoutThe(song)) {
+ fetchInfo(id, song, true);
+ }
+ else {
+ emit lyricsReady(id, QString());
+ }
+ return;
+ }
+
+ QUrl url;
+ QXmlStreamReader doc(reply->actualJob());
+ while (!doc.atEnd()) {
+ doc.readNext();
+ if (doc.isStartElement() && QLatin1String("url") == doc.name()) {
+ QString lyricsUrl = doc.readElementText();
+ if (!lyricsUrl.contains(QLatin1String("action=edit"))) {
+ url = QUrl::fromEncoded(lyricsUrl.toUtf8()).toString();
+ }
+ break;
+ }
+ }
+
+ if (url.isValid()) {
+ QString path = url.path();
+ QByteArray u = url.scheme().toLatin1() + "://" + url.host().toLatin1() + "/api.php?action=query&prop=revisions&rvprop=content&format=xml&titles=";
+ QByteArray titles = QUrl::toPercentEncoding(path.startsWith(QLatin1Char('/')) ? path.mid(1) : path).replace('+', "%2b");
+ NetworkJob* reply = NetworkAccessManager::self()->get(QUrl::fromEncoded(u + titles));
+ requests[reply] = id;
+ connect(reply, SIGNAL(finished()), this, SLOT(wikiMediaLyricsFetched()));
+ }
+ else {
+ emit lyricsReady(id, QString());
+ }
+}
+
+void UltimateLyricsHttpProvider::wikiMediaLyricsFetched()
+{
+ NetworkJob* reply = qobject_cast(sender());
+ if (!reply) {
+ return;
+ }
+
+ int id = requests.take(reply);
+ reply->deleteLater();
+
+ if (!reply->ok()) {
+ Song song = songs.take(id);
+ if (tryWithoutThe(song)) {
+ fetchInfo(id, song, true);
+ }
+ else {
+ emit lyricsReady(id, QString());
+ }
+ return;
+ }
+
+ auto fromCharset = QStringDecoder(charset.toLatin1().constData(), QStringConverter::Flag::Default);
+ QString contents = fromCharset(reply->readAll());
+ contents = contents.replace("
", "
");
+ DBUG << name << "response" << contents;
+ emit lyricsReady(id, extract(contents, QLatin1String("<lyrics>"), QLatin1String("</lyrics>")));
+}
+
+void UltimateLyricsHttpProvider::lyricsFetched()
+{
+ NetworkJob* reply = qobject_cast(sender());
+ if (!reply) {
+ return;
+ }
+
+ int id = requests.take(reply);
+ reply->deleteLater();
+ Song song = songs.take(id);
+
+ if (!reply->ok()) {
+ //emit Finished(id);
+ if (tryWithoutThe(song)) {
+ fetchInfo(id, song, true);
+ }
+ else {
+ emit lyricsReady(id, QString());
+ }
+ return;
+ }
+
+ auto decode = QStringDecoder(charset.toLatin1().constData());
+ QString originalContent = decode(reply->readAll());
+ originalContent = originalContent.replace("
", "
");
+
+ DBUG << name << "response" << originalContent;
+ // Check for invalid indicators
+ for (const QString& indicator : invalidIndicators) {
+ if (originalContent.contains(indicator)) {
+ //emit Finished(id);
+ DBUG << name << "invalid";
+ if (tryWithoutThe(song)) {
+ fetchInfo(id, song, true);
+ }
+ else {
+ emit lyricsReady(id, QString());
+ }
+ return;
+ }
+ }
+
+ QString lyrics;
+
+ // Apply extract rules
+ for (const Rule& rule : extractRules) {
+ QString content = originalContent;
+ applyExtractRule(rule, content, song);
+#ifndef Q_OS_WIN
+ content.replace(QLatin1String("\r"), QLatin1String(""));
+#endif
+ content = content.trimmed();
+
+ if (!content.isEmpty()) {
+ lyrics = content;
+ break;
+ }
+ }
+
+ // Apply exclude rules
+ for (const Rule& rule : excludeRules) {
+ applyExcludeRule(rule, lyrics, song);
+ }
+
+ lyrics = lyrics.trimmed();
+ lyrics.replace("
\n", "
");
+ lyrics.replace("
\n", "
");
+ DBUG << name << (lyrics.isEmpty() ? "empty" : "succeeded");
+ if (lyrics.isEmpty() && tryWithoutThe(song)) {
+ fetchInfo(id, song, true);
+ }
+ else {
+ emit lyricsReady(id, lyrics);
+ }
+}
+
+void UltimateLyricsHttpProvider::doUrlReplace(const QString& tag, const QString& value, QString& u) const
+{
+ if (!u.contains(tag)) {
+ return;
+ }
+
+ // Apply URL character replacement
+ QString valueCopy(value);
+ for (const UltimateLyricsHttpProvider::UrlFormat& format : urlFormats) {
+ QRegularExpression re("[" + QRegularExpression::escape(format.first) + "]");
+ valueCopy.replace(re, format.second);
+ }
+ u.replace(tag, urlEncode(valueCopy), Qt::CaseInsensitive);
+}
+
+static QString parseInvalidIndicator(QXmlStreamReader* reader)
+{
+ QString ret = reader->attributes().value("value").toString();
+ reader->skipCurrentElement();
+ return ret;
+}
+
+static UltimateLyricsHttpProvider::Rule parseRule(QXmlStreamReader* reader)
+{
+ UltimateLyricsHttpProvider::Rule ret;
+
+ while (!reader->atEnd()) {
+ reader->readNext();
+
+ if (QXmlStreamReader::EndElement == reader->tokenType()) {
+ break;
+ }
+
+ if (QXmlStreamReader::StartElement == reader->tokenType()) {
+ if (QLatin1String("item") == reader->name()) {
+ QXmlStreamAttributes attr = reader->attributes();
+ if (attr.hasAttribute("tag")) {
+ ret << UltimateLyricsHttpProvider::RuleItem(attr.value("tag").toString(), QString());
+ }
+ else if (attr.hasAttribute("begin")) {
+ ret << UltimateLyricsHttpProvider::RuleItem(attr.value("begin").toString(), attr.value("end").toString());
+ }
+ }
+ reader->skipCurrentElement();
+ }
+ }
+ return ret;
+}
+
+UltimateLyricsProvider* UltimateLyricsHttpProvider::parseProvider(QXmlStreamReader* reader)
+{
+ QXmlStreamAttributes attributes = reader->attributes();
+
+ auto* scraper = new UltimateLyricsHttpProvider;
+ scraper->setName(attributes.value("name").toString());
+ scraper->setCharset(attributes.value("charset").toString());
+ scraper->setUrl(attributes.value("url").toString());
+
+ while (!reader->atEnd()) {
+ reader->readNext();
+
+ if (QXmlStreamReader::EndElement == reader->tokenType()) {
+ break;
+ }
+
+ if (QXmlStreamReader::StartElement == reader->tokenType()) {
+ if (QLatin1String("extract") == reader->name()) {
+ scraper->addExtractRule(parseRule(reader));
+ }
+ else if (QLatin1String("exclude") == reader->name()) {
+ scraper->addExcludeRule(parseRule(reader));
+ }
+ else if (QLatin1String("invalidIndicator") == reader->name()) {
+ scraper->addInvalidIndicator(parseInvalidIndicator(reader));
+ }
+ else if (QLatin1String("urlFormat") == reader->name()) {
+ scraper->addUrlFormat(reader->attributes().value("replace").toString(), reader->attributes().value("with").toString());
+ reader->skipCurrentElement();
+ }
+ else {
+ reader->skipCurrentElement();
+ }
+ }
+ }
+ return scraper;
+}
+
+#include "moc_ultimatelyricshttpprovider.cpp"
diff --git a/context/ultimatelyricshttpprovider.h b/context/ultimatelyricshttpprovider.h
new file mode 100644
index 00000000..a54648b4
--- /dev/null
+++ b/context/ultimatelyricshttpprovider.h
@@ -0,0 +1,87 @@
+/*
+ * Cantata
+ *
+ * Copyright (c) 2011-2022 Craig Drummond
+ *
+ */
+/* This file is part of Clementine.
+ Copyright 2010, David Sansome
+
+ Clementine 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.
+
+ Clementine 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 Clementine. If not, see .
+*/
+
+#ifndef ULTIMATELYRICSHTTPPROVIDER_H
+#define ULTIMATELYRICSHTTPPROVIDER_H
+
+#include "mpd-interface/song.h"
+#include "ultimatelyricsprovider.h"
+#include
+#include
+#include
+#include
+#include
+
+class QXmlStreamReader;
+class NetworkJob;
+
+class UltimateLyricsHttpProvider : public UltimateLyricsProvider {
+ Q_OBJECT
+
+public:
+ static void enableDebug();
+
+ UltimateLyricsHttpProvider();
+ ~UltimateLyricsHttpProvider() override;
+
+ typedef QPair RuleItem;
+ typedef QList Rule;
+ typedef QPair UrlFormat;
+
+ QString displayName() const override;
+ void fetchInfo(int id, Song metadata, bool removeThe = false) override;
+ void abort() override;
+
+ void setUrl(const QString& u) { url = u; }
+ void setCharset(const QString& c) { charset = c; }
+ void addUrlFormat(const QString& replace, const QString& with) { urlFormats << UrlFormat(replace, with); }
+ void addExtractRule(const Rule& rule) { extractRules << rule; }
+ void addExcludeRule(const Rule& rule) { excludeRules << rule; }
+ void addInvalidIndicator(const QString& indicator) { invalidIndicators << indicator; }
+
+ static UltimateLyricsProvider* parseProvider(QXmlStreamReader* reader);
+
+Q_SIGNALS:
+ void lyricsReady(int id, const QString& data);
+
+private Q_SLOTS:
+ void wikiMediaSearchResponse();
+ void wikiMediaLyricsFetched();
+ void lyricsFetched();
+
+private:
+ QString doTagReplace(QString str, const Song& song, bool doAll = true);
+ void doUrlReplace(const QString& tag, const QString& value, QString& u) const;
+
+private:
+ QHash requests;
+ QMap songs;
+ QString url;
+ QString charset;
+ QList urlFormats;
+ QList extractRules;
+ QList excludeRules;
+ QStringList invalidIndicators;
+};
+
+#endif// ULTIMATELYRICSHTTPPROVIDER_H
diff --git a/context/ultimatelyricsprovider.cpp b/context/ultimatelyricsprovider.cpp
index 91b832ea..31564f9e 100644
--- a/context/ultimatelyricsprovider.cpp
+++ b/context/ultimatelyricsprovider.cpp
@@ -1,471 +1,3 @@
-/*
- * Cantata
- *
- * Copyright (c) 2011-2022 Craig Drummond
- *
- */
-/* This file is part of Clementine.
- Copyright 2010, David Sansome
-
- Clementine 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.
-
- Clementine 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 Clementine. If not, see .
-*/
-
#include "ultimatelyricsprovider.h"
-#include "network/networkaccessmanager.h"
-#include
-#include
-#include
-#include
-#include
-static bool debugEnabled = false;
-#define DBUG \
- if (debugEnabled) qWarning() << "Lyrics" << __FUNCTION__
-void UltimateLyricsProvider::enableDebug()
-{
- debugEnabled = true;
-}
-
-static const QString constArtistArg = QLatin1String("{Artist}");
-static const QString constArtistLowerArg = QLatin1String("{artist}");
-static const QString constArtistLowerNoSpaceArg = QLatin1String("{artist2}");
-static const QString constArtistFirstCharArg = QLatin1String("{a}");
-static const QString constAlbumArg = QLatin1String("{Album}");
-static const QString constAlbumLowerArg = QLatin1String("{album}");
-static const QString constAlbumLowerNoSpaceArg = QLatin1String("{album2}");
-static const QString constTitleLowerArg = QLatin1String("{title}");
-static const QString constTitleArg = QLatin1String("{Title}");
-static const QString constTitleCaseArg = QLatin1String("{Title2}");
-static const QString constYearArg = QLatin1String("{year}");
-static const QString constTrackNoArg = QLatin1String("{track}");
-static const QString constThe = QLatin1String("The ");
-
-static QString noSpace(const QString& text)
-{
- QString ret(text);
- ret.remove(' ');
- return ret;
-}
-
-static QString firstChar(const QString& text)
-{
- return text.isEmpty() ? text : text[0].toLower();
-}
-
-static QString titleCase(const QString& text)
-{
- if (0 == text.length()) {
- return QString();
- }
- if (1 == text.length()) {
- return text[0].toUpper();
- }
- return text[0].toUpper() + text.right(text.length() - 1).toLower();
-}
-
-static QString doTagReplace(QString str, const Song& song)
-{
- if (str.contains(QLatin1Char('{'))) {
- QString artistFixed = song.basicArtist();
- str.replace(constArtistArg, artistFixed);
- str.replace(constArtistFirstCharArg, firstChar(artistFixed));
- str.replace(constAlbumArg, song.album);
- str.replace(constTitleArg, song.basicTitle());
- str.replace(constYearArg, QString::number(song.year));
- str.replace(constTrackNoArg, QString::number(song.track));
- }
- return str;
-}
-
-static QString extract(const QString& source, const QString& begin, const QString& end, bool isTag = false)
-{
- DBUG << "Looking for" << begin << end;
- int beginIdx = source.indexOf(begin, 0, Qt::CaseInsensitive);
- bool skipTagClose = false;
-
- if (-1 == beginIdx && isTag) {
- beginIdx = source.indexOf(QString(begin).remove(">"), 0, Qt::CaseInsensitive);
- skipTagClose = true;
- }
- if (-1 == beginIdx) {
- DBUG << "Failed to find begin";
- return QString();
- }
- if (skipTagClose) {
- int closeIdx = source.indexOf(">", beginIdx);
- if (-1 != closeIdx) {
- beginIdx = closeIdx + 1;
- }
- else {
- beginIdx += begin.length();
- }
- }
- else {
- beginIdx += begin.length();
- }
-
- int endIdx = source.indexOf(end, beginIdx, Qt::CaseInsensitive);
- if (-1 == endIdx && QLatin1String("null") != end) {
- DBUG << "Failed to find end";
- return QString();
- }
-
- DBUG << "Found match";
- return source.mid(beginIdx, endIdx - beginIdx - 1);
-}
-
-static QRegularExpression xmlTagRegex = QRegularExpression("<(\\w+).*>");
-static QString extractXmlTag(const QString& source, const QString& tag)
-{
- DBUG << "Looking for" << tag;
- auto match = xmlTagRegex.match(tag);
- if (!match.hasMatch()) {
- DBUG << "Failed to find tag";
- return QString();
- }
-
- DBUG << "Found match";
- return extract(source, tag, "" + match.captured(1) + ">", true);
-}
-
-static QString exclude(const QString& source, const QString& begin, const QString& end)
-{
- int beginIdx = source.indexOf(begin, 0, Qt::CaseInsensitive);
- if (-1 == beginIdx) {
- return source;
- }
-
- int endIdx = source.indexOf(end, beginIdx + begin.length(), Qt::CaseInsensitive);
- if (-1 == endIdx) {
- return source;
- }
-
- return source.left(beginIdx) + source.right(source.length() - endIdx - end.length());
-}
-
-static QString excludeXmlTag(const QString& source, const QString& tag)
-{
- auto match = xmlTagRegex.match(tag);
- if (!match.hasMatch()) {
- return source;
- }
-
- return exclude(source, tag, "" + match.captured(1) + ">");
-}
-
-static void applyExtractRule(const UltimateLyricsProvider::Rule& rule, QString& content, const Song& song)
-{
- for (const UltimateLyricsProvider::RuleItem& item : rule) {
- if (item.second.isNull()) {
- content = extractXmlTag(content, doTagReplace(item.first, song));
- }
- else {
- content = extract(content, doTagReplace(item.first, song), doTagReplace(item.second, song));
- }
- }
-}
-
-static void applyExcludeRule(const UltimateLyricsProvider::Rule& rule, QString& content, const Song& song)
-{
- for (const UltimateLyricsProvider::RuleItem& item : rule) {
- if (item.second.isNull()) {
- content = excludeXmlTag(content, doTagReplace(item.first, song));
- }
- else {
- content = exclude(content, doTagReplace(item.first, song), doTagReplace(item.second, song));
- }
- }
-}
-
-static QString urlEncode(QString str)
-{
- str.replace(QLatin1Char('&'), QLatin1String("%26"));
- str.replace(QLatin1Char('?'), QLatin1String("%3f"));
- str.replace(QLatin1Char('+'), QLatin1String("%2b"));
- return str;
-}
-
-static bool tryWithoutThe(const Song& s)
-{
- return 0 == s.priority && s.basicArtist().startsWith(constThe);
-}
-
-UltimateLyricsProvider::UltimateLyricsProvider()
- : enabled(true), relevance(0)
-{
-}
-
-UltimateLyricsProvider::~UltimateLyricsProvider()
-{
- abort();
-}
-
-QString UltimateLyricsProvider::displayName() const
-{
- QString n(name);
- n.replace("(POLISH)", tr("(Polish Translations)"));
- n.replace("(PORTUGUESE)", tr("(Portuguese Translations)"));
- return n;
-}
-
-void UltimateLyricsProvider::fetchInfo(int id, Song metadata, bool removeThe)
-{
- auto converter = QStringDecoder(charset.toLatin1().constData(), QStringConverter::Flag::Default);
-
- if (!converter.isValid()) {
- emit lyricsReady(id, QString());
- return;
- }
-
- QString artistFixed = metadata.basicArtist();
- QString titleFixed = metadata.basicTitle();
- QString urlText(url);
-
- if (removeThe && artistFixed.startsWith(constThe)) {
- artistFixed = artistFixed.mid(constThe.length());
- }
-
- if (QLatin1String("lyrics.wikia.com") == name) {
- QUrl url(urlText);
- QUrlQuery query;
-
- query.addQueryItem(QLatin1String("artist"), artistFixed);
- query.addQueryItem(QLatin1String("song"), titleFixed);
- query.addQueryItem(QLatin1String("func"), QLatin1String("getSong"));
- query.addQueryItem(QLatin1String("fmt"), QLatin1String("xml"));
- url.setQuery(query);
-
- NetworkJob* reply = NetworkAccessManager::self()->get(url);
- requests[reply] = id;
- connect(reply, SIGNAL(finished()), this, SLOT(wikiMediaSearchResponse()));
- return;
- }
-
- metadata.priority = removeThe ? 1 : 0;// HACK Use this to indicate if searching without 'The '
- songs.insert(id, metadata);
-
- // Fill in fields in the URL
- bool urlContainsDetails = urlText.contains(QLatin1Char('{'));
- if (urlContainsDetails) {
- doUrlReplace(constArtistArg, artistFixed, urlText);
- doUrlReplace(constArtistLowerArg, artistFixed.toLower(), urlText);
- doUrlReplace(constArtistLowerNoSpaceArg, noSpace(artistFixed.toLower()), urlText);
- doUrlReplace(constArtistFirstCharArg, firstChar(artistFixed), urlText);
- doUrlReplace(constAlbumArg, metadata.album, urlText);
- doUrlReplace(constAlbumLowerArg, metadata.album.toLower(), urlText);
- doUrlReplace(constAlbumLowerNoSpaceArg, noSpace(metadata.album.toLower()), urlText);
- doUrlReplace(constTitleArg, titleFixed, urlText);
- doUrlReplace(constTitleLowerArg, titleFixed.toLower(), urlText);
- doUrlReplace(constTitleCaseArg, titleCase(titleFixed), urlText);
- doUrlReplace(constYearArg, QString::number(metadata.year), urlText);
- doUrlReplace(constTrackNoArg, QString::number(metadata.track), urlText);
- }
-
- // For some reason Qt messes up the ? -> %3F and & -> %26 conversions - by placing 25 after the %
- // So, try and revert this...
- QUrl url(urlText);
-
- if (urlContainsDetails) {
- QByteArray data = url.toEncoded();
- data.replace("%253F", "%3F");
- data.replace("%253f", "%3f");
- data.replace("%2526", "%26");
- url = QUrl::fromEncoded(data, QUrl::StrictMode);
- }
-
- QNetworkRequest req(url);
- req.setRawHeader("User-Agent", "Mozilla/5.0 (X11; Linux i686; rv:6.0) Gecko/20100101 Firefox/6.0");
- NetworkJob* reply = NetworkAccessManager::self()->get(req);
- requests[reply] = id;
- connect(reply, SIGNAL(finished()), this, SLOT(lyricsFetched()));
-}
-
-void UltimateLyricsProvider::abort()
-{
- QHash::ConstIterator it(requests.constBegin());
- QHash::ConstIterator end(requests.constEnd());
-
- for (; it != end; ++it) {
- it.key()->cancelAndDelete();
- }
- requests.clear();
- songs.clear();
-}
-
-void UltimateLyricsProvider::wikiMediaSearchResponse()
-{
- NetworkJob* reply = qobject_cast(sender());
- if (!reply) {
- return;
- }
-
- int id = requests.take(reply);
- reply->deleteLater();
-
- if (!reply->ok()) {
- Song song = songs.take(id);
- if (tryWithoutThe(song)) {
- fetchInfo(id, song, true);
- }
- else {
- emit lyricsReady(id, QString());
- }
- return;
- }
-
- QUrl url;
- QXmlStreamReader doc(reply->actualJob());
- while (!doc.atEnd()) {
- doc.readNext();
- if (doc.isStartElement() && QLatin1String("url") == doc.name()) {
- QString lyricsUrl = doc.readElementText();
- if (!lyricsUrl.contains(QLatin1String("action=edit"))) {
- url = QUrl::fromEncoded(lyricsUrl.toUtf8()).toString();
- }
- break;
- }
- }
-
- if (url.isValid()) {
- QString path = url.path();
- QByteArray u = url.scheme().toLatin1() + "://" + url.host().toLatin1() + "/api.php?action=query&prop=revisions&rvprop=content&format=xml&titles=";
- QByteArray titles = QUrl::toPercentEncoding(path.startsWith(QLatin1Char('/')) ? path.mid(1) : path).replace('+', "%2b");
- NetworkJob* reply = NetworkAccessManager::self()->get(QUrl::fromEncoded(u + titles));
- requests[reply] = id;
- connect(reply, SIGNAL(finished()), this, SLOT(wikiMediaLyricsFetched()));
- }
- else {
- emit lyricsReady(id, QString());
- }
-}
-
-void UltimateLyricsProvider::wikiMediaLyricsFetched()
-{
- NetworkJob* reply = qobject_cast(sender());
- if (!reply) {
- return;
- }
-
- int id = requests.take(reply);
- reply->deleteLater();
-
- if (!reply->ok()) {
- Song song = songs.take(id);
- if (tryWithoutThe(song)) {
- fetchInfo(id, song, true);
- }
- else {
- emit lyricsReady(id, QString());
- }
- return;
- }
-
- auto fromCharset = QStringDecoder(charset.toLatin1().constData(), QStringConverter::Flag::Default);
- QString contents = fromCharset(reply->readAll());
- contents = contents.replace("
", "
");
- DBUG << name << "response" << contents;
- emit lyricsReady(id, extract(contents, QLatin1String("<lyrics>"), QLatin1String("</lyrics>")));
-}
-
-void UltimateLyricsProvider::lyricsFetched()
-{
- NetworkJob* reply = qobject_cast(sender());
- if (!reply) {
- return;
- }
-
- int id = requests.take(reply);
- reply->deleteLater();
- Song song = songs.take(id);
-
- if (!reply->ok()) {
- //emit Finished(id);
- if (tryWithoutThe(song)) {
- fetchInfo(id, song, true);
- }
- else {
- emit lyricsReady(id, QString());
- }
- return;
- }
-
- auto decode = QStringDecoder(charset.toLatin1().constData());
- QString originalContent = decode(reply->readAll());
- originalContent = originalContent.replace("
", "
");
-
- DBUG << name << "response" << originalContent;
- // Check for invalid indicators
- for (const QString& indicator : invalidIndicators) {
- if (originalContent.contains(indicator)) {
- //emit Finished(id);
- DBUG << name << "invalid";
- if (tryWithoutThe(song)) {
- fetchInfo(id, song, true);
- }
- else {
- emit lyricsReady(id, QString());
- }
- return;
- }
- }
-
- QString lyrics;
-
- // Apply extract rules
- for (const Rule& rule : extractRules) {
- QString content = originalContent;
- applyExtractRule(rule, content, song);
-#ifndef Q_OS_WIN
- content.replace(QLatin1String("\r"), QLatin1String(""));
-#endif
- content = content.trimmed();
-
- if (!content.isEmpty()) {
- lyrics = content;
- break;
- }
- }
-
- // Apply exclude rules
- for (const Rule& rule : excludeRules) {
- applyExcludeRule(rule, lyrics, song);
- }
-
- lyrics = lyrics.trimmed();
- lyrics.replace("
\n", "
");
- lyrics.replace("
\n", "
");
- DBUG << name << (lyrics.isEmpty() ? "empty" : "succeeded");
- if (lyrics.isEmpty() && tryWithoutThe(song)) {
- fetchInfo(id, song, true);
- }
- else {
- emit lyricsReady(id, lyrics);
- }
-}
-
-void UltimateLyricsProvider::doUrlReplace(const QString& tag, const QString& value, QString& u) const
-{
- if (!u.contains(tag)) {
- return;
- }
-
- // Apply URL character replacement
- QString valueCopy(value);
- for (const UltimateLyricsProvider::UrlFormat& format : urlFormats) {
- QRegularExpression re("[" + QRegularExpression::escape(format.first) + "]");
- valueCopy.replace(re, format.second);
- }
- u.replace(tag, urlEncode(valueCopy), Qt::CaseInsensitive);
-}
-#include "moc_ultimatelyricsprovider.cpp"
+#include "moc_ultimatelyricsprovider.cpp"
\ No newline at end of file
diff --git a/context/ultimatelyricsprovider.h b/context/ultimatelyricsprovider.h
index 80378dbb..9ed2c5da 100644
--- a/context/ultimatelyricsprovider.h
+++ b/context/ultimatelyricsprovider.h
@@ -1,91 +1,48 @@
-/*
- * Cantata
- *
- * Copyright (c) 2011-2022 Craig Drummond
- *
- */
-/* This file is part of Clementine.
- Copyright 2010, David Sansome
-
- Clementine 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.
-
- Clementine 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 Clementine. If not, see .
-*/
-
-#ifndef ULTIMATELYRICSPROVIDER_H
-#define ULTIMATELYRICSPROVIDER_H
+#ifndef LYRICSPROVIDER_H
+#define LYRICSPROVIDER_H
#include "mpd-interface/song.h"
-#include
-#include
#include
-#include
-#include
class NetworkJob;
-class UltimateLyricsProvider : public QObject {
+static const QString constArtistArg = QLatin1String("{Artist}");
+static const QString constArtistLowerArg = QLatin1String("{artist}");
+static const QString constArtistLowerNoSpaceArg = QLatin1String("{artist2}");
+static const QString constArtistFirstCharArg = QLatin1String("{a}");
+static const QString constAlbumArg = QLatin1String("{Album}");
+static const QString constAlbumLowerArg = QLatin1String("{album}");
+static const QString constAlbumLowerNoSpaceArg = QLatin1String("{album2}");
+static const QString constTitleLowerArg = QLatin1String("{title}");
+static const QString constTitleArg = QLatin1String("{Title}");
+static const QString constTitleCaseArg = QLatin1String("{Title2}");
+static const QString constYearArg = QLatin1String("{year}");
+static const QString constTrackNoArg = QLatin1String("{track}");
+static const QString constDuration = QLatin1String("{duration}");
+static const QString constThe = QLatin1String("The ");
+
+class UltimateLyricsProvider : public QObject{
Q_OBJECT
public:
- static void enableDebug();
-
- UltimateLyricsProvider();
- ~UltimateLyricsProvider() override;
-
- typedef QPair RuleItem;
- typedef QList Rule;
- typedef QPair UrlFormat;
+ virtual QString displayName() const { return ""; };
+ virtual void fetchInfo(int id, Song metadata, bool removeThe = false) { (void)id; (void)metadata; (void)removeThe; };
+ virtual void abort() {};
void setName(const QString& n) { name = n; }
- void setUrl(const QString& u) { url = u; }
- void setCharset(const QString& c) { charset = c; }
- void setRelevance(int r) { relevance = r; }
- void addUrlFormat(const QString& replace, const QString& with) { urlFormats << UrlFormat(replace, with); }
- void addExtractRule(const Rule& rule) { extractRules << rule; }
- void addExcludeRule(const Rule& rule) { excludeRules << rule; }
- void addInvalidIndicator(const QString& indicator) { invalidIndicators << indicator; }
QString getName() const { return name; }
- QString displayName() const;
+ void setRelevance(int r) { relevance = r; }
int getRelevance() const { return relevance; }
- void fetchInfo(int id, Song metadata, bool removeThe = false);
bool isEnabled() const { return enabled; }
void setEnabled(bool e) { enabled = e; }
- void abort();
-
-Q_SIGNALS:
- void lyricsReady(int id, const QString& data);
-
-private Q_SLOTS:
- void wikiMediaSearchResponse();
- void wikiMediaLyricsFetched();
- void lyricsFetched();
-private:
- QString doTagReplace(QString str, const Song& song, bool doAll = true);
- void doUrlReplace(const QString& tag, const QString& value, QString& u) const;
+ Q_SIGNALS:
+ void lyricsReady(int id, const QString& data);
-private:
+protected:
bool enabled;
- QHash requests;
- QMap songs;
QString name;
- QString url;
- QString charset;
int relevance;
- QList urlFormats;
- QList extractRules;
- QList excludeRules;
- QStringList invalidIndicators;
};
-#endif// ULTIMATELYRICSPROVIDER_H
+#endif//LYRICSPROVIDER_H
diff --git a/contrib/lrclib.sh b/contrib/lrclib.sh
new file mode 100755
index 00000000..0b98393b
--- /dev/null
+++ b/contrib/lrclib.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env sh
+
+TITLE=$1
+ARTIST=$2
+LENGTH=$3
+
+curl -v --get --data-urlencode="track_name=${TITLE}" --data-urlencode="artist_name=${ARTIST}" --data-urlencode="track_duration=${TIME}" "https://lrclib.net/api/get" | jq -r .plainLyrics
diff --git a/gui/main.cpp b/gui/main.cpp
index e45d91c8..21b8a2c3 100644
--- a/gui/main.cpp
+++ b/gui/main.cpp
@@ -65,6 +65,8 @@
#ifdef Avahi_FOUND
#include "avahidiscovery.h"
#endif
+#include "context/ultimatelyricscommandprovider.h"
+#include "context/ultimatelyricshttpprovider.h"
#include "customactions.h"
#include
@@ -221,7 +223,8 @@ static void installDebugMessageHandler(const QString& cmdLine)
NetworkAccessManager::enableDebug();
}
if (all || QLatin1String("context-lyrics") == area) {
- UltimateLyricsProvider::enableDebug();
+ UltimateLyricsHttpProvider::enableDebug();
+ UltimateLyricsCommandProvider::enableDebug();
}
if (all || QLatin1String("threads") == area) {
ThreadCleaner::enableDebug();