#include "./musicbrainz.h" #include "../application/settings.h" #include "../misc/networkaccessmanager.h" #include "../misc/utility.h" #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace std::placeholders; using namespace Utility; #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) namespace std { template <> struct hash { std::size_t operator()(const QString &s) const { return qHash(s); } }; } // namespace std #endif namespace QtGui { MusicBrainzResultsModel::MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) : HttpResultsModel(std::move(initialSongDescription), reply) { } bool MusicBrainzResultsModel::fetchCover(const QModelIndex &index) { if (index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) { return true; } // skip if cover is already available auto &desc = m_results[index.row()]; if (!desc.cover.isEmpty()) { return true; } // fail if album ID is unknown if (desc.albumId.isEmpty()) { m_errorList << tr("Unable to fetch cover: Album ID unknown"); emit resultsAvailable(); return true; } // skip if the item belongs to an album which cover has already been fetched const auto coverData = s_coverData.find(desc.albumId); if (coverData != s_coverData.end()) { desc.cover = coverData->second; return true; } // request the cover art auto *const reply = queryCoverArtArchive(desc.albumId); addReply(reply, bind(&MusicBrainzResultsModel::handleCoverReplyFinished, this, reply, desc.albumId, index.row())); setFetchingCover(true); return false; } QUrl MusicBrainzResultsModel::webUrl(const QModelIndex &index) { if (index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) { return QUrl(); } return QUrl(QStringLiteral("https://musicbrainz.org/recording/") + m_results.at(index.row()).songId); } void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data) { beginResetModel(); m_results.clear(); // store all song information (called recordings by MusicBrainz) auto recordings = std::vector(); // store all albums/collections (called releases by MusicBrainz) for a song auto releasesByRecording = std::unordered_map>(); // parse XML tree auto xmlReader = QXmlStreamReader(data); // clang-format off #include children { iftag("metadata") { children { iftag("recording-list") { children { iftag("recording") { auto currentDescription = SongDescription(attribute("id").toString()); children { iftag("title") { currentDescription.title = text; } eliftag("artist-credit") { children { iftag("name-credit") { children { iftag("artist") { if (currentDescription.artistId.isEmpty()) { currentDescription.artistId = attribute("id").toString(); } children { iftag("name") { currentDescription.artist = text; } else_skip } } else_skip } } else_skip } } eliftag("release-list") { children { iftag("release") { auto releaseInfo = SongDescription(); releaseInfo.albumId = attribute("id").toString(); children { iftag("title") { releaseInfo.album = text; } eliftag("artist-credit") { children { iftag("name-credit") { children { iftag("artist") { if (currentDescription.artistId.isEmpty()) { currentDescription.artistId = attribute("id").toString(); } children { iftag("name") { currentDescription.artist = text; } else_skip } } else_skip } } else_skip } } eliftag("date") { releaseInfo.year = text; } eliftag("medium-list") { children { iftag("medium") { children { iftag("position") { releaseInfo.disk = text.toInt(); } eliftag("track-list") { releaseInfo.totalTracks = attribute("count").toInt(); children { iftag("track") { children { iftag("number") { releaseInfo.track = text.toInt(); } else_skip } } else_skip } } else_skip } } else_skip } } else_skip } releasesByRecording[currentDescription.songId].emplace_back(std::move(releaseInfo)); } else_skip } } eliftag("tag-list") { children { iftag("tag") { children { iftag("name") { if (!currentDescription.genre.isEmpty()) { currentDescription.genre.append(QStringLiteral(", ")); } currentDescription.genre.append(text); } else_skip } } else_skip } } else_skip } recordings.emplace_back(std::move(currentDescription)); } else_skip } } else_skip } } else_skip } #include // populate results // -> create a song for each recording/release combination and group those songs by their releases sorted ascendingly from oldest to latest auto recordingsByRelease = std::map>(); for (const auto &recording : recordings) { const auto &releases = releasesByRecording[recording.songId]; for (const auto &release : releases) { // make a copy of the recording/song information and add release/album specific information to it SongDescription releaseSpecificRecording = recording; if (!release.album.isEmpty()) { releaseSpecificRecording.album = release.album; releaseSpecificRecording.albumId = release.albumId; } if (!release.artist.isEmpty()) { releaseSpecificRecording.artist = release.artist; releaseSpecificRecording.artistId = release.artistId; } if (release.track) { releaseSpecificRecording.track = release.track; } if (release.totalTracks) { releaseSpecificRecording.totalTracks = release.totalTracks; } if (release.disk) { releaseSpecificRecording.disk = release.disk; } if (!release.year.isEmpty()) { releaseSpecificRecording.year = release.year; } recordingsByRelease[release.year % QChar('-') % release.albumId].emplace_back(std::move(releaseSpecificRecording)); } } // -> sort recordings within each release by track number and add recordings to results for (auto &releaseAndRecordings : recordingsByRelease) { auto &recordingsOfRelease = releaseAndRecordings.second; std::sort(recordingsOfRelease.begin(), recordingsOfRelease.end(), [](const auto &recording1, const auto &recording2) { if (recording1.disk != recording2.disk) { return recording1.disk < recording2.disk; } return recording1.track < recording2.track; }); for (auto &recording : recordingsOfRelease) { m_results << std::move(recording); } } // check for parsing errors switch (xmlReader.error()) { case QXmlStreamReader::NoError: case QXmlStreamReader::PrematureEndOfDocumentError: break; default: m_errorList << xmlReader.errorString(); } endResetModel(); } // clang-format on QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription) { static const auto defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/")); auto parts = QStringList(); parts.reserve(4); if (!songDescription.title.isEmpty()) { parts << QChar('\"') % songDescription.title % QChar('\"'); } if (!songDescription.artist.isEmpty()) { parts << QStringLiteral("artist:\"") % songDescription.artist % QChar('\"'); } if (!songDescription.album.isEmpty()) { parts << QStringLiteral("release:\"") % songDescription.album % QChar('\"'); } if (songDescription.track) { parts << QStringLiteral("number:") + QString::number(songDescription.track); } const auto &musicBrainzUrl = Settings::values().dbQuery.musicBrainzUrl; auto url = QUrl(musicBrainzUrl.isEmpty() ? defaultMusicBrainzUrl : (musicBrainzUrl + QStringLiteral("/recording/"))); auto query = QUrlQuery(); query.addQueryItem(QStringLiteral("query"), parts.join(QStringLiteral(" AND "))); url.setQuery(query); 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)); } QNetworkReply *queryCoverArtArchive(const QString &albumId) { static const auto defaultArchiveUrl(QStringLiteral("https://coverartarchive.org")); const auto &coverArtArchiveUrl = Settings::values().dbQuery.coverArtArchiveUrl; return networkAccessManager().get(QNetworkRequest(QUrl( (coverArtArchiveUrl.isEmpty() ? defaultArchiveUrl : coverArtArchiveUrl) % QStringLiteral("/release/") % albumId % QStringLiteral("/front")))); } } // namespace QtGui