summaryrefslogtreecommitdiffstats
path: root/src/musicbrainz
diff options
context:
space:
mode:
authorUwe Klotz <uklotz@mixxx.org>2020-02-23 02:04:55 +0100
committerUwe Klotz <uklotz@mixxx.org>2020-02-27 16:26:20 +0100
commit745f336b614c14662f62f358317e61669259559b (patch)
treead3685ee5fd0482eeb53522d381a1cc13d934ec9 /src/musicbrainz
parent540c901f873308227a34997c566a2f6f4f923019 (diff)
Add AcoustIdLookupTask
Diffstat (limited to 'src/musicbrainz')
-rw-r--r--src/musicbrainz/web/acoustidlookuptask.cpp225
-rw-r--r--src/musicbrainz/web/acoustidlookuptask.h43
2 files changed, 268 insertions, 0 deletions
diff --git a/src/musicbrainz/web/acoustidlookuptask.cpp b/src/musicbrainz/web/acoustidlookuptask.cpp
new file mode 100644
index 0000000000..1cb2d724f7
--- /dev/null
+++ b/src/musicbrainz/web/acoustidlookuptask.cpp
@@ -0,0 +1,225 @@
+#include "musicbrainz/web/acoustidlookuptask.h"
+
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QMetaMethod>
+
+#include "musicbrainz/gzip.h"
+#include "util/assert.h"
+#include "util/logger.h"
+
+namespace mixxx {
+
+namespace {
+
+const Logger kLogger("AcoustIdLookupTask");
+
+// see API-KEY site here http://acoustid.org/application/496
+// I registered the KEY for version 1.12 -- kain88 (may 2013)
+// See also: https://acoustid.org/webservice
+const QString kClientApiKey = QStringLiteral("czKxnkyO");
+
+const QUrl kBaseUrl = QStringLiteral("https://api.acoustid.org/");
+
+const QString kRequestPath = QStringLiteral("/v2/lookup");
+
+const QLatin1String kContentTypeHeaderValue("application/x-www-form-urlencoded");
+
+const QByteArray kContentEncodingRawHeaderKey = "Content-Encoding";
+const QByteArray kContentEncodingRawHeaderValue = "gzip";
+
+QUrlQuery lookupUrlQuery(
+ const QString& fingerprint,
+ int duration) {
+ DEBUG_ASSERT(!fingerprint.isEmpty());
+ DEBUG_ASSERT(duration >= 0);
+
+ QUrlQuery urlQuery;
+ urlQuery.addQueryItem(
+ QStringLiteral("format"),
+ QStringLiteral("json"));
+ urlQuery.addQueryItem(
+ QStringLiteral("client"),
+ kClientApiKey);
+ urlQuery.addQueryItem(
+ QStringLiteral("meta"),
+ QStringLiteral("recordingids"));
+ urlQuery.addQueryItem(
+ QStringLiteral("fingerprint"),
+ fingerprint);
+ urlQuery.addQueryItem(
+ QStringLiteral("duration"),
+ QString::number(duration));
+ return urlQuery;
+}
+
+network::JsonWebRequest lookupRequest() {
+ return network::JsonWebRequest{
+ network::HttpRequestMethod::Post,
+ kRequestPath,
+ QUrlQuery(), // custom query
+ QJsonDocument(), // custom body
+ };
+}
+
+} // anonymous namespace
+
+AcoustIdLookupTask::AcoustIdLookupTask(
+ QNetworkAccessManager* networkAccessManager,
+ const QString& fingerprint,
+ int duration,
+ QObject* parent)
+ : network::JsonWebTask(
+ networkAccessManager,
+ kBaseUrl,
+ lookupRequest(),
+ parent),
+ m_urlQuery(lookupUrlQuery(fingerprint, duration)) {
+}
+
+QNetworkReply* AcoustIdLookupTask::sendNetworkRequest(
+ QNetworkAccessManager* networkAccessManager,
+ network::HttpRequestMethod method,
+ const QUrl& url,
+ const QJsonDocument& content) {
+ Q_UNUSED(method);
+ DEBUG_ASSERT(method == network::HttpRequestMethod::Post);
+ Q_UNUSED(content);
+ DEBUG_ASSERT(content.isEmpty());
+
+ DEBUG_ASSERT(url.isValid());
+ QNetworkRequest req(url);
+ req.setHeader(
+ QNetworkRequest::ContentTypeHeader,
+ kContentTypeHeaderValue);
+ req.setRawHeader(
+ kContentEncodingRawHeaderKey,
+ kContentEncodingRawHeaderValue);
+
+ // application/x-www-form-urlencoded request bodies must be percent encoded.
+ DEBUG_ASSERT(!m_urlQuery.isEmpty());
+ QByteArray body = gzipCompress(
+ m_urlQuery.query(QUrl::FullyEncoded).toLatin1());
+
+ if (kLogger.traceEnabled()) {
+ kLogger.trace()
+ << "POST"
+ << url
+ << body;
+ }
+ DEBUG_ASSERT(networkAccessManager);
+ return networkAccessManager->post(req, body);
+}
+
+void AcoustIdLookupTask::onFinished(
+ network::JsonWebResponse response) {
+ if (!response.isStatusCodeSuccess()) {
+ kLogger.warning()
+ << "Request failed with HTTP status code"
+ << response.statusCode;
+ emitFailed(std::move(response));
+ return;
+ }
+ VERIFY_OR_DEBUG_ASSERT(response.statusCode == network::kHttpStatusCodeOk) {
+ kLogger.warning()
+ << "Unexpected HTTP status code"
+ << response.statusCode;
+ emitFailed(std::move(response));
+ return;
+ }
+
+ VERIFY_OR_DEBUG_ASSERT(response.content.isObject()) {
+ kLogger.warning()
+ << "Invalid JSON content"
+ << response.content;
+ emitFailed(std::move(response));
+ return;
+ }
+ const auto jsonObject = response.content.object();
+
+ const auto statusText = jsonObject.value(QStringLiteral("status")).toString();
+ if (statusText != QStringLiteral("ok")) {
+ kLogger.warning()
+ << "Unexpected response status"
+ << statusText;
+ emitFailed(std::move(response));
+ return;
+ }
+
+ QList<QUuid> recordingIds;
+ DEBUG_ASSERT(jsonObject.value(QStringLiteral("results")).isArray());
+ const QJsonArray results = jsonObject.value(QStringLiteral("results")).toArray();
+ double maxScore = -1.0; // uninitialized (< 0)
+ // Results are expected to be ordered by score (descending)
+ for (const auto result : results) {
+ DEBUG_ASSERT(result.isObject());
+ const auto resultObject = result.toObject();
+ const auto resultId =
+ resultObject.value(QStringLiteral("id")).toString();
+ DEBUG_ASSERT(!resultId.isEmpty());
+ // The default score is 1.0 if missing
+ const double score =
+ resultObject.value(QStringLiteral("score")).toDouble(1.0);
+ DEBUG_ASSERT(score >= 0.0);
+ DEBUG_ASSERT(score <= 1.0);
+ if (maxScore < 0.0) {
+ // Initialize the maximum score
+ maxScore = score;
+ }
+ DEBUG_ASSERT(score <= maxScore);
+ if (score < maxScore && !recordingIds.isEmpty()) {
+ // Ignore all remaining results with lower values
+ // than the maximum score
+ break;
+ }
+ const auto recordings = result.toObject().value(QStringLiteral("recordings"));
+ if (recordings.isUndefined()) {
+ if (kLogger.debugEnabled()) {
+ kLogger.debug()
+ << "No recording(s) available for result"
+ << resultId
+ << "with score"
+ << score;
+ }
+ continue;
+ } else {
+ DEBUG_ASSERT(recordings.isArray());
+ const QJsonArray recordingsArray = recordings.toArray();
+ if (kLogger.debugEnabled()) {
+ kLogger.debug()
+ << "Found"
+ << recordingsArray.size()
+ << "recording(s) for result"
+ << resultId
+ << "with score"
+ << score;
+ }
+ for (const auto recording : recordingsArray) {
+ DEBUG_ASSERT(recording.isObject());
+ const auto recordingObject = recording.toObject();
+ const auto recordingId =
+ QUuid(recordingObject.value(QStringLiteral("id")).toString());
+ VERIFY_OR_DEBUG_ASSERT(!recordingId.isNull()) {
+ continue;
+ }
+ recordingIds.append(recordingId);
+ }
+ }
+ }
+ emitSucceeded(std::move(recordingIds));
+}
+
+void AcoustIdLookupTask::emitSucceeded(
+ QList<QUuid>&& recordingIds) {
+ const auto signal = QMetaMethod::fromSignal(
+ &AcoustIdLookupTask::succeeded);
+ DEBUG_ASSERT(receivers(signal.name()) <= 1); // unique connection
+ if (isSignalConnected(signal)) {
+ emit succeeded(
+ std::move(recordingIds));
+ } else {
+ deleteLater();
+ }
+}
+
+} // namespace mixxx
diff --git a/src/musicbrainz/web/acoustidlookuptask.h b/src/musicbrainz/web/acoustidlookuptask.h
new file mode 100644
index 0000000000..e8d0a2cce6
--- /dev/null
+++ b/src/musicbrainz/web/acoustidlookuptask.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <QString>
+#include <QList>
+#include <QUuid>
+
+#include "network/jsonwebtask.h"
+
+namespace mixxx {
+
+class AcoustIdLookupTask : public network::JsonWebTask {
+ Q_OBJECT
+
+ public:
+ AcoustIdLookupTask(
+ QNetworkAccessManager* networkAccessManager,
+ const QString& fingerprint,
+ int duration,
+ QObject* parent = nullptr);
+ ~AcoustIdLookupTask() override = default;
+
+ signals:
+ void succeeded(
+ QList<QUuid> recordingIds);
+
+ protected:
+ QNetworkReply* sendNetworkRequest(
+ QNetworkAccessManager* networkAccessManager,
+ network::HttpRequestMethod method,
+ const QUrl& url,
+ const QJsonDocument& content) override;
+
+ private:
+ void onFinished(
+ network::JsonWebResponse response) override;
+
+ void emitSucceeded(
+ QList<QUuid>&& recordingIds);
+
+ const QUrlQuery m_urlQuery;
+};
+
+} // namespace mixxx