Initial commit.

This commit is contained in:
Jonas Kvinge
2018-02-27 18:06:05 +01:00
parent 85d9664df7
commit b2b1ba7abe
1393 changed files with 177311 additions and 1 deletions

View File

@@ -0,0 +1,348 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QAction>
#include <QDesktopWidget>
#include <QDialog>
#include <QDragEnterEvent>
#include <QFileDialog>
#include <QImageWriter>
#include <QLabel>
#include <QList>
#include <QMenu>
#include <QUrl>
#include <QMimeData>
#include "covermanager/albumcoverchoicecontroller.h"
#include "core/application.h"
#include "core/logging.h"
#include "core/utilities.h"
#include "core/iconloader.h"
#include "collection/collectionbackend.h"
#include "covermanager/albumcoverfetcher.h"
#include "covermanager/albumcoverloader.h"
#include "covermanager/currentartloader.h"
#include "covermanager/albumcovermanager.h"
#include "covermanager/albumcoversearcher.h"
#include "covermanager/coverfromurldialog.h"
const char *AlbumCoverChoiceController::kLoadImageFileFilter = QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
const char *AlbumCoverChoiceController::kSaveImageFileFilter = QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
const char *AlbumCoverChoiceController::kAllFilesFilter = QT_TR_NOOP("All files (*)");
QSet<QString> *AlbumCoverChoiceController::sImageExtensions = nullptr;
AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent) :
QWidget(parent),
app_(nullptr),
cover_searcher_(nullptr),
cover_fetcher_(nullptr),
save_file_dialog_(nullptr),
cover_from_url_dialog_(nullptr) {
cover_from_file_ = new QAction(IconLoader::Load("document-open"), tr("Load cover from disk..."), this);
cover_to_file_ = new QAction(IconLoader::Load("document-save"), tr("Save cover to disk..."), this);
cover_from_url_ = new QAction(IconLoader::Load("download"), tr("Load cover from URL..."), this);
search_for_cover_ = new QAction(IconLoader::Load("search"), tr("Search for album covers..."), this);
unset_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Unset cover"), this);
show_cover_ = new QAction(IconLoader::Load("zoom-in"), tr("Show fullsize..."), this);
search_cover_auto_ = new QAction(IconLoader::Load("search"), tr("Search automatically"), this);
search_cover_auto_->setCheckable(true);
search_cover_auto_->setChecked(false);
separator_ = new QAction(this);
separator_->setSeparator(true);
}
AlbumCoverChoiceController::~AlbumCoverChoiceController() {}
void AlbumCoverChoiceController::SetApplication(Application *app) {
app_ = app;
cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this);
cover_searcher_ = new AlbumCoverSearcher(QIcon(":/pictures/noalbumart.png"), app, this);
cover_searcher_->Init(cover_fetcher_);
connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)), this, SLOT(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)));
}
QList<QAction*> AlbumCoverChoiceController::GetAllActions() {
return QList<QAction*>() << cover_from_file_ << cover_to_file_ << separator_ << cover_from_url_ << search_for_cover_ << unset_cover_ << show_cover_;
}
QString AlbumCoverChoiceController::LoadCoverFromFile(Song *song) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QString cover = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (cover.isNull()) return QString();
// Can we load the image?
QImage image(cover);
if (!image.isNull()) {
SaveCover(song, cover);
return cover;
}
else {
return QString();
}
}
void AlbumCoverChoiceController::SaveCoverToFile(const Song &song, const QImage &image) {
QString initial_file_name = "/" + (song.effective_album().isEmpty() ? tr("Unknown") : song.effective_album()) + ".jpg";
QString save_filename = QFileDialog::getSaveFileName(this, tr("Save album cover"), GetInitialPathForFileDialog(song, initial_file_name), tr(kSaveImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (save_filename.isNull()) return;
QString extension = save_filename.right(4);
if (!extension.startsWith('.') || !QImageWriter::supportedImageFormats().contains(extension.right(3).toUtf8())) {
save_filename.append(".jpg");
}
image.save(save_filename);
}
QString AlbumCoverChoiceController::GetInitialPathForFileDialog(const Song &song, const QString &filename) {
// art automatic is first to show user which cover the album may be
// using now; the song is using it if there's no manual path but we
// cannot use manual path here because it can contain cached paths
if (!song.art_automatic().isEmpty() && !song.has_embedded_cover()) {
return song.art_automatic();
// if no automatic art, start in the song's folder
}
else if (!song.url().isEmpty() && song.url().toLocalFile().contains('/')) {
return song.url().toLocalFile().section('/', 0, -2) + filename;
// fallback - start in home
}
else {
return QDir::home().absolutePath() + filename;
}
}
QString AlbumCoverChoiceController::LoadCoverFromURL(Song *song) {
if (!cover_from_url_dialog_) { cover_from_url_dialog_ = new CoverFromURLDialog(this); }
QImage image = cover_from_url_dialog_->Exec();
if (!image.isNull()) {
QString cover = SaveCoverInCache(song->artist(), song->album(), image);
SaveCover(song, cover);
return cover;
}
else { return QString(); }
}
QString AlbumCoverChoiceController::SearchForCover(Song *song) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QString album = song->effective_album();
album = album.remove(QRegExp(" ?-? ?(\\(|\\[)(Disc|CD)? ?[0-9](\\)|\\])$"));
// Get something sensible to stick in the search box
QImage image = cover_searcher_->Exec(song->effective_albumartist(), album);
if (!image.isNull()) {
QString cover = SaveCoverInCache(song->artist(), song->album(), image);
SaveCover(song, cover);
return cover;
}
else { return QString(); }
}
QString AlbumCoverChoiceController::UnsetCover(Song *song) {
QString cover = Song::kManuallyUnsetCover;
SaveCover(song, cover);
return cover;
}
void AlbumCoverChoiceController::ShowCover(const Song &song) {
QDialog *dialog = new QDialog(this);
dialog->setAttribute(Qt::WA_DeleteOnClose, true);
// Use Artist - Album as the window title
QString title_text(song.effective_albumartist());
if (!song.effective_album().isEmpty()) title_text += " - " + song.effective_album();
QLabel *label = new QLabel(dialog);
label->setPixmap(AlbumCoverLoader::TryLoadPixmap(song.art_automatic(), song.art_manual(), song.url().toLocalFile()));
// add (WxHpx) to the title before possibly resizing
title_text += " (" + QString::number(label->pixmap()->width()) + "x" + QString::number(label->pixmap()->height()) + "px)";
// if the cover is larger than the screen, resize the window
// 85% seems to be enough to account for title bar and taskbar etc.
QDesktopWidget desktop;
int current_screen = desktop.screenNumber(this);
int desktop_height = desktop.screenGeometry(current_screen).height();
int desktop_width = desktop.screenGeometry(current_screen).width();
// resize differently if monitor is in portrait mode
if (desktop_width < desktop_height) {
const int new_width = (double)desktop_width * 0.95;
if (new_width < label->pixmap()->width()) {
label->setPixmap(
label->pixmap()->scaledToWidth(new_width, Qt::SmoothTransformation));
}
}
else {
const int new_height = (double)desktop_height * 0.85;
if (new_height < label->pixmap()->height()) {
label->setPixmap(label->pixmap()->scaledToHeight(
new_height, Qt::SmoothTransformation));
}
}
dialog->setWindowTitle(title_text);
dialog->setFixedSize(label->pixmap()->size());
dialog->show();
}
void AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) {
qint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(), song.effective_album());
cover_fetching_tasks_[id] = song;
}
void AlbumCoverChoiceController::AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics) {
Song song;
if (cover_fetching_tasks_.contains(id)) {
song = cover_fetching_tasks_.take(id);
}
if (!image.isNull()) {
QString cover = SaveCoverInCache(song.artist(), song.album(), image);
SaveCover(&song, cover);
}
emit AutomaticCoverSearchDone();
}
void AlbumCoverChoiceController::SaveCover(Song *song, const QString &cover) {
if (song->is_valid() && song->id() != -1) {
song->set_art_manual(cover);
app_->collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover);
if (song->url() == app_->current_art_loader()->last_song().url()) {
app_->current_art_loader()->LoadArt(*song);
}
}
}
QString AlbumCoverChoiceController::SaveCoverInCache(const QString &artist, const QString &album, const QImage &image) {
// Hash the artist and album into a filename for the image
QString filename(Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg");
QString path(AlbumCoverLoader::ImageCacheDir() + "/" + filename);
// Make sure this directory exists first
QDir dir;
dir.mkdir(AlbumCoverLoader::ImageCacheDir());
// Save the image to disk
image.save(path, "JPG");
return path;
}
bool AlbumCoverChoiceController::IsKnownImageExtension(const QString &suffix) {
if (!sImageExtensions) {
sImageExtensions = new QSet<QString>();
(*sImageExtensions) << "png" << "jpg" << "jpeg" << "bmp" << "gif" << "xpm" << "pbm" << "pgm" << "ppm" << "xbm";
}
return sImageExtensions->contains(suffix);
}
bool AlbumCoverChoiceController::CanAcceptDrag(const QDragEnterEvent *e) {
for (const QUrl &url : e->mimeData()->urls()) {
const QString suffix = QFileInfo(url.toLocalFile()).suffix().toLower();
if (IsKnownImageExtension(suffix)) return true;
}
if (e->mimeData()->hasImage()) {
return true;
}
return false;
}
QString AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
for (const QUrl &url : e->mimeData()->urls()) {
const QString filename = url.toLocalFile();
const QString suffix = QFileInfo(filename).suffix().toLower();
if (IsKnownImageExtension(suffix)) {
SaveCover(song, filename);
return filename;
}
}
if (e->mimeData()->hasImage()) {
QImage image = qvariant_cast<QImage>(e->mimeData()->imageData());
if (!image.isNull()) {
QString cover_path = SaveCoverInCache(song->artist(), song->album(), image);
SaveCover(song, cover_path);
return cover_path;
}
}
return QString();
}

View File

@@ -0,0 +1,148 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERCHOICECONTROLLER_H
#define ALBUMCOVERCHOICECONTROLLER_H
#include "config.h"
#include <QAction>
#include <QList>
#include <QMenu>
#include <QWidget>
class AlbumCoverFetcher;
class AlbumCoverSearcher;
class Application;
class CoverFromURLDialog;
class QFileDialog;
class Song;
struct CoverSearchStatistics;
// Controller for the common album cover related menu options.
class AlbumCoverChoiceController : public QWidget {
Q_OBJECT
public:
static const char *kLoadImageFileFilter;
static const char *kSaveImageFileFilter;
static const char *kAllFilesFilter;
AlbumCoverChoiceController(QWidget *parent = nullptr);
~AlbumCoverChoiceController();
void SetApplication(Application *app);
// Getters for all QActions implemented by this controller.
QAction *cover_from_file_action() const { return cover_from_file_; }
QAction *cover_to_file_action() const { return cover_to_file_; }
QAction *cover_from_url_action() const { return cover_from_url_; }
QAction *search_for_cover_action() const { return search_for_cover_; }
QAction *unset_cover_action() const { return unset_cover_; }
QAction *show_cover_action() const { return show_cover_; }
QAction *search_cover_auto_action() const { return search_cover_auto_; }
// Returns QAction* for every operation implemented by this controller.
// The list contains QAction* for:
// 1. loading cover from file
// 2. loading cover from URL
// 3. searching for cover using last.fm
// 4. unsetting the cover manually
// 5. showing the cover in original size
QList<QAction*> GetAllActions();
// All of the methods below require a currently selected song as an
// input parameter. Also - LoadCoverFromFile, LoadCoverFromURL,
// SearchForCover, UnsetCover and SaveCover all update manual path
// of the given song in collection to the new cover.
// Lets the user choose a cover from disk. If no cover will be chosen or the chosen
// cover will not be a proper image, this returns an empty string. Otherwise, the
// path to the chosen cover will be returned.
QString LoadCoverFromFile(Song *song);
// Shows a dialog that allows user to save the given image on disk. The image
// is supposed to be the cover of the given song's album.
void SaveCoverToFile(const Song &song, const QImage &image);
// Downloads the cover from an URL given by user. This returns the downloaded image
// or null image if something went wrong for example when user cancelled the
// dialog.
QString LoadCoverFromURL(Song *song);
// Lets the user choose a cover among all that have been found on last.fm.
// Returns the chosen cover or null cover if user didn't choose anything.
QString SearchForCover(Song *song);
// Returns a path which indicates that the cover has been unset manually.
QString UnsetCover(Song *song);
// Shows the cover of given song in it's original size.
void ShowCover(const Song &song);
// Search for covers automatically
void SearchCoverAutomatically(const Song &song);
// Saves the chosen cover as manual cover path of this song in collection.
void SaveCover(Song *song, const QString &cover);
// Saves the cover that the user picked through a drag and drop operation.
QString SaveCover(Song *song, const QDropEvent *e);
// Saves the given image in cache as a cover for 'artist' - 'album'.
// The method returns path of the cached image.
QString SaveCoverInCache(const QString &artist, const QString &album, const QImage &image);
static bool CanAcceptDrag(const QDragEnterEvent *e);
signals:
void AutomaticCoverSearchDone();
private slots:
void AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics);
private:
QString GetInitialPathForFileDialog(const Song &song, const QString &filename);
static bool IsKnownImageExtension(const QString &suffix);
static QSet<QString> *sImageExtensions;
Application *app_;
AlbumCoverSearcher *cover_searcher_;
AlbumCoverFetcher *cover_fetcher_;
QFileDialog *save_file_dialog_;
CoverFromURLDialog *cover_from_url_dialog_;
QAction *cover_from_file_;
QAction *cover_to_file_;
QAction *separator_;
QAction *cover_from_url_;
QAction *search_for_cover_;
QAction *unset_cover_;
QAction *show_cover_;
QAction *search_cover_auto_;
QMap<quint64, Song> cover_fetching_tasks_;
};
#endif // ALBUMCOVERCHOICECONTROLLER_H

View File

@@ -0,0 +1,96 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "albumcoverexport.h"
#include "ui_albumcoverexport.h"
#include <QSettings>
const char *AlbumCoverExport::kSettingsGroup = "AlbumCoverExport";
AlbumCoverExport::AlbumCoverExport(QWidget *parent) : QDialog(parent), ui_(new Ui_AlbumCoverExport) {
ui_->setupUi(this);
connect(ui_->forceSize, SIGNAL(stateChanged(int)), SLOT(ForceSizeToggled(int)));
}
AlbumCoverExport::~AlbumCoverExport() { delete ui_; }
AlbumCoverExport::DialogResult AlbumCoverExport::Exec() {
QSettings s;
s.beginGroup(kSettingsGroup);
// restore last accepted settings
ui_->fileName->setText(s.value("fileName", "cover").toString());
ui_->doNotOverwrite->setChecked(s.value("overwrite", OverwriteMode_None).toInt() == OverwriteMode_None);
ui_->overwriteAll->setChecked(s.value("overwrite", OverwriteMode_All).toInt() == OverwriteMode_All);
ui_->overwriteSmaller->setChecked(s.value("overwrite", OverwriteMode_Smaller).toInt() == OverwriteMode_Smaller);
ui_->forceSize->setChecked(s.value("forceSize", false).toBool());
ui_->width->setText(s.value("width", "").toString());
ui_->height->setText(s.value("height", "").toString());
ui_->export_downloaded->setChecked(s.value("export_downloaded", true).toBool());
ui_->export_embedded->setChecked(s.value("export_embedded", false).toBool());
ForceSizeToggled(ui_->forceSize->checkState());
DialogResult result = DialogResult();
result.cancelled_ = (exec() == QDialog::Rejected);
if (!result.cancelled_) {
QString fileName = ui_->fileName->text();
if (fileName.isEmpty()) {
fileName = "cover";
}
OverwriteMode overwrite = ui_->doNotOverwrite->isChecked() ? OverwriteMode_None : (ui_->overwriteAll->isChecked() ? OverwriteMode_All : OverwriteMode_Smaller);
bool forceSize = ui_->forceSize->isChecked();
QString width = ui_->width->text();
QString height = ui_->height->text();
s.setValue("fileName", fileName);
s.setValue("overwrite", overwrite);
s.setValue("forceSize", forceSize);
s.setValue("width", width);
s.setValue("height", height);
s.setValue("export_downloaded", ui_->export_downloaded->isChecked());
s.setValue("export_embedded", ui_->export_embedded->isChecked());
result.fileName_ = fileName;
result.overwrite_ = overwrite;
result.forceSize_ = forceSize;
result.width_ = width.toInt();
result.height_ = height.toInt();
result.export_downloaded_ = ui_->export_downloaded->isChecked();
result.export_embedded_ = ui_->export_embedded->isChecked();
}
return result;
}
void AlbumCoverExport::ForceSizeToggled(int state) {
ui_->width->setEnabled(state == Qt::Checked);
ui_->height->setEnabled(state == Qt::Checked);
}

View File

@@ -0,0 +1,77 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVEREXPORT_H
#define ALBUMCOVEREXPORT_H
#include "config.h"
#include <QDialog>
class Ui_AlbumCoverExport;
// Controller for the "Export covers" dialog.
class AlbumCoverExport : public QDialog {
Q_OBJECT
public:
AlbumCoverExport(QWidget *parent = nullptr);
~AlbumCoverExport();
enum OverwriteMode {
OverwriteMode_None = 0,
OverwriteMode_All = 1,
OverwriteMode_Smaller = 2
};
struct DialogResult {
bool cancelled_;
bool export_downloaded_;
bool export_embedded_;
QString fileName_;
OverwriteMode overwrite_;
bool forceSize_;
int width_;
int height_;
bool IsSizeForced() const {
return forceSize_ && width_ > 0 && height_ > 0;
}
bool RequiresCoverProcessing() const {
return IsSizeForced() || overwrite_ == OverwriteMode_Smaller;
}
};
DialogResult Exec();
private slots:
void ForceSizeToggled(int state);
private:
Ui_AlbumCoverExport *ui_;
static const char *kSettingsGroup;
};
#endif // ALBUMCOVEREXPORT_H

View File

@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AlbumCoverExport</class>
<widget class="QDialog" name="AlbumCoverExport">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>608</width>
<height>412</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Export covers</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Output</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enter a filename for exported covers (no extension):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="fileName"/>
</item>
<item>
<widget class="QCheckBox" name="export_downloaded">
<property name="text">
<string>Export downloaded covers</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="export_embedded">
<property name="text">
<string>Export embedded covers</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Existing covers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QRadioButton" name="doNotOverwrite">
<property name="text">
<string>Do not overwrite</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="overwriteAll">
<property name="text">
<string>Overwrite all</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="overwriteSmaller">
<property name="text">
<string>Overwrite smaller ones only</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Size</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QCheckBox" name="forceSize">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Scale size</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Size:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="width">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="inputMethodHints">
<set>Qt::ImhDigitsOnly</set>
</property>
<property name="inputMask">
<string notr="true"/>
</property>
<property name="maxLength">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string notr="true">×</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="height">
<property name="inputMethodHints">
<set>Qt::ImhDigitsOnly</set>
</property>
<property name="inputMask">
<string notr="true"/>
</property>
<property name="maxLength">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Pixel</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>200</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AlbumCoverExport</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AlbumCoverExport</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,82 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QFile>
#include <QThreadPool>
#include "albumcoverexporter.h"
#include "coverexportrunnable.h"
#include "core/song.h"
const int AlbumCoverExporter::kMaxConcurrentRequests = 3;
AlbumCoverExporter::AlbumCoverExporter(QObject *parent)
: QObject(parent),
thread_pool_(new QThreadPool(this)),
exported_(0),
skipped_(0),
all_(0) {
thread_pool_->setMaxThreadCount(kMaxConcurrentRequests);
}
void AlbumCoverExporter::SetDialogResult(const AlbumCoverExport::DialogResult& dialog_result) {
dialog_result_ = dialog_result;
}
void AlbumCoverExporter::AddExportRequest(Song song) {
requests_.append(new CoverExportRunnable(dialog_result_, song));
all_ = requests_.count();
}
void AlbumCoverExporter::Cancel() { requests_.clear(); }
void AlbumCoverExporter::StartExporting() {
exported_ = 0;
skipped_ = 0;
AddJobsToPool();
}
void AlbumCoverExporter::AddJobsToPool() {
while (!requests_.isEmpty() && thread_pool_->activeThreadCount() < thread_pool_->maxThreadCount()) {
CoverExportRunnable *runnable = requests_.dequeue();
connect(runnable, SIGNAL(CoverExported()), SLOT(CoverExported()));
connect(runnable, SIGNAL(CoverSkipped()), SLOT(CoverSkipped()));
thread_pool_->start(runnable);
}
}
void AlbumCoverExporter::CoverExported() {
exported_++;
emit AlbumCoversExportUpdate(exported_, skipped_, all_);
AddJobsToPool();
}
void AlbumCoverExporter::CoverSkipped() {
skipped_++;
emit AlbumCoversExportUpdate(exported_, skipped_, all_);
AddJobsToPool();
}

View File

@@ -0,0 +1,72 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVEREXPORTER_H
#define ALBUMCOVEREXPORTER_H
#include "config.h"
#include "coverexportrunnable.h"
#include "core/song.h"
#include "covermanager/albumcoverexport.h"
#include <QObject>
#include <QQueue>
#include <QTimer>
class QThreadPool;
class AlbumCoverExporter : public QObject {
Q_OBJECT
public:
explicit AlbumCoverExporter(QObject *parent = nullptr);
virtual ~AlbumCoverExporter() {}
static const int kMaxConcurrentRequests;
void SetDialogResult(const AlbumCoverExport::DialogResult &dialog_result);
void AddExportRequest(Song song);
void StartExporting();
void Cancel();
int request_count() { return requests_.size(); }
signals:
void AlbumCoversExportUpdate(int exported, int skipped, int all);
private slots:
void CoverExported();
void CoverSkipped();
private:
void AddJobsToPool();
AlbumCoverExport::DialogResult dialog_result_;
QQueue<CoverExportRunnable*> requests_;
QThreadPool *thread_pool_;
int exported_;
int skipped_;
int all_;
};
#endif // ALBUMCOVEREXPORTER_H

View File

@@ -0,0 +1,132 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QTimer>
#include "albumcoverfetcher.h"
#include "albumcoverfetchersearch.h"
#include "core/network.h"
const int AlbumCoverFetcher::kMaxConcurrentRequests = 5;
AlbumCoverFetcher::AlbumCoverFetcher(CoverProviders *cover_providers, QObject *parent, QNetworkAccessManager *network)
: QObject(parent),
cover_providers_(cover_providers),
network_(network ? network : new NetworkAccessManager(this)),
next_id_(0),
request_starter_(new QTimer(this)) {
request_starter_->setInterval(1000);
connect(request_starter_, SIGNAL(timeout()), SLOT(StartRequests()));
}
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album) {
CoverSearchRequest request;
request.artist = artist;
request.album = album;
request.search = false;
request.id = next_id_++;
AddRequest(request);
return request.id;
}
quint64 AlbumCoverFetcher::SearchForCovers(const QString &artist, const QString &album) {
CoverSearchRequest request;
request.artist = artist;
request.album = album;
request.search = true;
request.id = next_id_++;
AddRequest(request);
return request.id;
}
void AlbumCoverFetcher::AddRequest(const CoverSearchRequest &req) {
queued_requests_.enqueue(req);
if (!request_starter_->isActive()) request_starter_->start();
if (active_requests_.size() < kMaxConcurrentRequests) StartRequests();
}
void AlbumCoverFetcher::Clear() {
queued_requests_.clear();
for (AlbumCoverFetcherSearch *search : active_requests_.values()) {
search->Cancel();
search->deleteLater();
}
active_requests_.clear();
}
void AlbumCoverFetcher::StartRequests() {
if (queued_requests_.isEmpty()) {
request_starter_->stop();
return;
}
while (!queued_requests_.isEmpty() && active_requests_.size() < kMaxConcurrentRequests) {
CoverSearchRequest request = queued_requests_.dequeue();
// search objects are this fetcher's children so worst case scenario - they get
// deleted with it
AlbumCoverFetcherSearch *search = new AlbumCoverFetcherSearch(request, network_, this);
active_requests_.insert(request.id, search);
connect(search, SIGNAL(SearchFinished(quint64, CoverSearchResults)), SLOT(SingleSearchFinished(quint64, CoverSearchResults)));
connect(search, SIGNAL(AlbumCoverFetched(quint64, const QImage&)), SLOT(SingleCoverFetched(quint64, const QImage&)));
search->Start(cover_providers_);
}
}
void AlbumCoverFetcher::SingleSearchFinished(quint64 request_id, CoverSearchResults results) {
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
if (!search) return;
search->deleteLater();
emit SearchFinished(request_id, results, search->statistics());
}
void AlbumCoverFetcher::SingleCoverFetched(quint64 request_id, const QImage &image) {
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
if (!search) return;
search->deleteLater();
emit AlbumCoverFetched(request_id, image, search->statistics());
}

View File

@@ -0,0 +1,117 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERFETCHER_H
#define ALBUMCOVERFETCHER_H
#include "config.h"
#include "coversearchstatistics.h"
#include <QHash>
#include <QImage>
#include <QList>
#include <QMetaType>
#include <QNetworkAccessManager>
#include <QObject>
#include <QQueue>
#include <QUrl>
class QNetworkReply;
class QString;
class AlbumCoverFetcherSearch;
class CoverProviders;
// This class represents a single search-for-cover request. It identifies
// and describes the request.
struct CoverSearchRequest {
// an unique (for one AlbumCoverFetcher) request identifier
quint64 id;
// a search query
QString artist;
QString album;
// is this only a search request or should we also fetch the first
// cover that's found?
bool search;
};
// This structure represents a single result of some album's cover search request.
// It contains an URL that leads to a found cover plus its description (usually
// the "artist - album" string).
struct CoverSearchResult {
// used for grouping in the user interface. This is set automatically - don't
// set it manually in your cover provider.
QString provider;
// description of this result (we suggest using the "artist - album" format)
QString description;
// an URL of a cover image described by this CoverSearchResult
QUrl image_url;
};
Q_DECLARE_METATYPE(CoverSearchResult);
// This is a complete result of a single search request (a list of results, each
// describing one image, actually).
typedef QList<CoverSearchResult> CoverSearchResults;
Q_DECLARE_METATYPE(QList<CoverSearchResult>);
// This class searches for album covers for a given query or artist/album and
// returns URLs. It's NOT thread-safe.
class AlbumCoverFetcher : public QObject {
Q_OBJECT
public:
AlbumCoverFetcher(CoverProviders *cover_providers, QObject *parent = nullptr, QNetworkAccessManager *network = 0);
virtual ~AlbumCoverFetcher() {}
static const int kMaxConcurrentRequests;
quint64 SearchForCovers(const QString &artist, const QString &album);
quint64 FetchAlbumCover(const QString &artist, const QString &album);
void Clear();
signals:
void AlbumCoverFetched(quint64, const QImage &cover, const CoverSearchStatistics &statistics);
void SearchFinished(quint64, const CoverSearchResults &results, const CoverSearchStatistics &statistics);
private slots:
void SingleSearchFinished(quint64, CoverSearchResults results);
void SingleCoverFetched(quint64, const QImage &cover);
void StartRequests();
private:
void AddRequest(const CoverSearchRequest &req);
CoverProviders *cover_providers_;
QNetworkAccessManager *network_;
quint64 next_id_;
QQueue<CoverSearchRequest> queued_requests_;
QHash<quint64, AlbumCoverFetcherSearch*> active_requests_;
QTimer *request_starter_;
};
#endif // ALBUMCOVERFETCHER_H

View File

@@ -0,0 +1,271 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "albumcoverfetchersearch.h"
#include <cmath>
#include <QMutexLocker>
#include <QNetworkReply>
#include <QTimer>
#include <QtDebug>
#include "albumcoverfetcher.h"
#include "coverprovider.h"
#include "coverproviders.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/network.h"
const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 10000;
const int AlbumCoverFetcherSearch::kImageLoadTimeoutMs = 2500;
const int AlbumCoverFetcherSearch::kTargetSize = 500;
const float AlbumCoverFetcherSearch::kGoodScore = 1.85;
AlbumCoverFetcherSearch::AlbumCoverFetcherSearch(
const CoverSearchRequest &request, QNetworkAccessManager *network, QObject *parent)
: QObject(parent),
request_(request),
image_load_timeout_(new NetworkTimeouts(kImageLoadTimeoutMs, this)),
network_(network),
cancel_requested_(false) {
// we will terminate the search after kSearchTimeoutMs miliseconds if we are
// not
// able to find all of the results before that point in time
QTimer::singleShot(kSearchTimeoutMs, this, SLOT(TerminateSearch()));
}
void AlbumCoverFetcherSearch::TerminateSearch() {
for (int id : pending_requests_.keys()) {
pending_requests_.take(id)->CancelSearch(id);
}
AllProvidersFinished();
}
void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
for (CoverProvider *provider : cover_providers->List()) {
connect(provider, SIGNAL(SearchFinished(int, QList<CoverSearchResult>)), SLOT(ProviderSearchFinished(int, QList<CoverSearchResult>)));
const int id = cover_providers->NextId();
const bool success = provider->StartSearch(request_.artist, request_.album, id);
if (success) {
pending_requests_[id] = provider;
statistics_.network_requests_made_++;
}
}
// end this search before it even began if there are no providers...
if (pending_requests_.isEmpty()) {
TerminateSearch();
}
}
static bool CompareProviders(const CoverSearchResult &a, const CoverSearchResult &b) {
return a.provider < b.provider;
}
void AlbumCoverFetcherSearch::ProviderSearchFinished(int id, const QList<CoverSearchResult> &results) {
if (!pending_requests_.contains(id)) return;
CoverProvider *provider = pending_requests_.take(id);
CoverSearchResults results_copy(results);
// Set categories on the results
for (int i = 0; i < results_copy.count(); ++i) {
results_copy[i].provider = provider->name();
}
// Add results from the current provider to our pool
results_.append(results_copy);
statistics_.total_images_by_provider_[provider->name()]++;
// do we have more providers left?
if (!pending_requests_.isEmpty()) {
return;
}
AllProvidersFinished();
}
void AlbumCoverFetcherSearch::AllProvidersFinished() {
if (cancel_requested_) {
return;
}
// if we only wanted to do the search then we're done
if (request_.search) {
emit SearchFinished(request_.id, results_);
return;
}
// no results?
if (results_.isEmpty()) {
statistics_.missing_images_++;
emit AlbumCoverFetched(request_.id, QImage());
return;
}
// Now we have to load some images and figure out which one is the best.
// We'll sort the list of results by category, then load the first few images
// from each category and use some heuristics to score them. If no images
// are good enough we'll keep loading more images until we find one that is
// or we run out of results.
qStableSort(results_.begin(), results_.end(), CompareProviders);
FetchMoreImages();
}
void AlbumCoverFetcherSearch::FetchMoreImages() {
// Try the first one in each category.
QString last_provider;
for (int i = 0; i < results_.count(); ++i) {
if (results_[i].provider == last_provider) {
continue;
}
CoverSearchResult result = results_.takeAt(i--);
last_provider = result.provider;
qLog(Debug) << "Loading" << result.image_url << "from" << result.provider;
RedirectFollower *image_reply = new RedirectFollower(network_->get(QNetworkRequest(result.image_url)));
NewClosure(image_reply, SIGNAL(finished()), this, SLOT(ProviderCoverFetchFinished(RedirectFollower*)), image_reply);
pending_image_loads_[image_reply] = result.provider;
image_load_timeout_->AddReply(image_reply);
statistics_.network_requests_made_++;
}
if (pending_image_loads_.isEmpty()) {
// There were no more results? Time to give up.
SendBestImage();
}
}
void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(RedirectFollower *reply) {
reply->deleteLater();
const QString provider = pending_image_loads_.take(reply);
statistics_.bytes_transferred_ += reply->bytesAvailable();
if (cancel_requested_) {
return;
}
if (reply->error() != QNetworkReply::NoError) {
qLog(Info) << "Error requesting" << reply->url() << reply->errorString();
}
else {
QImage image;
if (!image.loadFromData(reply->readAll())) {
qLog(Info) << "Error decoding image data from" << reply->url();
}
else {
const float score = ScoreImage(image);
candidate_images_.insertMulti(score, CandidateImage(provider, image));
qLog(Debug) << reply->url() << "scored" << score;
}
}
if (pending_image_loads_.isEmpty()) {
// We've fetched everything we wanted to fetch for now, check if we have an
// image that's good enough.
float best_score = 0.0;
if (!candidate_images_.isEmpty()) {
best_score = candidate_images_.keys().last();
}
qLog(Debug) << "Best image so far has a score of" << best_score;
if (best_score >= kGoodScore) {
SendBestImage();
}
else {
FetchMoreImages();
}
}
}
float AlbumCoverFetcherSearch::ScoreImage(const QImage &image) const {
// Invalid images score nothing
if (image.isNull()) {
return 0.0;
}
// A 500x500px image scores 1.0, bigger scores higher
const float size_score = std::sqrt(float(image.width() * image.height())) / kTargetSize;
// A 1:1 image scores 1.0, anything else scores less
const float aspect_score = 1.0 - float(image.height() - image.width()) / std::max(image.height(), image.width());
return size_score + aspect_score;
}
void AlbumCoverFetcherSearch::SendBestImage() {
QImage image;
if (!candidate_images_.isEmpty()) {
const CandidateImage best_image = candidate_images_.values().back();
image = best_image.second;
statistics_.chosen_images_by_provider_[best_image.first]++;
statistics_.chosen_images_++;
statistics_.chosen_width_ += image.width();
statistics_.chosen_height_ += image.height();
}
else {
statistics_.missing_images_++;
}
emit AlbumCoverFetched(request_.id, image);
}
void AlbumCoverFetcherSearch::Cancel() {
cancel_requested_ = true;
if (!pending_requests_.isEmpty()) {
TerminateSearch();
}
else if (!pending_image_loads_.isEmpty()) {
for (RedirectFollower *reply : pending_image_loads_.keys()) {
reply->abort();
}
pending_image_loads_.clear();
}
}

View File

@@ -0,0 +1,102 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERFETCHERSEARCH_H
#define ALBUMCOVERFETCHERSEARCH_H
#include "config.h"
#include "albumcoverfetcher.h"
#include <QMap>
#include <QObject>
class CoverProvider;
class CoverProviders;
class NetworkTimeouts;
class NetworkAccessManager;
class RedirectFollower;
// This class encapsulates a single search for covers initiated by an
// AlbumCoverFetcher. The search engages all of the known cover providers.
// AlbumCoverFetcherSearch signals search results to an interested
// AlbumCoverFetcher when all of the providers have done their part.
class AlbumCoverFetcherSearch : public QObject {
Q_OBJECT
public:
AlbumCoverFetcherSearch(const CoverSearchRequest &request, QNetworkAccessManager *network, QObject *parent);
void Start(CoverProviders* cover_providers);
// Cancels all pending requests. No Finished signals will be emitted, and it
// is the caller's responsibility to delete the AlbumCoverFetcherSearch.
void Cancel();
CoverSearchStatistics statistics() const { return statistics_; }
signals:
// It's the end of search (when there was no fetch-me-a-cover request).
void SearchFinished(quint64, const CoverSearchResults& results);
// It's the end of search and we've fetched a cover.
void AlbumCoverFetched(quint64, const QImage &cover);
private slots:
void ProviderSearchFinished(int id, const QList<CoverSearchResult> &results);
void ProviderCoverFetchFinished(RedirectFollower *reply);
void TerminateSearch();
private:
void AllProvidersFinished();
void FetchMoreImages();
float ScoreImage(const QImage &image) const;
void SendBestImage();
private:
static const int kSearchTimeoutMs;
static const int kImageLoadTimeoutMs;
static const int kTargetSize;
static const float kGoodScore;
CoverSearchStatistics statistics_;
// Search request encapsulated by this AlbumCoverFetcherSearch.
CoverSearchRequest request_;
// Complete results (from all of the available providers).
CoverSearchResults results_;
QMap<int, CoverProvider*> pending_requests_;
QMap<RedirectFollower*, QString> pending_image_loads_;
NetworkTimeouts* image_load_timeout_;
// QMap is sorted by key (score). Values are (provider_name, image)
typedef QPair<QString, QImage> CandidateImage;
QMap<float, CandidateImage> candidate_images_;
QNetworkAccessManager *network_;
bool cancel_requested_;
};
#endif // ALBUMCOVERFETCHERSEARCH_H

View File

@@ -0,0 +1,279 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "albumcoverloader.h"
#include <QPainter>
#include <QDir>
#include <QCoreApplication>
#include <QUrl>
#include <QNetworkReply>
#include "config.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/network.h"
#include "core/tagreaderclient.h"
#include "core/utilities.h"
AlbumCoverLoader::AlbumCoverLoader(QObject *parent)
: QObject(parent),
stop_requested_(false),
next_id_(1),
network_(new NetworkAccessManager(this)){}
QString AlbumCoverLoader::ImageCacheDir() {
return Utilities::GetConfigPath(Utilities::Path_AlbumCovers);
}
void AlbumCoverLoader::CancelTask(quint64 id) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(&mutex_);
for (QQueue<Task>::iterator it = tasks_.begin(); it != tasks_.end(); ++it) {
if (it->id == id) {
tasks_.erase(it);
break;
}
}
}
void AlbumCoverLoader::CancelTasks(const QSet<quint64> &ids) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(&mutex_);
for (QQueue<Task>::iterator it = tasks_.begin(); it != tasks_.end();) {
if (ids.contains(it->id)) {
it = tasks_.erase(it);
}
else {
++it;
}
}
}
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options, const Song &song) {
return LoadImageAsync(options, song.art_automatic(), song.art_manual(), song.url().toLocalFile(), song.image());
}
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QString &art_automatic, const QString &art_manual, const QString &song_filename, const QImage &embedded_image) {
//qLog(Debug) << __PRETTY_FUNCTION__ << art_automatic << art_manual << song_filename;
Task task;
task.options = options;
task.art_automatic = art_automatic;
task.art_manual = art_manual;
task.song_filename = song_filename;
task.embedded_image = embedded_image;
task.state = State_TryingManual;
{
QMutexLocker l(&mutex_);
task.id = next_id_++;
tasks_.enqueue(task);
}
metaObject()->invokeMethod(this, "ProcessTasks", Qt::QueuedConnection);
return task.id;
}
void AlbumCoverLoader::ProcessTasks() {
//qLog(Debug) << __PRETTY_FUNCTION__;
while (!stop_requested_) {
// Get the next task
Task task;
{
QMutexLocker l(&mutex_);
if (tasks_.isEmpty()) return;
task = tasks_.dequeue();
}
ProcessTask(&task);
}
}
void AlbumCoverLoader::ProcessTask(Task *task) {
//qLog(Debug) << __PRETTY_FUNCTION__;
TryLoadResult result = TryLoadImage(*task);
if (result.started_async) {
// The image is being loaded from a remote URL, we'll carry on later
// when it's done
return;
}
if (result.loaded_success) {
QImage scaled = ScaleAndPad(task->options, result.image);
emit ImageLoaded(task->id, scaled);
emit ImageLoaded(task->id, scaled, result.image);
return;
}
NextState(task);
}
void AlbumCoverLoader::NextState(Task *task) {
//qLog(Debug) << __PRETTY_FUNCTION__;
if (task->state == State_TryingManual) {
// Try the automatic one next
task->state = State_TryingAuto;
ProcessTask(task);
}
else {
// Give up
emit ImageLoaded(task->id, task->options.default_output_image_);
emit ImageLoaded(task->id, task->options.default_output_image_, task->options.default_output_image_);
}
}
AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(const Task &task) {
//qLog(Debug) << __PRETTY_FUNCTION__;
// An image embedded in the song itself takes priority
if (!task.embedded_image.isNull())
return TryLoadResult(false, true, ScaleAndPad(task.options, task.embedded_image));
QString filename;
switch (task.state) {
case State_TryingAuto: filename = task.art_automatic; break;
case State_TryingManual: filename = task.art_manual; break;
}
if (filename == Song::kManuallyUnsetCover)
return TryLoadResult(false, true, task.options.default_output_image_);
if (filename == Song::kEmbeddedCover && !task.song_filename.isEmpty()) {
const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task.song_filename);
if (!taglib_image.isNull())
return TryLoadResult(false, true, ScaleAndPad(task.options, taglib_image));
}
if (filename.toLower().startsWith("http://") || filename.toLower().startsWith("https://")) {
QUrl url(filename);
QNetworkReply* reply = network_->get(QNetworkRequest(url));
NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*)), reply);
remote_tasks_.insert(reply, task);
return TryLoadResult(true, false, QImage());
}
else if (filename.isEmpty()) {
// Avoid "QFSFileEngine::open: No file name specified" messages if we know that the filename is empty
return TryLoadResult(false, false, task.options.default_output_image_);
}
QImage image(filename);
return TryLoadResult(false, !image.isNull(), image.isNull() ? task.options.default_output_image_ : image);
}
void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply) {
//qLog(Debug) << __PRETTY_FUNCTION__;
reply->deleteLater();
Task task = remote_tasks_.take(reply);
// Handle redirects.
QVariant redirect = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (redirect.isValid()) {
if (++task.redirects > kMaxRedirects) {
return; // Give up.
}
QNetworkRequest request = reply->request();
request.setUrl(redirect.toUrl());
QNetworkReply* redirected_reply = network_->get(request);
NewClosure(redirected_reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*)), redirected_reply);
remote_tasks_.insert(redirected_reply, task);
return;
}
if (reply->error() == QNetworkReply::NoError) {
// Try to load the image
QImage image;
if (image.load(reply, 0)) {
QImage scaled = ScaleAndPad(task.options, image);
emit ImageLoaded(task.id, scaled);
emit ImageLoaded(task.id, scaled, image);
return;
}
}
NextState(&task);
}
QImage AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image) {
//qLog(Debug) << __PRETTY_FUNCTION__;
if (image.isNull()) return image;
// Scale the image down
QImage copy;
if (options.scale_output_image_) {
copy = image.scaled(QSize(options.desired_height_, options.desired_height_), Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
else {
copy = image;
}
if (!options.pad_output_image_) return copy;
// Pad the image to height_ x height_
QImage padded_image(options.desired_height_, options.desired_height_, QImage::Format_ARGB32);
padded_image.fill(0);
QPainter p(&padded_image);
p.drawImage((options.desired_height_ - copy.width()) / 2, (options.desired_height_ - copy.height()) / 2, copy);
p.end();
return padded_image;
}
QPixmap AlbumCoverLoader::TryLoadPixmap(const QString &automatic, const QString &manual, const QString &filename) {
//qLog(Debug) << __PRETTY_FUNCTION__ << automatic << manual << filename;
QPixmap ret;
if (manual == Song::kManuallyUnsetCover) return ret;
if (!manual.isEmpty()) ret.load(manual);
if (ret.isNull()) {
if (automatic == Song::kEmbeddedCover && !filename.isNull())
ret = QPixmap::fromImage(TagReaderClient::Instance()->LoadEmbeddedArtBlocking(filename));
else if (!automatic.isEmpty())
ret.load(automatic);
}
return ret;
}

View File

@@ -0,0 +1,110 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERLOADER_H
#define ALBUMCOVERLOADER_H
#include "config.h"
#include "albumcoverloaderoptions.h"
#include "core/song.h"
#include <QImage>
#include <QMutex>
#include <QObject>
#include <QQueue>
#include <QUrl>
class NetworkAccessManager;
class QNetworkReply;
class AlbumCoverLoader : public QObject {
Q_OBJECT
public:
explicit AlbumCoverLoader(QObject *parent = nullptr);
void Stop() { stop_requested_ = true; }
static QString ImageCacheDir();
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song);
virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QString &art_automatic, const QString &art_manual, const QString &song_filename = QString(), const QImage &embedded_image = QImage());
void CancelTask(quint64 id);
void CancelTasks(const QSet<quint64> &ids);
static QPixmap TryLoadPixmap(const QString &automatic, const QString &manual, const QString &filename = QString());
static QImage ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image);
signals:
void ImageLoaded(quint64 id, const QImage &image);
void ImageLoaded(quint64 id, const QImage &scaled, const QImage &original);
protected slots:
void ProcessTasks();
void RemoteFetchFinished(QNetworkReply* reply);
protected:
enum State {
State_TryingManual,
State_TryingAuto,
};
struct Task {
Task() : redirects(0) {}
AlbumCoverLoaderOptions options;
quint64 id;
QString art_automatic;
QString art_manual;
QString song_filename;
QImage embedded_image;
State state;
int redirects;
};
struct TryLoadResult {
TryLoadResult(bool async, bool success, const QImage &i) : started_async(async), loaded_success(success), image(i) {}
bool started_async;
bool loaded_success;
QImage image;
};
void ProcessTask(Task* task);
void NextState(Task* task);
TryLoadResult TryLoadImage(const Task &task);
bool stop_requested_;
QMutex mutex_;
QQueue<Task> tasks_;
QMap<QNetworkReply *, Task> remote_tasks_;
quint64 next_id_;
NetworkAccessManager *network_;
static const int kMaxRedirects = 3;
};
#endif // ALBUMCOVERLOADER_H

View File

@@ -0,0 +1,2 @@
#include "config.h"
#include "albumcoverloaderoptions.h"

View File

@@ -0,0 +1,41 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERLOADEROPTIONS_H
#define ALBUMCOVERLOADEROPTIONS_H
#include "config.h"
#include <QImage>
struct AlbumCoverLoaderOptions {
AlbumCoverLoaderOptions()
: desired_height_(120),
scale_output_image_(true),
pad_output_image_(true) {}
int desired_height_;
bool scale_output_image_;
bool pad_output_image_;
QImage default_output_image_;
};
#endif // ALBUMCOVERLOADEROPTIONS_H

View File

@@ -0,0 +1,870 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QActionGroup>
#include <QPushButton>
#include <QContextMenuEvent>
#include <QEvent>
#include <QFileDialog>
#include <QKeySequence>
#include <QLabel>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
#include <QProgressBar>
#include <QSettings>
#include <QShortcut>
#include <QTimer>
#include "albumcovermanager.h"
#include "albumcoversearcher.h"
#include "ui_albumcovermanager.h"
#include "core/application.h"
#include "core/logging.h"
#include "core/utilities.h"
#include "core/iconloader.h"
#include "covermanager/albumcoverexporter.h"
#include "covermanager/albumcoverfetcher.h"
#include "covermanager/albumcoverloader.h"
#include "covermanager/coverproviders.h"
#include "covermanager/coversearchstatisticsdialog.h"
#include "collection/collectionbackend.h"
#include "collection/collectionquery.h"
#include "collection/sqlrow.h"
#include "playlist/songmimedata.h"
#include "widgets/forcescrollperpixel.h"
#include "covermanager/albumcoverchoicecontroller.h"
#include "covermanager/albumcoverexport.h"
const char *AlbumCoverManager::kSettingsGroup = "CoverManager";
AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collection_backend, QWidget *parent, QNetworkAccessManager *network)
: QMainWindow(parent),
ui_(new Ui_CoverManager),
app_(app),
album_cover_choice_controller_(new AlbumCoverChoiceController(this)),
cover_fetcher_(new AlbumCoverFetcher(app_->cover_providers(), this, network)),
cover_searcher_(nullptr),
cover_export_(nullptr),
cover_exporter_(new AlbumCoverExporter(this)),
artist_icon_(IconLoader::Load("guitar" )),
all_artists_icon_(IconLoader::Load("cd" )),
//no_cover_icon_(IconLoader::Load("nocover")),
no_cover_icon_(":/pictures/noalbumart.png"),
no_cover_image_(GenerateNoCoverImage(no_cover_icon_)),
no_cover_item_icon_(QPixmap::fromImage(no_cover_image_)),
context_menu_(new QMenu(this)),
progress_bar_(new QProgressBar(this)),
abort_progress_(new QPushButton(this)),
jobs_(0),
collection_backend_(collection_backend) {
ui_->setupUi(this);
ui_->albums->set_cover_manager(this);
// Icons
ui_->action_fetch->setIcon(IconLoader::Load("download" ));
ui_->export_covers->setIcon(IconLoader::Load("document-save" ));
ui_->view->setIcon(IconLoader::Load("view-choose" ));
ui_->fetch->setIcon(IconLoader::Load("download" ));
ui_->action_add_to_playlist->setIcon(IconLoader::Load("media-play" ));
ui_->action_load->setIcon(IconLoader::Load("media-play" ));
album_cover_choice_controller_->SetApplication(app_);
// Get a square version of noalbumart.png
QImage nocover(":/pictures/noalbumart.png");
nocover = nocover.scaled(120, 120, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage square_nocover(120, 120, QImage::Format_ARGB32);
square_nocover.fill(0);
QPainter p(&square_nocover);
p.setOpacity(0.4);
p.drawImage((120 - nocover.width()) / 2, (120 - nocover.height()) / 2, nocover);
p.end();
// no_cover_item_icon_ = QPixmap::fromImage(square_nocover);
cover_searcher_ = new AlbumCoverSearcher(no_cover_item_icon_, app_, this);
cover_export_ = new AlbumCoverExport(this);
// Set up the status bar
statusBar()->addPermanentWidget(progress_bar_);
statusBar()->addPermanentWidget(abort_progress_);
progress_bar_->hide();
abort_progress_->hide();
abort_progress_->setText(tr("Abort"));
connect(abort_progress_, SIGNAL(clicked()), this, SLOT(CancelRequests()));
ui_->albums->setAttribute(Qt::WA_MacShowFocusRect, false);
ui_->artists->setAttribute(Qt::WA_MacShowFocusRect, false);
QShortcut *close = new QShortcut(QKeySequence::Close, this);
connect(close, SIGNAL(activated()), SLOT(close()));
EnableCoversButtons();
}
AlbumCoverManager::~AlbumCoverManager() {
CancelRequests();
delete ui_;
}
CollectionBackend *AlbumCoverManager::backend() const {
return collection_backend_;
}
void AlbumCoverManager::Init() {
// View menu
QActionGroup *filter_group = new QActionGroup(this);
filter_all_ = filter_group->addAction(tr("All albums"));
filter_with_covers_ = filter_group->addAction(tr("Albums with covers"));
filter_without_covers_ = filter_group->addAction(tr("Albums without covers"));
filter_all_->setCheckable(true);
filter_with_covers_->setCheckable(true);
filter_without_covers_->setCheckable(true);
filter_group->setExclusive(true);
filter_all_->setChecked(true);
QMenu *view_menu = new QMenu(this);
view_menu->addActions(filter_group->actions());
ui_->view->setMenu(view_menu);
// Context menu
QList<QAction*> actions = album_cover_choice_controller_->GetAllActions();
connect(album_cover_choice_controller_->cover_from_file_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromFile()));
connect(album_cover_choice_controller_->cover_to_file_action(), SIGNAL(triggered()), this, SLOT(SaveCoverToFile()));
connect(album_cover_choice_controller_->cover_from_url_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromURL()));
connect(album_cover_choice_controller_->search_for_cover_action(), SIGNAL(triggered()), this, SLOT(SearchForCover()));
connect(album_cover_choice_controller_->unset_cover_action(), SIGNAL(triggered()), this, SLOT(UnsetCover()));
connect(album_cover_choice_controller_->show_cover_action(), SIGNAL(triggered()), this, SLOT(ShowCover()));
connect(cover_exporter_, SIGNAL(AlbumCoversExportUpdate(int, int, int)), SLOT(UpdateExportStatus(int, int, int)));
context_menu_->addActions(actions);
context_menu_->addSeparator();
context_menu_->addAction(ui_->action_load);
context_menu_->addAction(ui_->action_add_to_playlist);
ui_->albums->installEventFilter(this);
// Connections
connect(ui_->artists, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), SLOT(ArtistChanged(QListWidgetItem*)));
connect(ui_->filter, SIGNAL(textChanged(QString)), SLOT(UpdateFilter()));
connect(filter_group, SIGNAL(triggered(QAction*)), SLOT(UpdateFilter()));
connect(ui_->view, SIGNAL(clicked()), ui_->view, SLOT(showMenu()));
connect(ui_->fetch, SIGNAL(clicked()), SLOT(FetchAlbumCovers()));
connect(ui_->export_covers, SIGNAL(clicked()), SLOT(ExportCovers()));
connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)), SLOT(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)));
connect(ui_->action_fetch, SIGNAL(triggered()), SLOT(FetchSingleCover()));
connect(ui_->albums, SIGNAL(doubleClicked(QModelIndex)), SLOT(AlbumDoubleClicked(QModelIndex)));
connect(ui_->action_add_to_playlist, SIGNAL(triggered()), SLOT(AddSelectedToPlaylist()));
connect(ui_->action_load, SIGNAL(triggered()), SLOT(LoadSelectedToPlaylist()));
// Restore settings
QSettings s;
s.beginGroup(kSettingsGroup);
restoreGeometry(s.value("geometry").toByteArray());
if (!ui_->splitter->restoreState(s.value("splitter_state").toByteArray())) {
// Sensible default size for the artists view
ui_->splitter->setSizes(QList<int>() << 200 << width() - 200);
}
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(CoverImageLoaded(quint64, QImage)));
cover_searcher_->Init(cover_fetcher_);
new ForceScrollPerPixel(ui_->albums, this);
}
void AlbumCoverManager::showEvent(QShowEvent *) {
Reset();
}
void AlbumCoverManager::closeEvent(QCloseEvent *e) {
if (!cover_fetching_tasks_.isEmpty()) {
std::unique_ptr<QMessageBox> message_box(new QMessageBox(QMessageBox::Question, tr("Really cancel?"), tr("Closing this window will stop searching for album covers."), QMessageBox::Abort, this));
message_box->addButton(tr("Don't stop!"), QMessageBox::AcceptRole);
if (message_box->exec() != QMessageBox::Abort) {
e->ignore();
return;
}
}
// Save geometry
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("geometry", saveGeometry());
s.setValue("splitter_state", ui_->splitter->saveState());
// Cancel any outstanding requests
CancelRequests();
}
void AlbumCoverManager::CancelRequests() {
app_->album_cover_loader()->CancelTasks(QSet<quint64>::fromList(cover_loading_tasks_.keys()));
cover_loading_tasks_.clear();
cover_exporter_->Cancel();
cover_fetching_tasks_.clear();
cover_fetcher_->Clear();
progress_bar_->hide();
abort_progress_->hide();
statusBar()->clearMessage();
EnableCoversButtons();
}
static bool CompareNocase(const QString &left, const QString &right) {
return QString::localeAwareCompare(left, right) < 0;
}
static bool CompareAlbumNameNocase(const CollectionBackend::Album &left, const CollectionBackend::Album &right) {
return CompareNocase(left.album_name, right.album_name);
}
void AlbumCoverManager::Reset() {
EnableCoversButtons();
ui_->artists->clear();
new QListWidgetItem(all_artists_icon_, tr("All artists"), ui_->artists, All_Artists);
new QListWidgetItem(artist_icon_, tr("Various artists"), ui_->artists, Various_Artists);
QStringList artists(collection_backend_->GetAllArtistsWithAlbums());
qStableSort(artists.begin(), artists.end(), CompareNocase);
for (const QString &artist : artists) {
if (artist.isEmpty()) continue;
new QListWidgetItem(artist_icon_, artist, ui_->artists, Specific_Artist);
}
}
void AlbumCoverManager::EnableCoversButtons() {
ui_->fetch->setEnabled(app_->cover_providers()->HasAnyProviders());
ui_->export_covers->setEnabled(true);
}
void AlbumCoverManager::DisableCoversButtons() {
ui_->fetch->setEnabled(false);
ui_->export_covers->setEnabled(false);
}
void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
if (!current) return;
QString artist;
if (current->type() == Specific_Artist) artist = current->text();
ui_->albums->clear();
context_menu_items_.clear();
CancelRequests();
// Get the list of albums. How we do it depends on what thing we have selected in the artist list.
CollectionBackend::AlbumList albums;
switch (current->type()) {
case Various_Artists: albums = collection_backend_->GetCompilationAlbums(); break;
case Specific_Artist: albums = collection_backend_->GetAlbumsByArtist(current->text()); break;
case All_Artists:
default: albums = collection_backend_->GetAllAlbums(); break;
}
// Sort by album name. The list is already sorted by sqlite but it was done case sensitively.
qStableSort(albums.begin(), albums.end(), CompareAlbumNameNocase);
for (const CollectionBackend::Album &info : albums) {
// Don't show songs without an album, obviously
if (info.album_name.isEmpty()) continue;
QListWidgetItem *item = new QListWidgetItem(no_cover_item_icon_, info.album_name, ui_->albums);
item->setData(Role_ArtistName, info.artist);
item->setData(Role_AlbumArtistName, info.album_artist);
item->setData(Role_AlbumName, info.album_name);
item->setData(Role_FirstUrl, info.first_url);
item->setData(Qt::TextAlignmentRole, QVariant(Qt::AlignTop | Qt::AlignHCenter));
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled);
item->setToolTip(info.artist + " - " + info.album_name);
QString effective_artist = EffectiveAlbumArtistName(*item);
if (!artist.isEmpty()) {
item->setToolTip(effective_artist + " - " + info.album_name);
}
else {
item->setToolTip(info.album_name);
}
if (!info.art_automatic.isEmpty() || !info.art_manual.isEmpty()) {
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, info.art_automatic, info.art_manual, info.first_url.toLocalFile());
item->setData(Role_PathAutomatic, info.art_automatic);
item->setData(Role_PathManual, info.art_manual);
cover_loading_tasks_[id] = item;
}
}
UpdateFilter();
}
void AlbumCoverManager::CoverImageLoaded(quint64 id, const QImage &image) {
if (!cover_loading_tasks_.contains(id)) return;
QListWidgetItem *item = cover_loading_tasks_.take(id);
if (image.isNull()) return;
item->setIcon(QPixmap::fromImage(image));
UpdateFilter();
}
void AlbumCoverManager::UpdateFilter() {
const QString filter = ui_->filter->text().toLower();
const bool hide_with_covers = filter_without_covers_->isChecked();
const bool hide_without_covers = filter_with_covers_->isChecked();
HideCovers hide = Hide_None;
if (hide_with_covers) {
hide = Hide_WithCovers;
}
else if (hide_without_covers) {
hide = Hide_WithoutCovers;
}
qint32 total_count = 0;
qint32 without_cover = 0;
for (int i = 0; i < ui_->albums->count(); ++i) {
QListWidgetItem *item = ui_->albums->item(i);
bool should_hide = ShouldHide(*item, filter, hide);
item->setHidden(should_hide);
if (!should_hide) {
total_count++;
if (!ItemHasCover(*item)) {
without_cover++;
}
}
}
ui_->total_albums->setText(QString::number(total_count));
ui_->without_cover->setText(QString::number(without_cover));
}
bool AlbumCoverManager::ShouldHide(const QListWidgetItem &item, const QString &filter, HideCovers hide) const {
bool has_cover = ItemHasCover(item);
if (hide == Hide_WithCovers && has_cover) {
return true;
}
else if (hide == Hide_WithoutCovers && !has_cover) {
return true;
}
if (filter.isEmpty()) {
return false;
}
QStringList query = filter.split(' ');
for (const QString &s : query) {
bool in_text = item.text().contains(s, Qt::CaseInsensitive);
bool in_artist = item.data(Role_ArtistName).toString().contains(s, Qt::CaseInsensitive);
bool in_albumartist = item.data(Role_AlbumArtistName).toString().contains(s, Qt::CaseInsensitive);
if (!in_text && !in_artist && !in_albumartist) {
return true;
}
}
return false;
}
void AlbumCoverManager::FetchAlbumCovers() {
for (int i = 0; i < ui_->albums->count(); ++i) {
QListWidgetItem *item = ui_->albums->item(i);
if (item->isHidden()) continue;
if (ItemHasCover(*item)) continue;
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString());
cover_fetching_tasks_[id] = item;
jobs_++;
}
if (!cover_fetching_tasks_.isEmpty()) ui_->fetch->setEnabled(false);
progress_bar_->setMaximum(jobs_);
progress_bar_->show();
abort_progress_->show();
fetch_statistics_ = CoverSearchStatistics();
UpdateStatusText();
}
void AlbumCoverManager::AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics) {
if (!cover_fetching_tasks_.contains(id))
return;
QListWidgetItem *item = cover_fetching_tasks_.take(id);
if (!image.isNull()) {
SaveAndSetCover(item, image);
}
if (cover_fetching_tasks_.isEmpty()) {
EnableCoversButtons();
}
fetch_statistics_ += statistics;
UpdateStatusText();
}
void AlbumCoverManager::UpdateStatusText() {
QString message = tr("Got %1 covers out of %2 (%3 failed)")
.arg(fetch_statistics_.chosen_images_)
.arg(jobs_)
.arg(fetch_statistics_.missing_images_);
if (fetch_statistics_.bytes_transferred_) {
message += ", " + tr("%1 transferred").arg(Utilities::PrettySize(fetch_statistics_.bytes_transferred_));
}
statusBar()->showMessage(message);
progress_bar_->setValue(fetch_statistics_.chosen_images_ + fetch_statistics_.missing_images_);
if (cover_fetching_tasks_.isEmpty()) {
QTimer::singleShot(2000, statusBar(), SLOT(clearMessage()));
progress_bar_->hide();
abort_progress_->hide();
CoverSearchStatisticsDialog *dialog = new CoverSearchStatisticsDialog(this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->Show(fetch_statistics_);
jobs_ = 0;
}
}
bool AlbumCoverManager::eventFilter(QObject *obj, QEvent *event) {
if (obj == ui_->albums && event->type() == QEvent::ContextMenu) {
context_menu_items_ = ui_->albums->selectedItems();
if (context_menu_items_.isEmpty()) return false;
bool some_with_covers = false;
for (QListWidgetItem *item : context_menu_items_) {
if (ItemHasCover(*item)) some_with_covers = true;
}
album_cover_choice_controller_->cover_from_file_action()->setEnabled(context_menu_items_.size() == 1);
album_cover_choice_controller_->cover_from_url_action()->setEnabled(context_menu_items_.size() == 1);
album_cover_choice_controller_->show_cover_action()->setEnabled(some_with_covers && context_menu_items_.size() == 1);
album_cover_choice_controller_->unset_cover_action()->setEnabled(some_with_covers);
album_cover_choice_controller_->search_for_cover_action()->setEnabled(app_->cover_providers()->HasAnyProviders());
QContextMenuEvent *e = static_cast<QContextMenuEvent*>(event);
context_menu_->popup(e->globalPos());
return true;
}
return QMainWindow::eventFilter(obj, event);
}
Song AlbumCoverManager::GetSingleSelectionAsSong() {
return context_menu_items_.size() != 1 ? Song() : ItemAsSong(context_menu_items_[0]);
}
Song AlbumCoverManager::GetFirstSelectedAsSong() {
return context_menu_items_.isEmpty() ? Song() : ItemAsSong(context_menu_items_[0]);
}
Song AlbumCoverManager::ItemAsSong(QListWidgetItem *item) {
Song result;
QString title = item->data(Role_AlbumName).toString();
QString artist_name = EffectiveAlbumArtistName(*item);
if (!artist_name.isEmpty()) {
result.set_title(artist_name + " - " + title);
}
else {
result.set_title(title);
}
result.set_artist(item->data(Role_ArtistName).toString());
result.set_albumartist(item->data(Role_AlbumArtistName).toString());
result.set_album(item->data(Role_AlbumName).toString());
result.set_url(item->data(Role_FirstUrl).toUrl());
result.set_art_automatic(item->data(Role_PathAutomatic).toString());
result.set_art_manual(item->data(Role_PathManual).toString());
// force validity
result.set_valid(true);
result.set_id(0);
return result;
}
void AlbumCoverManager::ShowCover() {
Song song = GetSingleSelectionAsSong();
if (!song.is_valid()) return;
album_cover_choice_controller_->ShowCover(song);
}
void AlbumCoverManager::FetchSingleCover() {
for (QListWidgetItem *item : context_menu_items_) {
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString());
cover_fetching_tasks_[id] = item;
jobs_++;
}
progress_bar_->setMaximum(jobs_);
progress_bar_->show();
abort_progress_->show();
UpdateStatusText();
}
void AlbumCoverManager::UpdateCoverInList(QListWidgetItem *item, const QString &cover) {
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QString(), cover);
item->setData(Role_PathManual, cover);
cover_loading_tasks_[id] = item;
}
void AlbumCoverManager::LoadCoverFromFile() {
Song song = GetSingleSelectionAsSong();
if (!song.is_valid()) return;
QListWidgetItem *item = context_menu_items_[0];
QString cover = album_cover_choice_controller_->LoadCoverFromFile(&song);
if (!cover.isEmpty()) {
UpdateCoverInList(item, cover);
}
}
void AlbumCoverManager::SaveCoverToFile() {
Song song = GetSingleSelectionAsSong();
if (!song.is_valid()) return;
QImage image;
// load the image from disk
if (song.has_manually_unset_cover()) {
image = no_cover_image_;
}
else {
if (!song.art_manual().isEmpty() && QFile::exists(song.art_manual())) {
image = QImage(song.art_manual());
}
else if(!song.art_automatic().isEmpty() && QFile::exists(song.art_automatic())) {
image = QImage(song.art_automatic());
}
else {
image = no_cover_image_;
}
}
album_cover_choice_controller_->SaveCoverToFile(song, image);
}
void AlbumCoverManager::LoadCoverFromURL() {
Song song = GetSingleSelectionAsSong();
if (!song.is_valid()) return;
QListWidgetItem *item = context_menu_items_[0];
QString cover = album_cover_choice_controller_->LoadCoverFromURL(&song);
if (!cover.isEmpty()) {
UpdateCoverInList(item, cover);
}
}
void AlbumCoverManager::SearchForCover() {
Song song = GetFirstSelectedAsSong();
if (!song.is_valid()) return;
QListWidgetItem *item = context_menu_items_[0];
QString cover = album_cover_choice_controller_->SearchForCover(&song);
if (cover.isEmpty()) return;
// force the found cover on all of the selected items
for (QListWidgetItem *current : context_menu_items_) {
// don't save the first one twice
if (current != item) {
Song current_song = ItemAsSong(current);
album_cover_choice_controller_->SaveCover(&current_song, cover);
}
UpdateCoverInList(current, cover);
}
}
void AlbumCoverManager::UnsetCover() {
Song song = GetFirstSelectedAsSong();
if (!song.is_valid()) return;
QListWidgetItem *item = context_menu_items_[0];
QString cover = album_cover_choice_controller_->UnsetCover(&song);
// force the 'none' cover on all of the selected items
for (QListWidgetItem *current : context_menu_items_) {
current->setIcon(no_cover_item_icon_);
current->setData(Role_PathManual, cover);
// don't save the first one twice
if (current != item) {
Song current_song = ItemAsSong(current);
album_cover_choice_controller_->SaveCover(&current_song, cover);
}
}
}
SongList AlbumCoverManager::GetSongsInAlbum(const QModelIndex &index) const {
SongList ret;
CollectionQuery q;
q.SetColumnSpec("ROWID," + Song::kColumnSpec);
q.AddWhere("album", index.data(Role_AlbumName).toString());
q.SetOrderBy("disc, track, title");
QString artist = index.data(Role_ArtistName).toString();
QString albumartist = index.data(Role_AlbumArtistName).toString();
if (!albumartist.isEmpty()) {
q.AddWhere("albumartist", albumartist);
}
else if (!artist.isEmpty()) {
q.AddWhere("artist", artist);
}
q.AddCompilationRequirement(artist.isEmpty() && albumartist.isEmpty());
if (!collection_backend_->ExecQuery(&q)) return ret;
while (q.Next()) {
Song song;
song.InitFromQuery(q, true);
ret << song;
}
return ret;
}
SongList AlbumCoverManager::GetSongsInAlbums(const QModelIndexList &indexes) const {
SongList ret;
for (const QModelIndex &index : indexes) {
ret << GetSongsInAlbum(index);
}
return ret;
}
SongMimeData *AlbumCoverManager::GetMimeDataForAlbums(const QModelIndexList &indexes) const {
SongList songs = GetSongsInAlbums(indexes);
if (songs.isEmpty()) return nullptr;
SongMimeData *data = new SongMimeData;
data->backend = collection_backend_;
data->songs = songs;
return data;
}
void AlbumCoverManager::AlbumDoubleClicked(const QModelIndex &index) {
SongMimeData *data = GetMimeDataForAlbums(QModelIndexList() << index);
if (data) {
data->from_doubleclick_ = true;
emit AddToPlaylist(data);
}
}
void AlbumCoverManager::AddSelectedToPlaylist() {
emit AddToPlaylist(GetMimeDataForAlbums(ui_->albums->selectionModel()->selectedIndexes()));
}
void AlbumCoverManager::LoadSelectedToPlaylist() {
SongMimeData *data = GetMimeDataForAlbums(ui_->albums->selectionModel()->selectedIndexes());
if (data) {
data->clear_first_ = true;
emit AddToPlaylist(data);
}
}
void AlbumCoverManager::SaveAndSetCover(QListWidgetItem *item, const QImage &image) {
const QString artist = item->data(Role_ArtistName).toString();
const QString albumartist = item->data(Role_ArtistName).toString();
const QString album = item->data(Role_AlbumName).toString();
QString path = album_cover_choice_controller_->SaveCoverInCache(artist, album, image);
// Save the image in the database
collection_backend_->UpdateManualAlbumArtAsync(artist, albumartist, album, path);
// Update the icon in our list
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QString(), path);
item->setData(Role_PathManual, path);
cover_loading_tasks_[id] = item;
}
void AlbumCoverManager::ExportCovers() {
AlbumCoverExport::DialogResult result = cover_export_->Exec();
if (result.cancelled_) {
return;
}
DisableCoversButtons();
cover_exporter_->SetDialogResult(result);
for (int i = 0; i < ui_->albums->count(); ++i) {
QListWidgetItem *item = ui_->albums->item(i);
// skip hidden and coverless albums
if (item->isHidden() || !ItemHasCover(*item)) {
continue;
}
cover_exporter_->AddExportRequest(ItemAsSong(item));
}
if (cover_exporter_->request_count() > 0) {
progress_bar_->setMaximum(cover_exporter_->request_count());
progress_bar_->show();
abort_progress_->show();
cover_exporter_->StartExporting();
}
else {
QMessageBox msg;
msg.setWindowTitle(tr("Export finished"));
msg.setText(tr("No covers to export."));
msg.exec();
}
}
void AlbumCoverManager::UpdateExportStatus(int exported, int skipped, int max) {
progress_bar_->setValue(exported);
QString message = tr("Exported %1 covers out of %2 (%3 skipped)")
.arg(exported)
.arg(max)
.arg(skipped);
statusBar()->showMessage(message);
// end of the current process
if (exported + skipped >= max) {
QTimer::singleShot(2000, statusBar(), SLOT(clearMessage()));
progress_bar_->hide();
abort_progress_->hide();
EnableCoversButtons();
QMessageBox msg;
msg.setWindowTitle(tr("Export finished"));
msg.setText(message);
msg.exec();
}
}
QString AlbumCoverManager::EffectiveAlbumArtistName(const QListWidgetItem &item) const {
QString albumartist = item.data(Role_AlbumArtistName).toString();
if (!albumartist.isEmpty()) {
return albumartist;
}
return item.data(Role_ArtistName).toString();
}
QImage AlbumCoverManager::GenerateNoCoverImage(const QIcon &no_cover_icon) const {
// Get a square version of noalbumart.png with some transparency:
QImage nocover = no_cover_icon.pixmap(no_cover_icon.availableSizes().last()).toImage();
nocover = nocover.scaled(120, 120, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage square_nocover(120, 120, QImage::Format_ARGB32);
square_nocover.fill(0);
QPainter p(&square_nocover);
p.setOpacity(0.4);
p.drawImage((120 - nocover.width()) / 2, (120 - nocover.height()) / 2, nocover);
p.end();
return square_nocover;
}
bool AlbumCoverManager::ItemHasCover(const QListWidgetItem& item) const {
return item.icon().cacheKey() != no_cover_item_icon_.cacheKey();
}

View File

@@ -0,0 +1,194 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERMANAGER_H
#define ALBUMCOVERMANAGER_H
#include "config.h"
#include <QMainWindow>
#include <QIcon>
#include <QModelIndex>
#include "gtest/gtest_prod.h"
#include "core/song.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/coversearchstatistics.h"
class AlbumCoverChoiceController;
class AlbumCoverExport;
class AlbumCoverExporter;
class AlbumCoverFetcher;
class AlbumCoverSearcher;
class Application;
class CollectionBackend;
class SongMimeData;
class Ui_CoverManager;
class QListWidgetItem;
class QMenu;
class QNetworkAccessManager;
class QPushButton;
class QProgressBar;
class AlbumCoverManager : public QMainWindow {
Q_OBJECT
public:
AlbumCoverManager(Application *app, CollectionBackend *collection_backend, QWidget *parent = nullptr, QNetworkAccessManager *network = 0);
~AlbumCoverManager();
static const char *kSettingsGroup;
CollectionBackend *backend() const;
QIcon no_cover_icon() const { return no_cover_icon_; }
void Reset();
void Init();
void EnableCoversButtons();
void DisableCoversButtons();
SongList GetSongsInAlbum(const QModelIndex &index) const;
SongList GetSongsInAlbums(const QModelIndexList &indexes) const;
SongMimeData *GetMimeDataForAlbums(const QModelIndexList &indexes) const;
signals:
void AddToPlaylist(QMimeData *data);
protected:
void showEvent(QShowEvent *);
void closeEvent(QCloseEvent *);
// For the album view context menu events
bool eventFilter(QObject *obj, QEvent *event);
private slots:
void ArtistChanged(QListWidgetItem *current);
void CoverImageLoaded(quint64 id, const QImage &image);
void UpdateFilter();
void FetchAlbumCovers();
void ExportCovers();
void AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics);
void CancelRequests();
// On the context menu
void FetchSingleCover();
void LoadCoverFromFile();
void SaveCoverToFile();
void LoadCoverFromURL();
void SearchForCover();
void UnsetCover();
void ShowCover();
// For adding albums to the playlist
void AlbumDoubleClicked(const QModelIndex &index);
void AddSelectedToPlaylist();
void LoadSelectedToPlaylist();
void UpdateCoverInList(QListWidgetItem *item, const QString &cover);
void UpdateExportStatus(int exported, int bad, int count);
private:
enum ArtistItemType {
All_Artists,
Various_Artists,
Specific_Artist
};
enum Role {
Role_ArtistName = Qt::UserRole + 1,
Role_AlbumArtistName,
Role_AlbumName,
Role_PathAutomatic,
Role_PathManual,
Role_FirstUrl
};
enum HideCovers {
Hide_None,
Hide_WithCovers,
Hide_WithoutCovers
};
QString InitialPathForOpenCoverDialog(const QString &path_automatic, const QString &first_file_name) const;
QString EffectiveAlbumArtistName(const QListWidgetItem &item) const;
// Returns the selected element in form of a Song ready to be used
// by AlbumCoverChoiceController or invalid song if there's nothing
// or multiple elements selected.
Song GetSingleSelectionAsSong();
// Returns the first of the selected elements in form of a Song ready
// to be used by AlbumCoverChoiceController or invalid song if there's nothing
// selected.
Song GetFirstSelectedAsSong();
Song ItemAsSong(QListWidgetItem *item);
void UpdateStatusText();
bool ShouldHide(const QListWidgetItem &item, const QString &filter, HideCovers hide) const;
void SaveAndSetCover(QListWidgetItem *item, const QImage &image);
private:
Ui_CoverManager *ui_;
Application *app_;
AlbumCoverChoiceController *album_cover_choice_controller_;
QAction *filter_all_;
QAction *filter_with_covers_;
QAction *filter_without_covers_;
AlbumCoverLoaderOptions cover_loader_options_;
QMap<quint64, QListWidgetItem*> cover_loading_tasks_;
AlbumCoverFetcher *cover_fetcher_;
QMap<quint64, QListWidgetItem*> cover_fetching_tasks_;
CoverSearchStatistics fetch_statistics_;
AlbumCoverSearcher *cover_searcher_;
AlbumCoverExport *cover_export_;
AlbumCoverExporter *cover_exporter_;
QImage GenerateNoCoverImage(const QIcon &no_cover_icon) const;
bool ItemHasCover(const QListWidgetItem &item) const;
QIcon artist_icon_;
QIcon all_artists_icon_;
const QIcon no_cover_icon_;
const QImage no_cover_image_;
const QIcon no_cover_item_icon_;
QMenu *context_menu_;
QList<QListWidgetItem*> context_menu_items_;
QProgressBar *progress_bar_;
QPushButton *abort_progress_;
int jobs_;
CollectionBackend *collection_backend_;
FRIEND_TEST(AlbumCoverManagerTest, HidesItemsWithCover);
FRIEND_TEST(AlbumCoverManagerTest, HidesItemsWithoutCover);
FRIEND_TEST(AlbumCoverManagerTest, HidesItemsWithFilter);
};
#endif // ALBUMCOVERMANAGER_H

View File

@@ -0,0 +1,284 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CoverManager</class>
<widget class="QMainWindow" name="CoverManager">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>903</width>
<height>662</height>
</rect>
</property>
<property name="windowTitle">
<string>Cover Manager</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QListWidget" name="artists">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>6</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QSearchField" name="filter" native="true">
<property name="placeholderText" stdset="0">
<string>Enter search terms here</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="view">
<property name="text">
<string>View</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="popupMode">
<enum>QToolButton::MenuButtonPopup</enum>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,0">
<property name="topMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Total albums:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Without cover:</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="topMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="total_albums">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="without_cover">
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="fetch">
<property name="text">
<string>Fetch Missing Covers</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="export_covers">
<property name="text">
<string>Export Covers</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="AlbumCoverManagerList" name="albums">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<bool>false</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>120</width>
<height>120</height>
</size>
</property>
<property name="flow">
<enum>QListView::LeftToRight</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>true</bool>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
</property>
<property name="spacing">
<number>2</number>
</property>
<property name="viewMode">
<enum>QListView::IconMode</enum>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<action name="action_fetch">
<property name="text">
<string>Fetch automatically</string>
</property>
</action>
<action name="action_load">
<property name="text">
<string>Load</string>
</property>
</action>
<action name="action_add_to_playlist">
<property name="text">
<string>Add to playlist</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>QSearchField</class>
<extends>QWidget</extends>
<header>3rdparty/qocoa/qsearchfield.h</header>
</customwidget>
<customwidget>
<class>AlbumCoverManagerList</class>
<extends>QListWidget</extends>
<header>covermanager/albumcovermanagerlist.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>artists</tabstop>
<tabstop>albums</tabstop>
</tabstops>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,73 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "albumcovermanagerlist.h"
#include <memory>
#include <QDropEvent>
#include <QUrl>
#include "albumcovermanager.h"
#include "collection/collectionbackend.h"
#include "playlist/songmimedata.h"
AlbumCoverManagerList::AlbumCoverManagerList(QWidget *parent) : QListWidget(parent), manager_(nullptr) {}
QMimeData *AlbumCoverManagerList::mimeData(const QList<QListWidgetItem*> items) const {
// Get songs
SongList songs;
for (QListWidgetItem *item : items) {
songs << manager_->GetSongsInAlbum(indexFromItem(item));
}
if (songs.isEmpty()) return nullptr;
// Get URLs from the songs
QList<QUrl> urls;
for (const Song &song : songs) {
urls << song.url();
}
// Get the QAbstractItemModel data so the picture works
std::unique_ptr<QMimeData> orig_data(QListWidget::mimeData(items));
SongMimeData *mime_data = new SongMimeData;
mime_data->backend = manager_->backend();
mime_data->songs = songs;
mime_data->setUrls(urls);
mime_data->setData(orig_data->formats()[0], orig_data->data(orig_data->formats()[0]));
return mime_data;
}
void AlbumCoverManagerList::dropEvent(QDropEvent *e) {
// Set movement to Static just for this dropEvent so the user can't move the
// album covers. If it's set to Static all the time then the user can't even
// drag to the playlist
QListWidget::Movement old_movement = movement();
setMovement(QListWidget::Static);
QListWidget::dropEvent(e);
setMovement(old_movement);
}

View File

@@ -0,0 +1,46 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERMANAGERLIST_H
#define ALBUMCOVERMANAGERLIST_H
#include "config.h"
#include <QListWidget>
class AlbumCoverManager;
class AlbumCoverManagerList : public QListWidget {
Q_OBJECT
public:
AlbumCoverManagerList(QWidget *parent = nullptr);
void set_cover_manager(AlbumCoverManager* manager) { manager_ = manager; }
protected:
QMimeData *mimeData(const QList<QListWidgetItem*> items) const;
void dropEvent(QDropEvent *event);
private:
AlbumCoverManager* manager_;
};
#endif // ALBUMCOVERMANAGERLIST_H

View File

@@ -0,0 +1,259 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "albumcoversearcher.h"
#include "ui_albumcoversearcher.h"
#include "core/application.h"
#include "core/logging.h"
#include "core/utilities.h"
#include "covermanager/albumcoverfetcher.h"
#include "covermanager/albumcoverloader.h"
#include "widgets/forcescrollperpixel.h"
#include "widgets/groupediconview.h"
#include <QKeyEvent>
#include <QListWidgetItem>
#include <QPainter>
#include <QStandardItemModel>
const int SizeOverlayDelegate::kMargin = 4;
const int SizeOverlayDelegate::kPaddingX = 3;
const int SizeOverlayDelegate::kPaddingY = 1;
const qreal SizeOverlayDelegate::kBorder = 5.0;
const qreal SizeOverlayDelegate::kFontPointSize = 7.5;
const int SizeOverlayDelegate::kBorderAlpha = 200;
const int SizeOverlayDelegate::kBackgroundAlpha = 175;
SizeOverlayDelegate::SizeOverlayDelegate(QObject *parent)
: QStyledItemDelegate(parent) {}
void SizeOverlayDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
QStyledItemDelegate::paint(painter, option, index);
if (!index.data(AlbumCoverSearcher::Role_ImageFetchFinished).toBool()) {
return;
}
const QSize size = index.data(AlbumCoverSearcher::Role_ImageSize).toSize();
const QString text = Utilities::PrettySize(size);
QFont font(option.font);
font.setPointSizeF(kFontPointSize);
font.setBold(true);
const QFontMetrics metrics(font);
const int text_width = metrics.width(text);
const QRect icon_rect(option.rect.left(), option.rect.top(), option.rect.width(), option.rect.width());
const QRect background_rect(icon_rect.right() - kMargin - text_width - kPaddingX * 2, icon_rect.bottom() - kMargin - metrics.height() - kPaddingY * 2, text_width + kPaddingX * 2, metrics.height() + kPaddingY * 2);
const QRect text_rect(background_rect.left() + kPaddingX, background_rect.top() + kPaddingY, text_width, metrics.height());
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(QColor(0, 0, 0, kBorderAlpha));
painter->setBrush(QColor(0, 0, 0, kBackgroundAlpha));
painter->drawRoundedRect(background_rect, kBorder, kBorder);
painter->setPen(Qt::white);
painter->setFont(font);
painter->drawText(text_rect, text);
painter->restore();
}
AlbumCoverSearcher::AlbumCoverSearcher(const QIcon &no_cover_icon, Application *app, QWidget *parent)
: QDialog(parent),
ui_(new Ui_AlbumCoverSearcher),
app_(app),
model_(new QStandardItemModel(this)),
no_cover_icon_(no_cover_icon),
fetcher_(nullptr),
id_(0) {
setWindowModality(Qt::WindowModal);
ui_->setupUi(this);
ui_->busy->hide();
ui_->covers->set_header_text(tr("Covers from %1"));
ui_->covers->AddSortSpec(Role_ImageDimensions, Qt::DescendingOrder);
ui_->covers->setItemDelegate(new SizeOverlayDelegate(this));
ui_->covers->setModel(model_);
options_.scale_output_image_ = false;
options_.pad_output_image_ = false;
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(ImageLoaded(quint64, QImage)));
connect(ui_->search, SIGNAL(clicked()), SLOT(Search()));
connect(ui_->covers, SIGNAL(doubleClicked(QModelIndex)), SLOT(CoverDoubleClicked(QModelIndex)));
new ForceScrollPerPixel(ui_->covers, this);
ui_->buttonBox->button(QDialogButtonBox::Cancel)->setShortcut(QKeySequence::Close);
}
AlbumCoverSearcher::~AlbumCoverSearcher() {
delete ui_;
}
void AlbumCoverSearcher::Init(AlbumCoverFetcher *fetcher) {
fetcher_ = fetcher;
connect(fetcher_, SIGNAL(SearchFinished(quint64,CoverSearchResults,CoverSearchStatistics)), SLOT(SearchFinished(quint64,CoverSearchResults)));
}
QImage AlbumCoverSearcher::Exec(const QString &artist, const QString &album) {
ui_->artist->setText(artist);
ui_->album->setText(album);
ui_->artist->setFocus();
if (!artist.isEmpty() || !album.isEmpty()) {
Search();
}
if (exec() == QDialog::Rejected) return QImage();
QModelIndex selected = ui_->covers->currentIndex();
if (!selected.isValid() || !selected.data(Role_ImageFetchFinished).toBool())
return QImage();
QIcon icon = selected.data(Qt::DecorationRole).value<QIcon>();
if (icon.cacheKey() == no_cover_icon_.cacheKey()) return QImage();
return icon.pixmap(icon.availableSizes()[0]).toImage();
}
void AlbumCoverSearcher::Search() {
model_->clear();
cover_loading_tasks_.clear();
if (ui_->album->isEnabled()) {
id_ = fetcher_->SearchForCovers(ui_->artist->text(), ui_->album->text());
ui_->search->setText(tr("Abort"));
ui_->busy->show();
ui_->artist->setEnabled(false);
ui_->album->setEnabled(false);
ui_->covers->setEnabled(false);
}
else {
fetcher_->Clear();
ui_->search->setText(tr("Search"));
ui_->busy->hide();
ui_->search->setEnabled(true);
ui_->artist->setEnabled(true);
ui_->album->setEnabled(true);
ui_->covers->setEnabled(true);
}
}
void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults &results) {
if (id != id_)
return;
ui_->search->setEnabled(true);
ui_->artist->setEnabled(true);
ui_->album->setEnabled(true);
ui_->covers->setEnabled(true);
ui_->search->setText(tr("Search"));
id_ = 0;
for (const CoverSearchResult &result : results) {
if (result.image_url.isEmpty()) continue;
quint64 id = app_->album_cover_loader()->LoadImageAsync(options_, result.image_url.toString(), QString());
QStandardItem *item = new QStandardItem;
item->setIcon(no_cover_icon_);
item->setText(result.description);
item->setData(result.image_url, Role_ImageURL);
item->setData(id, Role_ImageRequestId);
item->setData(false, Role_ImageFetchFinished);
item->setData(QVariant(Qt::AlignTop | Qt::AlignHCenter), Qt::TextAlignmentRole);
item->setData(result.provider, GroupedIconView::Role_Group);
model_->appendRow(item);
cover_loading_tasks_[id] = item;
}
if (cover_loading_tasks_.isEmpty()) ui_->busy->hide();
}
void AlbumCoverSearcher::ImageLoaded(quint64 id, const QImage &image) {
if (!cover_loading_tasks_.contains(id)) return;
QStandardItem *item = cover_loading_tasks_.take(id);
if (cover_loading_tasks_.isEmpty()) ui_->busy->hide();
if (image.isNull()) {
model_->removeRow(item->row());
return;
}
QIcon icon(QPixmap::fromImage(image));
// Create a pixmap that's padded and exactly the right size for the icon.
QImage scaled_image(image.scaled(ui_->covers->iconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
QImage padded_image(ui_->covers->iconSize(), QImage::Format_ARGB32_Premultiplied);
padded_image.fill(0);
QPainter p(&padded_image);
p.drawImage((padded_image.width() - scaled_image.width()) / 2, (padded_image.height() - scaled_image.height()) / 2, scaled_image);
p.end();
icon.addPixmap(QPixmap::fromImage(padded_image));
item->setData(true, Role_ImageFetchFinished);
item->setData(image.width() * image.height(), Role_ImageDimensions);
item->setData(image.size(), Role_ImageSize);
item->setIcon(icon);
}
void AlbumCoverSearcher::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
e->ignore();
return;
}
QDialog::keyPressEvent(e);
}
void AlbumCoverSearcher::CoverDoubleClicked(const QModelIndex &index) {
if (index.isValid()) accept();
}

View File

@@ -0,0 +1,100 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERSEARCHER_H
#define ALBUMCOVERSEARCHER_H
#include "config.h"
#include <QDialog>
#include <QIcon>
#include <QStyledItemDelegate>
#include "covermanager/albumcoverfetcher.h"
#include "covermanager/albumcoverloaderoptions.h"
class AlbumCoverLoader;
class Application;
class Ui_AlbumCoverSearcher;
class QModelIndex;
class QStandardItem;
class QStandardItemModel;
class SizeOverlayDelegate : public QStyledItemDelegate {
public:
static const int kMargin;
static const int kPaddingX;
static const int kPaddingY;
static const qreal kBorder;
static const qreal kFontPointSize;
static const int kBorderAlpha;
static const int kBackgroundAlpha;
SizeOverlayDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
};
// This is a dialog that lets the user search for album covers
class AlbumCoverSearcher : public QDialog {
Q_OBJECT
public:
AlbumCoverSearcher(const QIcon &no_cover_icon, Application *app, QWidget *parent);
~AlbumCoverSearcher();
enum Role {
Role_ImageURL = Qt::UserRole + 1,
Role_ImageRequestId,
Role_ImageFetchFinished,
Role_ImageDimensions, // width * height
Role_ImageSize,
};
void Init(AlbumCoverFetcher *fetcher);
QImage Exec(const QString &artist, const QString &album);
protected:
void keyPressEvent(QKeyEvent *);
private slots:
void Search();
void SearchFinished(quint64 id, const CoverSearchResults &results);
void ImageLoaded(quint64 id, const QImage &image);
void CoverDoubleClicked(const QModelIndex &index);
private:
Ui_AlbumCoverSearcher *ui_;
Application *app_;
QStandardItemModel *model_;
QIcon no_cover_icon_;
AlbumCoverLoaderOptions options_;
AlbumCoverFetcher *fetcher_;
quint64 id_;
QMap<quint64, QStandardItem*> cover_loading_tasks_;
};
#endif // ALBUMCOVERSEARCHER_H

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AlbumCoverSearcher</class>
<widget class="QDialog" name="AlbumCoverSearcher">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>829</width>
<height>518</height>
</rect>
</property>
<property name="windowTitle">
<string>Cover Manager</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSearchField" name="artist">
<property name="toolTip">
<string>Artist</string>
</property>
<property name="placeholderText" stdset="0">
<string>Artist</string>
</property>
</widget>
</item>
<item>
<widget class="QSearchField" name="album">
<property name="toolTip">
<string>Album</string>
</property>
<property name="placeholderText" stdset="0">
<string>Album</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="search">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
<item>
<widget class="BusyIndicator" name="busy" native="true"/>
</item>
</layout>
</item>
<item>
<widget class="GroupedIconView" name="covers">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="iconSize">
<size>
<width>120</width>
<height>120</height>
</size>
</property>
<property name="spacing">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QSearchField</class>
<extends>QWidget</extends>
<header>3rdparty/qocoa/qsearchfield.h</header>
</customwidget>
<customwidget>
<class>BusyIndicator</class>
<extends>QWidget</extends>
<header>widgets/busyindicator.h</header>
</customwidget>
<customwidget>
<class>GroupedIconView</class>
<extends>QListView</extends>
<header>widgets/groupediconview.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>artist</tabstop>
<tabstop>album</tabstop>
<tabstop>search</tabstop>
<tabstop>covers</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AlbumCoverSearcher</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>257</x>
<y>508</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AlbumCoverSearcher</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>325</x>
<y>508</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>artist</sender>
<signal>returnPressed()</signal>
<receiver>search</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>357</x>
<y>35</y>
</hint>
<hint type="destinationlabel">
<x>812</x>
<y>36</y>
</hint>
</hints>
</connection>
<connection>
<sender>album</sender>
<signal>returnPressed()</signal>
<receiver>search</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>580</x>
<y>22</y>
</hint>
<hint type="destinationlabel">
<x>779</x>
<y>21</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,181 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2013, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QDateTime>
#include <QNetworkReply>
#include <QStringList>
#include <QXmlStreamReader>
#include <QUrlQuery>
#include "amazoncoverprovider.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/network.h"
#include "core/utilities.h"
const char *AmazonCoverProvider::kUrl = "http://ecs.amazonaws.com/onca/xml";
const char *AmazonCoverProvider::kAssociateTag = "jonas052-20";
const char *AmazonCoverProvider::kAccessKeyB64 = "QUtJQUozQ1dIQ0RWSVlYN1JMTFE=";
const char *AmazonCoverProvider::kSecretAccessKeyB64 = "TjFZU3F2c2hJZDVtUGxKVW1Ka0kvc2E1WkZESG9TYy9ZWkgxYWdJdQ==";
AmazonCoverProvider::AmazonCoverProvider(QObject *parent) : CoverProvider("Amazon", parent), network_(new NetworkAccessManager(this)) {}
bool AmazonCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
//qLog(Debug) << __PRETTY_FUNCTION__;
typedef QPair<QString, QString> Arg;
typedef QList<Arg> ArgList;
typedef QPair<QByteArray, QByteArray> EncodedArg;
typedef QList<EncodedArg> EncodedArgList;
// Must be sorted by parameter name
ArgList args = ArgList()
<< Arg("AWSAccessKeyId", QByteArray::fromBase64(kAccessKeyB64))
//<< Arg("AWSAccessKeyId", kAccessKey)
<< Arg("AssociateTag", kAssociateTag)
<< Arg("Keywords", artist + " " + album)
<< 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");
QUrlQuery url_query;
QUrl url(kUrl);
QStringList query_items;
// Encode the arguments
for (const Arg &arg : args) {
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
}
// Sign the request
const QByteArray data_to_sign = QString("GET\n%1\n%2\n%3").arg(url.host(), url.path(), query_items.join("&")).toLatin1();
//const QByteArray signature(Utilities::HmacSha256(kSecretAccessKey, data_to_sign));
const QByteArray signature(Utilities::HmacSha256(QByteArray::fromBase64(kSecretAccessKeyB64), data_to_sign));
// Add the signature to the request
url_query.addQueryItem("Signature", QUrl::toPercentEncoding(signature.toBase64()));
url.setQuery(url_query);
QNetworkReply *reply = network_->get(QNetworkRequest(url));
NewClosure(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(QueryError(QNetworkReply::NetworkError, QNetworkReply*, int)), reply, id);
NewClosure(reply, SIGNAL(finished()), this, SLOT(QueryFinished(QNetworkReply*, int)), reply, id);
return true;
}
void AmazonCoverProvider::QueryError(QNetworkReply::NetworkError error, QNetworkReply *reply, int id) {
//qLog(Debug) << __PRETTY_FUNCTION__;
}
void AmazonCoverProvider::QueryFinished(QNetworkReply *reply, int id) {
//qLog(Debug) << __PRETTY_FUNCTION__;
reply->deleteLater();
QString data=(QString)reply->readAll();
//qDebug() << data;
CoverSearchResults results;
QXmlStreamReader reader(data);
while (!reader.atEnd()) {
if (reader.readNext() == QXmlStreamReader::EndDocument) break;
if (reader.tokenType() == QXmlStreamReader::Invalid ) { qLog(Debug) << reader.error() << reader.errorString(); break; }
//qLog(Debug) << reader.tokenType() << reader.name();
if (reader.tokenType() == QXmlStreamReader::StartElement && reader.name() == "Item") {
ReadItem(&reader, &results);
}
}
emit SearchFinished(id, results);
}
void AmazonCoverProvider::ReadItem(QXmlStreamReader *reader, CoverSearchResults *results) {
//qLog(Debug) << __PRETTY_FUNCTION__ << "name: " << reader->name() << " text: " << reader->text();
while (!reader->atEnd()) {
switch (reader->readNext()) {
case QXmlStreamReader::StartElement:
if (reader->name() == "LargeImage") {
ReadLargeImage(reader, results);
}
else {
reader->skipCurrentElement();
}
break;
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}
void AmazonCoverProvider::ReadLargeImage(QXmlStreamReader *reader, CoverSearchResults *results) {
//qLog(Debug) << __PRETTY_FUNCTION__;
while (!reader->atEnd()) {
switch (reader->readNext()) {
case QXmlStreamReader::StartElement:
if (reader->name() == "URL") {
CoverSearchResult result;
result.image_url = QUrl(reader->readElementText());
results->append(result);
}
else {
reader->skipCurrentElement();
}
break;
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2013, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef AMAZONCOVERPROVIDER_H
#define AMAZONCOVERPROVIDER_H
#include "config.h"
#include <QXmlStreamReader>
#include <QNetworkReply>
#include "coverprovider.h"
class QNetworkAccessManager;
class AmazonCoverProvider : public CoverProvider {
Q_OBJECT
public:
explicit AmazonCoverProvider(QObject *parent = nullptr);
static const char *kUrl;
static const char *kAssociateTag;
static const char *kAccessKeyB64;
static const char *kSecretAccessKeyB64;
bool StartSearch(const QString &artist, const QString &album, int id);
private slots:
void QueryError(QNetworkReply::NetworkError error, QNetworkReply *reply, int id);
void QueryFinished(QNetworkReply *reply, int id);
private:
void ReadItem(QXmlStreamReader *reader, CoverSearchResults *results);
void ReadLargeImage(QXmlStreamReader *reader, CoverSearchResults *results);
private:
QNetworkAccessManager *network_;
};
#endif // AMAZONCOVERPROVIDER_H

View File

@@ -0,0 +1,203 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QFile>
#include <QUrl>
#include "albumcoverexporter.h"
#include "core/song.h"
#include "core/tagreaderclient.h"
#include "coverexportrunnable.h"
CoverExportRunnable::CoverExportRunnable(const AlbumCoverExport::DialogResult &dialog_result, const Song &song)
: dialog_result_(dialog_result), song_(song) {}
void CoverExportRunnable::run() {
QString cover_path = GetCoverPath();
// manually unset?
if (cover_path.isEmpty()) {
EmitCoverSkipped();
}
else {
if (dialog_result_.RequiresCoverProcessing())
ProcessAndExportCover();
else
ExportCover();
}
}
QString CoverExportRunnable::GetCoverPath() {
if (song_.has_manually_unset_cover()) {
return QString();
// Export downloaded covers?
}
else if (!song_.art_manual().isEmpty() && dialog_result_.export_downloaded_) {
return song_.art_manual();
// Export embedded covers?
}
else if (!song_.art_automatic().isEmpty() && song_.art_automatic() == Song::kEmbeddedCover && dialog_result_.export_embedded_) {
return song_.art_automatic();
}
else {
return QString();
}
}
// Exports a single album cover using a "save QImage to file" approach.
// For performance reasons this method will be invoked only if loading
// and in memory processing of images is necessary for current settings
// which means that:
// - either the force size flag is being used
// - or the "overwrite smaller" mode is used
// In all other cases, the faster ExportCover() method will be used.
void CoverExportRunnable::ProcessAndExportCover() {
QString cover_path = GetCoverPath();
// either embedded or disk - the one we'll export for the current album
QImage cover;
QImage embedded_cover;
QImage disk_cover;
// load embedded cover if any
if (song_.has_embedded_cover()) {
embedded_cover = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(song_.url().toLocalFile());
if (embedded_cover.isNull()) {
EmitCoverSkipped();
return;
}
cover = embedded_cover;
}
// load a file cover which iss mandatory if there's no embedded cover
disk_cover.load(cover_path);
if (embedded_cover.isNull()) {
if (disk_cover.isNull()) {
EmitCoverSkipped();
return;
}
cover = disk_cover;
}
// rescale if necessary
if (dialog_result_.IsSizeForced()) {
cover = cover.scaled(QSize(dialog_result_.width_, dialog_result_.height_), Qt::IgnoreAspectRatio);
}
QString dir = song_.url().toLocalFile().section('/', 0, -2);
QString extension = cover_path.section('.', -1);
QString new_file = dir + '/' + dialog_result_.fileName_ + '.' + (cover_path == Song::kEmbeddedCover ? "jpg" : extension);
// If the file exists, do not override!
if (dialog_result_.overwrite_ == AlbumCoverExport::OverwriteMode_None && QFile::exists(new_file)) {
EmitCoverSkipped();
return;
}
// we're handling overwrite as remove + copy so we need to delete the old file
// first
if (QFile::exists(new_file) && dialog_result_.overwrite_ != AlbumCoverExport::OverwriteMode_None) {
// if the mode is "overwrite smaller" then skip the cover if a bigger one
// is already available in the folder
if (dialog_result_.overwrite_ == AlbumCoverExport::OverwriteMode_Smaller) {
QImage existing;
existing.load(new_file);
if (existing.isNull() || existing.size().height() >= cover.size().height() || existing.size().width() >= cover.size().width()) {
EmitCoverSkipped();
return;
}
}
if (!QFile::remove(new_file)) {
EmitCoverSkipped();
return;
}
}
if (cover.save(new_file))
EmitCoverExported();
else
EmitCoverSkipped();
}
// Exports a single album cover using a "copy file" approach.
void CoverExportRunnable::ExportCover() {
QString cover_path = GetCoverPath();
QString dir = song_.url().toLocalFile().section('/', 0, -2);
QString extension = cover_path.section('.', -1);
QString new_file = dir + '/' + dialog_result_.fileName_ + '.' + (cover_path == Song::kEmbeddedCover ? "jpg" : extension);
// If the file exists, do not override!
if (dialog_result_.overwrite_ == AlbumCoverExport::OverwriteMode_None && QFile::exists(new_file)) {
EmitCoverSkipped();
return;
}
// we're handling overwrite as remove + copy so we need to delete the old file
// first
if (dialog_result_.overwrite_ != AlbumCoverExport::OverwriteMode_None && QFile::exists(new_file)) {
if (!QFile::remove(new_file)) {
EmitCoverSkipped();
return;
}
}
if (cover_path == Song::kEmbeddedCover) {
// an embedded cover
QImage embedded = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(song_.url().toLocalFile());
if (!embedded.save(new_file)) {
EmitCoverSkipped();
return;
}
}
else {
// automatic or manual cover, available in an image file
if (!QFile::copy(cover_path, new_file)) {
EmitCoverSkipped();
return;
}
}
EmitCoverExported();
}
void CoverExportRunnable::EmitCoverExported() { emit CoverExported(); }
void CoverExportRunnable::EmitCoverSkipped() { emit CoverSkipped(); }

View File

@@ -0,0 +1,61 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COVEREXPORTRUNNABLE_H
#define COVEREXPORTRUNNABLE_H
#include "config.h"
#include <QObject>
#include <QRunnable>
#include "core/song.h"
#include "covermanager/albumcoverexport.h"
class AlbumCoverExporter;
class CoverExportRunnable : public QObject, public QRunnable {
Q_OBJECT
public:
CoverExportRunnable(const AlbumCoverExport::DialogResult &dialog_result, const Song &song);
virtual ~CoverExportRunnable() {}
void run();
signals:
void CoverExported();
void CoverSkipped();
private:
void EmitCoverExported();
void EmitCoverSkipped();
void ProcessAndExportCover();
void ExportCover();
QString GetCoverPath();
AlbumCoverExport::DialogResult dialog_result_;
Song song_;
AlbumCoverExporter* album_cover_exporter_;
};
#endif // COVEREXPORTRUNNABLE_H

View File

@@ -0,0 +1,91 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QApplication>
#include <QClipboard>
#include <QImage>
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include "coverfromurldialog.h"
#include "ui_coverfromurldialog.h"
#include "core/network.h"
#include "covermanager/albumcoverloader.h"
CoverFromURLDialog::CoverFromURLDialog(QWidget *parent) : QDialog(parent), ui_(new Ui_CoverFromURLDialog), network_(new NetworkAccessManager(this)) {
ui_->setupUi(this);
ui_->busy->hide();
}
CoverFromURLDialog::~CoverFromURLDialog() {
delete ui_;
}
QImage CoverFromURLDialog::Exec() {
// reset state
ui_->url->setText("");;
last_image_ = QImage();
QClipboard *clipboard = QApplication::clipboard();
ui_->url->setText(clipboard->text());
exec();
return last_image_;
}
void CoverFromURLDialog::accept() {
ui_->busy->show();
QNetworkRequest network_request = QNetworkRequest(QUrl::fromUserInput(ui_->url->text()));
QNetworkReply *reply = network_->get(network_request);
connect(reply, SIGNAL(finished()), SLOT(LoadCoverFromURLFinished()));
}
void CoverFromURLDialog::LoadCoverFromURLFinished() {
ui_->busy->hide();
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
QMessageBox::information(this, tr("Fetching cover error"), tr("The site you requested does not exist!"));
return;
}
QImage image;
image.loadFromData(reply->readAll());
if (!image.isNull()) {
last_image_ = image;
QDialog::accept();
}
else {
QMessageBox::information(this, tr("Fetching cover error"), tr("The site you requested is not an image!"));
}
}

View File

@@ -0,0 +1,57 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COVERFROMURLDIALOG_H
#define COVERFROMURLDIALOG_H
#include "config.h"
#include <QDialog>
#include <QImage>
class NetworkAccessManager;
class Song;
class Ui_CoverFromURLDialog;
// Controller for a dialog which fetches covers from the given URL.
class CoverFromURLDialog : public QDialog {
Q_OBJECT
public:
CoverFromURLDialog(QWidget *parent = nullptr);
~CoverFromURLDialog();
// Opens the dialog. This returns an image found at the URL chosen by user
// or null image if the dialog got rejected.
QImage Exec();
private slots:
void accept();
void LoadCoverFromURLFinished();
private:
Ui_CoverFromURLDialog *ui_;
NetworkAccessManager *network_;
QImage last_image_;
};
#endif // COVERFROMURLDIALOG_H

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CoverFromURLDialog</class>
<widget class="QDialog" name="CoverFromURLDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>407</width>
<height>126</height>
</rect>
</property>
<property name="windowTitle">
<string>Load cover from URL</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="urlLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Enter a URL to download a cover from the Internet:</string>
</property>
</widget>
</item>
<item>
<widget class="BusyIndicator" name="busy"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="url"/>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>3</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BusyIndicator</class>
<extends>QWidget</extends>
<header>widgets/busyindicator.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CoverFromURLDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CoverFromURLDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,26 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "coverprovider.h"
CoverProvider::CoverProvider(const QString &name, QObject *parent)
: QObject(parent), name_(name) {}

View File

@@ -0,0 +1,61 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COVERPROVIDER_H
#define COVERPROVIDER_H
#include "config.h"
#include <QObject>
#include "albumcoverfetcher.h"
#include "coverproviders.h"
#include <QObject>
class QNetworkReply;
// Each implementation of this interface downloads covers from one online
// service. There are no limitations on what this service might be - last.fm,
// Amazon, Google Images - you name it.
class CoverProvider : public QObject {
Q_OBJECT
public:
explicit CoverProvider(const QString& name, QObject* parent);
// A name (very short description) of this provider, like "last.fm".
QString name() const { return name_; }
// Starts searching for covers matching the given query text. Returns true
// if the query has been started, or false if an error occurred. The provider
// should remember the ID and emit it along with the result when it finishes.
virtual bool StartSearch(const QString &artist, const QString &album, int id) = 0;
virtual void CancelSearch(int id) {}
signals:
void SearchFinished(int id, const QList<CoverSearchResult>& results);
private:
QString name_;
};
#endif // COVERPROVIDER_H

View File

@@ -0,0 +1,72 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "coverprovider.h"
#include "coverproviders.h"
#include "core/logging.h"
CoverProviders::CoverProviders(QObject *parent) : QObject(parent) {}
void CoverProviders::AddProvider(CoverProvider *provider) {
{
QMutexLocker locker(&mutex_);
cover_providers_.insert(provider, provider->name());
connect(provider, SIGNAL(destroyed()), SLOT(ProviderDestroyed()));
}
qLog(Debug) << "Registered cover provider" << provider->name();
}
void CoverProviders::RemoveProvider(CoverProvider *provider) {
if (!provider) return;
// It's not safe to dereference provider at this pointbecause it might have
// already been destroyed.
QString name;
{
QMutexLocker locker(&mutex_);
name = cover_providers_.take(provider);
}
if (name.isNull()) {
qLog(Debug) << "Tried to remove a cover provider that was not registered";
}
else {
qLog(Debug) << "Unregistered cover provider" << name;
}
}
void CoverProviders::ProviderDestroyed() {
CoverProvider *provider = static_cast<CoverProvider*>(sender());
RemoveProvider(provider);
}
int CoverProviders::NextId() { return next_id_.fetchAndAddRelaxed(1); }

View File

@@ -0,0 +1,67 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COVERPROVIDERS_H
#define COVERPROVIDERS_H
#include "config.h"
#include <QMap>
#include <QMutex>
#include <QObject>
class AlbumCoverFetcherSearch;
class CoverProvider;
// This is a repository for cover providers.
// Providers are automatically unregistered from the repository when they are
// deleted. The class is thread safe.
class CoverProviders : public QObject {
Q_OBJECT
public:
explicit CoverProviders(QObject *parent = nullptr);
// Lets a cover provider register itself in the repository.
void AddProvider(CoverProvider *provider);
void RemoveProvider(CoverProvider *provider);
// Returns a list of cover providers
QList<CoverProvider*> List() const { return cover_providers_.keys(); }
// Returns true if this repository has at least one registered provider.
bool HasAnyProviders() const { return !cover_providers_.isEmpty(); }
int NextId();
private slots:
void ProviderDestroyed();
private:
Q_DISABLE_COPY(CoverProviders);
QMap<CoverProvider *, QString> cover_providers_;
QMutex mutex_;
QAtomicInt next_id_;
};
#endif // COVERPROVIDERS_H

View File

@@ -0,0 +1,64 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "coversearchstatistics.h"
CoverSearchStatistics::CoverSearchStatistics()
: network_requests_made_(0),
bytes_transferred_(0),
chosen_images_(0),
missing_images_(0),
chosen_width_(0),
chosen_height_(0) {}
CoverSearchStatistics &CoverSearchStatistics::operator +=(const CoverSearchStatistics &other) {
network_requests_made_ += other.network_requests_made_;
bytes_transferred_ += other.bytes_transferred_;
for (const QString& key : other.chosen_images_by_provider_.keys()) {
chosen_images_by_provider_[key] += other.chosen_images_by_provider_[key];
}
for (const QString& key : other.total_images_by_provider_.keys()) {
total_images_by_provider_[key] += other.total_images_by_provider_[key];
}
chosen_images_ += other.chosen_images_;
missing_images_ += other.missing_images_;
chosen_width_ += other.chosen_width_;
chosen_height_ += other.chosen_height_;
return *this;
}
QString CoverSearchStatistics::AverageDimensions() const {
if (chosen_images_ == 0) {
return "0x0";
}
return QString::number(chosen_width_ / chosen_images_) + "x" + QString::number(chosen_height_ / chosen_images_);
}

View File

@@ -0,0 +1,50 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COVERSEARCHSTATISTICS_H
#define COVERSEARCHSTATISTICS_H
#include "config.h"
#include <QMap>
#include <QString>
struct CoverSearchStatistics {
CoverSearchStatistics();
CoverSearchStatistics& operator +=(const CoverSearchStatistics &other);
quint64 network_requests_made_;
quint64 bytes_transferred_;
QMap<QString, quint64> total_images_by_provider_;
QMap<QString, quint64> chosen_images_by_provider_;
quint64 chosen_images_;
quint64 missing_images_;
quint64 chosen_width_;
quint64 chosen_height_;
QString AverageDimensions() const;
};
#endif // COVERSEARCHSTATISTICS_H

View File

@@ -0,0 +1,97 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "coversearchstatisticsdialog.h"
#include "core/utilities.h"
#include "ui_coversearchstatisticsdialog.h"
CoverSearchStatisticsDialog::CoverSearchStatisticsDialog(QWidget *parent)
: QDialog(parent), ui_(new Ui_CoverSearchStatisticsDialog) {
ui_->setupUi(this);
details_layout_ = new QVBoxLayout(ui_->details);
details_layout_->setSpacing(0);
setStyleSheet(
"#details {"
" background-color: palette(base);"
"}"
"#details QLabel[type=\"label\"] {"
" border: 2px solid transparent;"
" border-right: 2px solid palette(midlight);"
" margin-right: 10px;"
"}"
"#details QLabel[type=\"value\"] {"
" font-weight: bold;"
" max-width: 100px;"
"}");
}
CoverSearchStatisticsDialog::~CoverSearchStatisticsDialog() { delete ui_; }
void CoverSearchStatisticsDialog::Show(const CoverSearchStatistics &statistics) {
QStringList providers(statistics.total_images_by_provider_.keys());
qSort(providers);
ui_->summary->setText(tr("Got %1 covers out of %2 (%3 failed)")
.arg(statistics.chosen_images_)
.arg(statistics.chosen_images_ + statistics.missing_images_)
.arg(statistics.missing_images_));
for (const QString& provider : providers) {
AddLine(tr("Covers from %1").arg(provider), QString::number(statistics.chosen_images_by_provider_[provider]));
}
if (!providers.isEmpty()) {
AddSpacer();
}
AddLine(tr("Total network requests made"), QString::number(statistics.network_requests_made_));
AddLine(tr("Average image size"), statistics.AverageDimensions());
AddLine(tr("Total bytes transferred"), statistics.bytes_transferred_ ? Utilities::PrettySize(statistics.bytes_transferred_) : "0 bytes");
details_layout_->addStretch();
show();
}
void CoverSearchStatisticsDialog::AddLine(const QString &label, const QString &value) {
QLabel *label1 = new QLabel(label);
QLabel *label2 = new QLabel(value);
label1->setProperty("type", "label");
label2->setProperty("type", "value");
QHBoxLayout* layout = new QHBoxLayout;
layout->addWidget(label1);
layout->addWidget(label2);
details_layout_->addLayout(layout);
}
void CoverSearchStatisticsDialog::AddSpacer() {
details_layout_->addSpacing(20);
}

View File

@@ -0,0 +1,53 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COVERSEARCHSTATISTICSDIALOG_H
#define COVERSEARCHSTATISTICSDIALOG_H
#include "config.h"
#include <QDialog>
#include "coversearchstatistics.h"
class Ui_CoverSearchStatisticsDialog;
class QVBoxLayout;
class CoverSearchStatisticsDialog : public QDialog {
Q_OBJECT
public:
explicit CoverSearchStatisticsDialog(QWidget *parent = nullptr);
~CoverSearchStatisticsDialog();
void Show(const CoverSearchStatistics& statistics);
private:
void AddLine(const QString &label, const QString &value);
void AddSpacer();
private:
Ui_CoverSearchStatisticsDialog *ui_;
QVBoxLayout *details_layout_;
};
#endif // COVERSEARCHSTATISTICSDIALOG_H

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CoverSearchStatisticsDialog</class>
<widget class="QDialog" name="CoverSearchStatisticsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>425</width>
<height>214</height>
</rect>
</property>
<property name="windowTitle">
<string>Fetch completed</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="summary">
<property name="text">
<string>Got %1 covers out of %2 (%3 failed)</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="details">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CoverSearchStatisticsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CoverSearchStatisticsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,87 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QDir>
#include <QTemporaryFile>
#include <QUrl>
#include "currentartloader.h"
#include "core/application.h"
#include "core/iconloader.h"
#include "covermanager/albumcoverloader.h"
#include "playlist/playlistmanager.h"
CurrentArtLoader::CurrentArtLoader(Application *app, QObject *parent)
: QObject(parent),
app_(app),
temp_file_pattern_(QDir::tempPath() + "/strawberry-art-XXXXXX.jpg"),
id_(0)
{
options_.scale_output_image_ = false;
options_.pad_output_image_ = false;
//QIcon nocover = IconLoader::Load("nocover");
//options_.default_output_image_ = nocover.pixmap(nocover.availableSizes().last()).toImage();
options_.default_output_image_ = QImage(":/pictures/noalbumart.png");
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(TempArtLoaded(quint64, QImage)));
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(LoadArt(Song)));
}
CurrentArtLoader::~CurrentArtLoader() {}
void CurrentArtLoader::LoadArt(const Song &song) {
last_song_ = song;
id_ = app_->album_cover_loader()->LoadImageAsync(options_, last_song_);
}
void CurrentArtLoader::TempArtLoaded(quint64 id, const QImage &image) {
if (id != id_) return;
id_ = 0;
QString uri;
QString thumbnail_uri;
QImage thumbnail;
if (!image.isNull()) {
temp_art_.reset(new QTemporaryFile(temp_file_pattern_));
temp_art_->setAutoRemove(true);
temp_art_->open();
image.save(temp_art_->fileName(), "JPEG");
// Scale the image down to make a thumbnail. It's a bit crap doing it here
// since it's the GUI thread, but the alternative is hard.
temp_art_thumbnail_.reset(new QTemporaryFile(temp_file_pattern_));
temp_art_thumbnail_->open();
temp_art_thumbnail_->setAutoRemove(true);
thumbnail = image.scaledToHeight(120, Qt::SmoothTransformation);
thumbnail.save(temp_art_thumbnail_->fileName(), "JPEG");
uri = "file://" + temp_art_->fileName();
thumbnail_uri = "file://" + temp_art_thumbnail_->fileName();
}
emit ArtLoaded(last_song_, uri, image);
emit ThumbnailLoaded(last_song_, thumbnail_uri, thumbnail);
}

View File

@@ -0,0 +1,71 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef CURRENTARTLOADER_H
#define CURRENTARTLOADER_H
#include "config.h"
#include <memory>
#include <QObject>
#include "core/song.h"
#include "covermanager/albumcoverloaderoptions.h"
class Application;
class QImage;
class QTemporaryFile;
class CurrentArtLoader : public QObject {
Q_OBJECT
public:
explicit CurrentArtLoader(Application *app, QObject *parent = nullptr);
~CurrentArtLoader();
const AlbumCoverLoaderOptions &options() const { return options_; }
const Song &last_song() const { return last_song_; }
public slots:
void LoadArt(const Song &song);
signals:
void ArtLoaded(const Song &song, const QString &uri, const QImage &image);
void ThumbnailLoaded(const Song &song, const QString &uri, const QImage &image);
private slots:
void TempArtLoaded(quint64 id, const QImage &image);
private:
Application *app_;
AlbumCoverLoaderOptions options_;
QString temp_file_pattern_;
std::unique_ptr<QTemporaryFile> temp_art_;
std::unique_ptr<QTemporaryFile> temp_art_thumbnail_;
quint64 id_;
Song last_song_;
};
#endif // CURRENTARTLOADER_H

View File

@@ -0,0 +1,216 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2012, Martin Björklund <mbj4668@gmail.com>
* Copyright 2016, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
//#include <qjson/parser.h>
#include <QJson/Parser>
#include <QVariant>
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QStringList>
#include <QUrlQuery>
#include "discogscoverprovider.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/network.h"
#include "core/utilities.h"
const char *DiscogsCoverProvider::kUrl = "https://api.discogs.com/database/search";
const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
const char *DiscogsCoverProvider::kSecretAccessKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
DiscogsCoverProvider::DiscogsCoverProvider(QObject *parent) : CoverProvider("Discogs", parent), network_(new NetworkAccessManager(this)) {}
bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
//qLog(Debug) << __PRETTY_FUNCTION__;
DiscogsCoverSearchContext *ctx = new DiscogsCoverSearchContext;
ctx->id = id;
ctx->artist = artist;
ctx->album = album;
ctx->state = DiscogsCoverSearchContext::State_Init;
pending_requests_.insert(id, ctx);
SendSearchRequest(ctx);
return true;
}
void DiscogsCoverProvider::SendSearchRequest(DiscogsCoverSearchContext *ctx) {
//qLog(Debug) << __PRETTY_FUNCTION__;
typedef QPair<QString, QString> Arg;
typedef QList<Arg> ArgList;
typedef QPair<QByteArray, QByteArray> EncodedArg;
typedef QList<EncodedArg> EncodedArgList;
QString type;
switch (ctx->state) {
case DiscogsCoverSearchContext::State_Init:
type = "master";
ctx->state = DiscogsCoverSearchContext::State_MastersRequested;
break;
case DiscogsCoverSearchContext::State_MastersRequested:
type = "release";
ctx->state = DiscogsCoverSearchContext::State_ReleasesRequested;
break;
default:
EndSearch(ctx);
return;
}
ArgList args = ArgList()
<< Arg("key", QByteArray::fromBase64(kAccessKeyB64))
<< Arg("secret", QByteArray::fromBase64(kSecretAccessKeyB64));
if (!ctx->artist.isEmpty()) {
args.append(Arg("artist", ctx->artist.toLower()));
}
if (!ctx->album.isEmpty()) {
args.append(Arg("release_title", ctx->album.toLower()));
}
args.append(Arg("type", type));
QUrlQuery url_query;
QUrl url(kUrl);
QStringList query_items;
// Encode the arguments
for (const Arg &arg : args) {
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
}
// Sign the request
const QByteArray data_to_sign = QString("GET\n%1\n%2\n%3").arg(url.host(), url.path(), query_items.join("&")).toLatin1();
//const QByteArray signature(Utilities::HmacSha256(kSecretAccessKey, data_to_sign));
const QByteArray signature(Utilities::HmacSha256(QByteArray::fromBase64(kSecretAccessKeyB64), data_to_sign));
// Add the signature to the request
url_query.addQueryItem("Signature", QUrl::toPercentEncoding(signature.toBase64()));
url.setQuery(url_query);
QNetworkReply *reply = network_->get(QNetworkRequest(url));
NewClosure(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(QueryError(QNetworkReply::NetworkError, QNetworkReply*, int)), reply, ctx->id);
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, int)), reply, ctx->id);
return true;
}
void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, int id) {
//qLog(Debug) << __PRETTY_FUNCTION__;
//QString text(reply->readAll());
//qLog(Debug) << text;
reply->deleteLater();
DiscogsCoverSearchContext *ctx;
if (!pending_requests_.contains(id)) {
// the request was cancelled while we were waiting for the reply
qLog(Debug) << "Discogs: got reply for cancelled request" << id;
return;
}
ctx = pending_requests_.value(id);
QJson::Parser parser;
bool ok;
bool found = false;
QVariantMap reply_map = parser.parse(reply, &ok).toMap();
if (!ok || !reply_map.contains("results")) {
// this is an error; either parse error or bad response from the server
EndSearch(ctx);
return;
}
QVariantList results = reply_map["results"].toList();
for (const QVariant &result : results) {
QVariantMap result_map = result.toMap();
// In order to use less round-trips, we cheat here. Instead of
// following the "resource_url", and then scan all images in the
// resource, we go directly to the largest primary image by
// constructing the primary image's url from the thmub's url.
if (result_map.contains("thumb")) {
CoverSearchResult cover_result;
cover_result.image_url = QUrl(result_map["thumb"].toString().replace("R-90-", "R-"));
if (result_map.contains("title")) {
cover_result.description = result_map["title"].toString();
}
ctx->results.append(cover_result);
found = true;
}
}
if (found) {
EndSearch(ctx);
return;
}
// otherwise, no results
switch (ctx->state) {
case DiscogsCoverSearchContext::State_MastersRequested:
// search again, this time for releases
SendSearchRequest(ctx);
break;
default:
EndSearch(ctx);
break;
}
}
void DiscogsCoverProvider::CancelSearch(int id) {
//qLog(Debug) << __PRETTY_FUNCTION__ << id;
delete pending_requests_.take(id);
}
void DiscogsCoverProvider::QueryError(QNetworkReply::NetworkError error, QNetworkReply *reply, int id) {
//qLog(Debug) << __PRETTY_FUNCTION__;
}
void DiscogsCoverProvider::EndSearch(DiscogsCoverSearchContext *ctx) {
//qLog(Debug) << __PRETTY_FUNCTION__;
(void)pending_requests_.remove(ctx->id);
emit SearchFinished(ctx->id, ctx->results);
delete ctx;
}

View File

@@ -0,0 +1,90 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2012, Martin Björklund <mbj4668@gmail.com>
* Copyright 2016, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef DISCOGSCOVERPROVIDER_H
#define DISCOGSCOVERPROVIDER_H
#include "config.h"
#include <QXmlStreamReader>
#include <QNetworkReply>
#include "coverprovider.h"
class QNetworkAccessManager;
// This struct represents a single search-for-cover request. It identifies and describes the request.
struct DiscogsCoverSearchContext {
enum State { State_Init, State_MastersRequested, State_ReleasesRequested };
// the unique request identifier
int id;
// the search query
QString artist;
QString album;
State state;
CoverSearchResults results;
};
Q_DECLARE_METATYPE(DiscogsCoverSearchContext)
class DiscogsCoverProvider : public CoverProvider {
Q_OBJECT
public:
explicit DiscogsCoverProvider(QObject *parent = nullptr);
static const char *kUrl;
static const char *kRequestTokenURL;
static const char *kAuthorizeURL;
static const char *kAccessTokenURL;
static const char *kAccessKey;
static const char *kSecretAccessKey;
static const char *kAccessKeyB64;
static const char *kSecretAccessKeyB64;
bool StartSearch(const QString &artist, const QString &album, int id);
void CancelSearch(int id);
private slots:
void QueryError(QNetworkReply::NetworkError error, QNetworkReply *reply, int id);
void HandleSearchReply(QNetworkReply* reply, int id);
private:
QNetworkAccessManager *network_;
QHash<int, DiscogsCoverSearchContext*> pending_requests_;
void SendSearchRequest(DiscogsCoverSearchContext *ctx);
void ReadItem(QXmlStreamReader *reader, CoverSearchResults *results);
void ReadLargeImage(QXmlStreamReader *reader, CoverSearchResults *results);
void QueryFinished(QNetworkReply *reply, int id);
void EndSearch(DiscogsCoverSearchContext *ctx);
};
#endif // DISCOGSCOVERPROVIDER_H

View File

@@ -0,0 +1,121 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "lastfmcompat.h"
#include "core/logging.h"
namespace lastfm {
namespace compat {
#ifdef HAVE_LIBLASTFM1
XmlQuery EmptyXmlQuery() { return XmlQuery(); }
bool ParseQuery(const QByteArray &data, XmlQuery *query, bool *connection_problems) {
const bool ret = query->parse(data);
if (connection_problems) {
*connection_problems = !ret && query->parseError().enumValue() == lastfm::ws::MalformedResponse;
}
return ret;
}
bool ParseUserList(QNetworkReply *reply, QList<User> *users) {
lastfm::XmlQuery lfm;
if (!lfm.parse(reply->readAll())) {
return false;
}
*users = lastfm::UserList(lfm).users();
return true;
}
#else // HAVE_LIBLASTFM1
XmlQuery EmptyXmlQuery() {
QByteArray dummy;
return XmlQuery(dummy);
}
bool ParseQuery(const QByteArray &data, XmlQuery *query, bool *connection_problems) {
try {
*query = lastfm::XmlQuery(data);
#ifdef Q_OS_WIN32
if (lastfm::ws::last_parse_error != lastfm::ws::NoError) {
return false;
}
#endif // Q_OS_WIN32
}
catch (lastfm::ws::ParseError e) {
qLog(Error) << "Last.fm parse error: " << e.enumValue();
if (connection_problems) {
*connection_problems = e.enumValue() == lastfm::ws::MalformedResponse;
}
return false;
}
catch (std::runtime_error &e) {
qLog(Error) << __FUNCTION__ << e.what();
return false;
}
if (connection_problems) {
*connection_problems = false;
}
// Check for app errors.
if (QDomElement(*query).attribute("status") == "failed") {
return false;
}
return true;
}
bool ParseUserList(QNetworkReply *reply, QList<User> *users) {
try {
*users = lastfm::User::list(reply);
#ifdef Q_OS_WIN32
if (lastfm::ws::last_parse_error != lastfm::ws::NoError) {
return false;
}
#endif // Q_OS_WIN32
}
catch (std::runtime_error &e) {
qLog(Error) << __FUNCTION__ << e.what();
return false;
}
return true;
}
#endif // HAVE_LIBLASTFM1
}
}

View File

@@ -0,0 +1,54 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef LASTFMCOMPAT_H
#define LASTFMCOMPAT_H
#include "config.h"
#ifdef HAVE_LIBLASTFM1
#include <lastfm5/misc.h>
#include <lastfm5/User.h>
#include <lastfm5/ws.h>
#include <lastfm5/XmlQuery.h>
#else
#include <lastfm5/misc.h>
#include <lastfm5/User>
#include <lastfm5/ws.h>
#include <lastfm5/XmlQuery>
#endif
namespace lastfm {
namespace compat {
lastfm::XmlQuery EmptyXmlQuery();
bool ParseQuery(const QByteArray &data, lastfm::XmlQuery *query, bool *connection_problems = nullptr);
bool ParseUserList(QNetworkReply *reply, QList<lastfm::User> *users);
#ifdef HAVE_LIBLASTFM1
typedef lastfm::User AuthenticatedUser;
#else
typedef lastfm::AuthenticatedUser AuthenticatedUser;
#endif
}
}
#endif // LASTFMCOMPAT_H

View File

@@ -0,0 +1,91 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
/*
Application name Strawberry Music Player
API key 211990b4c96782c05d1536e7219eb56e
Shared secret 80fd738f49596e9709b1bf9319c444a8
Registered to jonaskvinge
*/
#include "config.h"
#include <QNetworkReply>
#include <QtDebug>
#include "lastfmcoverprovider.h"
#include "lastfmcompat.h"
#include "coverprovider.h"
#include "albumcoverfetcher.h"
#include "core/closure.h"
#include "core/network.h"
#include "core/logging.h"
const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e";
const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8";
LastFmCoverProvider::LastFmCoverProvider(QObject *parent) : CoverProvider("last.fm", parent), network_(new NetworkAccessManager(this)) {
lastfm::ws::ApiKey = kApiKey;
lastfm::ws::SharedSecret = kSecret;
lastfm::setNetworkAccessManager(network_);
}
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
//qLog(Debug) << "LastFmCoverProvider artist:" << artist << "album:" << album;
QMap<QString, QString> params;
params["method"] = "album.search";
params["album"] = album + " " + artist;
QNetworkReply* reply = lastfm::ws::post(params);
NewClosure(reply, SIGNAL(finished()), this, SLOT(QueryFinished(QNetworkReply*, int)), reply, id);
return true;
}
void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, int id) {
reply->deleteLater();
CoverSearchResults results;
lastfm::XmlQuery query(lastfm::compat::EmptyXmlQuery());
if (lastfm::compat::ParseQuery(reply->readAll(), &query)) {
// parse the list of search results
QList<lastfm::XmlQuery> elements = query["results"]["albummatches"].children("album");
for (const lastfm::XmlQuery& element : elements) {
CoverSearchResult result;
result.description = element["artist"].text() + " - " + element["name"].text();
result.image_url = QUrl(element["image size=extralarge"].text());
results << result;
}
}
else {
// Drop through and emit an empty list of results.
}
emit SearchFinished(id, results);
}

View File

@@ -0,0 +1,59 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef LASTFMCOVERPROVIDER_H
#define LASTFMCOVERPROVIDER_H
#include "config.h"
#include <QMap>
#include <QObject>
#include "albumcoverfetcher.h"
#include "coverprovider.h"
#include <QMap>
#include <QObject>
class QNetworkAccessManager;
class QNetworkReply;
// A built-in cover provider which fetches covers from last.fm.
class LastFmCoverProvider : public CoverProvider {
Q_OBJECT
public:
explicit LastFmCoverProvider(QObject *parent = nullptr);
bool StartSearch(const QString &artist, const QString &album, int id);
static const char *kApiKey;
static const char *kSecret;
private slots:
void QueryFinished(QNetworkReply *reply, int id);
private:
QNetworkAccessManager *network_;
QMap <QNetworkReply *, int> pending_queries_;
};
#endif // LASTFMCOVERPROVIDER_H

View File

@@ -0,0 +1,119 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <functional>
#include <QXmlStreamReader>
#include <QUrlQuery>
#include "core/closure.h"
#include "core/network.h"
#include "musicbrainzcoverprovider.h"
using std::mem_fun;
namespace {
static const char *kReleaseSearchUrl = "https://musicbrainz.org/ws/2/release/";
static const char *kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front";
} // namespace
MusicbrainzCoverProvider::MusicbrainzCoverProvider(QObject *parent): CoverProvider("MusicBrainz", parent), network_(new NetworkAccessManager(this)) {}
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
// Find release information.
QUrl url(kReleaseSearchUrl);
QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(album.trimmed().replace('"', "\\\"")).arg(artist.trimmed().replace('"', "\\\""));
QUrlQuery url_query;
url_query.addQueryItem("query", query);
url_query.addQueryItem("limit", "5");
url.setQuery(url_query);
QNetworkRequest request(url);
QNetworkReply *reply = network_->get(request);
NewClosure(reply, SIGNAL(finished()), this, SLOT(ReleaseSearchFinished(QNetworkReply*, int)), reply, id);
cover_names_[id] = QString("%1 - %2").arg(artist, album);
return true;
}
void MusicbrainzCoverProvider::ReleaseSearchFinished(QNetworkReply *reply, int id) {
reply->deleteLater();
QList<QString> releases;
QXmlStreamReader reader(reply);
while (!reader.atEnd()) {
QXmlStreamReader::TokenType type = reader.readNext();
if (type == QXmlStreamReader::StartElement && reader.name() == "release") {
QStringRef release_id = reader.attributes().value("id");
if (!release_id.isEmpty()) {
releases.append(release_id.toString());
}
}
}
for (const QString& release_id : releases) {
QUrl url(QString(kAlbumCoverUrl).arg(release_id));
QNetworkReply *reply = network_->head(QNetworkRequest(url));
image_checks_.insert(id, reply);
NewClosure(reply, SIGNAL(finished()), this, SLOT(ImageCheckFinished(int)), id);
}
}
void MusicbrainzCoverProvider::ImageCheckFinished(int id) {
QList<QNetworkReply*> replies = image_checks_.values(id);
int finished_count = std::count_if(replies.constBegin(), replies.constEnd(), mem_fun(&QNetworkReply::isFinished));
if (finished_count == replies.size()) {
QString cover_name = cover_names_.take(id);
QList<CoverSearchResult> results;
for (QNetworkReply* reply : replies) {
reply->deleteLater();
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() < 400) {
CoverSearchResult result;
result.description = cover_name;
result.image_url = reply->url();
results.append(result);
}
}
image_checks_.remove(id);
emit SearchFinished(id, results);
}
}
void MusicbrainzCoverProvider::CancelSearch(int id) {
QList<QNetworkReply*> replies = image_checks_.values(id);
for (QNetworkReply* reply : replies) {
reply->abort();
reply->deleteLater();
}
image_checks_.remove(id);
}

View File

@@ -0,0 +1,53 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef MUSICBRAINZCOVERPROVIDER_H
#define MUSICBRAINZCOVERPROVIDER_H
#include "config.h"
#include <QMultiMap>
#include "coverprovider.h"
class QNetworkAccessManager;
class QNetworkReply;
class MusicbrainzCoverProvider : public CoverProvider {
Q_OBJECT
public:
explicit MusicbrainzCoverProvider(QObject *parent = nullptr);
// CoverProvider
virtual bool StartSearch(const QString &artist, const QString &album, int id);
virtual void CancelSearch(int id);
private slots:
void ReleaseSearchFinished(QNetworkReply *reply, int id);
void ImageCheckFinished(int id);
private:
QNetworkAccessManager *network_;
QMultiMap<int, QNetworkReply *> image_checks_;
QMap<int, QString> cover_names_;
};
#endif // MUSICBRAINZCOVERPROVIDER_H