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:
parent
4cd0e2b232
commit
a56947e356
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
122
src/covers/musicbrainzcoverprovider.cpp
Normal file
122
src/covers/musicbrainzcoverprovider.cpp
Normal 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);
|
||||
}
|
46
src/covers/musicbrainzcoverprovider.h
Normal file
46
src/covers/musicbrainzcoverprovider.h
Normal 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
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user