Files
cantata/gui/coverdialog.cpp
craig.p.drummond dabf0da2f8 Fix Qt5 compile
2013-04-23 16:53:03 +00:00

1492 lines
50 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011-2013 Craig Drummond <craig.p.drummond@gmail.com>
*
* ----
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "coverdialog.h"
#include "messagebox.h"
#include "localize.h"
#include "listview.h"
#include "networkaccessmanager.h"
#include "settings.h"
#include "mpdconnection.h"
#include "utils.h"
#include "spinner.h"
#include "icon.h"
#include "sha2/sha2.h"
#include "qjson/parser.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QPixmap>
#include <QFont>
#include <QFontMetrics>
#include <QPainter>
#include <QStyle>
#include <QApplication>
#include <QProgressBar>
#include <QScrollArea>
#include <QDesktopWidget>
#include <QWheelEvent>
#include <QScrollBar>
#include <QMenu>
#include <QAction>
#ifdef ENABLE_KDE_SUPPORT
#include <KDE/KFileDialog>
#else
#include <QFileDialog>
#endif
#include <QFileInfo>
#include <QUrl>
#if QT_VERSION >= 0x050000
#include <QUrlQuery>
#include <QMimeData>
#endif
#include <QTemporaryFile>
#include <QDir>
#include <QXmlStreamReader>
#include <QDomDocument>
#include <QDomElement>
static int iCount=0;
static const int constMaxTempFiles=20;
static QByteArray sha256(const QByteArray &data)
{
SHA256_CTX context;
SHA256_Init(&context);
SHA256_Update(&context, reinterpret_cast<const u_int8_t*>(data.constData()),
data.length());
QByteArray ret(SHA256_DIGEST_LENGTH, '\0');
SHA256_Final(reinterpret_cast<u_int8_t*>(ret.data()), &context);
return ret;
}
static QByteArray hmacSha256(const QByteArray &key, const QByteArray &data)
{
static const int constBlockSize = 64; // bytes
Q_ASSERT(key.length() <= constBlockSize);
QByteArray innerPadding(constBlockSize, char(0x36));
QByteArray outerPadding(constBlockSize, char(0x5c));
for (int i=0 ; i<key.length() ; ++i) {
innerPadding[i] = innerPadding[i] ^ key[i];
outerPadding[i] = outerPadding[i] ^ key[i];
}
return sha256(outerPadding + sha256(innerPadding + data));
}
int CoverDialog::instanceCount()
{
return iCount;
}
// Only really want square-ish covers!
bool canUse(int w, int h)
{
return w>90 && h>90 && (w==h || (h<=(w*1.1) && w<=(h*1.1)));
}
class CoverItem : public QListWidgetItem
{
public:
enum Type {
Type_Existing,
Type_Local,
Type_LastFm,
Type_Google,
Type_Discogs,
Type_Amazon,
Type_CoverArtArchive
};
CoverItem(const QString &u, const QString &tu, QListWidget *parent)
: QListWidgetItem(parent)
, imgUrl(u)
, thmbUrl(tu)
, list(parent) {
setSizeHint(parent->gridSize());
setTextAlignment(Qt::AlignHCenter | Qt::AlignTop);
setToolTip(u);
}
virtual quint32 key() const =0;
virtual Type type() const =0;
const QString & url() const { return imgUrl; }
const QString & thumbUrl() const { return thmbUrl; }
//bool operator<(const CoverItem &o) const {
// return key()<o.key();
//}
protected:
void setImage(const QImage &img) {
int size=list && list->parentWidget() && qobject_cast<CoverDialog *>(list->parentWidget())
? static_cast<CoverDialog *>(list->parentWidget())->imageSize() : 100;
QPixmap pix=QPixmap::fromImage(img.scaled(QSize(size, size), Qt::KeepAspectRatio, Qt::SmoothTransformation));
if (pix.width()<size || pix.height()<size) {
QPixmap newPix(size, size);
newPix.fill(Qt::transparent);
QPainter p(&newPix);
p.drawPixmap((size-pix.width())/2, (size-pix.height())/2, pix.width(), pix.height(), pix);
p.end();
pix=newPix;
}
setIcon(pix);
}
protected:
QString imgUrl;
QString thmbUrl;
QListWidget *list;
};
class LastFmCover : public CoverItem
{
public:
LastFmCover(const QString &u, const QString &tu, const QImage &img, QListWidget *parent)
: CoverItem(u, tu, parent) {
setImage(img);
setText(i18n("Last.fm"));
}
quint32 key() const { return 0xFFFFFFFD; }
Type type() const { return Type_LastFm; }
};
class LocalCover : public CoverItem
{
public:
LocalCover(const QString &u, const QImage &i, QListWidget *parent)
: CoverItem(u, QString(), parent)
, img(i) {
setImage(i);
setText(i18nc("name\nwidth x height (file size)", "%1\n%2 x %3 (%4)")
.arg(Utils::getFile(u)).arg(img.width()).arg(img.height()).arg(Utils::formatByteSize(QFileInfo(u).size())));
}
quint32 key() const { return 0xFFFFFFFE; }
Type type() const { return Type_Local; }
const QImage & image() const { return img; }
private:
QImage img;
};
class GoogleCover : public CoverItem
{
public:
GoogleCover(const QString &u, const QString &tu, const QImage &img, int w, int h, int size, QListWidget *parent)
: CoverItem(u, tu, parent)
, width(w)
, height(h) {
setImage(img);
setText(i18nc("Google\nwidth x height (file size)", "Google\n%1 x %2 (%3)").arg(width).arg(height).arg(Utils::formatByteSize(size*1024)));
}
quint32 key() const { return width*height; }
Type type() const { return Type_Google; }
private:
int width;
int height;
};
class DiscogsCover : public CoverItem
{
public:
DiscogsCover(const QString &u, const QString &tu, const QImage &img, int w, int h, QListWidget *parent)
: CoverItem(u, tu, parent)
, width(w)
, height(h) {
setImage(img);
setText(i18nc("Discogs\nwidth x height", "Discogs\n%1 x %2").arg(width).arg(height));
}
quint32 key() const { return width*height; }
Type type() const { return Type_Discogs; }
private:
int width;
int height;
};
class AmazonCover : public CoverItem
{
public:
AmazonCover(const QString &u, const QString &tu, const QImage &img, int w, int h, QListWidget *parent)
: CoverItem(u, tu, parent)
, width(w)
, height(h) {
setImage(img);
setText(i18nc("Amazon\nwidth x height", "Amazon\n%1 x %2").arg(width).arg(height));
}
quint32 key() const { return width*height; }
Type type() const { return Type_Amazon; }
private:
int width;
int height;
};
class CoverArtArchiveCover : public CoverItem
{
public:
CoverArtArchiveCover(const QString &u, const QString &tu, const QImage &img, QListWidget *parent)
: CoverItem(u, tu, parent) {
setImage(img);
setText("coverartarchive.org");
}
quint32 key() const { return 0xFFFFFFFC; }
Type type() const { return Type_CoverArtArchive; }
};
class ExistingCover : public CoverItem
{
public:
ExistingCover(const Covers::Image &i, QListWidget *parent)
: CoverItem(i.fileName, QString(), parent)
, img(i) {
setImage(img.img);
QFont f(font());
f.setBold((true));
setFont(f);
setText(i18nc("Current Cover\nwidth x height", "Current Cover\n%1 x %2").arg(img.img.width()).arg(img.img.height()));
}
quint32 key() const { return 0xFFFFFFFF; }
Type type() const { return Type_Existing; }
const QImage & image() const { return img.img; }
private:
Covers::Image img;
};
CoverPreview::CoverPreview(QWidget *p)
: Dialog(p)
, zoom(1.0)
, imgW(0),
imgH(0)
{
setButtons(Close);
setWindowTitle(i18n("Image"));
QWidget *mw=new QWidget(this);
QVBoxLayout *layout=new QVBoxLayout(mw);
loadingLabel=new QLabel(i18n("Downloading..."), mw);
pbar=new QProgressBar(mw);
imageLabel=new QLabel;
pbar->setRange(0, 100);
imageLabel->setBackgroundRole(QPalette::Base);
imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
imageLabel->setScaledContents(true);
scrollArea = new QScrollArea(mw);
scrollArea->setBackgroundRole(QPalette::Dark);
scrollArea->setWidget(imageLabel);
layout->addWidget(loadingLabel);
layout->addWidget(pbar);
layout->addWidget(scrollArea);
layout->setMargin(0);
setMainWidget(mw);
}
void CoverPreview::showImage(const QImage &img, const QString &u)
{
if (u==url) {
zoom=1.0;
url=u;
loadingLabel->hide();
pbar->hide();
imageLabel->setPixmap(QPixmap::fromImage(img));
imageLabel->adjustSize();
scrollArea->show();
QApplication::processEvents();
adjustSize();
QStyleOptionFrameV3 opt;
opt.init(scrollArea);
int fw=style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, scrollArea);
if (fw<0) {
fw=2;
}
fw*=2;
QRect desktop = qApp->desktop()->screenGeometry(this);
int maxWidth=desktop.width()*0.75;
int maxHeight=desktop.height()*0.75;
int lrPad=width()-mainWidget()->width();
int tbPad=height()-mainWidget()->height();
imgW=img.width();
imgH=img.height();
resize(lrPad+qMax(100, qMin(maxWidth, imgW+fw)), tbPad+qMax(100, qMin(maxHeight, imgH+fw)));
setWindowTitle(i18nc("Image (width x height zoom%)", "Image (%1 x %2 %3%)").arg(imgW).arg(imgH).arg(zoom*100));
show();
}
}
void CoverPreview::downloading(const QString &u)
{
url=u;
if (!url.isEmpty()) {
loadingLabel->show();
scrollArea->hide();
pbar->show();
pbar->setValue(0);
int spacing=style()->layoutSpacing(QSizePolicy::DefaultType, QSizePolicy::DefaultType, Qt::Vertical);
if (spacing<0) {
spacing=4;
}
QApplication::processEvents();
adjustSize();
resize(qMin(200, loadingLabel->width()+32), loadingLabel->height()+pbar->height()+(3*spacing));
show();
}
}
void CoverPreview::progress(qint64 rx, qint64 total)
{
pbar->setValue((int)(((rx*100.0)/(total*1.0))+0.5));
}
void CoverPreview::scaleImage(int adjust)
{
double newZoom=zoom+(adjust*0.25);
if (newZoom<0.25 || newZoom>4.0 || (fabs(newZoom-zoom)<0.01)) {
return;
}
zoom=newZoom;
imageLabel->resize(zoom * imageLabel->pixmap()->size());
setWindowTitle(i18nc("Image (width x height zoom%)", "Image (%1 x %2 %3%)").arg(imgW).arg(imgH).arg(zoom*100));
}
void CoverPreview::wheelEvent(QWheelEvent *event)
{
if (scrollArea->isVisible() && QApplication::keyboardModifiers() & Qt::ControlModifier) {
const int numDegrees = event->delta() / 8;
const int numSteps = numDegrees / 15;
if (0!=numSteps) {
scaleImage(numSteps);
}
event->accept();
return;
}
Dialog::wheelEvent(event);
}
CoverDialog::CoverDialog(QWidget *parent)
: Dialog(parent, "CoverDialog")
, existing(0)
, preview(0)
, saving(false)
, spinner(0)
, page(0)
, menu(0)
, showAction(0)
, removeAction(0)
{
iCount++;
QWidget *mainWidet = new QWidget(this);
setupUi(mainWidet);
setMainWidget(mainWidet);
setAttribute(Qt::WA_DeleteOnClose);
setButtons(Cancel|Ok);
enableButton(Ok, false);
connect(list, SIGNAL(itemDoubleClicked(QListWidgetItem*)), SLOT(showImage(QListWidgetItem*)));
connect(list, SIGNAL(itemSelectionChanged()), SLOT(checkStatus()));
connect(search, SIGNAL(clicked()), SLOT(sendQuery()));
connect(query, SIGNAL(returnPressed()), SLOT(sendQuery()));
connect(cancelButton, SIGNAL(clicked()), SLOT(cancelQuery()));
connect(addFileButton, SIGNAL(clicked()), SLOT(addLocalFile()));
connect(list, SIGNAL(customContextMenuRequested(const QPoint&)), SLOT(menuRequested(const QPoint&)));
QFont f(list->font());
QFontMetrics origFm(f);
iSize=origFm.height()*7;
iSize=((iSize/10)*10)+(iSize%10 ? 10 : 0);
f.setPointSizeF(f.pointSizeF()*0.75);
QFontMetrics fm(f);
list->setFont(f);
list->setAcceptDrops(false);
list->setContextMenuPolicy(Qt::CustomContextMenu);
list->setDragDropMode(QAbstractItemView::NoDragDrop);
list->setDragEnabled(false);
list->setDropIndicatorShown(false);
list->setMovement(QListView::Static);
list->setGridSize(QSize(imageSize()+10, imageSize()+10+(2.25*fm.height())));
list->setIconSize(QSize(imageSize(), imageSize()));
int spacing=style()->layoutSpacing(QSizePolicy::DefaultType, QSizePolicy::DefaultType, Qt::Vertical);
if (spacing<0) {
spacing=4;
}
list->setSpacing(spacing);
list->setViewMode(QListView::IconMode);
list->setResizeMode(QListView::Adjust);
list->setMinimumSize((list->gridSize().width()*4)+(spacing*5), list->gridSize().height()*3);
list->setSortingEnabled(false);
addFileButton->setIcon(Icon("document-open"));
cancelButton->setIcon(Icon("stop"));
cancelButton->setEnabled(false);
cancelButton->setAutoRaise(true);
addFileButton->setAutoRaise(true);
setAcceptDrops(true);
amazonAccessKey=Settings::self()->amazonAccessKey();
amazonSecretAccessKey=Settings::self()->amazonSecretAccessKey();
}
CoverDialog::~CoverDialog()
{
iCount--;
cancelQuery();
clearTempFiles();
}
void CoverDialog::show(const Song &s, const Covers::Image &current)
{
song=s;
Covers::Image img=current.img.isNull() ? Covers::self()->getImage(song) : current;
if (!img.fileName.isEmpty() && !QFileInfo(img.fileName).isWritable()) {
MessageBox::error(parentWidget(), i18n("<p>A cover already exists for this album, and the file is not writeable.<p></p><i>%1</i></p>").arg(img.fileName));
deleteLater();
return;
}
setCaption(i18nc("Album by Artist", "%1 by %2").arg(song.album).arg(song.albumArtist()));
if (!img.img.isNull()) {
existing=new ExistingCover(img, list);
list->addItem(existing);
}
query->setText(song.album+" "+song.albumArtist());
sendQuery();
Dialog::show();
}
static const char * constHostProperty="host";
static const char * constLargeProperty="large";
static const char * constThumbProperty="thumb";
static const char * constWidthProperty="w";
static const char * constHeightProperty="h";
static const char * constSizeProperty="sz";
static const char * constTypeProperty="type";
static const char * constRedirectsProperty="redirects";
static const char * constLastFmHost="ws.audioscrobbler.com";
static const char * constGoogleHost="images.google.com";
static const char * constDiscogsHost="api.discogs.com";
static const char * constCoverArtArchiveHost="coverartarchive.org";
static const char * constAmazonHost="ecs.amazonaws.com";
static const char * constAmazonUrl="http://ecs.amazonaws.com/onca/xml";
static const char * constAmazonAssociateTag="cantata";
static const int constMaxRedirects=5;
void CoverDialog::queryJobFinished()
{
QNetworkReply *reply=qobject_cast<QNetworkReply *>(sender());
if (!reply || !currentQuery.contains(reply)) {
return;
}
currentQuery.remove(reply);
if(QNetworkReply::NoError==reply->error()) {
QString host=reply->property(constHostProperty).toString();
QVariant redirect = reply->header(QNetworkRequest::LocationHeader);
if (redirect.isValid()) {
int rd=reply->property(constRedirectsProperty).toInt();
if (rd<constMaxRedirects) {
sendQueryRequest(redirect.toUrl(), rd+1, host);
}
} else {
QByteArray resp=reply->readAll();
if (constLastFmHost==host) {
parseLstFmQueryResponse(resp);
} else if (constGoogleHost==host) {
parseGoogleQueryResponse(resp);
} else if (constDiscogsHost==host) {
parseDiscogsQueryResponse(resp);
} else if (constCoverArtArchiveHost==host) {
parseCoverArtArchiveQueryResponse(resp);
} else if (constAmazonHost==host) {
parseAmazonQueryResponse(resp);
}
}
}
reply->deleteLater();
if (spinner && currentQuery.isEmpty()) {
spinner->stop();
}
}
void CoverDialog::insertItem(CoverItem *item)
{
/*
int pos=0;
for (; pos<list->count(); ++pos) {
CoverItem *c=(CoverItem *)list->item(pos);
if (c->key()<item->key()) {
break;
}
}
qWarning() << "INSERT" << pos << item->key();
list->insertItem(pos, item);
for (; pos<list->count(); ++pos) {
CoverItem *c=(CoverItem *)list->item(pos);
qWarning() << "... " << pos << c->key();
}*/
/*
QMultiMap<int, CoverItem *> sortItems;
for (int pos=0; pos<list->count(); ++pos) {
CoverItem *i=(CoverItem *)list->item(pos);
sortItems.insert(i->key(), i);
}
sortItems.insert(item->key(), item);
QList<CoverItem *> coverItems = sortItems.values();
foreach (CoverItem *i, coverItems) {
list->addItem(i);
}
*/
list->addItem(item);
if (CoverItem::Type_Local==item->type()) {
list->scrollToItem(item);
list->setItemSelected(item, true);
}
}
void CoverDialog::downloadJobFinished()
{
QNetworkReply *reply=qobject_cast<QNetworkReply *>(sender());
if (!reply || !currentQuery.contains(reply)) {
return;
}
DownloadType dlType=(DownloadType)reply->property(constTypeProperty).toInt();
if (DL_LargeSave==dlType) {
saving=false;
}
currentQuery.remove(reply);
if(QNetworkReply::NoError==reply->error()) {
QString host=reply->property(constHostProperty).toString();
QVariant redirect = reply->header(QNetworkRequest::LocationHeader);
if (redirect.isValid()) {
int rd=reply->property(constRedirectsProperty).toInt();
if (rd<constMaxRedirects) {
QNetworkReply *j=downloadImage(redirect.toString(), dlType);
if (j) {
j->setProperty(constRedirectsProperty, rd+1);
QStringList stringProps=QStringList() << constHostProperty << constLargeProperty << constThumbProperty;
QStringList intProps=QStringList() << constWidthProperty << constHeightProperty << constSizeProperty;
foreach (const QString &prop, stringProps) {
QVariant p=reply->property(prop.toLatin1().constData());
if (p.isValid()) {
j->setProperty(prop.toLatin1().constData(), p.toString());
}
}
foreach (const QString &prop, intProps) {
QVariant p=reply->property(prop.toLatin1().constData());
if (p.isValid()) {
j->setProperty(prop.toLatin1().constData(), p.toInt());
}
}
}
}
} else {
QString url=reply->url().toString();
QByteArray data=reply->readAll();
FileType fileType=url.endsWith(".jpg") || url.endsWith(".jpeg") ? FT_Jpg : (url.endsWith(".png") ? FT_Png : FT_Other);
QImage img=QImage::fromData(data, FT_Jpg==fileType ? "JPG" : (FT_Png==fileType ? "PNG" : 0));
if (!img.isNull()) {
bool isLarge=reply->property(constThumbProperty).toString().isEmpty();
QTemporaryFile *temp=0;
if (isLarge || (reply->property(constThumbProperty).toString()==reply->property(constLargeProperty).toString())) {
temp=new QTemporaryFile(QDir::tempPath()+"/cantata_XXXXXX."+(FT_Jpg==fileType ? "jpg" : "png"));
if (temp->open()) {
if (FT_Other==fileType) {
img.save(temp, "PNG");
} else {
temp->write(data);
}
if (tempFiles.size()>=constMaxTempFiles) {
QTemporaryFile *last=tempFiles.takeLast();
last->remove();
delete last;
}
temp->setProperty(constLargeProperty, reply->property(constLargeProperty));
tempFiles.prepend(temp);
} else {
delete temp;
temp=0;
}
}
if (isLarge) {
if (DL_LargePreview==dlType) {
previewDialog()->showImage(img, reply->property(constLargeProperty).toString());
} else if (DL_LargeSave==dlType) {
if (!temp) {
MessageBox::error(this, i18n("Failed to set cover!\nCould not download to temporary file!"));
} else if (saveCover(temp->fileName(), img)) {
accept();
}
}
} else {
CoverItem *item=0;
if (constLastFmHost==host) {
item=new LastFmCover(reply->property(constLargeProperty).toString(), url, img, list);
} else if (constGoogleHost==host) {
int w=reply->property(constWidthProperty).toInt();
int h=reply->property(constHeightProperty).toInt();
if (canUse(w, h)) {
item=new GoogleCover(reply->property(constLargeProperty).toString(), url, img, w, h,
reply->property(constSizeProperty).toInt(), list);
}
} else if (constDiscogsHost==host) {
int w=reply->property(constWidthProperty).toInt();
int h=reply->property(constHeightProperty).toInt();
if (canUse(w, h)) {
item=new DiscogsCover(reply->property(constLargeProperty).toString(), url, img, w, h, list);
}
} else if (constCoverArtArchiveHost==host) {
item=new CoverArtArchiveCover(reply->property(constLargeProperty).toString(), url, img, list);
} else if (constAmazonHost==host) {
item=new AmazonCover(reply->property(constLargeProperty).toString(), url, img,
reply->property(constWidthProperty).toInt(), reply->property(constHeightProperty).toInt(), list);
}
if (item) {
insertItem(item);
}
}
}
}
} else if (reply->property(constThumbProperty).toString().isEmpty()) {
if (preview && preview->aboutToShow(reply->property(constLargeProperty).toString())) {
preview->hide();
}
MessageBox::error(this, i18n("Failed to download image!"));
}
reply->deleteLater();
if (spinner && currentQuery.isEmpty()) {
spinner->stop();
cancelButton->setEnabled(false);
}
}
void CoverDialog::showImage(QListWidgetItem *item)
{
if (saving) {
return;
}
CoverItem *cover=(CoverItem *)item;
if (CoverItem::Type_Existing==cover->type()) {
previewDialog()->downloading(cover->url());
previewDialog()->showImage(((ExistingCover *)cover)->image(), cover->url());
} else if (CoverItem::Type_Local==cover->type()) {
previewDialog()->downloading(cover->url());
previewDialog()->showImage(((LocalCover *)cover)->image(), cover->url());
} else {
previewDialog()->downloading(cover->url());
QNetworkReply *j=downloadImage(cover->url(), DL_LargePreview);
if (j) {
j->setProperty(constLargeProperty, cover->url());
connect(j, SIGNAL(downloadProgress(qint64, qint64)), preview, SLOT(progress(qint64, qint64)));
}
}
}
CoverPreview *CoverDialog::previewDialog() {
if (!preview) {
preview=new CoverPreview(this);
}
return preview;
}
void CoverDialog::sendQuery()
{
if (saving) {
return;
}
QString fixedQuery(query->text().trimmed());
fixedQuery.remove(QChar('?'));
if (fixedQuery.isEmpty()) {
return;
}
if (currentQueryString==fixedQuery) {
page++;
} else {
page=0;
}
if (0==page) {
QList<CoverItem *> keep;
while (list->count()) {
CoverItem *item=(CoverItem *)list->takeItem(0);
if (CoverItem::Type_Existing==item->type() || CoverItem::Type_Local==item->type()) {
keep.append(item);
} else {
currentUrls.remove(item->url());
currentUrls.remove(item->thumbUrl());
delete item;
}
}
foreach (CoverItem *item, keep) {
list->addItem(item);
}
cancelQuery();
}
if (!spinner) {
spinner=new Spinner(this);
spinner->setWidget(list->viewport());
}
spinner->start();
cancelButton->setEnabled(true);
currentQueryString=fixedQuery;
sendLastFmQuery(fixedQuery, page);
sendGoogleQuery(fixedQuery, page);
sendDiscoGsQuery(fixedQuery, page);
sendAmazonQuery(fixedQuery, page);
}
void CoverDialog::sendLastFmQuery(const QString &fixedQuery, int page)
{
QUrl url;
#if QT_VERSION < 0x050000
QUrl &query=url;
#else
QUrlQuery query;
#endif
url.setScheme("http");
url.setHost(constLastFmHost);
url.setPath("/2.0/");
query.addQueryItem("api_key", Covers::constLastFmApiKey);
query.addQueryItem("limit", QString::number(20));
query.addQueryItem("page", QString::number(page));
query.addQueryItem("album", fixedQuery);
query.addQueryItem("method", "album.search");
#if QT_VERSION >= 0x050000
url.setQuery(query);
#endif
sendQueryRequest(url);
}
void CoverDialog::sendGoogleQuery(const QString &fixedQuery, int page)
{
QUrl url;
#if QT_VERSION < 0x050000
QUrl &query=url;
#else
QUrlQuery query;
#endif
url.setScheme("http");
url.setHost(constGoogleHost);
url.setPath("/images");
query.addQueryItem("q", fixedQuery);
query.addQueryItem("gbv", QChar('1'));
query.addQueryItem("filter", QChar('1'));
query.addQueryItem("start", QString::number(20 * page));
#if QT_VERSION >= 0x050000
url.setQuery(query);
#endif
sendQueryRequest(url);
}
void CoverDialog::sendDiscoGsQuery(const QString &fixedQuery, int page)
{
QUrl url;
#if QT_VERSION < 0x050000
QUrl &query=url;
#else
QUrlQuery query;
#endif
url.setScheme("http");
url.setHost(constDiscogsHost);
url.setPath("/search");
query.addQueryItem("page", QString::number(page + 1));
query.addQueryItem("per_page", QString::number(20));
query.addQueryItem("type", "release");
query.addQueryItem("q", fixedQuery);
query.addQueryItem("f", "json");
#if QT_VERSION >= 0x050000
url.setQuery(query);
#endif
sendQueryRequest(url);
}
void CoverDialog::sendAmazonQuery(const QString &fixedQuery, int page)
{
#if QT_VERSION < 0x050000
if (0!=page || amazonAccessKey.isEmpty()) {
return;
}
// Taken from Clementine!!!
typedef QPair<QString, QString> Arg;
typedef QList<Arg> ArgList;
typedef QPair<QByteArray, QByteArray> EncodedArg;
typedef QList<EncodedArg> EncodedArgList;
ArgList args = ArgList()
<< Arg("AWSAccessKeyId", amazonAccessKey)
<< Arg("AssociateTag", constAmazonAssociateTag)
<< Arg("Keywords", fixedQuery)
<< Arg("Operation", "ItemSearch")
<< Arg("ResponseGroup", "Images")
<< Arg("SearchIndex", "All")
<< Arg("Service", "AWSECommerceService")
<< Arg("Timestamp", QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss.zzzZ"))
<< Arg("Version", "2009-11-01");
EncodedArgList encodedArgs;
QStringList queryItems;
// Encode the arguments
foreach (const Arg& arg, args) {
EncodedArg encodedArg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
encodedArgs << encodedArg;
queryItems << encodedArg.first+"="+encodedArg.second;
}
// Sign the request
QUrl url(constAmazonUrl);
#if QT_VERSION < 0x050000
QUrl &query=url;
#else
QUrlQuery query;
#endif
const QByteArray dataToSign = QString("GET\n%1\n%2\n%3").arg(url.host()).arg(url.path()).arg(queryItems.join("&")).toAscii();
const QByteArray signature(hmacSha256(amazonSecretAccessKey.toLatin1(), dataToSign));
// Add the signature to the request
encodedArgs << EncodedArg("Signature", QUrl::toPercentEncoding(signature.toBase64()));
query.setEncodedQueryItems(encodedArgs);
#if QT_VERSION >= 0x050000
url.setQuery(query);
#endif
sendQueryRequest(url);
#else
// Qt has no non-deprecated version of 'setEncodedQueryItems', so for the moment Amazon searches are disabled for Qt5 builds...
Q_UNUSED(fixedQuery)
Q_UNUSED(page)
#endif
}
void CoverDialog::checkStatus()
{
QList<QListWidgetItem*> items=list->selectedItems();
enableButtonOk(1==items.size() && CoverItem::Type_Existing!=((CoverItem *)items.at(0))->type());
}
void CoverDialog::cancelQuery()
{
foreach (QNetworkReply *job, currentQuery) {
if (DL_Query==job->property(constTypeProperty).toInt()) {
disconnect(job, SIGNAL(finished()), this, SLOT(queryJobFinished()));
} else {
disconnect(job, SIGNAL(finished()), this, SLOT(downloadJobFinished()));
}
job->close();
job->deleteLater();
}
currentQuery.clear();
if (spinner) {
spinner->stop();
}
cancelButton->setEnabled(false);
}
void CoverDialog::addLocalFile()
{
#ifdef ENABLE_KDE_SUPPORT
QString fileName=KFileDialog::getOpenFileName(KUrl(), "image/jpeg image/png", this, i18n("Load Local Cover"));
#else
QString fileName=QFileDialog::getOpenFileName(this, i18n("Load Local Cover"), QDir::homePath(), i18n("Images (*.png *.jpg)"));
#endif
if (!fileName.isEmpty()) {
if (currentLocalCovers.contains(fileName)) {
MessageBox::error(this, i18n("File is already in list!"));
} else {
QImage img(fileName);
if (img.isNull()) {
MessageBox::error(this, i18n("Failed to read image!"));
} else {
currentLocalCovers.insert(fileName);
insertItem(new LocalCover(fileName, img, list));
}
}
}
}
void CoverDialog::menuRequested(const QPoint &pos)
{
if (!menu) {
menu=new QMenu(list);
showAction=menu->addAction(Icon("zoom-original"), i18n("Display"));
removeAction=menu->addAction(Icon("list-remove"), i18n("Remove"));
connect(showAction, SIGNAL(triggered(bool)), SLOT(showImage()));
connect(removeAction, SIGNAL(triggered(bool)), SLOT(removeImages()));
}
QList<QListWidgetItem*> items=list->selectedItems();
showAction->setEnabled(1==items.count());
removeAction->setEnabled(!items.isEmpty());
if (removeAction->isEnabled()) {
foreach (QListWidgetItem *i, items) {
CoverItem *c=(CoverItem *)i;
if (CoverItem::Type_Existing==c->type()) {
removeAction->setEnabled(false);
}
}
}
menu->popup(list->mapToGlobal(pos));
}
void CoverDialog::showImage()
{
QList<QListWidgetItem*> items=list->selectedItems();
if (1==items.count()) {
showImage(items.at(0));
}
}
void CoverDialog::removeImages()
{
QList<QListWidgetItem*> items=list->selectedItems();
foreach (QListWidgetItem *i, items) {
delete i;
}
}
void CoverDialog::clearTempFiles()
{
foreach (QTemporaryFile *file, tempFiles) {
file->remove();
delete file;
}
}
void CoverDialog::sendQueryRequest(const QUrl &url, int redirects, const QString &host)
{
QNetworkReply *j=NetworkAccessManager::self()->get(QNetworkRequest(url));
j->setProperty(constHostProperty, host.isEmpty() ? url.host() : host);
j->setProperty(constTypeProperty, (int)DL_Query);
j->setProperty(constRedirectsProperty, redirects);
connect(j, SIGNAL(finished()), this, SLOT(queryJobFinished()));
currentQuery.insert(j);
}
QNetworkReply * CoverDialog::downloadImage(const QString &url, DownloadType dlType)
{
if (DL_Thumbnail==dlType) {
if (currentUrls.contains(url)) {
return 0;
}
currentUrls.insert(url);
} else {
foreach (QTemporaryFile *tmp, tempFiles) {
if (tmp->property(constLargeProperty).toString()==url) {
QImage img;
if (img.load(tmp->fileName())) {
if (DL_LargePreview==dlType) {
previewDialog()->downloading(url);
previewDialog()->showImage(img, url);
} else if (DL_LargeSave==dlType) {
if (saveCover(tmp->fileName(), img)) {
accept();
}
}
return 0;
}
tmp->remove();
delete tmp;
tempFiles.removeAll(tmp);
break;
}
}
}
if (DL_LargeSave==dlType) {
saving=true;
}
QNetworkReply *j=NetworkAccessManager::self()->get(QNetworkRequest(QUrl(url)));
connect(j, SIGNAL(finished()), this, SLOT(downloadJobFinished()));
currentQuery.insert(j);
j->setProperty(constTypeProperty, (int)dlType);
j->setProperty(constRedirectsProperty, 0);
if (saving) {
previewDialog()->downloading(url);
connect(j, SIGNAL(downloadProgress(qint64, qint64)), preview, SLOT(progress(qint64, qint64)));
}
return j;
}
typedef QMap<QString, QString> SizeMap;
void CoverDialog::parseLstFmQueryResponse(const QByteArray &resp)
{
bool inSection=false;
QXmlStreamReader doc(resp);
SizeMap urls;
QList<SizeMap> entries;
QStringList musibBrainzIds;
while (!doc.atEnd()) {
doc.readNext();
if (doc.isStartElement()) {
if (!inSection && QLatin1String("album")==doc.name()) {
inSection=true;
urls.clear();
} else if (inSection && QLatin1String("image")==doc.name()) {
QString size=doc.attributes().value("size").toString();
QString url=doc.readElementText();
if (!size.isEmpty() && !url.isEmpty()) {
urls.insert(size, url);
}
} else if (inSection && QLatin1String("mbid")==doc.name()) {
QString id=doc.readElementText();
if (id.length()>4) {
musibBrainzIds.append(id);
}
}
} else if (doc.isEndElement() && inSection && QLatin1String("album")==doc.name()) {
if (!urls.isEmpty()) {
entries.append(urls);
urls.clear();
}
inSection=false;
}
}
QStringList largeUrls=QStringList() << "extralarge" << "large";
QStringList thumbUrls=QStringList() << "large" << "medium" << "small";
foreach (const SizeMap &urls, entries) {
QString largeUrl;
QString thumbUrl;
foreach (const QString &u, largeUrls) {
if (urls.contains(u)) {
largeUrl=urls[u];
break;
}
}
foreach (const QString &u, thumbUrls) {
if (urls.contains(u)) {
thumbUrl=urls[u];
break;
}
}
if (!largeUrl.isEmpty() && !thumbUrl.isEmpty()) {
QNetworkReply *j=downloadImage(thumbUrl, DL_Thumbnail);
if (j) {
j->setProperty(constHostProperty, constLastFmHost);
j->setProperty(constLargeProperty, largeUrl);
j->setProperty(constThumbProperty, thumbUrl);
}
}
}
foreach (const QString &id, musibBrainzIds) {
QUrl coverartUrl;
coverartUrl.setScheme("http");
coverartUrl.setHost(constCoverArtArchiveHost);
coverartUrl.setPath("/release/"+id);
sendQueryRequest(coverartUrl);
}
}
void CoverDialog::parseGoogleQueryResponse(const QByteArray &resp)
{
// Code based on Audex CDDA Extractor
QRegExp rx("<a\\shref=\"(\\/imgres\\?imgurl=[^\"]+)\">[\\s\\n]*<img[^>]+src=\"([^\"]+)\"");
rx.setCaseSensitivity(Qt::CaseInsensitive);
rx.setMinimal(true);
int pos = 0;
QString xml(QString::fromUtf8(resp));
QString html = xml.replace(QLatin1String("&amp;"), QLatin1String("&"));
while (-1!=(pos=rx.indexIn(html, pos))) {
#if QT_VERSION < 0x050000
QUrl url("http://www.google.com"+rx.cap(1));
#else
QUrl u("http://www.google.com"+rx.cap(1));
QUrlQuery url(u);
#endif
int width=url.queryItemValue("w").toInt();
int height=url.queryItemValue("h").toInt();
if (width>=100 && height>=100) {
QString largeUrl=url.queryItemValue("imgurl");
QString thumbUrl=rx.cap(2);
if (!thumbUrl.isEmpty() && !largeUrl.isEmpty()) {
QNetworkReply *j=downloadImage(thumbUrl, DL_Thumbnail);
if (j) {
j->setProperty(constHostProperty, constGoogleHost);
j->setProperty(constLargeProperty, largeUrl);
j->setProperty(constThumbProperty, thumbUrl);
j->setProperty(constWidthProperty, width);
j->setProperty(constHeightProperty, height);
j->setProperty(constSizeProperty, url.queryItemValue("sz").toInt());
}
}
}
pos += rx.matchedLength();
}
}
void CoverDialog::parseDiscogsQueryResponse(const QByteArray &resp)
{
QJson::Parser parser;
bool ok=false;
QVariantMap parsed=parser.parse(resp, &ok).toMap();
if (ok && parsed.contains("resp")) {
QVariantMap response=parsed["resp"].toMap();
if (response.contains("search")) {
QVariantMap search=response["search"].toMap();
if (search.contains("searchresults")) {
QVariantMap searchresults=search["searchresults"].toMap();
if (searchresults.contains("results")) {
QVariantList results=searchresults["results"].toList();
foreach (const QVariant &r, results) {
QVariantMap rm=r.toMap();
if (rm.contains("uri")) {
QStringList parts=rm["uri"].toString().split("/", QString::SkipEmptyParts);
if (!parts.isEmpty()) {
QUrl discogsUrl;
#if QT_VERSION < 0x050000
QUrl &discogsQuery=discogsUrl;
#else
QUrlQuery discogsQuery;
#endif
discogsUrl.setScheme("http");
discogsUrl.setHost(constDiscogsHost);
discogsUrl.setPath("/release/"+parts.last());
discogsQuery.addQueryItem("f", "json");
#if QT_VERSION >= 0x050000
discogsUrl.setQuery(discogsQuery);
#endif
sendQueryRequest(discogsUrl);
}
}
// if (rm.contains("thumb")) {
// QString thumbUrl=rm["thumb"].toString();
// if (thumbUrl.contains("/image/R-150-")) {
// QString largeUrl=thumbUrl.replace("image/R-150-", "/image/R-");
// QNetworkReply *j=downloadImage(thumbUrl, DL_Thumbnail);
// if (j) {
// j->setProperty(constHostProperty, constDiscogsHost);
// j->setProperty(constLargeProperty, largeUrl);
// j->setProperty(constThumbProperty, thumbUrl);
// }
// }
// }
}
}
}
} else if (response.contains("release")) {
QVariantMap release=response["release"].toMap();
if (release.contains("images")) {
QVariantList images=release["images"].toList();
foreach (const QVariant &i, images) {
QVariantMap im=i.toMap();
if (im.contains("uri") && im.contains("uri150")) {
QString thumbUrl=im["uri150"].toString();
QString largeUrl=im["uri"].toString();
if (!thumbUrl.isEmpty() && !largeUrl.isEmpty()) {
QNetworkReply *j=downloadImage(thumbUrl, DL_Thumbnail);
if (j) {
j->setProperty(constHostProperty, constDiscogsHost);
j->setProperty(constLargeProperty, largeUrl);
j->setProperty(constThumbProperty, thumbUrl);
j->setProperty(constWidthProperty, im["width"].toString().toInt());
j->setProperty(constHeightProperty, im["height"].toString().toInt());
}
}
}
}
}
}
}
}
void CoverDialog::parseCoverArtArchiveQueryResponse(const QByteArray &resp)
{
QJson::Parser parser;
bool ok=false;
QVariantMap parsed=parser.parse(resp, &ok).toMap();
if (ok && parsed.contains("images")) {
QVariantList images=parsed["images"].toList();
foreach (const QVariant &i, images) {
QVariantMap im=i.toMap();
if (im.contains("front") && im["front"].toBool() && im.contains("image") && im.contains("thumbnails")) {
QVariantMap thumb=im["thumbnails"].toMap();
QString largeUrl=im["image"].toString();
QString thumbUrl;
if (thumb.contains("small")) {
thumbUrl=thumb["small"].toString();
} else if (thumb.contains("large")) {
thumbUrl=thumb["large"].toString();
}
if (!thumbUrl.isEmpty() && !largeUrl.isEmpty()) {
QNetworkReply *j=downloadImage(thumbUrl, DL_Thumbnail);
if (j) {
j->setProperty(constHostProperty, constCoverArtArchiveHost);
j->setProperty(constLargeProperty, largeUrl);
j->setProperty(constThumbProperty, thumbUrl);
}
}
}
}
}
}
struct AmazonImage
{
AmazonImage() : w(-1), h(-1) { }
bool isNull() { return w<0 || h<0 || url.isEmpty(); }
QString url;
int w;
int h;
};
struct AmazonEntry
{
AmazonImage small;
AmazonImage large;
};
static AmazonImage readImage(QXmlStreamReader &doc)
{
AmazonImage image;
QString tag=doc.name().toString();
while (!doc.atEnd()) {
doc.readNext();
if (QXmlStreamReader::StartElement==doc.tokenType()) {
if (QLatin1String("URL")==doc.name()) {
image.url= doc.readElementText();
} else if (QLatin1String("Width")==doc.name()) {
image.w=doc.readElementText().toInt();
} else if (QLatin1String("Height")==doc.name()) {
image.h=doc.readElementText().toInt();
} else {
doc.skipCurrentElement();
}
} else if (QXmlStreamReader::EndElement==doc.tokenType() && tag==doc.name()) {
break;
}
}
return image;
}
static AmazonEntry readItem(QXmlStreamReader &doc)
{
AmazonEntry entry;
QString tag=doc.name().toString();
while (!doc.atEnd()) {
doc.readNext();
if (QXmlStreamReader::StartElement==doc.tokenType()) {
if (QLatin1String("LargeImage")==doc.name()) {
entry.large=readImage(doc);
} else if (QLatin1String("SmallImage")==doc.name()) {
entry.small=readImage(doc);
} else if (entry.small.h<0 && QLatin1String("MediumImage")==doc.name()) {
entry.small=readImage(doc);
} else {
doc.skipCurrentElement();
}
} else if (QXmlStreamReader::EndElement==doc.tokenType() && tag==doc.name()) {
break;
}
}
return entry;
}
void CoverDialog::parseAmazonQueryResponse(const QByteArray &resp)
{
QXmlStreamReader doc(resp);
while (!doc.atEnd()) {
doc.readNext();
if (QXmlStreamReader::StartElement==doc.tokenType()) {
if (QLatin1String("Item")==doc.name()) {
AmazonEntry entry=readItem(doc);
if (!entry.small.isNull() && !entry.large.isNull()) {
QNetworkReply *j=downloadImage(entry.small.url, DL_Thumbnail);
if (j) {
j->setProperty(constHostProperty, constAmazonHost);
j->setProperty(constLargeProperty, entry.large.url);
j->setProperty(constThumbProperty, entry.small.url);
j->setProperty(constWidthProperty, entry.large.w);
j->setProperty(constHeightProperty, entry.large.h);
}
}
}
}
}
}
void CoverDialog::slotButtonClicked(int button)
{
switch (button) {
case Ok: {
QList<QListWidgetItem*> items=list->selectedItems();
if (1==items.size()) {
CoverItem *cover=(CoverItem *)items.at(0);
if (CoverItem::Type_Local==cover->type()) {
if (saveCover(cover->url(), ((LocalCover *)cover)->image())) {
accept();
}
} else if (CoverItem::Type_Existing!=cover->type()) {
QNetworkReply *j=downloadImage(cover->url(), DL_LargeSave);
if (j) {
j->setProperty(constLargeProperty, cover->url());
}
}
}
break;
}
case Cancel:
reject();
// Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
Dialog::slotButtonClicked(button);
break;
default:
break;
}
}
bool CoverDialog::saveCover(const QString &src, const QImage &img)
{
if (song.isCdda()) {
QString dir = Utils::cacheDir(Covers::constCddaCoverDir, true);
if (!dir.isEmpty()) {
QString destName=dir+song.file.mid(7)+src.mid(src.length()-4);
if (QFile::exists(destName)) {
QFile::remove(destName);
}
if (QFile::copy(src, destName)) {
emit selectedCover(img, destName);
return true;
}
}
MessageBox::error(this, i18n("Failed to set cover!\nCould not make copy!"));
return false;
}
QString existingBackup;
if (existing && !existing->url().isEmpty()) {
static const QLatin1String constBakExt(".bak");
existingBackup=existing->url()+constBakExt;
if (!QFile::rename(existing->url(), existingBackup)) {
MessageBox::error(this, i18n("Failed to set cover!\nCould not backup original!"));
return false;
}
}
QString destName;
if (Settings::self()->storeCoversInMpdDir()) {
QString coverName=MPDConnection::self()->getDetails().coverName;
if (coverName.isEmpty()) {
coverName=Covers::constFileName;
}
bool haveAbsPath=song.file.startsWith('/');
QString dirName=song.file.endsWith('/') ? (haveAbsPath ? QString() : MPDConnection::self()->getDetails().dir)+song.file
: Utils::getDir((haveAbsPath ? QString() : MPDConnection::self()->getDetails().dir)+song.file);
destName=dirName+coverName+src.mid(src.length()-4);
} else { // Save to cache dir...
QString dir(Utils::cacheDir(Covers::constCoverDir+Covers::encodeName(song.albumArtist()), true));
destName=dir+Covers::encodeName(song.album)+src.mid(src.length()-4);
}
if (QFile::copy(src, destName)) {
Utils::setFilePerms(destName);
if (!existingBackup.isEmpty() && QFile::exists(existingBackup)) {
QFile::remove(existingBackup);
}
Covers::self()->emitCoverUpdated(song, img, destName);
return true;
} else {
if (existing && !existingBackup.isEmpty()) {
QFile::rename(existingBackup, existing->url());
}
MessageBox::error(this, i18n("Failed to set cover!\nCould not copy file to '%1'!").arg(destName));
return false;
}
}
static bool hasMimeType(QDropEvent *event)
{
#if QT_VERSION < 0x050000
return event->provides("text/uri-list");
#else
return event->mimeData()->formats().contains(QLatin1String("text/uri-list"));
#endif
}
void CoverDialog::dragEnterEvent(QDragEnterEvent *event)
{
if (hasMimeType(event)) {
event->acceptProposedAction();
}
}
void CoverDialog::dropEvent(QDropEvent *event)
{
if (hasMimeType(event) && Qt::CopyAction==event->dropAction()) {
event->acceptProposedAction();
QList<QUrl> urls(event->mimeData()->urls());
foreach (const QUrl &url, urls) {
if (url.scheme().isEmpty() || "file"==url.scheme()) {
QString path=url.path();
if (!currentLocalCovers.contains(path) && (path.endsWith(".jpg", Qt::CaseInsensitive) || path.endsWith(".png", Qt::CaseInsensitive))) {
QImage img(path);
if (!img.isNull()) {
currentLocalCovers.insert(path);
insertItem(new LocalCover(path, img, list));
}
}
}
}
}
}