Files
cantata/gui/coverdialog.cpp
rezodlub e8cc1ab8ef Fixes:
empty folders on MPD side (crash inserting/removing to/from model)
usage of sys/time.h changed to QTime
no unistd.h in Visual Studio
if QT was build without SSL support, there is no QSslSocket class (at least in 5.6 and 5.8)
missing strncasecmp in Visual Studio
utime.sys is actually sys/utime.h in Visual Studio
2017-06-11 15:31:35 +02:00

1329 lines
44 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011-2017 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 "support/messagebox.h"
#include "widgets/listview.h"
#include "network/networkaccessmanager.h"
#include "settings.h"
#include "mpd-interface/mpdconnection.h"
#include "support/utils.h"
#include "support/spinner.h"
#include "widgets/messageoverlay.h"
#include "support/icon.h"
#include "widgets/icons.h"
#include "config.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>
#include <QFileDialog>
#include <QFileInfo>
#include <QUrl>
#include <QUrlQuery>
#include <QMimeData>
#include <QJsonDocument>
#include <QTemporaryFile>
#include <QDir>
#include <QXmlStreamReader>
#include <QDomDocument>
#include <QDomElement>
#include <QSslSocket>
#include <QDebug>
#define DBUG if (Covers::debugEnabled()) qWarning() << "CoverDialog" << __FUNCTION__
static int iCount=0;
static const int constMaxTempFiles=20;
static QImage cropImage(QImage img, bool isArtist)
{
if (isArtist && img.width()!=img.height()) {
int size=qMin(img.width(), img.height());
return img.copy((img.width()-size)/2, 0, size, size);
}
return img;
}
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)));
}
enum Providers {
Prov_LastFm = 0x0001,
Prov_Google = 0x0002,
Prov_CoverArt = 0x0004,
Prov_Deezer = 0x0008,
Prov_Spotify = 0x0010,
Prov_ITunes = 0x0020,
Prov_All = 0x003F
};
class CoverItem : public QListWidgetItem
{
public:
CoverItem(const QString &u, const QString &tu, const QImage &img, const QString &text, QListWidget *parent, int w=-1, int h=-1, int sz=-1)
: QListWidgetItem(parent)
, imgUrl(u)
, thmbUrl(tu)
, list(parent) {
setSizeHint(parent->gridSize());
setTextAlignment(Qt::AlignHCenter | Qt::AlignTop);
setToolTip(u);
if (!img.isNull()) {
setImage(img);
}
if (sz>0) {
setText(QObject::tr("%1\n%2 x %3 (%4)", "name\nwidth x height (file size)").arg(text).arg(w).arg(h).arg(Utils::formatByteSize(sz)));
} else if (w>0 && h>0) {
setText(QObject::tr("%1\n%2 x %3", "name\nwidth x height").arg(text).arg(w).arg(h));
} else {
setText(text);
}
}
const QString & url() const { return imgUrl; }
const QString & thumbUrl() const { return thmbUrl; }
virtual bool isLocal() const { return false; }
virtual bool isExisting() const { return false; }
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 ExistingCover : public CoverItem
{
public:
ExistingCover(const Covers::Image &i, QListWidget *parent)
: CoverItem(i.fileName, QString(), i.img, QObject::tr("Current Cover"), parent, i.img.width(), i.img.height(), QFileInfo(i.fileName).size())
, img(i.img) {
QFont f(font());
f.setBold((true));
setFont(f);
}
bool isExisting() const { return true; }
const QImage & image() const { return img; }
private:
QImage img;
};
class LocalCover : public CoverItem
{
public:
LocalCover(const QString &u, const QImage &i, QListWidget *parent)
: CoverItem(u, QString(), i, Utils::getFile(u), parent, i.width(), i.height(), QFileInfo(u).size())
, img(i) { }
bool isLocal() const { return true; }
const QImage & image() const { return img; }
private:
QImage img;
};
class LastFmCover : public CoverItem
{
public:
LastFmCover(const QString &u, const QString &tu, const QImage &img, QListWidget *parent)
: CoverItem(u, tu, img, QLatin1String("Last.fm"), parent) { }
};
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, img, QLatin1String("Google"), parent, w, h, size*1024) { }
};
class CoverArtArchiveCover : public CoverItem
{
public:
CoverArtArchiveCover(const QString &u, const QString &tu, const QImage &img, QListWidget *parent)
: CoverItem(u, tu, img, QObject::tr("CoverArt Archive"), parent) { }
};
class DeezerCover : public CoverItem
{
public:
DeezerCover(const QString &u, const QString &tu, const QImage &img, QListWidget *parent)
: CoverItem(u, tu, img, QLatin1String("Deezer"), parent) { }
};
class SpotifyCover : public CoverItem
{
public:
SpotifyCover(const QString &u, const QString &tu, const QImage &img, QListWidget *parent)
: CoverItem(u, tu, img, QLatin1String("Spotify"), parent, 640, 640) { }
};
class ITunesCover : public CoverItem
{
public:
ITunesCover(const QString &u, const QString &tu, const QImage &img, QListWidget *parent)
: CoverItem(u, tu, img, QLatin1String("iTunes"), parent, 600, 600) { }
};
CoverPreview::CoverPreview(QWidget *p)
: Dialog(p)
, zoom(1.0)
, imgW(0),
imgH(0)
{
setButtons(Close);
setWindowTitle(tr("Image"));
QWidget *mw=new QWidget(this);
QVBoxLayout *layout=new QVBoxLayout(mw);
loadingLabel=new QLabel(tr("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();
QStyleOptionFrame 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(tr("Image (%1 x %2 %3%)", "Image (width x height zoom%)").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=Utils::layoutSpacing(this);
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(tr("Image (%1 x %2 %3%)", "Image (width x height zoom%)").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)
, currentQueryProviders(0)
, preview(0)
, saving(false)
, isArtist(false)
, spinner(0)
, msgOverlay(0)
, page(0)
, menu(0)
, showAction(0)
, removeAction(0)
{
Configuration cfg(objectName());
enabledProviders=cfg.get("enabledProviders", (int)Prov_All);
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(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()*3)+style()->pixelMetric(QStyle::PM_ScrollBarExtent)+spacing, list->gridSize().height()*2);
list->setSortingEnabled(false);
addFileButton->setIcon(Icons::self()->folderListIcon);
addFileButton->setAutoRaise(true);
configureButton->setIcon(Icons::self()->configureIcon);
configureButton->setAutoRaise(true);
QMenu *configMenu=new QMenu(configureButton);
addProvider(configMenu, QLatin1String("Last.fm"), Prov_LastFm, enabledProviders);
addProvider(configMenu, tr("CoverArt Archive"), Prov_CoverArt, enabledProviders);
addProvider(configMenu, QLatin1String("Google"), Prov_Google, enabledProviders);
addProvider(configMenu, QLatin1String("Deezer"), Prov_Deezer, enabledProviders);
addProvider(configMenu, QLatin1String("Spotify"), Prov_Spotify, enabledProviders);
addProvider(configMenu, QLatin1String("iTunes"), Prov_ITunes, enabledProviders);
configureButton->setMenu(configMenu);
configureButton->setPopupMode(QToolButton::InstantPopup);
setAcceptDrops(true);
}
CoverDialog::~CoverDialog()
{
Configuration cfg(objectName());
cfg.set("enabledProviders", enabledProviders);
iCount--;
cancelQuery();
clearTempFiles();
}
void CoverDialog::show(const Song &s, const Covers::Image &current)
{
song=s;
isArtist=song.isArtistImageRequest();
Covers::Image img=current.img.isNull() ? Covers::locateImage(song) : current;
if (img.validFileName() && !QFileInfo(img.fileName).isWritable()) {
MessageBox::error(parentWidget(),
(isArtist
? tr("An image already exists for this artist, and the file is not writeable.")
: tr("A cover already exists for this album, and the file is not writeable."))+
QString("<br/><br/><a href=\"%1\">%1</a>").arg(img.fileName));
deleteLater();
return;
}
note->setVisible(!isArtist);
if (isArtist) {
setCaption(tr("'%1' Artist Image").arg(song.albumArtist()));
} else {
setCaption(tr("'%1 - %2' Album Cover", "'Artist - Album' Album Cover").arg(song.albumArtist()).arg(song.album));
}
if (!img.img.isNull()) {
existing=new ExistingCover(isArtist ? Covers::Image(cropImage(img.img, true), img.fileName) : img, list);
list->addItem(existing);
}
/*
Add other images in the source folder? #716
if (!isArtist) {
QString dirName=MPDConnection::self()->getDetails().dir+Utils::getDir(s.file);
QStringList files=QDir(dirName).entryList(QStringList() << QLatin1String("*.jpg") << QLatin1String("*.png"), QDir::Files|QDir::Readable);
qWarning() << files;
foreach (const QString &f, files) {
QString fileName=dirName+f;
if (fileName!=img.fileName) {
QImage i(fileName);
if (!i.isNull()) {
currentLocalCovers.insert(fileName);
insertItem(new LocalCover(fileName, i, list));
}
}
}
}
*/
query->setText(isArtist ? song.albumArtist() : QString(song.albumArtist()+QLatin1String(" ")+song.album));
Dialog::show();
sendQuery();
}
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 * constLastFmHost="ws.audioscrobbler.com";
static const char * constGoogleHost="images.google.com";
static const char * constCoverArtArchiveHost="coverartarchive.org";
static const char * constSpotifyHost="ws.spotify.com";
static const char * constITunesHost="itunes.apple.com";
static const char * constDeezerHost="api.deezer.com";
void CoverDialog::queryJobFinished()
{
NetworkJob *reply=qobject_cast<NetworkJob *>(sender());
if (!reply) {
return;
}
reply->deleteLater();
if (!currentQuery.contains(reply)) {
return;
}
DBUG << reply->origUrl().toString() << reply->ok();
currentQuery.remove(reply);
if (reply->ok()) {
QString host=reply->property(constHostProperty).toString();
QByteArray resp=reply->readAll();
if (constLastFmHost==host) {
parseLastFmQueryResponse(resp);
} else if (constGoogleHost==host) {
parseGoogleQueryResponse(resp);
} else if (constCoverArtArchiveHost==host) {
parseCoverArtArchiveQueryResponse(resp);
} else if (constSpotifyHost==host) {
parseSpotifyQueryResponse(resp);
} else if (constITunesHost==host) {
parseITunesQueryResponse(resp);
} else if (constDeezerHost==host) {
parseDeezerQueryResponse(resp);
}
}
if (currentQuery.isEmpty()) {
setSearching(false);
}
}
void CoverDialog::insertItem(CoverItem *item)
{
list->addItem(item);
if (item->isLocal()) {
list->scrollToItem(item);
list->setItemSelected(item, true);
}
}
void CoverDialog::downloadJobFinished()
{
NetworkJob *reply=qobject_cast<NetworkJob *>(sender());
if (!reply) {
return;
}
reply->deleteLater();
if (!currentQuery.contains(reply)) {
return;
}
DownloadType dlType=(DownloadType)reply->property(constTypeProperty).toInt();
if (DL_LargeSave==dlType) {
saving=false;
}
DBUG << reply->origUrl().toString() << reply->ok();
currentQuery.remove(reply);
if (reply->ok()) {
QString host=reply->property(constHostProperty).toString();
QString url=reply->url().toString();
QByteArray data=reply->readAll();
const char *format=Covers::imageFormat(data);
QImage img=QImage::fromData(data, format);
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."+(format ? QString(QLatin1String(format)).toLower() : "png"));
if (temp->open()) {
if (!format) {
img.save(temp, "PNG");
} else {
temp->write(data);
}
if (tempFiles.size()>=constMaxTempFiles) {
QTemporaryFile *last=tempFiles.takeLast();
last->remove();
delete last;
}
temp->close();
temp->setProperty(constLargeProperty, reply->property(constLargeProperty));
tempFiles.prepend(temp);
} else {
delete temp;
temp=0;
}
}
if (isLarge) {
if (DL_LargePreview==dlType) {
previewDialog()->showImage(cropImage(img, isArtist), reply->property(constLargeProperty).toString());
} else if (DL_LargeSave==dlType) {
if (!temp) {
MessageBox::error(this, tr("Failed to set cover!\n\nCould not download to temporary file!"));
} else if (saveCover(temp->fileName(), img)) {
accept();
}
}
} else {
CoverItem *item=0;
img=cropImage(img, isArtist);
if (constLastFmHost==host) {
item=new LastFmCover(reply->property(constLargeProperty).toString(), url, img, list);
} else if (constGoogleHost==host) {
item=new GoogleCover(reply->property(constLargeProperty).toString(), url, img, reply->property(constWidthProperty).toInt(),
reply->property(constHeightProperty).toInt(), reply->property(constSizeProperty).toInt(), list);
} else if (constCoverArtArchiveHost==host) {
item=new CoverArtArchiveCover(reply->property(constLargeProperty).toString(), url, img, list);
} else if (constSpotifyHost==host) {
item=new SpotifyCover(reply->property(constLargeProperty).toString(), url, img, list);
} else if (constITunesHost==host) {
item=new ITunesCover(reply->property(constLargeProperty).toString(), url, img, list);
} else if (constDeezerHost==host) {
item=new DeezerCover(reply->property(constLargeProperty).toString(), url, img, 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, tr("Failed to download image!"));
}
if (currentQuery.isEmpty()) {
setSearching(false);
}
}
void CoverDialog::showImage(QListWidgetItem *item)
{
if (saving) {
return;
}
CoverItem *cover=static_cast<CoverItem *>(item);
if (cover->isExisting()) {
previewDialog()->downloading(cover->url());
previewDialog()->showImage(static_cast<ExistingCover *>(cover)->image(), cover->url());
} else if (cover->isLocal()) {
previewDialog()->downloading(cover->url());
previewDialog()->showImage(static_cast<LocalCover *>(cover)->image(), cover->url());
} else {
previewDialog()->downloading(cover->url());
NetworkJob *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 && enabledProviders==currentQueryProviders) {
page++;
} else {
page=0;
}
if (0==page) {
QList<CoverItem *> keep;
while (list->count()) {
CoverItem *item=static_cast<CoverItem *>(list->takeItem(0));
if (item->isExisting() || item->isLocal()) {
keep.append(item);
} else {
currentUrls.remove(item->url());
currentUrls.remove(item->thumbUrl());
delete item;
}
}
foreach (CoverItem *item, keep) {
list->addItem(item);
}
cancelQuery();
}
currentQueryString=fixedQuery;
if (enabledProviders&Prov_LastFm) {
sendLastFmQuery(fixedQuery, page);
}
if (enabledProviders&Prov_Google) {
sendGoogleQuery(fixedQuery, page);
}
if (page==0) {
if (enabledProviders&Prov_Spotify) {
sendSpotifyQuery(fixedQuery);
}
if (enabledProviders&Prov_ITunes) {
sendITunesQuery(fixedQuery);
}
if (enabledProviders&Prov_Deezer) {
sendDeezerQuery(fixedQuery);
}
}
currentQueryProviders=enabledProviders;
setSearching(!currentQuery.isEmpty());
}
void CoverDialog::sendLastFmQuery(const QString &fixedQuery, int page)
{
QUrl url;
QUrlQuery query;
url.setScheme("https");
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+1));
query.addQueryItem(isArtist ? "artist" : "album", fixedQuery);
query.addQueryItem("method", isArtist ? "artist.search" : "album.search");
url.setQuery(query);
sendQueryRequest(url);
}
void CoverDialog::sendGoogleQuery(const QString &fixedQuery, int page)
{
QUrl url;
QUrlQuery query;
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));
url.setQuery(query);
sendQueryRequest(url);
}
void CoverDialog::sendSpotifyQuery(const QString &fixedQuery)
{
#ifdef QT_NO_SSL
return;
#else
if (!QSslSocket::supportsSsl()) {
return;
}
QUrl url;
QUrlQuery query;
url.setScheme("http");
url.setHost(constSpotifyHost);
url.setPath(isArtist ? "/search/1/artist.json" : "/search/1/album.json");
query.addQueryItem("q", fixedQuery);
url.setQuery(query);
sendQueryRequest(url);
#endif
}
void CoverDialog::sendITunesQuery(const QString &fixedQuery)
{
if (isArtist) { // TODO???
return;
}
QUrl url;
QUrlQuery query;
url.setScheme("http");
url.setHost(constITunesHost);
url.setPath("/search");
query.addQueryItem("term", fixedQuery);
query.addQueryItem("limit", QString::number(10));
query.addQueryItem("media", "music");
query.addQueryItem("entity", "album");
url.setQuery(query);
sendQueryRequest(url);
}
void CoverDialog::sendDeezerQuery(const QString &fixedQuery)
{
QUrl url;
QUrlQuery query;
url.setScheme("http");
url.setHost(constDeezerHost);
url.setPath(isArtist ? "/search/artist" : "/search/album");
query.addQueryItem("q", fixedQuery);
query.addQueryItem("nb_items", QString::number(10));
query.addQueryItem("output", "json");
url.setQuery(query);
sendQueryRequest(url);
}
void CoverDialog::checkStatus()
{
QList<QListWidgetItem*> items=list->selectedItems();
enableButtonOk(1==items.size() && !static_cast<CoverItem *>(items.at(0))->isExisting());
}
void CoverDialog::cancelQuery()
{
foreach (NetworkJob *job, currentQuery) {
job->cancelAndDelete();
}
currentQuery.clear();
setSearching(false);
}
void CoverDialog::addLocalFile()
{
QString fileName=QFileDialog::getOpenFileName(this, tr("Load Local Cover"), QDir::homePath(), tr("Images (*.png *.jpg)"));
if (!fileName.isEmpty()) {
if (currentLocalCovers.contains(fileName)) {
MessageBox::error(this, tr("File is already in list!"));
} else {
QImage img(fileName);
if (img.isNull()) {
MessageBox::error(this, tr("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"), tr("Display"));
removeAction=menu->addAction(Icon("list-remove"), tr("Remove"));
connect(showAction, SIGNAL(triggered()), SLOT(showImage()));
connect(removeAction, SIGNAL(triggered()), SLOT(removeImages()));
}
QList<QListWidgetItem*> items=list->selectedItems();
showAction->setEnabled(1==items.count());
removeAction->setEnabled(!items.isEmpty());
if (removeAction->isEnabled()) {
foreach (QListWidgetItem *i, items) {
if (static_cast<CoverItem *>(i)->isExisting()) {
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::updateProviders()
{
QAction *s=qobject_cast<QAction *>(sender());
enabledProviders=0;
if (s) {
if (Prov_LastFm==s->data().toUInt() && !s->isChecked()) {
providers[Prov_CoverArt]->setChecked(false);
} else if (Prov_CoverArt==s->data().toUInt() && s->isChecked()) {
providers[Prov_LastFm]->setChecked(true);
}
}
foreach (const QAction *act, configureButton->menu()->actions()) {
if (act->isChecked()) {
enabledProviders|=act->data().toUInt();
}
}
DBUG << enabledProviders;
}
void CoverDialog::clearTempFiles()
{
foreach (QTemporaryFile *file, tempFiles) {
file->remove();
delete file;
}
}
void CoverDialog::sendQueryRequest(const QUrl &url, const QString &host)
{
DBUG << url.toString();
NetworkJob *j=NetworkAccessManager::self()->get(QNetworkRequest(url));
j->setProperty(constHostProperty, host.isEmpty() ? url.host() : host);
j->setProperty(constTypeProperty, (int)DL_Query);
connect(j, SIGNAL(finished()), this, SLOT(queryJobFinished()));
currentQuery.insert(j);
}
NetworkJob * CoverDialog::downloadImage(const QString &url, DownloadType dlType)
{
DBUG << url << 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;
}
NetworkJob *j=NetworkAccessManager::self()->get(QNetworkRequest(QUrl(url)));
connect(j, SIGNAL(finished()), this, SLOT(downloadJobFinished()));
currentQuery.insert(j);
j->setProperty(constTypeProperty, (int)dlType);
if (saving) {
previewDialog()->downloading(url);
connect(j, SIGNAL(downloadProgress(qint64, qint64)), preview, SLOT(progress(qint64, qint64)));
}
return j;
}
void CoverDialog::downloadThumbnail(const QString &thumbUrl, const QString &largeUrl, const QString &host, int w, int h, int sz)
{
if (thumbUrl.isEmpty() || largeUrl.isEmpty()) {
return;
}
NetworkJob *j=downloadImage(thumbUrl, DL_Thumbnail);
if (j) {
j->setProperty(constThumbProperty, thumbUrl);
j->setProperty(constLargeProperty, largeUrl);
j->setProperty(constHostProperty, host);
if (w>0) {
j->setProperty(constWidthProperty, w);
}
if (h>0) {
j->setProperty(constHeightProperty, h);
}
if (sz>0) {
j->setProperty(constSizeProperty, sz);
}
}
}
typedef QMap<QString, QString> SizeMap;
void CoverDialog::parseLastFmQueryResponse(const QByteArray &resp)
{
bool inSection=false;
QXmlStreamReader doc(resp);
SizeMap urls;
QList<SizeMap> entries;
QStringList musibBrainzIds;
doc.setNamespaceProcessing(false);
while (!doc.atEnd()) {
doc.readNext();
if (doc.isStartElement()) {
if (!inSection && QLatin1String(isArtist ? "artist" : "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(isArtist ? "artist" : "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;
}
}
downloadThumbnail(thumbUrl, largeUrl, constLastFmHost);
}
if (enabledProviders&Prov_CoverArt) {
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))) {
QUrl u("http://www.google.com"+rx.cap(1));
QUrlQuery url(u);
int width=url.queryItemValue("w").toInt();
int height=url.queryItemValue("h").toInt();
if (canUse(width, height)) {
downloadThumbnail(rx.cap(2), url.queryItemValue("imgurl"), constGoogleHost, width, height, url.queryItemValue("sz").toInt());
}
pos += rx.matchedLength();
}
}
void CoverDialog::parseCoverArtArchiveQueryResponse(const QByteArray &resp)
{
QJsonParseError jsonParseError;
QVariantMap parsed=QJsonDocument::fromJson(resp, &jsonParseError).toVariant().toMap();
bool ok=QJsonParseError::NoError==jsonParseError.error;
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();
}
downloadThumbnail(thumbUrl, largeUrl, constCoverArtArchiveHost);
}
}
}
}
void CoverDialog::parseSpotifyQueryResponse(const QByteArray &resp)
{
QJsonParseError jsonParseError;
QVariantMap parsed=QJsonDocument::fromJson(resp, &jsonParseError).toVariant().toMap();
bool ok=QJsonParseError::NoError==jsonParseError.error;
if (ok) {
if (parsed.contains("info")) { // Initial query response...
const QString key=QLatin1String(isArtist ? "artists" : "albums");
const QString href=QLatin1String("spotify:")+QLatin1String(isArtist ? "artist:" : "album:");
const QString baseUrl=QLatin1String("https://embed.spotify.com/oembed/?url=http://open.spotify.com/")+QLatin1String(isArtist ? "artist/" : "album/");
if (parsed.contains(key)) {
QVariantList results=parsed[key].toList();
foreach (const QVariant &res, results) {
QVariantMap item=res.toMap();
if (item.contains("href")) {
QString url=item["href"].toString();
if (url.contains(href)) {
url=baseUrl+url.remove(href);
sendQueryRequest(QUrl(url), constSpotifyHost);
}
}
}
}
} else if (parsed.contains("provider_url") && parsed.contains("thumbnail_url")) { // Response to query above
QString thumbUrl=parsed["thumbnail_url"].toString();
downloadThumbnail(thumbUrl, QString(thumbUrl).replace("/cover/", "/640/"), constSpotifyHost);
}
}
}
void CoverDialog::parseITunesQueryResponse(const QByteArray &resp)
{
QJsonParseError jsonParseError;
QVariantMap parsed=QJsonDocument::fromJson(resp, &jsonParseError).toVariant().toMap();
bool ok=QJsonParseError::NoError==jsonParseError.error;
if (ok && parsed.contains("results")) {
QVariantList results=parsed["results"].toList();
foreach (const QVariant &res, results) {
QVariantMap item=res.toMap();
if (item.contains("artworkUrl100")) {
QString thumbUrl=item["artworkUrl100"].toString();
downloadThumbnail(thumbUrl, QString(thumbUrl).replace("100x100", "600x600"), constITunesHost);
}
}
}
}
void CoverDialog::parseDeezerQueryResponse(const QByteArray &resp)
{
const QString key=QLatin1String(isArtist ? "picture" : "cover");
QJsonParseError jsonParseError;
QVariantMap parsed=QJsonDocument::fromJson(resp, &jsonParseError).toVariant().toMap();
bool ok=QJsonParseError::NoError==jsonParseError.error;
if (ok && parsed.contains("data")) {
QVariantList results=parsed["data"].toList();
foreach (const QVariant &res, results) {
QVariantMap item=res.toMap();
if (item.contains(key)) {
QString thumbUrl=item[key].toString();
downloadThumbnail(thumbUrl, thumbUrl+"&size=big", constDeezerHost);
}
}
}
}
void CoverDialog::slotButtonClicked(int button)
{
switch (button) {
case Ok: {
QList<QListWidgetItem*> items=list->selectedItems();
if (1==items.size()) {
CoverItem *cover=static_cast<CoverItem *>(items.at(0));
if (cover->isLocal()) {
if (saveCover(cover->url(), static_cast<LocalCover *>(cover)->image())) {
accept();
}
} else if (!cover->isExisting()) {
NetworkJob *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)
{
QString filePath=song.filePath();
if (song.isCdda()) {
QString dir = Utils::cacheDir(Covers::constCddaCoverDir, true);
if (!dir.isEmpty()) {
QString destName=dir+filePath.mid(Song::constCddaProtocol.length())+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, tr("Failed to set cover!\n\nCould not make copy!"));
return false;
}
QString existingBackup;
if (existing && !existing->url().isEmpty() && !existing->url().startsWith(Covers::constCoverInTagPrefix)) {
static const QLatin1String constBakExt(".bak");
existingBackup=existing->url()+constBakExt;
if (!QFile::rename(existing->url(), existingBackup)) {
MessageBox::error(this, tr("Failed to set cover!\n\nCould not backup original!"));
return false;
}
}
QString destName;
QString mpdDir;
QString dirName;
bool saveInMpd=Settings::self()->storeCoversInMpdDir();
if (saveInMpd) {
bool haveAbsPath=filePath.startsWith('/');
mpdDir=MPDConnection::self()->getDetails().dir;
if (haveAbsPath || !mpdDir.isEmpty()) {
dirName=filePath.endsWith('/') ? (haveAbsPath ? QString() : mpdDir)+filePath
: Utils::getDir((haveAbsPath ? QString() : mpdDir)+filePath);
}
saveInMpd=QDir(dirName).exists();
}
if (isArtist) {
if (saveInMpd && !mpdDir.isEmpty() && dirName.startsWith(mpdDir) && 2==dirName.mid(mpdDir.length()).split('/', QString::SkipEmptyParts).count()) {
QDir d(dirName);
d.cdUp();
destName=d.absolutePath()+'/'+Covers::artistFileName(song)+src.mid(src.length()-4);
} else {
destName=Utils::cacheDir(Covers::constCoverDir, true)+Covers::encodeName(song.albumArtist())+src.mid(src.length()-4);
}
} else {
if (saveInMpd) {
destName=dirName+Covers::albumFileName(song)+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 (!destName.startsWith("http://") && QFile::copy(src, destName)) {
Utils::setFilePerms(destName);
if (!existingBackup.isEmpty() && QFile::exists(existingBackup)) {
QFile::remove(existingBackup);
}
Covers::self()->updateCover(song, img, destName);
return true;
} else {
if (existing && !existingBackup.isEmpty()) {
QFile::rename(existingBackup, existing->url());
}
MessageBox::error(this, tr("Failed to set cover!\n\nCould not copy file to '%1'!").arg(destName));
return false;
}
}
static bool hasMimeType(QDropEvent *event)
{
return event->mimeData()->formats().contains(QLatin1String("text/uri-list"));
}
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));
}
}
}
}
}
}
void CoverDialog::setSearching(bool s)
{
if (!spinner && !s) {
return;
}
if (!spinner) {
spinner=new Spinner(this);
spinner->setWidget(list);
}
if (!msgOverlay) {
msgOverlay=new MessageOverlay(this);
msgOverlay->setWidget(list);
connect(msgOverlay, SIGNAL(cancel()), SLOT(cancelQuery()));
}
if (s) {
spinner->start();
msgOverlay->setText(tr("Searching..."));
} else {
spinner->stop();
msgOverlay->setText(QString());
}
}
void CoverDialog::addProvider(QMenu *mnu, const QString &name, int bit, int value)
{
QAction *act=mnu->addAction(name);
act->setData(bit);
act->setCheckable(true);
act->setChecked(value&bit);
connect(act, SIGNAL(toggled(bool)), this, SLOT(updateProviders()));
providers.insert(bit, act);
}