1
0
mirror of https://github.com/clementine-player/Clementine synced 2025-01-27 09:41:32 +01:00

Add support for new musicbrainz cover art archive.

See: http://blog.musicbrainz.org/?p=1616
This commit is contained in:
John Maguire 2012-10-10 14:45:13 +02:00
parent 4cd0e2b232
commit a56947e356
9 changed files with 235 additions and 30 deletions

View File

@ -123,6 +123,7 @@ set(SOURCES
covers/currentartloader.cpp
covers/discogscoverprovider.cpp
covers/kittenloader.cpp
covers/musicbrainzcoverprovider.cpp
devices/connecteddevice.cpp
devices/devicedatabasebackend.cpp
@ -408,6 +409,7 @@ set(HEADERS
covers/currentartloader.h
covers/discogscoverprovider.h
covers/kittenloader.h
covers/musicbrainzcoverprovider.h
devices/connecteddevice.h
devices/devicedatabasebackend.h

View File

@ -23,6 +23,7 @@
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include "core/closure.h"
#include "utilities.h"
QMutex ThreadSafeNetworkDiskCache::sMutex;
@ -121,6 +122,18 @@ void NetworkTimeouts::AddReply(QNetworkReply* reply) {
timers_[reply] = startTimer(timeout_msec_);
}
void NetworkTimeouts::AddReply(RedirectFollower* reply) {
if (redirect_timers_.contains(reply)) {
return;
}
NewClosure(reply, SIGNAL(destroyed()), this,
SLOT(RedirectFinished(RedirectFollower*)), reply);
NewClosure(reply, SIGNAL(finished()), this,
SLOT(RedirectFinished(RedirectFollower*)), reply);
redirect_timers_[reply] = startTimer(timeout_msec_);
}
void NetworkTimeouts::ReplyFinished() {
QNetworkReply* reply = reinterpret_cast<QNetworkReply*>(sender());
if (timers_.contains(reply)) {
@ -128,11 +141,22 @@ void NetworkTimeouts::ReplyFinished() {
}
}
void NetworkTimeouts::RedirectFinished(RedirectFollower* reply) {
if (redirect_timers_.contains(reply)) {
killTimer(redirect_timers_.take(reply));
}
}
void NetworkTimeouts::timerEvent(QTimerEvent* e) {
QNetworkReply* reply = timers_.key(e->timerId());
if (reply) {
reply->abort();
}
RedirectFollower* redirect = redirect_timers_.key(e->timerId());
if (redirect) {
redirect->abort();
}
}
@ -156,7 +180,7 @@ void RedirectFollower::ReadyRead() {
if (current_reply_->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
return;
}
emit readyRead();
}

View File

@ -57,27 +57,6 @@ protected:
};
class NetworkTimeouts : public QObject {
Q_OBJECT
public:
NetworkTimeouts(int timeout_msec, QObject* parent = 0);
void AddReply(QNetworkReply* reply);
void SetTimeout(int msec) { timeout_msec_ = msec; }
protected:
void timerEvent(QTimerEvent* e);
private slots:
void ReplyFinished();
private:
int timeout_msec_;
QMap<QNetworkReply*, int> timers_;
};
class RedirectFollower : public QObject {
Q_OBJECT
@ -92,6 +71,10 @@ public:
QString errorString() const { return current_reply_->errorString(); }
QVariant attribute(QNetworkRequest::Attribute code) const { return current_reply_->attribute(code); }
QVariant header(QNetworkRequest::KnownHeaders header) const { return current_reply_->header(header); }
qint64 bytesAvailable() const { return current_reply_->bytesAvailable(); }
QUrl url() const { return current_reply_->url(); }
QByteArray readAll() { return current_reply_->readAll(); }
void abort() { current_reply_->abort(); }
signals:
// These are all forwarded from the current reply.
@ -115,4 +98,29 @@ private:
int redirects_remaining_;
};
class NetworkTimeouts : public QObject {
Q_OBJECT
public:
NetworkTimeouts(int timeout_msec, QObject* parent = 0);
// TODO: Template this to avoid code duplication.
void AddReply(QNetworkReply* reply);
void AddReply(RedirectFollower* reply);
void SetTimeout(int msec) { timeout_msec_ = msec; }
protected:
void timerEvent(QTimerEvent* e);
private slots:
void ReplyFinished();
void RedirectFinished(RedirectFollower* redirect);
private:
int timeout_msec_;
QMap<QNetworkReply*, int> timers_;
QMap<RedirectFollower*, int> redirect_timers_;
};
#endif // NETWORK_H

View File

@ -53,7 +53,7 @@ struct CoverSearchRequest {
};
// This structure represents a single result of some album's cover search request.
// It contains an URL that leads to a found cover plus it's description (usually
// It contains an URL that leads to a found cover plus its description (usually
// the "artist - album" string).
struct CoverSearchResult {
// used for grouping in the user interface. This is set automatically - don't

View File

@ -146,7 +146,8 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
qLog(Debug) << "Loading" << result.image_url << "from" << result.provider;
QNetworkReply* image_reply = network_->get(QNetworkRequest(result.image_url));
RedirectFollower* image_reply = new RedirectFollower(
network_->get(QNetworkRequest(result.image_url)));
connect(image_reply, SIGNAL(finished()), SLOT(ProviderCoverFetchFinished()));
pending_image_loads_[image_reply] = result.provider;
image_load_timeout_->AddReply(image_reply);
@ -161,7 +162,7 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
}
void AlbumCoverFetcherSearch::ProviderCoverFetchFinished() {
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
RedirectFollower* reply = qobject_cast<RedirectFollower*>(sender());
reply->deleteLater();
const QString provider = pending_image_loads_.take(reply);
@ -243,7 +244,7 @@ void AlbumCoverFetcherSearch::Cancel() {
if (!pending_requests_.isEmpty()) {
TerminateSearch();
} else if (!pending_image_loads_.isEmpty()) {
foreach (QNetworkReply* reply, pending_image_loads_.keys()) {
foreach (RedirectFollower* reply, pending_image_loads_.keys()) {
reply->abort();
}
pending_image_loads_.clear();

View File

@ -26,8 +26,8 @@
class CoverProvider;
class CoverProviders;
class NetworkTimeouts;
class QNetworkAccessManager;
class QNetworkReply;
class NetworkAccessManager;
class RedirectFollower;
// This class encapsulates a single search for covers initiated by an
// AlbumCoverFetcher. The search engages all of the known cover providers.
@ -82,7 +82,7 @@ private:
CoverSearchResults results_;
QMap<int, CoverProvider*> pending_requests_;
QMap<QNetworkReply*, QString> pending_image_loads_;
QMap<RedirectFollower*, QString> pending_image_loads_;
NetworkTimeouts* image_load_timeout_;
// QMap is sorted by key (score). Values are (provider_name, image)

View File

@ -0,0 +1,122 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "musicbrainzcoverprovider.h"
#include <boost/bind.hpp>
#include <QXmlStreamReader>
#include "core/closure.h"
#include "core/network.h"
namespace {
static const char* kReleaseSearchUrl =
"http://musicbrainz.org/ws/2/release/";
static const char* kAlbumCoverUrl =
"http://coverartarchive.org/release/%1/front";
} // namespace
MusicbrainzCoverProvider::MusicbrainzCoverProvider(QObject* parent)
: CoverProvider("MusicBrainz", parent),
network_(new NetworkAccessManager(this)) {
}
bool MusicbrainzCoverProvider::StartSearch(
const QString& artist, const QString& album, int id) {
// Find release information.
QUrl url(kReleaseSearchUrl);
QString query = QString("release:\"%1\" AND artist:\"%2\"")
.arg(album.trimmed().replace('"', "\\\""))
.arg(artist.trimmed().replace('"', "\\\""));
url.addQueryItem("query", query);
url.addQueryItem("limit", "5");
QNetworkRequest request(url);
QNetworkReply* reply = network_->get(request);
NewClosure(
reply,
SIGNAL(finished()),
this,
SLOT(ReleaseSearchFinished(QNetworkReply*, int)),
reply,
id);
return true;
}
void MusicbrainzCoverProvider::ReleaseSearchFinished(
QNetworkReply* reply, int id) {
reply->deleteLater();
QList<QString> releases;
QXmlStreamReader reader(reply);
while (!reader.atEnd()) {
QXmlStreamReader::TokenType type = reader.readNext();
if (type == QXmlStreamReader::StartElement &&
reader.name() == "release") {
QStringRef release_id = reader.attributes().value("id");
if (!release_id.isEmpty()) {
releases.append(release_id.toString());
}
}
}
foreach (const QString& release_id, releases) {
QUrl url(QString(kAlbumCoverUrl).arg(release_id));
QNetworkReply* reply = network_->head(QNetworkRequest(url));
image_checks_.insert(id, reply);
NewClosure(
reply,
SIGNAL(finished()),
this,
SLOT(ImageCheckFinished(int)),
id);
}
}
void MusicbrainzCoverProvider::ImageCheckFinished(int id) {
QList<QNetworkReply*> replies = image_checks_.values(id);
int finished_count = std::count_if(
replies.constBegin(), replies.constEnd(),
boost::bind(&QNetworkReply::isFinished, _1));
if (finished_count == replies.size()) {
QList<CoverSearchResult> results;
foreach (QNetworkReply* reply, replies) {
reply->deleteLater();
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() < 400) {
CoverSearchResult result;
result.description = "foobar";
result.image_url = reply->url();
results.append(result);
}
}
image_checks_.remove(id);
emit SearchFinished(id, results);
}
}
void MusicbrainzCoverProvider::CancelSearch(int id) {
QList<QNetworkReply*> replies = image_checks_.values(id);
foreach (QNetworkReply* reply, replies) {
reply->abort();
reply->deleteLater();
}
image_checks_.remove(id);
}

View File

@ -0,0 +1,46 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef MUSICBRAINZCOVERPROVIDER_H
#define MUSICBRAINZCOVERPROVIDER_H
#include "coverprovider.h"
#include <QMultiMap>
class QNetworkAccessManager;
class QNetworkReply;
class MusicbrainzCoverProvider : public CoverProvider {
Q_OBJECT
public:
MusicbrainzCoverProvider(QObject* parent = 0);
// CoverProvider
virtual bool StartSearch(const QString& artist, const QString& album, int id);
virtual void CancelSearch(int id);
private slots:
void ReleaseSearchFinished(QNetworkReply* reply, int id);
void ImageCheckFinished(int id);
private:
QNetworkAccessManager* network_;
QMultiMap<int, QNetworkReply*> image_checks_;
};
#endif // MUSICBRAINZCOVERPROVIDER_H

View File

@ -39,8 +39,9 @@
#include "core/ubuntuunityhack.h"
#include "core/utilities.h"
#include "covers/amazoncoverprovider.h"
#include "covers/discogscoverprovider.h"
#include "covers/coverproviders.h"
#include "covers/discogscoverprovider.h"
#include "covers/musicbrainzcoverprovider.h"
#include "engines/enginebase.h"
#include "smartplaylists/generator.h"
#include "ui/iconloader.h"
@ -413,6 +414,7 @@ int main(int argc, char *argv[]) {
// when its service is created.
app.cover_providers()->AddProvider(new AmazonCoverProvider);
app.cover_providers()->AddProvider(new DiscogsCoverProvider);
app.cover_providers()->AddProvider(new MusicbrainzCoverProvider);
#ifdef Q_OS_LINUX
// In 11.04 Ubuntu decided that the system tray should be reserved for certain