Initial commit.
This commit is contained in:
159
src/musicbrainz/acoustidclient.cpp
Normal file
159
src/musicbrainz/acoustidclient.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
//#include <qjson/parser.h>
|
||||
#include <QJson/Parser>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QNetworkReply>
|
||||
#include <QStringList>
|
||||
#include <QUrlQuery>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "acoustidclient.h"
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/timeconstants.h"
|
||||
|
||||
const char *AcoustidClient::kClientId = "76JYVgslck";
|
||||
const char *AcoustidClient::kUrl = "http://api.acoustid.org/v2/lookup";
|
||||
const int AcoustidClient::kDefaultTimeout = 5000; // msec
|
||||
|
||||
AcoustidClient::AcoustidClient(QObject *parent)
|
||||
: QObject(parent),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
timeouts_(new NetworkTimeouts(kDefaultTimeout, this)) {}
|
||||
|
||||
void AcoustidClient::SetTimeout(int msec) { timeouts_->SetTimeout(msec); }
|
||||
|
||||
void AcoustidClient::Start(int id, const QString& fingerprint, int duration_msec) {
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
|
||||
QList<Param> parameters;
|
||||
parameters << Param("format", "json")
|
||||
<< Param("client", kClientId)
|
||||
<< Param("duration", QString::number(duration_msec / kMsecPerSec))
|
||||
<< Param("meta", "recordingids+sources")
|
||||
<< Param("fingerprint", fingerprint);
|
||||
|
||||
QUrl url(kUrl);
|
||||
QUrlQuery url_query;
|
||||
url_query.setQueryItems(parameters);
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(RequestFinished(QNetworkReply*, int)), reply, id);
|
||||
requests_[id] = reply;
|
||||
|
||||
timeouts_->AddReply(reply);
|
||||
}
|
||||
|
||||
void AcoustidClient::Cancel(int id) { delete requests_.take(id); }
|
||||
|
||||
void AcoustidClient::CancelAll() {
|
||||
qDeleteAll(requests_.values());
|
||||
requests_.clear();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Struct used when extracting results in RequestFinished
|
||||
struct IdSource {
|
||||
IdSource(const QString& id, int source)
|
||||
: id_(id), nb_sources_(source) {}
|
||||
|
||||
bool operator<(const IdSource& other) const {
|
||||
// We want the items with more sources to be at the beginning of the list
|
||||
return nb_sources_ > other.nb_sources_;
|
||||
}
|
||||
|
||||
QString id_;
|
||||
int nb_sources_;
|
||||
};
|
||||
}
|
||||
|
||||
void AcoustidClient::RequestFinished(QNetworkReply *reply, int request_id) {
|
||||
|
||||
reply->deleteLater();
|
||||
requests_.remove(request_id);
|
||||
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
emit Finished(request_id, QStringList());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_document = QJsonDocument::fromJson(reply->readAll(), &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
emit Finished(request_id, QStringList());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_object = json_document.object();
|
||||
|
||||
QString status = json_object["status"].toString();
|
||||
if (status != "ok") {
|
||||
emit Finished(request_id, QStringList());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the results:
|
||||
// -in a first step, gather ids and their corresponding number of sources
|
||||
// -then sort results by number of sources (the results are originally
|
||||
// unsorted but results with more sources are likely to be more accurate)
|
||||
// -keep only the ids, as sources where useful only to sort the results
|
||||
QJsonArray json_results = json_object["results"].toArray();
|
||||
|
||||
// List of <id, nb of sources> pairs
|
||||
QList<IdSource> id_source_list;
|
||||
|
||||
for (const QJsonValue& v : json_results) {
|
||||
QJsonObject r = v.toObject();
|
||||
if (!r["recordings"].isUndefined()) {
|
||||
QJsonArray json_recordings = r["recordings"].toArray();
|
||||
for (const QJsonValue& recording : json_recordings) {
|
||||
QJsonObject o = recording.toObject();
|
||||
if (!o["id"].isUndefined()) {
|
||||
id_source_list << IdSource(o["id"].toString(), o["sources"].toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qStableSort(id_source_list);
|
||||
|
||||
QList<QString> id_list;
|
||||
for (const IdSource& is : id_source_list) {
|
||||
id_list << is.id_;
|
||||
}
|
||||
|
||||
emit Finished(request_id, id_list);
|
||||
|
||||
}
|
||||
|
||||
81
src/musicbrainz/acoustidclient.h
Normal file
81
src/musicbrainz/acoustidclient.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ACOUSTIDCLIENT_H
|
||||
#define ACOUSTIDCLIENT_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
||||
class NetworkTimeouts;
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class AcoustidClient : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
// Gets a MBID from a Chromaprint fingerprint.
|
||||
// A fingerprint identifies one particular encoding of a song and is created
|
||||
// by Fingerprinter. An MBID identifies the actual song and can be passed to
|
||||
// Musicbrainz to get metadata.
|
||||
// You can create one AcoustidClient and make multiple requests using it.
|
||||
// IDs are provided by the caller when a request is started and included in
|
||||
// the Finished signal - they have no meaning to AcoustidClient.
|
||||
|
||||
public:
|
||||
AcoustidClient(QObject *parent = nullptr);
|
||||
|
||||
// Network requests will be aborted after this interval.
|
||||
void SetTimeout(int msec);
|
||||
|
||||
// Starts a request and returns immediately. Finished() will be emitted
|
||||
// later with the same ID.
|
||||
void Start(int id, const QString& fingerprint, int duration_msec);
|
||||
|
||||
// Cancels the request with the given ID. Finished() will never be emitted
|
||||
// for that ID. Does nothing if there is no request with the given ID.
|
||||
void Cancel(int id);
|
||||
|
||||
// Cancels all requests. Finished() will never be emitted for any pending
|
||||
// requests.
|
||||
void CancelAll();
|
||||
|
||||
signals:
|
||||
void Finished(int id, const QStringList &mbid_list);
|
||||
|
||||
private slots:
|
||||
void RequestFinished(QNetworkReply *reply, int id);
|
||||
|
||||
private:
|
||||
static const char *kClientId;
|
||||
static const char *kUrl;
|
||||
static const int kDefaultTimeout;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
NetworkTimeouts *timeouts_;
|
||||
QMap<int, QNetworkReply*> requests_;
|
||||
};
|
||||
|
||||
#endif // ACOUSTIDCLIENT_H
|
||||
|
||||
221
src/musicbrainz/chromaprinter.cpp
Normal file
221
src/musicbrainz/chromaprinter.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "chromaprinter.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QEventLoop>
|
||||
#include <QThread>
|
||||
#include <QtDebug>
|
||||
#include <QTime>
|
||||
|
||||
#include <chromaprint.h>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/signalchecker.h"
|
||||
|
||||
static const int kDecodeRate = 11025;
|
||||
static const int kDecodeChannels = 1;
|
||||
static const int kPlayLengthSecs = 30;
|
||||
static const int kTimeoutSecs = 10;
|
||||
|
||||
Chromaprinter::Chromaprinter(const QString &filename)
|
||||
: filename_(filename),
|
||||
convert_element_(nullptr) {}
|
||||
|
||||
Chromaprinter::~Chromaprinter() {}
|
||||
|
||||
GstElement *Chromaprinter::CreateElement(const QString &factory_name, GstElement *bin) {
|
||||
|
||||
GstElement *ret = gst_element_factory_make( factory_name.toLatin1().constData(), factory_name.toLatin1().constData());
|
||||
|
||||
if (ret && bin) gst_bin_add(GST_BIN(bin), ret);
|
||||
|
||||
if (!ret) {
|
||||
qLog(Warning) << "Couldn't create the gstreamer element" << factory_name;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QString Chromaprinter::CreateFingerprint() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||
|
||||
buffer_.open(QIODevice::WriteOnly);
|
||||
|
||||
GstElement *pipeline = gst_pipeline_new("pipeline");
|
||||
GstElement *src = CreateElement("filesrc", pipeline);
|
||||
GstElement *decode = CreateElement("decodebin", pipeline);
|
||||
GstElement *convert = CreateElement("audioconvert", pipeline);
|
||||
GstElement *resample = CreateElement("audioresample", pipeline);
|
||||
GstElement *sink = CreateElement("appsink", pipeline);
|
||||
|
||||
if (!src || !decode || !convert || !resample || !sink) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
convert_element_ = convert;
|
||||
|
||||
// Connect the elements
|
||||
gst_element_link_many(src, decode, nullptr);
|
||||
gst_element_link_many(convert, resample, nullptr);
|
||||
|
||||
// Chromaprint expects mono 16-bit ints at a sample rate of 11025Hz.
|
||||
GstCaps *caps = gst_caps_new_simple(
|
||||
"audio/x-raw",
|
||||
"format", G_TYPE_STRING, "S16LE",
|
||||
"channels", G_TYPE_INT, kDecodeChannels,
|
||||
"rate", G_TYPE_INT, kDecodeRate,
|
||||
NULL);
|
||||
gst_element_link_filtered(resample, sink, caps);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
GstAppSinkCallbacks callbacks;
|
||||
memset(&callbacks, 0, sizeof(callbacks));
|
||||
callbacks.new_sample = NewBufferCallback;
|
||||
gst_app_sink_set_callbacks(reinterpret_cast<GstAppSink*>(sink), &callbacks,
|
||||
this, nullptr);
|
||||
g_object_set(G_OBJECT(sink), "sync", FALSE, nullptr);
|
||||
g_object_set(G_OBJECT(sink), "emit-signals", TRUE, nullptr);
|
||||
|
||||
// Set the filename
|
||||
g_object_set(src, "location", filename_.toUtf8().constData(), nullptr);
|
||||
|
||||
// Connect signals
|
||||
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
|
||||
CHECKED_GCONNECT(decode, "pad-added", &NewPadCallback, this);
|
||||
|
||||
// Play only first x seconds
|
||||
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
||||
// wait for state change before seeking
|
||||
gst_element_get_state(pipeline, nullptr, nullptr, kTimeoutSecs * GST_SECOND);
|
||||
gst_element_seek(pipeline, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET, 0 * GST_SECOND, GST_SEEK_TYPE_SET, kPlayLengthSecs * GST_SECOND);
|
||||
|
||||
QTime time;
|
||||
time.start();
|
||||
|
||||
// Start playing
|
||||
gst_element_set_state(pipeline, GST_STATE_PLAYING);
|
||||
|
||||
// Wait until EOS or error
|
||||
GstMessage *msg = gst_bus_timed_pop_filtered(bus, kTimeoutSecs * GST_SECOND, static_cast<GstMessageType>(GST_MESSAGE_EOS | GST_MESSAGE_ERROR));
|
||||
|
||||
if (msg != nullptr) {
|
||||
if (msg->type == GST_MESSAGE_ERROR) {
|
||||
// Report error
|
||||
GError *error = nullptr;
|
||||
gchar *debugs = nullptr;
|
||||
|
||||
gst_message_parse_error(msg, &error, &debugs);
|
||||
QString message = QString::fromLocal8Bit(error->message);
|
||||
|
||||
g_error_free(error);
|
||||
free(debugs);
|
||||
|
||||
qLog(Debug) << "Error processing" << filename_ << ":" << message;
|
||||
}
|
||||
gst_message_unref(msg);
|
||||
}
|
||||
|
||||
int decode_time = time.restart();
|
||||
|
||||
buffer_.close();
|
||||
|
||||
// Generate fingerprint from recorded buffer data
|
||||
QByteArray data = buffer_.data();
|
||||
|
||||
ChromaprintContext *chromaprint = chromaprint_new(CHROMAPRINT_ALGORITHM_DEFAULT);
|
||||
chromaprint_start(chromaprint, kDecodeRate, kDecodeChannels);
|
||||
chromaprint_feed(chromaprint, reinterpret_cast<int16_t *>(data.data()), data.size() / 2);
|
||||
chromaprint_finish(chromaprint);
|
||||
|
||||
int size = 0;
|
||||
|
||||
#if CHROMAPRINT_VERSION_MAJOR >= 1 && CHROMAPRINT_VERSION_MINOR >= 4
|
||||
u_int32_t *fprint = nullptr;
|
||||
char *encoded = nullptr;
|
||||
#else
|
||||
void *fprint = nullptr;
|
||||
void *encoded = nullptr;
|
||||
#endif
|
||||
|
||||
int ret = chromaprint_get_raw_fingerprint(chromaprint, &fprint, &size);
|
||||
QByteArray fingerprint;
|
||||
if (ret == 1) {
|
||||
|
||||
int encoded_size = 0;
|
||||
chromaprint_encode_fingerprint(fprint, size, CHROMAPRINT_ALGORITHM_DEFAULT, &encoded, &encoded_size, 1);
|
||||
|
||||
fingerprint.append(reinterpret_cast<char*>(encoded), encoded_size);
|
||||
|
||||
chromaprint_dealloc(fprint);
|
||||
chromaprint_dealloc(encoded);
|
||||
}
|
||||
chromaprint_free(chromaprint);
|
||||
int codegen_time = time.elapsed();
|
||||
|
||||
qLog(Debug) << "Decode time:" << decode_time << "Codegen time:" << codegen_time;
|
||||
|
||||
// Cleanup
|
||||
callbacks.new_sample = nullptr;
|
||||
gst_object_unref(bus);
|
||||
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||
gst_object_unref(pipeline);
|
||||
|
||||
return fingerprint;
|
||||
|
||||
}
|
||||
|
||||
void Chromaprinter::NewPadCallback(GstElement*, GstPad *pad, gpointer data) {
|
||||
|
||||
Chromaprinter *instance = reinterpret_cast<Chromaprinter*>(data);
|
||||
GstPad *const audiopad = gst_element_get_static_pad(instance->convert_element_, "sink");
|
||||
|
||||
if (GST_PAD_IS_LINKED(audiopad)) {
|
||||
qLog(Warning) << "audiopad is already linked, unlinking old pad";
|
||||
gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad));
|
||||
}
|
||||
|
||||
gst_pad_link(pad, audiopad);
|
||||
gst_object_unref(audiopad);
|
||||
|
||||
}
|
||||
|
||||
GstFlowReturn Chromaprinter::NewBufferCallback(GstAppSink *app_sink, gpointer self) {
|
||||
|
||||
Chromaprinter *me = reinterpret_cast<Chromaprinter*>(self);
|
||||
|
||||
GstSample *sample = gst_app_sink_pull_sample(app_sink);
|
||||
GstBuffer *buffer = gst_sample_get_buffer(sample);
|
||||
GstMapInfo map;
|
||||
gst_buffer_map(buffer, &map, GST_MAP_READ);
|
||||
me->buffer_.write(reinterpret_cast<const char*>(map.data), map.size);
|
||||
gst_buffer_unmap(buffer, &map);
|
||||
gst_buffer_unref(buffer);
|
||||
|
||||
return GST_FLOW_OK;
|
||||
|
||||
}
|
||||
|
||||
64
src/musicbrainz/chromaprinter.h
Normal file
64
src/musicbrainz/chromaprinter.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CHROMAPRINTER_H
|
||||
#define CHROMAPRINTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/app/gstappsink.h>
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QString>
|
||||
|
||||
class Chromaprinter {
|
||||
// Creates a Chromaprint fingerprint from a song.
|
||||
// Uses GStreamer to open and decode the file as PCM data and passes this
|
||||
// to Chromaprint's code generator. The generated code can be used to identify
|
||||
// a song via Acoustid.
|
||||
// You should create one Chromaprinter for each file you want to fingerprint.
|
||||
// This class works well with QtConcurrentMap.
|
||||
|
||||
public:
|
||||
Chromaprinter(const QString& filename);
|
||||
~Chromaprinter();
|
||||
|
||||
// Creates a fingerprint from the song. This method is blocking, so you want
|
||||
// to call it in another thread. Returns an empty string if no fingerprint
|
||||
// could be created.
|
||||
QString CreateFingerprint();
|
||||
|
||||
private:
|
||||
GstElement *CreateElement(const QString &factory_name, GstElement *bin = nullptr);
|
||||
|
||||
static void NewPadCallback(GstElement*, GstPad *pad, gpointer data);
|
||||
static GstFlowReturn NewBufferCallback(GstAppSink *app_sink, gpointer self);
|
||||
|
||||
private:
|
||||
QString filename_;
|
||||
|
||||
GstElement *convert_element_;
|
||||
|
||||
QBuffer buffer_;
|
||||
|
||||
};
|
||||
|
||||
#endif // CHROMAPRINTER_H
|
||||
408
src/musicbrainz/musicbrainzclient.cpp
Normal file
408
src/musicbrainz/musicbrainzclient.cpp
Normal file
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "musicbrainzclient.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QNetworkReply>
|
||||
#include <QSet>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
const char *MusicBrainzClient::kTrackUrl = "http://musicbrainz.org/ws/2/recording/";
|
||||
const char *MusicBrainzClient::kDiscUrl = "http://musicbrainz.org/ws/2/discid/";
|
||||
const char *MusicBrainzClient::kDateRegex = "^[12]\\d{3}";
|
||||
const int MusicBrainzClient::kDefaultTimeout = 5000; // msec
|
||||
const int MusicBrainzClient::kMaxRequestPerTrack = 3;
|
||||
|
||||
MusicBrainzClient::MusicBrainzClient(QObject *parent, QNetworkAccessManager *network)
|
||||
: QObject(parent),
|
||||
network_(network ? network : new NetworkAccessManager(this)),
|
||||
timeouts_(new NetworkTimeouts(kDefaultTimeout, this)) {}
|
||||
|
||||
void MusicBrainzClient::Start(int id, const QStringList &mbid_list) {
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
|
||||
int request_number = 0;
|
||||
for (const QString &mbid : mbid_list) {
|
||||
QList<Param> parameters;
|
||||
parameters << Param("inc", "artists+releases+media");
|
||||
|
||||
QUrl url(kTrackUrl + mbid);
|
||||
QUrlQuery url_query;
|
||||
url_query.setQueryItems(parameters);
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(RequestFinished(QNetworkReply*, int, int)), reply, id, request_number++);
|
||||
requests_.insert(id, reply);
|
||||
|
||||
timeouts_->AddReply(reply);
|
||||
|
||||
if (request_number >= kMaxRequestPerTrack) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MusicBrainzClient::StartDiscIdRequest(const QString &discid) {
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
|
||||
QList<Param> parameters;
|
||||
parameters << Param("inc", "artists+recordings");
|
||||
|
||||
QUrl url(kDiscUrl + discid);
|
||||
QUrlQuery url_query;
|
||||
url_query.setQueryItems(parameters);
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(DiscIdRequestFinished(const QString&, QNetworkReply*)), discid, reply);
|
||||
|
||||
timeouts_->AddReply(reply);
|
||||
}
|
||||
|
||||
void MusicBrainzClient::Cancel(int id) { delete requests_.take(id); }
|
||||
|
||||
void MusicBrainzClient::CancelAll() {
|
||||
qDeleteAll(requests_.values());
|
||||
requests_.clear();
|
||||
}
|
||||
|
||||
void MusicBrainzClient::DiscIdRequestFinished(const QString &discid, QNetworkReply *reply) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
ResultList ret;
|
||||
QString artist;
|
||||
QString album;
|
||||
int year = 0;
|
||||
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
qLog(Error) << "Error:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << "http status code received";
|
||||
qLog(Error) << reply->readAll();
|
||||
emit Finished(artist, album, ret);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse xml result:
|
||||
// -get title
|
||||
// -get artist
|
||||
// -get year
|
||||
// -get all the tracks' tags
|
||||
// Note: If there are multiple releases for the discid, the first
|
||||
// release is chosen.
|
||||
QXmlStreamReader reader(reply);
|
||||
while (!reader.atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader.readNext();
|
||||
if (type == QXmlStreamReader::StartElement) {
|
||||
QStringRef name = reader.name();
|
||||
if (name == "title") {
|
||||
album = reader.readElementText();
|
||||
}
|
||||
else if (name == "date") {
|
||||
QRegExp regex(kDateRegex);
|
||||
if (regex.indexIn(reader.readElementText()) == 0) {
|
||||
year = regex.cap(0).toInt();
|
||||
}
|
||||
}
|
||||
else if (name == "artist-credit") {
|
||||
ParseArtist(&reader, &artist);
|
||||
}
|
||||
else if (name == "medium-list") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
QXmlStreamReader::TokenType token = reader.readNext();
|
||||
if (token == QXmlStreamReader::StartElement && reader.name() == "medium") {
|
||||
// Get the medium with a matching discid.
|
||||
if (MediumHasDiscid(discid, &reader)) {
|
||||
ResultList tracks = ParseMedium(&reader);
|
||||
for (const Result &track : tracks) {
|
||||
if (!track.title_.isEmpty()) {
|
||||
ret << track;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Utilities::ConsumeCurrentElement(&reader);
|
||||
}
|
||||
}
|
||||
else if (token == QXmlStreamReader::EndElement && reader.name() == "medium-list") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we parsed a year, copy it to the tracks.
|
||||
if (year > 0) {
|
||||
for (ResultList::iterator it = ret.begin(); it != ret.end(); ++it) {
|
||||
it->year_ = year;
|
||||
}
|
||||
}
|
||||
|
||||
emit Finished(artist, album, UniqueResults(ret, SortResults));
|
||||
}
|
||||
|
||||
void MusicBrainzClient::RequestFinished(QNetworkReply *reply, int id, int request_number) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
const int nb_removed = requests_.remove(id, reply);
|
||||
if (nb_removed != 1) {
|
||||
qLog(Error) << "Error: unknown reply received:" << nb_removed <<
|
||||
"requests removed, while only one was supposed to be removed";
|
||||
}
|
||||
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
QXmlStreamReader reader(reply);
|
||||
ResultList res;
|
||||
while (!reader.atEnd()) {
|
||||
if (reader.readNext() == QXmlStreamReader::StartElement && reader.name() == "recording") {
|
||||
ResultList tracks = ParseTrack(&reader);
|
||||
for (const Result &track : tracks) {
|
||||
if (!track.title_.isEmpty()) {
|
||||
res << track;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pending_results_[id] << PendingResults(request_number, res);
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Error:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << "http status code received";
|
||||
qLog(Error) << reply->readAll();
|
||||
}
|
||||
|
||||
// No more pending requests for this id: emit the results we have.
|
||||
if (!requests_.contains(id)) {
|
||||
// Merge the results we have
|
||||
ResultList ret;
|
||||
QList<PendingResults> result_list_list = pending_results_.take(id);
|
||||
qSort(result_list_list);
|
||||
for (const PendingResults &result_list : result_list_list) {
|
||||
ret << result_list.results_;
|
||||
}
|
||||
emit Finished(id, UniqueResults(ret, KeepOriginalOrder));
|
||||
}
|
||||
}
|
||||
|
||||
bool MusicBrainzClient::MediumHasDiscid(const QString &discid, QXmlStreamReader *reader) {
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
|
||||
if (type == QXmlStreamReader::StartElement && reader->name() == "disc" && reader->attributes().value("id").toString() == discid) {
|
||||
return true;
|
||||
}
|
||||
else if (type == QXmlStreamReader::EndElement && reader->name() == "disc-list") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qLog(Debug) << "Reached end of xml stream without encountering </disc-list>";
|
||||
return false;
|
||||
}
|
||||
|
||||
MusicBrainzClient::ResultList MusicBrainzClient::ParseMedium(QXmlStreamReader *reader) {
|
||||
|
||||
ResultList ret;
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
|
||||
if (type == QXmlStreamReader::StartElement) {
|
||||
if (reader->name() == "track") {
|
||||
Result result;
|
||||
result = ParseTrackFromDisc(reader);
|
||||
ret << result;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == QXmlStreamReader::EndElement && reader->name() == "track-list") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
MusicBrainzClient::Result MusicBrainzClient::ParseTrackFromDisc(QXmlStreamReader *reader) {
|
||||
|
||||
Result result;
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
|
||||
if (type == QXmlStreamReader::StartElement) {
|
||||
QStringRef name = reader->name();
|
||||
if (name == "position") {
|
||||
result.track_ = reader->readElementText().toInt();
|
||||
}
|
||||
else if (name == "length") {
|
||||
result.duration_msec_ = reader->readElementText().toInt();
|
||||
}
|
||||
else if (name == "title") {
|
||||
result.title_ = reader->readElementText();
|
||||
}
|
||||
}
|
||||
|
||||
if (type == QXmlStreamReader::EndElement && reader->name() == "track") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
MusicBrainzClient::ResultList MusicBrainzClient::ParseTrack(QXmlStreamReader *reader) {
|
||||
|
||||
Result result;
|
||||
QList<Release> releases;
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
|
||||
if (type == QXmlStreamReader::StartElement) {
|
||||
QStringRef name = reader->name();
|
||||
|
||||
if (name == "title") {
|
||||
result.title_ = reader->readElementText();
|
||||
}
|
||||
else if (name == "length") {
|
||||
result.duration_msec_ = reader->readElementText().toInt();
|
||||
}
|
||||
else if (name == "artist-credit") {
|
||||
ParseArtist(reader, &result.artist_);
|
||||
}
|
||||
else if (name == "release") {
|
||||
releases << ParseRelease(reader);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == QXmlStreamReader::EndElement && reader->name() == "recording") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ResultList ret;
|
||||
if (releases.isEmpty()) {
|
||||
ret << result;
|
||||
}
|
||||
else {
|
||||
qStableSort(releases);
|
||||
for (const Release &release : releases) {
|
||||
ret << release.CopyAndMergeInto(result);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Parse the artist. Multiple artists are joined together with the
|
||||
// joinphrase from musicbrainz.
|
||||
void MusicBrainzClient::ParseArtist(QXmlStreamReader *reader, QString *artist) {
|
||||
|
||||
QString join_phrase;
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
|
||||
if (type == QXmlStreamReader::StartElement && reader->name() == "name-credit") {
|
||||
join_phrase = reader->attributes().value("joinphrase").toString();
|
||||
}
|
||||
|
||||
if (type == QXmlStreamReader::StartElement && reader->name() == "name") {
|
||||
*artist += reader->readElementText() + join_phrase;
|
||||
}
|
||||
|
||||
if (type == QXmlStreamReader::EndElement && reader->name() == "artist-credit") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MusicBrainzClient::Release MusicBrainzClient::ParseRelease(QXmlStreamReader *reader) {
|
||||
|
||||
Release ret;
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
|
||||
if (type == QXmlStreamReader::StartElement) {
|
||||
QStringRef name = reader->name();
|
||||
if (name == "title") {
|
||||
ret.album_ = reader->readElementText();
|
||||
}
|
||||
else if (name == "status") {
|
||||
ret.SetStatusFromString(reader->readElementText());
|
||||
}
|
||||
else if (name == "date") {
|
||||
QRegExp regex(kDateRegex);
|
||||
if (regex.indexIn(reader->readElementText()) == 0) {
|
||||
ret.year_ = regex.cap(0).toInt();
|
||||
}
|
||||
}
|
||||
else if (name == "track-list") {
|
||||
ret.track_ = reader->attributes().value("offset").toString().toInt() + 1;
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == QXmlStreamReader::EndElement && reader->name() == "release") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
MusicBrainzClient::ResultList MusicBrainzClient::UniqueResults(const ResultList& results, UniqueResultsSortOption opt) {
|
||||
|
||||
ResultList ret;
|
||||
if (opt == SortResults) {
|
||||
ret = QSet<Result>::fromList(results).toList();
|
||||
qSort(ret);
|
||||
}
|
||||
else { // KeepOriginalOrder
|
||||
// Qt doesn't provide a ordered set (QSet "stores values in an unspecified
|
||||
// order" according to Qt documentation).
|
||||
// We might use std::set instead, but it's probably faster to use ResultList
|
||||
// directly to avoid converting from one structure to another.
|
||||
for (const Result& res : results) {
|
||||
if (!ret.contains(res)) {
|
||||
ret << res;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
210
src/musicbrainz/musicbrainzclient.h
Normal file
210
src/musicbrainz/musicbrainzclient.h
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MUSICBRAINZCLIENT_H
|
||||
#define MUSICBRAINZCLIENT_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QMultiMap>
|
||||
#include <QObject>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
class NetworkTimeouts;
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class MusicBrainzClient : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
// Gets metadata for a particular MBID.
|
||||
// An MBID is created from a fingerprint using MusicDnsClient.
|
||||
// You can create one MusicBrainzClient and make multiple requests using it.
|
||||
// IDs are provided by the caller when a request is started and included in
|
||||
// the Finished signal - they have no meaning to MusicBrainzClient.
|
||||
|
||||
public:
|
||||
// The second argument allows for specifying a custom network access
|
||||
// manager. It is used in tests. The ownership of network
|
||||
// is not transferred.
|
||||
MusicBrainzClient(QObject* parent = nullptr,
|
||||
QNetworkAccessManager* network = nullptr);
|
||||
|
||||
struct Result {
|
||||
Result() : duration_msec_(0), track_(0), year_(-1) {}
|
||||
|
||||
bool operator<(const Result& other) const {
|
||||
#define cmp(field) \
|
||||
if (field < other.field) return true; \
|
||||
if (field > other.field) return false;
|
||||
|
||||
cmp(track_);
|
||||
cmp(year_);
|
||||
cmp(title_);
|
||||
cmp(artist_);
|
||||
return false;
|
||||
|
||||
#undef cmp
|
||||
}
|
||||
|
||||
bool operator==(const Result& other) const {
|
||||
return
|
||||
title_ == other.title_ &&
|
||||
artist_ == other.artist_ &&
|
||||
album_ == other.album_ &&
|
||||
duration_msec_ == other.duration_msec_ &&
|
||||
track_ == other.track_ &&
|
||||
year_ == other.year_;
|
||||
}
|
||||
|
||||
QString title_;
|
||||
QString artist_;
|
||||
QString album_;
|
||||
int duration_msec_;
|
||||
int track_;
|
||||
int year_;
|
||||
};
|
||||
typedef QList<Result> ResultList;
|
||||
|
||||
// Starts a request and returns immediately. Finished() will be emitted
|
||||
// later with the same ID.
|
||||
void Start(int id, const QStringList& mbid);
|
||||
void StartDiscIdRequest(const QString& discid);
|
||||
|
||||
// Cancels the request with the given ID. Finished() will never be emitted
|
||||
// for that ID. Does nothing if there is no request with the given ID.
|
||||
void Cancel(int id);
|
||||
|
||||
// Cancels all requests. Finished() will never be emitted for any pending
|
||||
// requests.
|
||||
void CancelAll();
|
||||
|
||||
signals:
|
||||
// Finished signal emitted when fechting songs tags
|
||||
void Finished(int id, const MusicBrainzClient::ResultList& result);
|
||||
// Finished signal emitted when fechting album's songs tags using DiscId
|
||||
void Finished(const QString& artist, const QString album,
|
||||
const MusicBrainzClient::ResultList& result);
|
||||
|
||||
private slots:
|
||||
// id identifies the track, and request_number means it's the
|
||||
// 'request_number'th request for this track
|
||||
void RequestFinished(QNetworkReply* reply, int id, int request_number);
|
||||
void DiscIdRequestFinished(const QString& discid, QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
// Used as parameter for UniqueResults
|
||||
enum UniqueResultsSortOption {
|
||||
SortResults = 0,
|
||||
KeepOriginalOrder
|
||||
};
|
||||
|
||||
struct Release {
|
||||
|
||||
enum Status {
|
||||
Status_Unknown = 0,
|
||||
Status_PseudoRelease,
|
||||
Status_Bootleg,
|
||||
Status_Promotional,
|
||||
Status_Official
|
||||
};
|
||||
|
||||
Release() : track_(0), year_(0), status_(Status_Unknown) {}
|
||||
|
||||
Result CopyAndMergeInto(const Result& orig) const {
|
||||
Result ret(orig);
|
||||
ret.album_ = album_;
|
||||
ret.track_ = track_;
|
||||
ret.year_ = year_;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void SetStatusFromString(const QString& s) {
|
||||
if (s.compare("Official", Qt::CaseInsensitive) == 0) {
|
||||
status_ = Status_Official;
|
||||
}
|
||||
else if (s.compare("Promotion", Qt::CaseInsensitive) == 0) {
|
||||
status_ = Status_Promotional;
|
||||
}
|
||||
else if (s.compare("Bootleg", Qt::CaseInsensitive) == 0) {
|
||||
status_ = Status_Bootleg;
|
||||
}
|
||||
else if (s.compare("Pseudo-release", Qt::CaseInsensitive) == 0) {
|
||||
status_ = Status_PseudoRelease;
|
||||
}
|
||||
else {
|
||||
status_ = Status_Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
bool operator<(const Release& other) const {
|
||||
// Compare status so that "best" status (e.g. Official) will be first
|
||||
// when sorting a list of releases.
|
||||
return status_ > other.status_;
|
||||
}
|
||||
|
||||
QString album_;
|
||||
int track_;
|
||||
int year_;
|
||||
Status status_;
|
||||
};
|
||||
|
||||
struct PendingResults {
|
||||
PendingResults(int sort_id, const ResultList& results) : sort_id_(sort_id), results_(results) {}
|
||||
|
||||
bool operator<(const PendingResults& other) const {
|
||||
return sort_id_ < other.sort_id_;
|
||||
}
|
||||
|
||||
int sort_id_;
|
||||
ResultList results_;
|
||||
};
|
||||
|
||||
static bool MediumHasDiscid(const QString& discid, QXmlStreamReader* reader);
|
||||
static ResultList ParseMedium(QXmlStreamReader* reader);
|
||||
static Result ParseTrackFromDisc(QXmlStreamReader* reader);
|
||||
static ResultList ParseTrack(QXmlStreamReader* reader);
|
||||
static void ParseArtist(QXmlStreamReader* reader, QString* artist);
|
||||
static Release ParseRelease(QXmlStreamReader* reader);
|
||||
static ResultList UniqueResults(const ResultList& results, UniqueResultsSortOption opt = SortResults);
|
||||
|
||||
|
||||
private:
|
||||
static const char* kTrackUrl;
|
||||
static const char* kDiscUrl;
|
||||
static const char* kDateRegex;
|
||||
static const int kDefaultTimeout;
|
||||
static const int kMaxRequestPerTrack;
|
||||
|
||||
QNetworkAccessManager* network_;
|
||||
NetworkTimeouts* timeouts_;
|
||||
QMultiMap<int, QNetworkReply*> requests_;
|
||||
// Results we received so far, kept here until all the replies are finished
|
||||
QMap<int, QList<PendingResults>> pending_results_;
|
||||
};
|
||||
|
||||
inline uint qHash(const MusicBrainzClient::Result& result) {
|
||||
return qHash(result.album_) ^ qHash(result.artist_) ^ result.duration_msec_ ^
|
||||
qHash(result.title_) ^ result.track_ ^ result.year_;
|
||||
}
|
||||
|
||||
#endif // MUSICBRAINZCLIENT_H
|
||||
140
src/musicbrainz/tagfetcher.cpp
Normal file
140
src/musicbrainz/tagfetcher.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "tagfetcher.h"
|
||||
|
||||
#include "acoustidclient.h"
|
||||
#include "chromaprinter.h"
|
||||
#include "musicbrainzclient.h"
|
||||
#include "core/timeconstants.h"
|
||||
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrentMap>
|
||||
|
||||
TagFetcher::TagFetcher(QObject* parent)
|
||||
: QObject(parent),
|
||||
fingerprint_watcher_(nullptr),
|
||||
acoustid_client_(new AcoustidClient(this)),
|
||||
musicbrainz_client_(new MusicBrainzClient(this)) {
|
||||
|
||||
connect(acoustid_client_, SIGNAL(Finished(int, QStringList)), SLOT(PuidsFound(int, QStringList)));
|
||||
connect(musicbrainz_client_, SIGNAL(Finished(int, MusicBrainzClient::ResultList)), SLOT(TagsFetched(int, MusicBrainzClient::ResultList)));
|
||||
|
||||
}
|
||||
|
||||
QString TagFetcher::GetFingerprint(const Song &song) {
|
||||
return Chromaprinter(song.url().toLocalFile()).CreateFingerprint();
|
||||
}
|
||||
|
||||
void TagFetcher::StartFetch(const SongList &songs) {
|
||||
|
||||
Cancel();
|
||||
|
||||
songs_ = songs;
|
||||
|
||||
QFuture<QString> future = QtConcurrent::mapped(songs_, GetFingerprint);
|
||||
fingerprint_watcher_ = new QFutureWatcher<QString>(this);
|
||||
fingerprint_watcher_->setFuture(future);
|
||||
connect(fingerprint_watcher_, SIGNAL(resultReadyAt(int)), SLOT(FingerprintFound(int)));
|
||||
|
||||
for (const Song &song : songs) {
|
||||
emit Progress(song, tr("Fingerprinting song"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TagFetcher::Cancel() {
|
||||
|
||||
if (fingerprint_watcher_) {
|
||||
fingerprint_watcher_->cancel();
|
||||
|
||||
delete fingerprint_watcher_;
|
||||
fingerprint_watcher_ = nullptr;
|
||||
}
|
||||
|
||||
acoustid_client_->CancelAll();
|
||||
musicbrainz_client_->CancelAll();
|
||||
songs_.clear();
|
||||
|
||||
}
|
||||
|
||||
void TagFetcher::FingerprintFound(int index) {
|
||||
|
||||
QFutureWatcher<QString>* watcher = reinterpret_cast<QFutureWatcher<QString>*>(sender());
|
||||
if (!watcher || index >= songs_.count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString fingerprint = watcher->resultAt(index);
|
||||
const Song &song = songs_[index];
|
||||
|
||||
if (fingerprint.isEmpty()) {
|
||||
emit ResultAvailable(song, SongList());
|
||||
return;
|
||||
}
|
||||
|
||||
emit Progress(song, tr("Identifying song"));
|
||||
acoustid_client_->Start(index, fingerprint, song.length_nanosec() / kNsecPerMsec);
|
||||
|
||||
}
|
||||
|
||||
void TagFetcher::PuidsFound(int index, const QStringList &puid_list) {
|
||||
|
||||
if (index >= songs_.count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Song &song = songs_[index];
|
||||
|
||||
if (puid_list.isEmpty()) {
|
||||
emit ResultAvailable(song, SongList());
|
||||
return;
|
||||
}
|
||||
|
||||
emit Progress(song, tr("Downloading metadata"));
|
||||
musicbrainz_client_->Start(index, puid_list);
|
||||
|
||||
}
|
||||
|
||||
void TagFetcher::TagsFetched(int index, const MusicBrainzClient::ResultList &results) {
|
||||
|
||||
if (index >= songs_.count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Song &original_song = songs_[index];
|
||||
SongList songs_guessed;
|
||||
|
||||
for (const MusicBrainzClient::Result &result : results) {
|
||||
Song song;
|
||||
song.Init(result.title_, result.artist_, result.album_, result.duration_msec_ * kNsecPerMsec);
|
||||
song.set_track(result.track_);
|
||||
song.set_year(result.year_);
|
||||
songs_guessed << song;
|
||||
}
|
||||
|
||||
emit ResultAvailable(original_song, songs_guessed);
|
||||
|
||||
}
|
||||
|
||||
67
src/musicbrainz/tagfetcher.h
Normal file
67
src/musicbrainz/tagfetcher.h
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TAGFETCHER_H
|
||||
#define TAGFETCHER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "musicbrainzclient.h"
|
||||
#include "core/song.h"
|
||||
|
||||
#include <QFutureWatcher>
|
||||
#include <QObject>
|
||||
|
||||
class AcoustidClient;
|
||||
|
||||
class TagFetcher : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
// High level interface to Fingerprinter, AcoustidClient and
|
||||
// MusicBrainzClient.
|
||||
|
||||
public:
|
||||
TagFetcher(QObject *parent = nullptr);
|
||||
|
||||
void StartFetch(const SongList &songs);
|
||||
|
||||
public slots:
|
||||
void Cancel();
|
||||
|
||||
signals:
|
||||
void Progress(const Song &original_song, const QString &stage);
|
||||
void ResultAvailable(const Song &original_song, const SongList &songs_guessed);
|
||||
|
||||
private slots:
|
||||
void FingerprintFound(int index);
|
||||
void PuidsFound(int index, const QStringList &puid_list);
|
||||
void TagsFetched(int index, const MusicBrainzClient::ResultList &result);
|
||||
|
||||
private:
|
||||
static QString GetFingerprint(const Song &song);
|
||||
|
||||
QFutureWatcher<QString> *fingerprint_watcher_;
|
||||
AcoustidClient *acoustid_client_;
|
||||
MusicBrainzClient *musicbrainz_client_;
|
||||
|
||||
SongList songs_;
|
||||
};
|
||||
|
||||
#endif // TAGFETCHER_H
|
||||
Reference in New Issue
Block a user