diff --git a/CMakeLists.txt b/CMakeLists.txt index dae07f4..43e46ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/application/settings.cpp b/application/settings.cpp index da463fd..5cd524b 100644 --- a/application/settings.cpp +++ b/application/settings.cpp @@ -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(); diff --git a/application/settings.h b/application/settings.h index 998530c..edafa7c 100644 --- a/application/settings.h +++ b/application/settings.h @@ -100,6 +100,7 @@ struct DbQuery { QString coverArtArchiveUrl; QString lyricsWikiaUrl; QString makeItPersonalUrl; + QString tekstowoUrl; }; struct RenamingUtility { diff --git a/cli/mediafileinfoobject.cpp b/cli/mediafileinfoobject.cpp index 1bac823..70bd816 100644 --- a/cli/mediafileinfoobject.cpp +++ b/cli/mediafileinfoobject.cpp @@ -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()); diff --git a/cli/mediafileinfoobject.h b/cli/mediafileinfoobject.h index 020a3ae..437c7b8 100644 --- a/cli/mediafileinfoobject.h +++ b/cli/mediafileinfoobject.h @@ -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); diff --git a/dbquery/dbquery.cpp b/dbquery/dbquery.cpp index b613a54..cede1b0 100644 --- a/dbquery/dbquery.cpp +++ b/dbquery/dbquery.cpp @@ -3,6 +3,8 @@ #include "../misc/networkaccessmanager.h" #include "../misc/utility.h" +#include "resources/config.h" + #include #include #include @@ -25,7 +27,7 @@ SongDescription::SongDescription(const QString &songId) } std::list QueryResultsModel::s_coverNames = std::list(); -map QueryResultsModel::s_coverData = map(); +std::map QueryResultsModel::s_coverData = std::map(); 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; diff --git a/dbquery/dbquery.h b/dbquery/dbquery.h index 3ae0484..a9c1b6f 100644 --- a/dbquery/dbquery.h +++ b/dbquery/dbquery.h @@ -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 void addReply(QNetworkReply *reply, Object object, Function handler); template 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 m_replies; - const SongDescription m_initialDescription; + SongDescription m_initialDescription; }; template 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 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 diff --git a/dbquery/lyricswikia.cpp b/dbquery/lyricswikia.cpp index 2baa28b..292d981 100644 --- a/dbquery/lyricswikia.cpp +++ b/dbquery/lyricswikia.cpp @@ -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 children { iftag("getArtistResponse") { - QString artist; + auto artist = QString(); children { iftag("artist") { artist = text; } eliftag("albums") { children { iftag("albumResult") { - QString album, year; - QList songs; + auto album = QString(), year = QString(); + auto songs = QList(); 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 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("
")); 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("
"), 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("
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 diff --git a/dbquery/lyricswikia.h b/dbquery/lyricswikia.h index 9cbefca..3ef4f93 100644 --- a/dbquery/lyricswikia.h +++ b/dbquery/lyricswikia.h @@ -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; diff --git a/dbquery/makeitpersonal.cpp b/dbquery/makeitpersonal.cpp index 3ea7c3c..cd9922e 100644 --- a/dbquery/makeitpersonal.cpp +++ b/dbquery/makeitpersonal.cpp @@ -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))); } diff --git a/dbquery/makeitpersonal.h b/dbquery/makeitpersonal.h index e140508..8d543ec 100644 --- a/dbquery/makeitpersonal.h +++ b/dbquery/makeitpersonal.h @@ -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: diff --git a/dbquery/musicbrainz.cpp b/dbquery/musicbrainz.cpp index b5ffef5..bba3f02 100644 --- a/dbquery/musicbrainz.cpp +++ b/dbquery/musicbrainz.cpp @@ -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 recordings; + auto recordings = std::vector(); // store all albums/collections (called releases by MusicBrainz) for a song - unordered_map> releasesByRecording; + auto releasesByRecording = std::unordered_map>(); // parse XML tree - QXmlStreamReader xmlReader(data); + auto xmlReader = QXmlStreamReader(data); // clang-format off #include 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> recordingsByRelease; + auto recordingsByRelease = std::map>(); 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)); } diff --git a/dbquery/musicbrainz.h b/dbquery/musicbrainz.h index 264f87e..0c714be 100644 --- a/dbquery/musicbrainz.h +++ b/dbquery/musicbrainz.h @@ -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; diff --git a/dbquery/tekstowo.cpp b/dbquery/tekstowo.cpp new file mode 100644 index 0000000..3c3315d --- /dev/null +++ b/dbquery/tekstowo.cpp @@ -0,0 +1,156 @@ +#include "./tekstowo.h" + +#include "../application/settings.h" +#include "../misc/networkaccessmanager.h" +#include "../misc/utility.h" + +#include +#include +#include +#include + +#include + +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::size_type(); + for (auto index = exactMatch; index >= 0;) { + const auto linkStart = data.indexOf("", 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("
"); + 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("
", lyricsStart += 24); // hopefully lyrics don't contain nested
+ 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 diff --git a/dbquery/tekstowo.h b/dbquery/tekstowo.h new file mode 100644 index 0000000..312b7cf --- /dev/null +++ b/dbquery/tekstowo.h @@ -0,0 +1,28 @@ +#ifndef QTGUI_TEKSTOWO_H +#define QTGUI_TEKSTOWO_H + +#include "./dbquery.h" + +#include + +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 diff --git a/gui/dbquerywidget.cpp b/gui/dbquerywidget.cpp index 7a6d1a9..7c2866c 100644 --- a/gui/dbquerywidget.cpp +++ b/gui/dbquerywidget.cpp @@ -7,6 +7,7 @@ #include "../dbquery/dbquery.h" #include "../misc/utility.h" +#include "resources/config.h" #include "ui_dbquerywidget.h" #include @@ -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); } diff --git a/gui/dbquerywidget.h b/gui/dbquerywidget.h index 9915137..341e56b 100644 --- a/gui/dbquerywidget.h +++ b/gui/dbquerywidget.h @@ -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; diff --git a/testfiles/metadatasearch.js b/testfiles/metadatasearch.js index 1a8ace3..1b922e2 100644 --- a/testfiles/metadatasearch.js +++ b/testfiles/metadatasearch.js @@ -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); }