Overhaul meta-data search

* Improve coding style
* Remove useless code comments
* Hide legacy providers by default
* Add Tekstowo to have at least one functioning provider for lyrics again
* Enable query logging only if an environment variable is set
* Use Tekstowo in example JavaScript
This commit is contained in:
Martchus 2023-08-06 02:25:53 +02:00
parent dace19b2bf
commit e69278634f
18 changed files with 341 additions and 106 deletions

View File

@ -52,6 +52,7 @@ set(WIDGETS_HEADER_FILES
dbquery/musicbrainz.h
dbquery/makeitpersonal.h
dbquery/lyricswikia.h
dbquery/tekstowo.h
gui/dbquerywidget.h
misc/networkaccessmanager.h
renamingutility/filesystemitem.h
@ -82,6 +83,7 @@ set(WIDGETS_SRC_FILES
dbquery/musicbrainz.cpp
dbquery/makeitpersonal.cpp
dbquery/lyricswikia.cpp
dbquery/tekstowo.cpp
gui/dbquerywidget.cpp
misc/networkaccessmanager.cpp
renamingutility/filesystemitem.cpp

View File

@ -206,6 +206,7 @@ void restore()
v.dbQuery.musicBrainzUrl = settings.value(QStringLiteral("musicbrainzurl")).toString();
v.dbQuery.lyricsWikiaUrl = settings.value(QStringLiteral("lyricwikiurl")).toString();
v.dbQuery.makeItPersonalUrl = settings.value(QStringLiteral("makeitpersonalurl")).toString();
v.dbQuery.tekstowoUrl = settings.value(QStringLiteral("tekstowourl")).toString();
v.dbQuery.coverArtArchiveUrl = settings.value(QStringLiteral("coverartarchiveurl")).toString();
settings.endGroup();
@ -302,6 +303,7 @@ void save()
settings.setValue(QStringLiteral("musicbrainzurl"), v.dbQuery.musicBrainzUrl);
settings.setValue(QStringLiteral("lyricwikiurl"), v.dbQuery.lyricsWikiaUrl);
settings.setValue(QStringLiteral("makeitpersonalurl"), v.dbQuery.makeItPersonalUrl);
settings.setValue(QStringLiteral("tekstowourl"), v.dbQuery.tekstowoUrl);
settings.setValue(QStringLiteral("coverartarchiveurl"), v.dbQuery.coverArtArchiveUrl);
settings.endGroup();

View File

@ -100,6 +100,7 @@ struct DbQuery {
QString coverArtArchiveUrl;
QString lyricsWikiaUrl;
QString makeItPersonalUrl;
QString tekstowoUrl;
};
struct RenamingUtility {

View File

@ -142,6 +142,11 @@ QJSValue UtilityObject::queryMakeItPersonal(const QJSValue &songDescription)
return m_engine->newQObject(QtGui::queryMakeItPersonal(makeSongDescription(songDescription)));
}
QJSValue UtilityObject::queryTekstowo(const QJSValue &songDescription)
{
return m_engine->newQObject(QtGui::queryTekstowo(makeSongDescription(songDescription)));
}
QtGui::SongDescription UtilityObject::makeSongDescription(const QJSValue &obj)
{
auto desc = QtGui::SongDescription(obj.property(QStringLiteral("songId")).toString());

View File

@ -61,6 +61,7 @@ public Q_SLOTS:
QJSValue queryMusicBrainz(const QJSValue &songDescription);
QJSValue queryLyricsWikia(const QJSValue &songDescription);
QJSValue queryMakeItPersonal(const QJSValue &songDescription);
QJSValue queryTekstowo(const QJSValue &songDescription);
private:
static QtGui::SongDescription makeSongDescription(const QJSValue &obj);

View File

@ -3,6 +3,8 @@
#include "../misc/networkaccessmanager.h"
#include "../misc/utility.h"
#include "resources/config.h"
#include <tagparser/signature.h>
#include <tagparser/tag.h>
#include <tagparser/tagvalue.h>
@ -25,7 +27,7 @@ SongDescription::SongDescription(const QString &songId)
}
std::list<QString> QueryResultsModel::s_coverNames = std::list<QString>();
map<QString, QByteArray> QueryResultsModel::s_coverData = map<QString, QByteArray>();
std::map<QString, QByteArray> QueryResultsModel::s_coverData = std::map<QString, QByteArray>();
QueryResultsModel::QueryResultsModel(QObject *parent)
: QAbstractTableModel(parent)
@ -104,7 +106,7 @@ QVariant QueryResultsModel::data(const QModelIndex &index, int role) const
if (!index.isValid() || index.row() >= m_results.size()) {
return QVariant();
}
const SongDescription &res = m_results.at(index.row());
const auto &res = m_results.at(index.row());
switch (role) {
case Qt::DisplayRole:
switch (index.column()) {
@ -146,7 +148,7 @@ QVariant QueryResultsModel::data(const QModelIndex &index, int role) const
Qt::ItemFlags QueryResultsModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
auto flags = Qt::ItemFlags(Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | Qt::ItemIsEnabled);
if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
@ -311,6 +313,16 @@ void HttpResultsModel::handleInitialReplyFinished()
setResultsAvailable(true); // update status, emit resultsAvailable()
}
#ifdef CPP_UTILITIES_DEBUG_BUILD
void HttpResultsModel::logReply(QNetworkReply *reply)
{
static const auto enableQueryLogging = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_ENABLE_QUERY_LOGGING");
if (enableQueryLogging) {
std::cerr << "HTTP query: " << reply->url().toString().toUtf8().data() << std::endl;
}
}
#endif
QNetworkReply *HttpResultsModel::evaluateReplyResults(QNetworkReply *reply, QByteArray &data, bool alwaysFollowRedirection)
{
// delete reply (later)
@ -328,8 +340,10 @@ QNetworkReply *HttpResultsModel::evaluateReplyResults(QNetworkReply *reply, QByt
m_errorList << tr("Server replied no data.");
}
#ifdef CPP_UTILITIES_DEBUG_BUILD
cerr << "Results from HTTP query:" << endl;
cerr << data.data() << endl;
static const auto enableQueryLogging = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_ENABLE_QUERY_LOGGING");
if (enableQueryLogging) {
std::cerr << "Results from HTTP query:\n" << data.data() << '\n';
}
#endif
return nullptr;
}
@ -366,7 +380,7 @@ void HttpResultsModel::abort()
void HttpResultsModel::handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row)
{
QByteArray data;
auto data = QByteArray();
if (auto *const newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&HttpResultsModel::handleCoverReplyFinished, this, newReply, albumId, row));
return;

View File

@ -22,7 +22,7 @@ TAGEDITOR_ENUM_CLASS KnownField : unsigned int;
namespace QtGui {
struct SongDescription {
SongDescription(const QString &songId = QString());
explicit SongDescription(const QString &songId = QString());
QString songId;
QString title;
@ -85,7 +85,7 @@ Q_SIGNALS:
void lyricsAvailable(const QModelIndex &index);
protected:
QueryResultsModel(QObject *parent = nullptr);
explicit QueryResultsModel(QObject *parent = nullptr);
void setResultsAvailable(bool resultsAvailable);
void setFetchingCover(bool fetchingCover);
@ -124,7 +124,7 @@ public:
void abort() override;
protected:
HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
explicit HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
template <class Object, class Function> void addReply(QNetworkReply *reply, Object object, Function handler);
template <class Function> void addReply(QNetworkReply *reply, Function handler);
virtual void parseInitialResults(const QByteArray &data) = 0;
@ -135,17 +135,20 @@ protected:
private Q_SLOTS:
void handleInitialReplyFinished();
#ifdef CPP_UTILITIES_DEBUG_BUILD
void logReply(QNetworkReply *reply);
#endif
protected:
QList<QNetworkReply *> m_replies;
const SongDescription m_initialDescription;
SongDescription m_initialDescription;
};
template <class Object, class Function> inline void HttpResultsModel::addReply(QNetworkReply *reply, Object object, Function handler)
{
(m_replies << reply), connect(reply, &QNetworkReply::finished, object, handler);
#ifdef CPP_UTILITIES_DEBUG_BUILD
std::cerr << "HTTP query: " << reply->url().toString().toUtf8().data() << std::endl;
logReply(reply);
#endif
}
@ -157,7 +160,7 @@ template <class Function> inline void HttpResultsModel::addReply(QNetworkReply *
{
(m_replies << reply), connect(reply, &QNetworkReply::finished, handler);
#ifdef CPP_UTILITIES_DEBUG_BUILD
std::cerr << "HTTP query: " << reply->url().toString().toUtf8().data() << std::endl;
logReply(reply);
#endif
}
@ -165,6 +168,7 @@ QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription);
QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription);
QNetworkReply *queryCoverArtArchive(const QString &albumId);
QueryResultsModel *queryMakeItPersonal(SongDescription &&songDescription);
QueryResultsModel *queryTekstowo(SongDescription &&songDescription);
} // namespace QtGui

View File

@ -19,7 +19,7 @@ using namespace Utility;
namespace QtGui {
static const QString defaultLyricsWikiaUrl(QStringLiteral("https://lyrics.fandom.com"));
static const auto defaultLyricsWikiaUrl = QStringLiteral("https://lyrics.fandom.com");
static QUrl lyricsWikiaApiUrl()
{
@ -43,15 +43,12 @@ LyricsWikiaResultsModel::LyricsWikiaResultsModel(SongDescription &&initialSongDe
bool LyricsWikiaResultsModel::fetchCover(const QModelIndex &index)
{
// FIXME: avoid code duplication with musicbrainz.cpp
// find song description
if (index.parent().isValid() || index.row() >= m_results.size()) {
return true;
}
SongDescription &desc = m_results[index.row()];
// skip if cover is already available
auto &desc = m_results[index.row()];
if (!desc.cover.isEmpty()) {
return true;
}
@ -87,13 +84,12 @@ bool LyricsWikiaResultsModel::fetchCover(const QModelIndex &index)
bool LyricsWikiaResultsModel::fetchLyrics(const QModelIndex &index)
{
// find song description
if (index.parent().isValid() || index.row() >= m_results.size()) {
return true;
}
SongDescription &desc = m_results[index.row()];
// skip if lyrics already present
auto &desc = m_results[index.row()];
if (!desc.lyrics.isEmpty()) {
return true;
}
@ -113,25 +109,24 @@ bool LyricsWikiaResultsModel::fetchLyrics(const QModelIndex &index)
void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
{
// prepare parsing LyricsWikia meta data
beginResetModel();
m_results.clear();
QXmlStreamReader xmlReader(data);
// parse XML tree
auto xmlReader = QXmlStreamReader(data);
// clang-format off
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("getArtistResponse") {
QString artist;
auto artist = QString();
children {
iftag("artist") {
artist = text;
} eliftag("albums") {
children {
iftag("albumResult") {
QString album, year;
QList<SongDescription> songs;
auto album = QString(), year = QString();
auto songs = QList<SongDescription>();
children {
iftag("album") {
album = text;
@ -169,7 +164,7 @@ void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
}
else_skip
}
for (SongDescription &song : m_results) {
for (auto &song : m_results) {
// set the arist which is the same for all results
song.artist = artist;
// set the album ID (album is identified by its artist, year and name)
@ -191,14 +186,13 @@ void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
m_errorList << xmlReader.errorString();
}
// promote changes
endResetModel();
}
QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription &songDescription)
{
// compose URL
QUrlQuery query;
auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("func"), QStringLiteral("getSong"));
query.addQueryItem(QStringLiteral("action"), QStringLiteral("lyrics"));
query.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));
@ -209,22 +203,21 @@ QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription
// specifying album seems to have no effect but also doesn't hurt
query.addQueryItem(QStringLiteral("album"), songDescription.album);
}
QUrl url(lyricsWikiaApiUrl());
auto url = lyricsWikiaApiUrl();
url.setQuery(query);
return Utility::networkAccessManager().get(QNetworkRequest(url));
}
QNetworkReply *LyricsWikiaResultsModel::requestAlbumDetails(const SongDescription &songDescription)
{
QUrl url(lyricsWikiaApiUrl());
auto url = lyricsWikiaApiUrl();
url.setPath(QStringLiteral("/wiki/") + songDescription.albumId);
return Utility::networkAccessManager().get(QNetworkRequest(url));
}
void LyricsWikiaResultsModel::handleSongDetailsFinished(QNetworkReply *reply, int row)
{
QByteArray data;
auto data = QByteArray();
if (auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&LyricsWikiaResultsModel::handleSongDetailsFinished, this, newReply, row));
} else if (!data.isEmpty()) {
@ -234,23 +227,21 @@ void LyricsWikiaResultsModel::handleSongDetailsFinished(QNetworkReply *reply, in
void LyricsWikiaResultsModel::parseSongDetails(int row, const QByteArray &data)
{
// find associated result/desc
if (row >= m_results.size()) {
m_errorList << tr("Internal error: context for song details reply invalid");
setResultsAvailable(true);
return;
}
SongDescription &assocDesc = m_results[row];
QUrl parsedUrl;
// parse XML tree
auto &assocDesc = m_results[row];
auto parsedUrl = QUrl();
auto xmlReader = QXmlStreamReader(data);
// clang-format off
QXmlStreamReader xmlReader(data);
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("LyricsResult") {
SongDescription parsedDesc;
auto parsedDesc = SongDescription();
children {
iftag("artist") {
parsedDesc.artist = text;
@ -297,7 +288,7 @@ void LyricsWikiaResultsModel::parseSongDetails(int row, const QByteArray &data)
.arg(assocDesc.artist, assocDesc.title);
}
// -> do not use parsed URL "as-is" in any case to avoid unintended requests
QUrl requestUrl(lyricsWikiaApiUrl());
auto requestUrl = lyricsWikiaApiUrl();
requestUrl.setPath(parsedUrl.path());
// -> initialize the actual request
auto *const reply = Utility::networkAccessManager().get(QNetworkRequest(requestUrl));
@ -306,7 +297,7 @@ void LyricsWikiaResultsModel::parseSongDetails(int row, const QByteArray &data)
void LyricsWikiaResultsModel::handleLyricsReplyFinished(QNetworkReply *reply, int row)
{
QByteArray data;
auto data = QByteArray();
if (auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&LyricsWikiaResultsModel::handleLyricsReplyFinished, this, newReply, row));
return;
@ -321,18 +312,15 @@ void LyricsWikiaResultsModel::handleLyricsReplyFinished(QNetworkReply *reply, in
void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data)
{
// find associated result/desc
if (row >= m_results.size()) {
m_errorList << tr("Internal error: context for LyricsWikia page reply invalid");
setResultsAvailable(true);
return;
}
SongDescription &assocDesc = m_results[row];
// convert data to QString
const QString html(data);
// parse lyrics from HTML
auto &assocDesc = m_results[row];
const auto html = QString(data);
const auto lyricsStart = html.indexOf(QLatin1String("<div class='lyricbox'>"));
if (lyricsStart < 0) {
m_errorList << tr("Song details requested for %1/%2 do not contain lyrics").arg(assocDesc.artist, assocDesc.title);
@ -340,7 +328,7 @@ void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data
return;
}
const auto lyricsEnd = html.indexOf(QLatin1String("<div class='lyricsbreak'></div>"), lyricsStart);
QTextDocument textDoc;
auto textDoc = QTextDocument();
textDoc.setHtml(html.mid(lyricsStart, (lyricsEnd > lyricsStart) ? (lyricsEnd - lyricsStart) : -1));
assocDesc.lyrics = textDoc.toPlainText();
@ -350,7 +338,7 @@ void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data
void LyricsWikiaResultsModel::handleAlbumDetailsReplyFinished(QNetworkReply *reply, int row)
{
QByteArray data;
auto data = QByteArray();
if (auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&LyricsWikiaResultsModel::handleAlbumDetailsReplyFinished, this, newReply, row));
} else {
@ -367,19 +355,16 @@ void LyricsWikiaResultsModel::parseAlbumDetailsAndFetchCover(int row, const QByt
return;
}
// find associated result/desc
if (row >= m_results.size()) {
m_errorList << tr("Internal error: context for LyricsWikia page reply invalid");
setFetchingCover(false);
setResultsAvailable(true);
return;
}
SongDescription &assocDesc = m_results[row];
// convert data to QString
const auto html = QString(data);
// parse cover URL from HTML
auto &assocDesc = m_results[row];
const auto html = QString(data);
const auto coverDivStart = html.indexOf(QLatin1String("<div class=\"plainlinks\" style=\"clear:right; float:right;")) + 56;
if (coverDivStart > 56) {
const auto coverHrefStart = html.indexOf(QLatin1String("href=\""), coverDivStart) + 6;
@ -410,31 +395,28 @@ QUrl LyricsWikiaResultsModel::webUrl(const QModelIndex &index)
return QUrl();
}
SongDescription &desc = m_results[index.row()];
auto &desc = m_results[index.row()];
lazyInitializeLyricsWikiaSongId(desc);
// return URL
QUrl url(lyricsWikiaApiUrl());
auto url = lyricsWikiaApiUrl();
url.setPath(QStringLiteral("/wiki/") + desc.songId);
return url;
}
QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription)
{
// compose URL
QUrlQuery query;
auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("func"), QStringLiteral("getArtist"));
query.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));
query.addQueryItem(QStringLiteral("fixXML"), QString());
query.addQueryItem(QStringLiteral("artist"), songDescription.artist);
QUrl url(lyricsWikiaApiUrl());
auto url = lyricsWikiaApiUrl();
url.setQuery(query);
return new LyricsWikiaResultsModel(std::move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url)));
// NOTE: Only getArtist seems to work, so artist must be specified and filtering must
// be done manually when parsing results.
// make request
return new LyricsWikiaResultsModel(std::move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url)));
}
} // namespace QtGui

View File

@ -11,7 +11,7 @@ class LyricsWikiaResultsModel : public HttpResultsModel {
Q_OBJECT
public:
LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
explicit LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchCover(const QModelIndex &index) override;
bool fetchLyrics(const QModelIndex &index) override;
QUrl webUrl(const QModelIndex &index) override;

View File

@ -37,32 +37,25 @@ bool MakeItPersonalResultsModel::fetchLyrics(const QModelIndex &index)
void MakeItPersonalResultsModel::parseInitialResults(const QByteArray &data)
{
// prepare parsing meta data
beginResetModel();
m_results.clear();
SongDescription desc = m_initialDescription;
auto desc = m_initialDescription;
desc.songId = m_initialDescription.artist + m_initialDescription.title;
desc.artistId = m_initialDescription.artist;
desc.lyrics = QString::fromUtf8(data).trimmed();
if (desc.lyrics != QLatin1String("Sorry, We don't have lyrics for this song yet.")) {
m_results << std::move(desc);
}
// promote changes
endResetModel();
}
QueryResultsModel *queryMakeItPersonal(SongDescription &&songDescription)
{
// compose URL
QUrlQuery query;
auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("artist"), songDescription.artist);
query.addQueryItem(QStringLiteral("title"), songDescription.title);
QUrl url(makeItPersonalApiUrl());
auto url = makeItPersonalApiUrl();
url.setQuery(query);
// make request
return new MakeItPersonalResultsModel(std::move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url)));
}

View File

@ -11,7 +11,7 @@ class MakeItPersonalResultsModel : public HttpResultsModel {
Q_OBJECT
public:
MakeItPersonalResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
explicit MakeItPersonalResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchLyrics(const QModelIndex &index) override;
protected:

View File

@ -40,15 +40,12 @@ MusicBrainzResultsModel::MusicBrainzResultsModel(SongDescription &&initialSongDe
bool MusicBrainzResultsModel::fetchCover(const QModelIndex &index)
{
// FIXME: avoid code duplication with lyricswikia.cpp
// find song description
if (index.parent().isValid() || index.row() >= m_results.size()) {
return true;
}
SongDescription &desc = m_results[index.row()];
// skip if cover is already available
auto &desc = m_results[index.row()];
if (!desc.cover.isEmpty()) {
return true;
}
@ -84,17 +81,16 @@ QUrl MusicBrainzResultsModel::webUrl(const QModelIndex &index)
void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
{
// prepare parsing MusicBrainz meta data
beginResetModel();
m_results.clear();
// store all song information (called recordings by MusicBrainz)
vector<SongDescription> recordings;
auto recordings = std::vector<SongDescription>();
// store all albums/collections (called releases by MusicBrainz) for a song
unordered_map<QString, vector<SongDescription>> releasesByRecording;
auto releasesByRecording = std::unordered_map<QString, std::vector<SongDescription>>();
// parse XML tree
QXmlStreamReader xmlReader(data);
auto xmlReader = QXmlStreamReader(data);
// clang-format off
#include <qtutilities/misc/xmlparsermacros.h>
children {
@ -103,7 +99,7 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
iftag("recording-list") {
children {
iftag("recording") {
SongDescription currentDescription(attribute("id").toString());
auto currentDescription = SongDescription(attribute("id").toString());
children {
iftag("title") {
currentDescription.title = text;
@ -130,7 +126,7 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
} eliftag("release-list") {
children {
iftag("release") {
SongDescription releaseInfo;
auto releaseInfo = SongDescription();
releaseInfo.albumId = attribute("id").toString();
children {
iftag("title") {
@ -220,7 +216,7 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
// populate results
// -> create a song for each recording/release combination and group those songs by their releases sorted ascendingly from oldest to latest
map<QString, vector<SongDescription>> recordingsByRelease;
auto recordingsByRelease = std::map<QString, std::vector<SongDescription>>();
for (const auto &recording : recordings) {
const auto &releases = releasesByRecording[recording.songId];
for (const auto &release : releases) {
@ -272,7 +268,6 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
m_errorList << xmlReader.errorString();
}
// promote changes
endResetModel();
}
// clang-format on
@ -281,8 +276,7 @@ QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription)
{
static const auto defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/"));
// compose parts
QStringList parts;
auto parts = QStringList();
parts.reserve(4);
if (!songDescription.title.isEmpty()) {
parts << QChar('\"') % songDescription.title % QChar('\"');
@ -297,15 +291,12 @@ QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription)
parts << QStringLiteral("number:") + QString::number(songDescription.track);
}
// compose URL
const auto &musicBrainzUrl = Settings::values().dbQuery.musicBrainzUrl;
QUrl url(musicBrainzUrl.isEmpty() ? defaultMusicBrainzUrl : (musicBrainzUrl + QStringLiteral("/recording/")));
QUrlQuery query;
auto url = QUrl(musicBrainzUrl.isEmpty() ? defaultMusicBrainzUrl : (musicBrainzUrl + QStringLiteral("/recording/")));
auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("query"), parts.join(QStringLiteral(" AND ")));
url.setQuery(query);
// make request
QNetworkRequest request(url);
auto request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("Mozilla/5.0 (X11; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"));
return new MusicBrainzResultsModel(std::move(songDescription), Utility::networkAccessManager().get(request));
}

View File

@ -15,7 +15,7 @@ private:
enum What { MusicBrainzMetaData, CoverArt };
public:
MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
explicit MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchCover(const QModelIndex &index) override;
QUrl webUrl(const QModelIndex &index) override;

156
dbquery/tekstowo.cpp Normal file
View File

@ -0,0 +1,156 @@
#include "./tekstowo.h"
#include "../application/settings.h"
#include "../misc/networkaccessmanager.h"
#include "../misc/utility.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QTextDocumentFragment>
#include <QUrl>
#include <functional>
using namespace std;
using namespace std::placeholders;
using namespace Utility;
namespace QtGui {
static const auto defaultTekstowoUrl = QStringLiteral("https://www.tekstowo.pl");
static QUrl tekstowoUrl()
{
const auto &url = Settings::values().dbQuery.tekstowoUrl;
return QUrl(url.isEmpty() ? defaultTekstowoUrl : url);
}
TekstowoResultsModel::TekstowoResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply)
: HttpResultsModel(std::move(initialSongDescription), reply)
{
}
bool TekstowoResultsModel::fetchLyrics(const QModelIndex &index)
{
if ((index.parent().isValid() || index.row() >= m_results.size()) && !m_results[index.row()].lyrics.isEmpty()) {
return true;
}
const auto url = webUrl(index);
if (url.isEmpty()) {
m_errorList << tr("Unable to fetch lyrics: web URL is unknown.");
emit resultsAvailable();
return true;
}
auto *reply = Utility::networkAccessManager().get(QNetworkRequest(url));
addReply(reply, bind(&TekstowoResultsModel::handleLyricsReplyFinished, this, reply, index.row()));
return false;
}
void TekstowoResultsModel::parseInitialResults(const QByteArray &data)
{
beginResetModel();
m_results.clear();
auto dropLast = false;
auto hasExactMatch = false;
auto exactMatch = QList<SongDescription>::size_type();
for (auto index = exactMatch; index >= 0;) {
const auto linkStart = data.indexOf("<a href=\"/piosenka,", index);
if (linkStart < 0) {
break;
}
const auto hrefStart = linkStart + 9;
const auto hrefEnd = data.indexOf("\"", hrefStart + 1);
if (hrefEnd <= hrefStart) {
break;
}
const auto linkEnd = data.indexOf("</a>", hrefEnd);
if (linkEnd < linkStart) {
break;
}
index = linkEnd + 4;
auto linkText = QTextDocumentFragment::fromHtml(QString::fromUtf8(data.begin() + linkStart, linkEnd + 3 - linkStart)).toPlainText().trimmed();
auto titleStart = linkText.indexOf(QLatin1String(" - "));
auto &songDetails = dropLast ? m_results.back() : m_results.emplace_back();
songDetails.songId = QTextDocumentFragment::fromHtml(QString::fromUtf8(data.begin() + hrefStart, hrefEnd - hrefStart)).toPlainText();
if (titleStart > -1) {
songDetails.artist = linkText.mid(0, titleStart);
if (songDetails.artist != m_initialDescription.artist) {
dropLast = true;
continue;
}
songDetails.title = linkText.mid(titleStart + 3);
} else {
songDetails.title = std::move(linkText);
}
if (!hasExactMatch && songDetails.title == m_initialDescription.title) {
hasExactMatch = true;
exactMatch = m_results.size() - 1;
}
dropLast = false;
}
if (dropLast) {
m_results.pop_back();
}
// ensure the first exact match for the song title is placed first
if (hasExactMatch && exactMatch != 0) {
std::swap(m_results[exactMatch], m_results[0]);
}
endResetModel();
}
void TekstowoResultsModel::handleLyricsReplyFinished(QNetworkReply *reply, int row)
{
auto data = QByteArray();
if (auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&TekstowoResultsModel::handleLyricsReplyFinished, this, newReply, row));
return;
}
if (!data.isEmpty()) {
parseLyricsResults(row, data);
}
if (!m_resultsAvailable) {
setResultsAvailable(true);
}
}
void TekstowoResultsModel::parseLyricsResults(int row, const QByteArray &data)
{
if (row >= m_results.size()) {
m_errorList << tr("Internal error: context for Tekstowo page reply invalid");
setResultsAvailable(true);
return;
}
auto lyricsStart = data.indexOf("<div class=\"inner-text\">");
if (lyricsStart < 0) {
const auto &assocDesc = m_results[row];
m_errorList << tr("Song details requested for %1/%2 do not contain lyrics").arg(assocDesc.artist, assocDesc.title);
setResultsAvailable(true);
return;
}
const auto lyricsEnd = data.indexOf("</div>", lyricsStart += 24); // hopefully lyrics don't contain nested </div>
m_results[row].lyrics = QTextDocumentFragment::fromHtml(
QString::fromUtf8(data.data() + lyricsStart, lyricsEnd > -1 ? lyricsEnd - lyricsStart : data.size() - lyricsStart))
.toPlainText()
.trimmed();
setResultsAvailable(true);
emit lyricsAvailable(index(row, 0));
}
QUrl TekstowoResultsModel::webUrl(const QModelIndex &index)
{
if (index.parent().isValid() || index.row() >= results().size()) {
return QUrl();
}
auto url = tekstowoUrl();
url.setPath(m_results[index.row()].songId);
return url;
}
QueryResultsModel *queryTekstowo(SongDescription &&songDescription)
{
auto url = tekstowoUrl();
url.setPath(QStringLiteral("/szukaj,wykonawca,%1,tytul,%2.html").arg(songDescription.artist, songDescription.title));
return new TekstowoResultsModel(std::move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url)));
}
} // namespace QtGui

28
dbquery/tekstowo.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef QTGUI_TEKSTOWO_H
#define QTGUI_TEKSTOWO_H
#include "./dbquery.h"
#include <map>
namespace QtGui {
class TekstowoResultsModel : public HttpResultsModel {
Q_OBJECT
public:
explicit TekstowoResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchLyrics(const QModelIndex &index) override;
QUrl webUrl(const QModelIndex &index) override;
protected:
void parseInitialResults(const QByteArray &data) override;
private:
void handleLyricsReplyFinished(QNetworkReply *reply, int row);
void parseLyricsResults(int row, const QByteArray &data);
};
} // namespace QtGui
#endif // QTGUI_TEKSTOWO_H

View File

@ -7,6 +7,7 @@
#include "../dbquery/dbquery.h"
#include "../misc/utility.h"
#include "resources/config.h"
#include "ui_dbquerywidget.h"
#include <tagparser/tag.h>
@ -47,6 +48,13 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent)
, m_coverIndex(-1)
, m_lyricsIndex(-1)
, m_menu(new QMenu(parent))
, m_insertPresentDataAction(nullptr)
, m_searchMusicBrainzAction(nullptr)
, m_searchLyricsWikiaAction(nullptr)
, m_searchMakeItPersonalAction(nullptr)
, m_searchTekstowoAction(nullptr)
, m_lastSearchAction(nullptr)
, m_refreshAutomaticallyAction(nullptr)
{
m_ui->setupUi(this);
updateStyleSheet();
@ -72,20 +80,27 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent)
// setup menu
const auto searchIcon = QIcon::fromTheme(QStringLiteral("search"));
const auto enableLegacyProvider = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_ENABLE_LEGACY_METADATA_PROVIDERS");
m_menu->setTitle(tr("New search"));
m_menu->setIcon(searchIcon);
m_searchMusicBrainzAction = m_lastSearchAction = m_menu->addAction(tr("Query MusicBrainz"));
m_searchMusicBrainzAction->setIcon(searchIcon);
m_searchMusicBrainzAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_M));
connect(m_searchMusicBrainzAction, &QAction::triggered, this, &DbQueryWidget::searchMusicBrainz);
m_searchLyricsWikiaAction = m_menu->addAction(tr("Query LyricsWikia"));
m_searchLyricsWikiaAction->setIcon(searchIcon);
m_searchLyricsWikiaAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_L));
connect(m_searchLyricsWikiaAction, &QAction::triggered, this, &DbQueryWidget::searchLyricsWikia);
m_searchMakeItPersonalAction = m_menu->addAction(tr("Query makeitpersonal"));
m_searchMakeItPersonalAction->setIcon(searchIcon);
m_searchMakeItPersonalAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_K));
connect(m_searchMakeItPersonalAction, &QAction::triggered, this, &DbQueryWidget::searchMakeItPersonal);
if (enableLegacyProvider) {
m_searchLyricsWikiaAction = m_menu->addAction(tr("Query LyricsWikia"));
m_searchLyricsWikiaAction->setIcon(searchIcon);
m_searchLyricsWikiaAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_L));
connect(m_searchLyricsWikiaAction, &QAction::triggered, this, &DbQueryWidget::searchLyricsWikia);
m_searchMakeItPersonalAction = m_menu->addAction(tr("Query makeitpersonal"));
m_searchMakeItPersonalAction->setIcon(searchIcon);
m_searchMakeItPersonalAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_K));
connect(m_searchMakeItPersonalAction, &QAction::triggered, this, &DbQueryWidget::searchMakeItPersonal);
}
m_searchTekstowoAction = m_menu->addAction(tr("Query Tekstowo"));
m_searchTekstowoAction->setIcon(searchIcon);
m_searchTekstowoAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_T));
connect(m_searchTekstowoAction, &QAction::triggered, this, &DbQueryWidget::searchTekstowo);
m_menu->addSeparator();
m_insertPresentDataAction = m_menu->addAction(tr("Use present data as search criteria"));
m_insertPresentDataAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
@ -150,7 +165,7 @@ void DbQueryWidget::insertSearchTermsFromTagEdit(TagEdit *tagEdit, bool songSpec
m_ui->titleLineEdit->setText(newTitle);
somethingChanged = true;
}
if (m_lastSearchAction != m_searchMakeItPersonalAction) {
if (m_lastSearchAction != m_searchTekstowoAction && m_lastSearchAction != m_searchMakeItPersonalAction) {
const auto newTrackNumber = tagEdit->trackNumber();
if (m_ui->trackSpinBox->value() != newTrackNumber) {
m_ui->trackSpinBox->setValue(newTrackNumber);
@ -246,6 +261,30 @@ void DbQueryWidget::searchMakeItPersonal()
useQueryResults(queryMakeItPersonal(currentSongDescription()));
}
void DbQueryWidget::searchTekstowo()
{
m_lastSearchAction = m_searchMakeItPersonalAction;
// check whether enough search terms are supplied
if (m_ui->artistLineEdit->text().isEmpty() || m_ui->titleLineEdit->text().isEmpty()) {
m_ui->notificationLabel->setNotificationType(NotificationType::Critical);
m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied - artist and title are mandatory"));
return;
}
// delete current model
m_ui->resultsTreeView->setModel(nullptr);
delete m_model;
// show status
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
m_ui->notificationLabel->setText(tr("Retrieving lyrics from Tekstowo ..."));
setStatus(false);
// do actual query
useQueryResults(queryTekstowo(currentSongDescription()));
}
void DbQueryWidget::abortSearch()
{
if (!m_model) {
@ -331,7 +370,9 @@ void DbQueryWidget::setStatus(bool aborted)
{
m_ui->abortPushButton->setVisible(!aborted);
m_searchMusicBrainzAction->setEnabled(aborted);
m_searchLyricsWikiaAction->setEnabled(aborted);
if (m_searchLyricsWikiaAction) {
m_searchLyricsWikiaAction->setEnabled(aborted);
}
m_ui->applyPushButton->setVisible(aborted);
}

View File

@ -39,6 +39,7 @@ public Q_SLOTS:
void searchMusicBrainz();
void searchLyricsWikia();
void searchMakeItPersonal();
void searchTekstowo();
void abortSearch();
void applySelectedResults();
void applySpecifiedResults(const QModelIndex &modelIndex);
@ -82,6 +83,7 @@ private:
QAction *m_searchMusicBrainzAction;
QAction *m_searchLyricsWikiaAction;
QAction *m_searchMakeItPersonalAction;
QAction *m_searchTekstowoAction;
QAction *m_lastSearchAction;
QAction *m_refreshAutomaticallyAction;
QPoint m_contextMenuPos;

View File

@ -1,12 +1,12 @@
import * as http from "http.js"
const cache = {};
function waitFor(signal) {
signal.connect(() => { utility.exit(); });
utility.exec();
}
function queryMakeItPersonal(searchCriteria) {
const lyricsModel = utility.queryMakeItPersonal(searchCriteria);
function queryProvider(provider, searchCriteria) {
const lyricsModel = utility["query" + provider](searchCriteria);
if (!lyricsModel.areResultsAvailable) {
waitFor(lyricsModel.resultsAvailable);
}
@ -20,8 +20,21 @@ function queryMakeItPersonal(searchCriteria) {
return lyrics;
}
function queryProviders(providers, searchCriteria) {
for (const provider of providers) {
const res = queryProvider(provider, searchCriteria);
if (res) {
return res;
}
}
}
export function queryLyrics(searchCriteria) {
return queryMakeItPersonal(searchCriteria);
const cacheKey = searchCriteria.title + "_" + searchCriteria.artist;
const cachedValue = cache[cacheKey];
return cachedValue
? cachedValue
: cache[cacheKey] = queryProviders(["Tekstowo", "MakeItPersonal"], searchCriteria);
}