Files
cantata/gui/coverdialog.cpp
2021-07-29 12:13:42 +01:00

1290 lines
43 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011-2021 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 "apikeys.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 <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 isArtistOrComposer)
{
if (isArtistOrComposer && 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)));
}
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 override { 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 override { 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 * CANTATA_GET_LABEL_PIXMAP(imageLabel).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->angleDelta().y() / 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(nullptr)
, preview(nullptr)
, saving(false)
, isArtist(false)
, isComposer(false)
, spinner(nullptr)
, msgOverlay(nullptr)
, page(0)
, menu(nullptr)
, showAction(nullptr)
, removeAction(nullptr)
{
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);
setAcceptDrops(true);
}
CoverDialog::~CoverDialog()
{
iCount--;
cancelQuery();
clearTempFiles();
}
void CoverDialog::show(const Song &s, const Covers::Image &current)
{
song=s;
isArtist=song.isArtistImageRequest();
isComposer=song.isComposerImageRequest();
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.")
: isComposer
? tr("An image already exists for this composer, 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 && !isComposer);
if (isArtist) {
setCaption(tr("'%1' Artist Image").arg(song.albumArtist()));
} else if (isComposer) {
setCaption(tr("'%1' Composer 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 || isComposer ? 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("*.jpeg") << QLatin1String("*.png"), QDir::Files|QDir::Readable);
qWarning() << files;
for (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() : isComposer ? song.albumArtistOrComposer() : 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);
item->setSelected(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=nullptr;
if (isLarge || (reply->property(constThumbProperty).toString()==reply->property(constLargeProperty).toString())) {
temp=new QTemporaryFile("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=nullptr;
}
}
if (isLarge) {
if (DL_LargePreview==dlType) {
previewDialog()->showImage(cropImage(img, isArtist||isComposer), 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=nullptr;
img=cropImage(img, isArtist||isComposer);
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) {
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;
}
}
for (CoverItem *item: keep) {
list->addItem(item);
}
cancelQuery();
}
currentQueryString=fixedQuery;
sendLastFmQuery(fixedQuery, page);
//sendGoogleQuery(fixedQuery, page);
if (page==0) {
//sendSpotifyQuery(fixedQuery);
sendITunesQuery(fixedQuery);
sendDeezerQuery(fixedQuery);
}
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/");
ApiKeys::self()->addKey(query, ApiKeys::LastFm);
query.addQueryItem("limit", QString::number(20));
query.addQueryItem("page", QString::number(page+1));
query.addQueryItem(isArtist||isComposer ? "artist" : "album", fixedQuery);
query.addQueryItem("method", isArtist||isComposer ? "artist.search" : "album.search");
url.setQuery(query);
sendQueryRequest(url);
}
/*
void CoverDialog::sendGoogleQuery(const QString &fixedQuery, int page)
{
QUrl url;
QUrlQuery query;
url.setScheme("https");
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("https");
url.setHost(constSpotifyHost);
url.setPath(isArtist||isComposer ? "/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||isComposer) { // TODO???
return;
}
QUrl url;
QUrlQuery query;
url.setScheme("https");
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("https");
url.setHost(constDeezerHost);
url.setPath(isArtist||isComposer ? "/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()
{
for (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 *.jpeg)"));
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(tr("Display"));
removeAction=menu->addAction(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()) {
for (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();
for (QListWidgetItem *i: items) {
delete i;
}
}
void CoverDialog::clearTempFiles()
{
for (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 nullptr;
}
currentUrls.insert(url);
} else {
for (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 nullptr;
}
tmp->remove();
tempFiles.removeAll(tmp);
delete 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 musicBrainzIds;
doc.setNamespaceProcessing(false);
while (!doc.atEnd()) {
doc.readNext();
if (doc.isStartElement()) {
if (!inSection && QLatin1String(isArtist||isComposer ? "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) {
musicBrainzIds.append(id);
}
}
} else if (doc.isEndElement() && inSection && QLatin1String(isArtist||isComposer ? "artist" : "album")==doc.name()) {
if (!urls.isEmpty()) {
entries.append(urls);
urls.clear();
}
inSection=false;
}
}
if (!(isArtist||isComposer)) {
QStringList largeUrls=QStringList() << "extralarge" << "large";
QStringList thumbUrls=QStringList() << "large" << "medium" << "small";
for (const SizeMap &urls: entries) {
QString largeUrl;
QString thumbUrl;
for (const QString &u: largeUrls) {
if (urls.contains(u)) {
largeUrl=urls[u];
break;
}
}
for (const QString &u: thumbUrls) {
if (urls.contains(u)) {
thumbUrl=urls[u];
break;
}
}
downloadThumbnail(thumbUrl, largeUrl, constLastFmHost);
}
}
for (const QString &id: musicBrainzIds) {
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();
for (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||isComposer ? "artists" : "albums");
const QString href=QLatin1String("spotify:")+QLatin1String(isArtist||isComposer ? "artist:" : "album:");
const QString baseUrl=QLatin1String("https://embed.spotify.com/oembed/?url=http://open.spotify.com/")+QLatin1String(isArtist ||isComposer? "artist/" : "album/");
if (parsed.contains(key)) {
QVariantList results=parsed[key].toList();
for (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();
for (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||isComposer ? "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();
for (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();
QString ext = Utils::getExtension(src);
if (0==ext.compare(".jpeg", Qt::CaseInsensitive)) {
ext = ".jpg";
}
if (song.isCdda()) {
QString dir = Utils::cacheDir(Covers::constCddaCoverDir, true);
if (!dir.isEmpty()) {
QString destName=dir+filePath.mid(Song::constCddaProtocol.length())+ext;
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=!isArtist && !isComposer && 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('/', CANTATA_SKIP_EMPTY).count()) {
QDir d(dirName);
d.cdUp();
destName=d.absolutePath()+'/'+Covers::constArtistImage+ext;
} else*/ {
destName=Utils::cacheDir(Covers::constCoverDir, true)+Covers::encodeName(song.albumArtist())+ext;
}
} else if (isComposer) {
/*if (saveInMpd && !mpdDir.isEmpty() && dirName.startsWith(mpdDir) && 2==dirName.mid(mpdDir.length()).split('/', CANTATA_SKIP_EMPTY).count()) {
QDir d(dirName);
d.cdUp();
destName=d.absolutePath()+'/'+Covers::constComposerImage+ext;
} else*/ {
destName=Utils::cacheDir(Covers::constCoverDir, true)+Covers::encodeName(song.albumArtistOrComposer())+ext;
}
} else {
if (saveInMpd) {
destName=dirName+Covers::albumFileName(song)+ext;
} else { // Save to cache dir...
QString dir(Utils::cacheDir(Covers::constCoverDir+Covers::encodeName(song.albumArtist()), true));
destName=dir+Covers::encodeName(song.album)+ext;
}
}
if (!destName.startsWith("http:/", Qt::CaseInsensitive) && !destName.startsWith("https:/", Qt::CaseInsensitive) && 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());
for (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(".jpeg", 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());
}
}
#include "moc_coverdialog.cpp"