Initial commit.

This commit is contained in:
Jonas Kvinge
2018-02-27 18:06:05 +01:00
parent 85d9664df7
commit b2b1ba7abe
1393 changed files with 177311 additions and 1 deletions

View 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);
}

View 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

View 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;
}

View 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

View 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;
}

View 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

View 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);
}

View 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