From 1339d18850d10e7021426bbf3edbe3496fe717ef Mon Sep 17 00:00:00 2001 From: Bartkk Date: Wed, 15 Oct 2025 06:30:16 +0200 Subject: [PATCH] Initial additional lyrics providers work --- CMakeLists.txt | 3 + context/lyrics_providers.xml | 61 +- .../lyrics_providers/lrcliblyricsprovider.cpp | 66 +++ .../lyrics_providers/lrcliblyricsprovider.h | 21 + context/ultimatelyrics.cpp | 102 +--- context/ultimatelyrics.h | 1 + context/ultimatelyricscommandprovider.cpp | 128 +++++ context/ultimatelyricscommandprovider.h | 32 ++ context/ultimatelyricshttpprovider.cpp | 531 ++++++++++++++++++ context/ultimatelyricshttpprovider.h | 87 +++ context/ultimatelyricsprovider.cpp | 470 +--------------- context/ultimatelyricsprovider.h | 95 +--- contrib/lrclib.sh | 7 + gui/main.cpp | 5 +- 14 files changed, 968 insertions(+), 641 deletions(-) create mode 100644 context/lyrics_providers/lrcliblyricsprovider.cpp create mode 100644 context/lyrics_providers/lrcliblyricsprovider.h create mode 100644 context/ultimatelyricscommandprovider.cpp create mode 100644 context/ultimatelyricscommandprovider.h create mode 100644 context/ultimatelyricshttpprovider.cpp create mode 100644 context/ultimatelyricshttpprovider.h create mode 100755 contrib/lrclib.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 61e03fbdb..e5f388d82 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 a82171936..48fc36db4 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 000000000..0361c4cef --- /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 000000000..a499dbb63 --- /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 8c98514de..8ad026f16 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 3c175105e..3eafdcf76 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 000000000..32cb14023 --- /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 000000000..8dea8c71c --- /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 000000000..c277df809 --- /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, "", 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, ""); +} + +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 000000000..a54648b47 --- /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 91b832ea9..31564f9e1 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, "", 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, ""); -} - -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 80378dbb9..9ed2c5da4 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 000000000..0b98393b6 --- /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 e45d91c8e..21b8a2c31 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();