Improve album cover loader, lyrics search and streaming support

- Improve album cover loader
- Add album cover loader result struct
- Move album cover thumbnail scaling to album cover loader
- Make init art manual look for album cover images in song directory
- Make album cover search work for songs outside of collection and
  streams
- Make album cover search work based on artist + title if album is not
  present
- Update art manual in playlist for local files, devices and CDDA
- Make lyrics search work for streams
- Add stream dialog to menu
- Remove dead code in InternetSearchModel
- Simplify code in InternetSearchView
This commit is contained in:
Jonas Kvinge
2020-04-20 18:03:18 +02:00
parent ab2ffd9ac1
commit a2c0e4d4b1
77 changed files with 1057 additions and 584 deletions

View File

@@ -203,6 +203,7 @@
<file>icons/48x48/document-download.png</file> <file>icons/48x48/document-download.png</file>
<file>icons/48x48/document-new.png</file> <file>icons/48x48/document-new.png</file>
<file>icons/48x48/document-open-folder.png</file> <file>icons/48x48/document-open-folder.png</file>
<file>icons/48x48/document-open-remote.png</file>
<file>icons/48x48/document-open.png</file> <file>icons/48x48/document-open.png</file>
<file>icons/48x48/document-save.png</file> <file>icons/48x48/document-save.png</file>
<file>icons/48x48/document-search.png</file> <file>icons/48x48/document-search.png</file>
@@ -297,6 +298,7 @@
<file>icons/32x32/document-download.png</file> <file>icons/32x32/document-download.png</file>
<file>icons/32x32/document-new.png</file> <file>icons/32x32/document-new.png</file>
<file>icons/32x32/document-open-folder.png</file> <file>icons/32x32/document-open-folder.png</file>
<file>icons/32x32/document-open-remote.png</file>
<file>icons/32x32/document-open.png</file> <file>icons/32x32/document-open.png</file>
<file>icons/32x32/document-save.png</file> <file>icons/32x32/document-save.png</file>
<file>icons/32x32/document-search.png</file> <file>icons/32x32/document-search.png</file>
@@ -391,6 +393,7 @@
<file>icons/22x22/document-download.png</file> <file>icons/22x22/document-download.png</file>
<file>icons/22x22/document-new.png</file> <file>icons/22x22/document-new.png</file>
<file>icons/22x22/document-open-folder.png</file> <file>icons/22x22/document-open-folder.png</file>
<file>icons/22x22/document-open-remote.png</file>
<file>icons/22x22/document-open.png</file> <file>icons/22x22/document-open.png</file>
<file>icons/22x22/document-save.png</file> <file>icons/22x22/document-save.png</file>
<file>icons/22x22/document-search.png</file> <file>icons/22x22/document-search.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -254,6 +254,7 @@ set(SOURCES
dialogs/errordialog.cpp dialogs/errordialog.cpp
dialogs/edittagdialog.cpp dialogs/edittagdialog.cpp
dialogs/trackselectiondialog.cpp dialogs/trackselectiondialog.cpp
dialogs/addstreamdialog.cpp
widgets/autoexpandingtreeview.cpp widgets/autoexpandingtreeview.cpp
widgets/busyindicator.cpp widgets/busyindicator.cpp
@@ -439,6 +440,7 @@ set(HEADERS
dialogs/console.h dialogs/console.h
dialogs/edittagdialog.h dialogs/edittagdialog.h
dialogs/trackselectiondialog.h dialogs/trackselectiondialog.h
dialogs/addstreamdialog.h
widgets/autoexpandingtreeview.h widgets/autoexpandingtreeview.h
widgets/busyindicator.h widgets/busyindicator.h
@@ -537,6 +539,7 @@ set(UI
dialogs/console.ui dialogs/console.ui
dialogs/edittagdialog.ui dialogs/edittagdialog.ui
dialogs/trackselectiondialog.ui dialogs/trackselectiondialog.ui
dialogs/addstreamdialog.ui
widgets/trackslider.ui widgets/trackslider.ui
widgets/osdpretty.ui widgets/osdpretty.ui

View File

@@ -297,6 +297,7 @@ void CollectionFilterWidget::SetCollectionModel(CollectionModel *model) {
} }
void CollectionFilterWidget::GroupByClicked(QAction *action) { void CollectionFilterWidget::GroupByClicked(QAction *action) {
if (action->property("group_by").isNull()) { if (action->property("group_by").isNull()) {
group_by_dialog_->show(); group_by_dialog_->show();
return; return;
@@ -304,6 +305,7 @@ void CollectionFilterWidget::GroupByClicked(QAction *action) {
CollectionModel::Grouping g = action->property("group_by").value<CollectionModel::Grouping>(); CollectionModel::Grouping g = action->property("group_by").value<CollectionModel::Grouping>();
model_->SetGroupBy(g); model_->SetGroupBy(g);
} }
void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping &g) { void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping &g) {

View File

@@ -66,10 +66,9 @@
#include "playlist/playlistmanager.h" #include "playlist/playlistmanager.h"
#include "playlist/songmimedata.h" #include "playlist/songmimedata.h"
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderresult.h"
#include "settings/collectionsettingspage.h" #include "settings/collectionsettingspage.h"
using std::bind;
using std::sort;
using std::placeholders::_1; using std::placeholders::_1;
using std::placeholders::_2; using std::placeholders::_2;
@@ -116,7 +115,7 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
cover_loader_options_.scale_output_image_ = true; cover_loader_options_.scale_output_image_ = true;
if (app_) if (app_)
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(AlbumCoverLoaded(quint64, QUrl, QImage))); connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
QIcon nocover = IconLoader::Load("cdcase"); QIcon nocover = IconLoader::Load("cdcase");
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
@@ -613,9 +612,7 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
} }
void CollectionModel::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
Q_UNUSED(cover_url);
if (!pending_art_.contains(id)) return; if (!pending_art_.contains(id)) return;
@@ -628,26 +625,26 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url,
pending_cache_keys_.remove(cache_key); pending_cache_keys_.remove(cache_key);
// Insert this image in the cache. // Insert this image in the cache.
if (image.isNull()) { if (result.image_scaled.isNull()) {
// Set the no_cover image so we don't continually try to load art. // Set the no_cover image so we don't continually try to load art.
QPixmapCache::insert(cache_key, no_cover_icon_); QPixmapCache::insert(cache_key, no_cover_icon_);
} }
else { else {
QPixmap image_pixmap; QPixmap image_pixmap;
image_pixmap = QPixmap::fromImage(image); image_pixmap = QPixmap::fromImage(result.image_scaled);
QPixmapCache::insert(cache_key, image_pixmap); QPixmapCache::insert(cache_key, image_pixmap);
} }
// If we have a valid cover not already in the disk cache // If we have a valid cover not already in the disk cache
if (use_disk_cache_) { if (use_disk_cache_) {
std::unique_ptr<QIODevice> cached_img(sIconCache->data(QUrl(cache_key))); std::unique_ptr<QIODevice> cached_img(sIconCache->data(QUrl(cache_key)));
if (!cached_img && !image.isNull()) { if (!cached_img && !result.image_scaled.isNull()) {
QNetworkCacheMetaData item_metadata; QNetworkCacheMetaData item_metadata;
item_metadata.setSaveToDisk(true); item_metadata.setSaveToDisk(true);
item_metadata.setUrl(QUrl(cache_key)); item_metadata.setUrl(QUrl(cache_key));
QIODevice* cache = sIconCache->prepare(item_metadata); QIODevice* cache = sIconCache->prepare(item_metadata);
if (cache) { if (cache) {
image.save(cache, "XPM"); result.image_scaled.save(cache, "XPM");
sIconCache->insert(cache); sIconCache->insert(cache);
} }
} }

View File

@@ -44,6 +44,7 @@
#include "core/simpletreemodel.h" #include "core/simpletreemodel.h"
#include "core/song.h" #include "core/song.h"
#include "covermanager/albumcoverloader.h"
#include "collectionquery.h" #include "collectionquery.h"
#include "collectionitem.h" #include "collectionitem.h"
#include "sqlrow.h" #include "sqlrow.h"
@@ -210,7 +211,7 @@ signals:
// Called after ResetAsync // Called after ResetAsync
void ResetAsyncQueryFinished(QFuture<CollectionModel::QueryResult> future); void ResetAsyncQueryFinished(QFuture<CollectionModel::QueryResult> future);
void AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
private: private:
// Provides some optimisations for loading the list of items in the root. // Provides some optimisations for loading the list of items in the root.

View File

@@ -43,20 +43,33 @@ void CollectionPlaylistItem::Reload() {
} }
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) { bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
// Rows from the songs tables come first // Rows from the songs tables come first
song_.InitFromQuery(query, true); song_.InitFromQuery(query, true);
song_.set_source(Song::Source_Collection); song_.set_source(Song::Source_Collection);
return song_.is_valid(); return song_.is_valid();
} }
QVariant CollectionPlaylistItem::DatabaseValue(DatabaseColumn column) const { QVariant CollectionPlaylistItem::DatabaseValue(DatabaseColumn column) const {
switch (column) { switch (column) {
case Column_CollectionId: return song_.id(); case Column_CollectionId: return song_.id();
default: return PlaylistItem::DatabaseValue(column); default: return PlaylistItem::DatabaseValue(column);
} }
} }
Song CollectionPlaylistItem::Metadata() const { Song CollectionPlaylistItem::Metadata() const {
if (HasTemporaryMetadata()) return temp_metadata_; if (HasTemporaryMetadata()) return temp_metadata_;
return song_; return song_;
}
void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) {
song_.set_art_manual(cover_url);
temp_metadata_.set_art_manual(cover_url);
} }

View File

@@ -46,6 +46,8 @@ class CollectionPlaylistItem : public PlaylistItem {
bool IsLocalCollectionItem() const { return true; } bool IsLocalCollectionItem() const { return true; }
void SetArtManual(const QUrl &cover_url);
protected: protected:
QVariant DatabaseValue(DatabaseColumn column) const; QVariant DatabaseValue(DatabaseColumn column) const;
Song DatabaseSongMetadata() const { return Song(Song::Source_Collection); } Song DatabaseSongMetadata() const { return Song(Song::Source_Collection); }

View File

@@ -57,7 +57,8 @@ ContextAlbum::ContextAlbum(QWidget *parent) :
cover_loader_options_.desired_height_ = 600; cover_loader_options_.desired_height_ = 600;
cover_loader_options_.pad_output_image_ = true; cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.scale_output_image_ = true; cover_loader_options_.scale_output_image_ = true;
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_strawberry_)); QPair<QImage, QImage> images = AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_strawberry_);
pixmap_current_ = QPixmap::fromImage(images.first);
connect(timeline_fade_, SIGNAL(valueChanged(qreal)), SLOT(FadePreviousTrack(qreal))); connect(timeline_fade_, SIGNAL(valueChanged(qreal)), SLOT(FadePreviousTrack(qreal)));
timeline_fade_->setDirection(QTimeLine::Backward); // 1.0 -> 0.0 timeline_fade_->setDirection(QTimeLine::Backward); // 1.0 -> 0.0
@@ -89,7 +90,7 @@ void ContextAlbum::DrawImage(QPainter *p) {
if (width() != prev_width_) { if (width() != prev_width_) {
cover_loader_options_.desired_height_ = width() - kWidgetSpacing; cover_loader_options_.desired_height_ = width() - kWidgetSpacing;
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_)); pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_).first);
prev_width_ = width(); prev_width_ = width();
} }
@@ -118,7 +119,7 @@ void ContextAlbum::FadePreviousTrack(const qreal value) {
void ContextAlbum::ScaleCover() { void ContextAlbum::ScaleCover() {
cover_loader_options_.desired_height_ = width() - kWidgetSpacing; cover_loader_options_.desired_height_ = width() - kWidgetSpacing;
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_)); pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_).first);
prev_width_ = width(); prev_width_ = width();
update(); update();

View File

@@ -48,11 +48,11 @@
#include "playlist/playlistmanager.h" #include "playlist/playlistmanager.h"
#include "playlist/songmimedata.h" #include "playlist/songmimedata.h"
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "contextalbumsmodel.h" #include "contextalbumsmodel.h"
using std::bind;
using std::sort;
using std::placeholders::_1; using std::placeholders::_1;
using std::placeholders::_2; using std::placeholders::_2;
@@ -71,7 +71,7 @@ ContextAlbumsModel::ContextAlbumsModel(CollectionBackend *backend, Application *
cover_loader_options_.pad_output_image_ = true; cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.scale_output_image_ = true; cover_loader_options_.scale_output_image_ = true;
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(AlbumCoverLoaded(quint64, QUrl, QImage))); connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
QIcon nocover = IconLoader::Load("cdcase"); QIcon nocover = IconLoader::Load("cdcase");
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
@@ -146,9 +146,7 @@ QVariant ContextAlbumsModel::AlbumIcon(const QModelIndex &index) {
} }
void ContextAlbumsModel::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { void ContextAlbumsModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
Q_UNUSED(cover_url);
if (!pending_art_.contains(id)) return; if (!pending_art_.contains(id)) return;
@@ -161,13 +159,13 @@ void ContextAlbumsModel::AlbumCoverLoaded(const quint64 id, const QUrl &cover_ur
pending_cache_keys_.remove(cache_key); pending_cache_keys_.remove(cache_key);
// Insert this image in the cache. // Insert this image in the cache.
if (image.isNull()) { if (result.image_scaled.isNull()) {
// Set the no_cover image so we don't continually try to load art. // Set the no_cover image so we don't continually try to load art.
QPixmapCache::insert(cache_key, no_cover_icon_); QPixmapCache::insert(cache_key, no_cover_icon_);
} }
else { else {
QPixmap image_pixmap; QPixmap image_pixmap;
image_pixmap = QPixmap::fromImage(image); image_pixmap = QPixmap::fromImage(result.image_scaled);
QPixmapCache::insert(cache_key, image_pixmap); QPixmapCache::insert(cache_key, image_pixmap);
} }

View File

@@ -45,6 +45,7 @@
#include "collection/collectionitem.h" #include "collection/collectionitem.h"
#include "collection/sqlrow.h" #include "collection/sqlrow.h"
#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
class QMimeData; class QMimeData;
@@ -99,7 +100,7 @@ class ContextAlbumsModel : public SimpleTreeModel<CollectionItem> {
void LazyPopulate(CollectionItem *item, bool signal); void LazyPopulate(CollectionItem *item, bool signal);
private slots: private slots:
void AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
private: private:
QueryResult RunQuery(CollectionItem *parent); QueryResult RunQuery(CollectionItem *parent);

View File

@@ -124,6 +124,7 @@ ContextView::ContextView(QWidget *parent) :
label_device_icon_(new QLabel(this)), label_device_icon_(new QLabel(this)),
label_engine_icon_(new QLabel(this)), label_engine_icon_(new QLabel(this)),
spacer_bottom_(new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Expanding)), spacer_bottom_(new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Expanding)),
lyrics_tried_(false),
lyrics_id_(-1), lyrics_id_(-1),
prev_width_(0) prev_width_(0)
{ {
@@ -365,19 +366,22 @@ void ContextView::Error() {}
void ContextView::SongChanged(const Song &song) { void ContextView::SongChanged(const Song &song) {
if (widget_stacked_->currentWidget() == widget_play_ && song_playing_.is_valid() && song == song_playing_) { if (widget_stacked_->currentWidget() == widget_play_ && song_playing_.is_valid() && song == song_playing_ && song.title() == song_playing_.title() && song.album() == song_playing_.album() && song.artist() == song_playing_.artist()) {
UpdateSong(song); UpdateSong(song);
} }
else { else {
song_prev_ = song_playing_; song_prev_ = song_playing_;
song_playing_ = song;
lyrics_ = song.lyrics(); lyrics_ = song.lyrics();
lyrics_id_ = -1; lyrics_id_ = -1;
song_playing_ = song; lyrics_tried_ = false;
SetSong(); SetSong();
if (lyrics_.isEmpty() && action_show_lyrics_->isChecked() && !song.artist().isEmpty() && !song.title().isEmpty()) { }
lyrics_fetcher_->Clear();
lyrics_id_ = lyrics_fetcher_->Search(song.effective_albumartist(), song.album(), song.title()); if (lyrics_.isEmpty() && action_show_lyrics_->isChecked() && !song.artist().isEmpty() && !song.title().isEmpty() && !lyrics_tried_ && lyrics_id_ == -1) {
} lyrics_fetcher_->Clear();
lyrics_tried_ = true;
lyrics_id_ = lyrics_fetcher_->Search(song.effective_albumartist(), song.album(), song.title());
} }
} }
@@ -684,9 +688,7 @@ void ContextView::dropEvent(QDropEvent *e) {
} }
void ContextView::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { void ContextView::AlbumCoverLoaded(const Song &song, const QImage &image) {
Q_UNUSED(cover_url);
if (song != song_playing_ || image == image_original_) return; if (song != song_playing_ || image == image_original_) return;

View File

@@ -91,7 +91,7 @@ class ContextView : public QWidget {
void Stopped(); void Stopped();
void Error(); void Error();
void SongChanged(const Song &song); void SongChanged(const Song &song);
void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const Song &song, const QImage &image);
void FadeStopFinished(); void FadeStopFinished();
void UpdateLyrics(const quint64 id, const QString &provider, const QString &lyrics); void UpdateLyrics(const quint64 id, const QString &provider, const QString &lyrics);
@@ -164,6 +164,7 @@ class ContextView : public QWidget {
Song song_playing_; Song song_playing_;
Song song_prev_; Song song_prev_;
QImage image_original_; QImage image_original_;
bool lyrics_tried_;
qint64 lyrics_id_; qint64 lyrics_id_;
QString lyrics_; QString lyrics_;
QString title_fmt_; QString title_fmt_;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2013, Jonas Kvinge <jonas@strawbs.net> * Copyright 2013-2020, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -96,6 +96,7 @@
#include "dialogs/console.h" #include "dialogs/console.h"
#include "dialogs/trackselectiondialog.h" #include "dialogs/trackselectiondialog.h"
#include "dialogs/edittagdialog.h" #include "dialogs/edittagdialog.h"
#include "dialogs/addstreamdialog.h"
#include "organise/organisedialog.h" #include "organise/organisedialog.h"
#include "widgets/fancytabwidget.h" #include "widgets/fancytabwidget.h"
#include "widgets/playingwidget.h" #include "widgets/playingwidget.h"
@@ -131,7 +132,7 @@
#endif #endif
#include "covermanager/albumcovermanager.h" #include "covermanager/albumcovermanager.h"
#include "covermanager/albumcoverchoicecontroller.h" #include "covermanager/albumcoverchoicecontroller.h"
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloaderresult.h"
#include "covermanager/currentalbumcoverloader.h" #include "covermanager/currentalbumcoverloader.h"
#ifndef Q_OS_WIN #ifndef Q_OS_WIN
# include "device/devicemanager.h" # include "device/devicemanager.h"
@@ -178,10 +179,6 @@
# include "windows7thumbbar.h" # include "windows7thumbbar.h"
#endif #endif
using std::bind;
using std::floor;
using std::stable_sort;
const char *MainWindow::kSettingsGroup = "MainWindow"; const char *MainWindow::kSettingsGroup = "MainWindow";
const char *MainWindow::kAllFilesFilterSpec = QT_TR_NOOP("All Files (*)"); const char *MainWindow::kAllFilesFilterSpec = QT_TR_NOOP("All Files (*)");
@@ -231,6 +228,11 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
TranscodeDialog *dialog = new TranscodeDialog(this); TranscodeDialog *dialog = new TranscodeDialog(this);
return dialog; return dialog;
}), }),
add_stream_dialog_([=]() {
AddStreamDialog *add_stream_dialog = new AddStreamDialog;
connect(add_stream_dialog, SIGNAL(accepted()), this, SLOT(AddStreamAccepted()));
return add_stream_dialog;
}),
#ifdef HAVE_SUBSONIC #ifdef HAVE_SUBSONIC
subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source_Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, this)), subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source_Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, this)),
#endif #endif
@@ -260,7 +262,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
// Initialise the UI // Initialise the UI
ui_->setupUi(this); ui_->setupUi(this);
connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, QUrl, QImage)), SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)));
album_cover_choice_controller_->Init(app); album_cover_choice_controller_->Init(app);
connect(album_cover_choice_controller_->cover_from_file_action(), SIGNAL(triggered()), this, SLOT(LoadCoverFromFile())); 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_to_file_action(), SIGNAL(triggered()), this, SLOT(SaveCoverToFile()));
@@ -361,6 +363,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
ui_->action_add_file->setIcon(IconLoader::Load("document-open")); ui_->action_add_file->setIcon(IconLoader::Load("document-open"));
ui_->action_add_folder->setIcon(IconLoader::Load("document-open-folder")); ui_->action_add_folder->setIcon(IconLoader::Load("document-open-folder"));
ui_->action_add_stream->setIcon(IconLoader::Load("document-open-remote"));
ui_->action_shuffle_mode->setIcon(IconLoader::Load("media-playlist-shuffle")); ui_->action_shuffle_mode->setIcon(IconLoader::Load("media-playlist-shuffle"));
ui_->action_repeat_mode->setIcon(IconLoader::Load("media-playlist-repeat")); ui_->action_repeat_mode->setIcon(IconLoader::Load("media-playlist-repeat"));
ui_->action_new_playlist->setIcon(IconLoader::Load("document-new")); ui_->action_new_playlist->setIcon(IconLoader::Load("document-new"));
@@ -431,6 +434,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(ui_->action_open_cd, SIGNAL(triggered()), SLOT(AddCDTracks())); connect(ui_->action_open_cd, SIGNAL(triggered()), SLOT(AddCDTracks()));
connect(ui_->action_add_file, SIGNAL(triggered()), SLOT(AddFile())); connect(ui_->action_add_file, SIGNAL(triggered()), SLOT(AddFile()));
connect(ui_->action_add_folder, SIGNAL(triggered()), SLOT(AddFolder())); connect(ui_->action_add_folder, SIGNAL(triggered()), SLOT(AddFolder()));
connect(ui_->action_add_stream, SIGNAL(triggered()), SLOT(AddStream()));
connect(ui_->action_cover_manager, SIGNAL(triggered()), SLOT(ShowCoverManager())); connect(ui_->action_cover_manager, SIGNAL(triggered()), SLOT(ShowCoverManager()));
connect(ui_->action_equalizer, SIGNAL(triggered()), equalizer_.get(), SLOT(show())); connect(ui_->action_equalizer, SIGNAL(triggered()), equalizer_.get(), SLOT(show()));
#if defined(HAVE_GSTREAMER) #if defined(HAVE_GSTREAMER)
@@ -705,7 +709,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(app_->player(), SIGNAL(Playing()), context_view_, SLOT(Playing())); connect(app_->player(), SIGNAL(Playing()), context_view_, SLOT(Playing()));
connect(app_->player(), SIGNAL(Stopped()), context_view_, SLOT(Stopped())); connect(app_->player(), SIGNAL(Stopped()), context_view_, SLOT(Stopped()));
connect(app_->player(), SIGNAL(Error()), context_view_, SLOT(Error())); connect(app_->player(), SIGNAL(Error()), context_view_, SLOT(Error()));
connect(this, SIGNAL(AlbumCoverReady(Song, QUrl, QImage)), context_view_, SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); connect(this, SIGNAL(AlbumCoverReady(Song, QImage)), context_view_, SLOT(AlbumCoverLoaded(Song, QImage)));
connect(this, SIGNAL(SearchCoverInProgress()), context_view_->album_widget(), SLOT(SearchCoverInProgress())); connect(this, SIGNAL(SearchCoverInProgress()), context_view_->album_widget(), SLOT(SearchCoverInProgress()));
connect(context_view_, SIGNAL(AlbumEnabledChanged()), SLOT(TabSwitched())); connect(context_view_, SIGNAL(AlbumEnabledChanged()), SLOT(TabSwitched()));
connect(context_view_->albums_widget(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); connect(context_view_->albums_widget(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
@@ -738,7 +742,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(app_->player(), SIGNAL(Stopped()), ui_->widget_playing, SLOT(Stopped())); connect(app_->player(), SIGNAL(Stopped()), ui_->widget_playing, SLOT(Stopped()));
connect(app_->player(), SIGNAL(Error()), ui_->widget_playing, SLOT(Error())); connect(app_->player(), SIGNAL(Error()), ui_->widget_playing, SLOT(Error()));
connect(ui_->widget_playing, SIGNAL(ShowAboveStatusBarChanged(bool)), SLOT(PlayingWidgetPositionChanged(bool))); connect(ui_->widget_playing, SIGNAL(ShowAboveStatusBarChanged(bool)), SLOT(PlayingWidgetPositionChanged(bool)));
connect(this, SIGNAL(AlbumCoverReady(Song, QUrl, QImage)), ui_->widget_playing, SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); connect(this, SIGNAL(AlbumCoverReady(Song, QImage)), ui_->widget_playing, SLOT(AlbumCoverLoaded(Song, QImage)));
connect(this, SIGNAL(SearchCoverInProgress()), ui_->widget_playing, SLOT(SearchCoverInProgress())); connect(this, SIGNAL(SearchCoverInProgress()), ui_->widget_playing, SLOT(SearchCoverInProgress()));
connect(ui_->action_console, SIGNAL(triggered()), SLOT(ShowConsole())); connect(ui_->action_console, SIGNAL(triggered()), SLOT(ShowConsole()));
@@ -1777,7 +1781,7 @@ void MainWindow::EditTracks() {
void MainWindow::EditTagDialogAccepted() { void MainWindow::EditTagDialogAccepted() {
for (const PlaylistItemPtr item : edit_tag_dialog_->playlist_items()) { for (PlaylistItemPtr item : edit_tag_dialog_->playlist_items()) {
item->Reload(); item->Reload();
} }
@@ -1923,6 +1927,16 @@ void MainWindow::AddCDTracks() {
} }
void MainWindow::AddStream() { add_stream_dialog_->show(); }
void MainWindow::AddStreamAccepted() {
MimeData* data = new MimeData;
data->setUrls(QList<QUrl>() << add_stream_dialog_->url());
AddToPlaylist(data);
}
void MainWindow::ShowInCollection() { void MainWindow::ShowInCollection() {
// Show the first valid selected track artist/album in CollectionView // Show the first valid selected track artist/album in CollectionView
@@ -2512,7 +2526,7 @@ void MainWindow::AutoCompleteTags() {
void MainWindow::AutoCompleteTagsAccepted() { void MainWindow::AutoCompleteTagsAccepted() {
for (const PlaylistItemPtr item : autocomplete_tag_items_) { for (PlaylistItemPtr item : autocomplete_tag_items_) {
item->Reload(); item->Reload();
} }
autocomplete_tag_items_.clear(); autocomplete_tag_items_.clear();
@@ -2600,14 +2614,14 @@ void MainWindow::SearchCoverAutomatically() {
} }
void MainWindow::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { void MainWindow::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
if (song.effective_albumartist() != song_playing_.effective_albumartist() || song.effective_album() != song_playing_.effective_album() || song.title() != song_playing_.title()) return; if (song != song_playing_) return;
song_ = song; song_ = song;
image_original_ = image; image_original_ = result.image_original;
emit AlbumCoverReady(song, cover_url, image); emit AlbumCoverReady(song, result.image_original);
GetCoverAutomatically(); GetCoverAutomatically();
@@ -2617,13 +2631,12 @@ void MainWindow::GetCoverAutomatically() {
// Search for cover automatically? // Search for cover automatically?
bool search = bool search =
song_.source() == Song::Source_Collection && album_cover_choice_controller_->search_cover_auto_action()->isChecked() &&
album_cover_choice_controller_->search_cover_auto_action()->isChecked() && !song_.has_manually_unset_cover() &&
!song_.has_manually_unset_cover() && !song_.art_automatic_is_valid() &&
!song_.art_automatic_is_valid() && !song_.art_manual_is_valid() &&
!song_.art_manual_is_valid() && !song_.effective_albumartist().isEmpty() &&
!song_.effective_albumartist().isEmpty() && !song_.effective_album().isEmpty();
!song_.effective_album().isEmpty();
if (search) { if (search) {
album_cover_choice_controller_->SearchCoverAutomatically(song_); album_cover_choice_controller_->SearchCoverAutomatically(song_);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2013, Jonas Kvinge <jonas@strawbs.net> * Copyright 2013-2020, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -59,8 +59,7 @@
#include "playlist/playlistitem.h" #include "playlist/playlistitem.h"
#include "settings/settingsdialog.h" #include "settings/settingsdialog.h"
#include "settings/behavioursettingspage.h" #include "settings/behavioursettingspage.h"
#include "covermanager/albumcoverloaderresult.h"
using std::unique_ptr;
class About; class About;
class AlbumCoverManager; class AlbumCoverManager;
@@ -95,6 +94,7 @@ class InternetTabsView;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
class Windows7ThumbBar; class Windows7ThumbBar;
#endif #endif
class AddStreamDialog;
class MainWindow : public QMainWindow, public PlatformInterface { class MainWindow : public QMainWindow, public PlatformInterface {
Q_OBJECT Q_OBJECT
@@ -126,7 +126,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
bool LoadUrl(const QString& url); bool LoadUrl(const QString& url);
signals: signals:
void AlbumCoverReady(const Song &song, const QUrl &cover_url, const QImage &image); void AlbumCoverReady(const Song &song, const QImage &image);
void SearchCoverInProgress(); void SearchCoverInProgress();
// Signals that stop playing after track was toggled. // Signals that stop playing after track was toggled.
void StopAfterToggled(bool stop); void StopAfterToggled(bool stop);
@@ -210,6 +210,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void AddFile(); void AddFile();
void AddFolder(); void AddFolder();
void AddCDTracks(); void AddCDTracks();
void AddStream();
void AddStreamAccepted();
void CommandlineOptionsReceived(const quint32 instanceId, const QByteArray &string_options); void CommandlineOptionsReceived(const quint32 instanceId, const QByteArray &string_options);
@@ -251,7 +253,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void UnsetCover(); void UnsetCover();
void ShowCover(); void ShowCover();
void SearchCoverAutomatically(); void SearchCoverAutomatically();
void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result);
void ScrobblingEnabledChanged(const bool value); void ScrobblingEnabledChanged(const bool value);
void ScrobbleButtonVisibilityChanged(const bool value); void ScrobbleButtonVisibilityChanged(const bool value);
@@ -308,6 +310,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
#ifdef HAVE_GSTREAMER #ifdef HAVE_GSTREAMER
Lazy<TranscodeDialog> transcode_dialog_; Lazy<TranscodeDialog> transcode_dialog_;
#endif #endif
Lazy<AddStreamDialog> add_stream_dialog_;
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
std::unique_ptr<TagFetcher> tag_fetcher_; std::unique_ptr<TagFetcher> tag_fetcher_;

View File

@@ -469,6 +469,7 @@
</property> </property>
<addaction name="action_add_file"/> <addaction name="action_add_file"/>
<addaction name="action_add_folder"/> <addaction name="action_add_folder"/>
<addaction name="action_add_stream"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_shuffle_mode"/> <addaction name="action_shuffle_mode"/>
<addaction name="action_repeat_mode"/> <addaction name="action_repeat_mode"/>
@@ -827,6 +828,11 @@
<string>Rescan songs(s)</string> <string>Rescan songs(s)</string>
</property> </property>
</action> </action>
<action name="action_add_stream">
<property name="text">
<string>Add stream...</string>
</property>
</action>
</widget> </widget>
<layoutdefault spacing="6" margin="11"/> <layoutdefault spacing="6" margin="11"/>
<customwidgets> <customwidgets>

View File

@@ -53,6 +53,7 @@
#include "collection/directory.h" #include "collection/directory.h"
#include "playlist/playlistitem.h" #include "playlist/playlistitem.h"
#include "playlist/playlistsequence.h" #include "playlist/playlistsequence.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/albumcoverfetcher.h" #include "covermanager/albumcoverfetcher.h"
#include "equalizer/equalizer.h" #include "equalizer/equalizer.h"
@@ -98,6 +99,8 @@ void RegisterMetaTypes() {
qRegisterMetaType<QList<PlaylistItemPtr> >("QList<PlaylistItemPtr>"); qRegisterMetaType<QList<PlaylistItemPtr> >("QList<PlaylistItemPtr>");
qRegisterMetaType<PlaylistSequence::RepeatMode>("PlaylistSequence::RepeatMode"); qRegisterMetaType<PlaylistSequence::RepeatMode>("PlaylistSequence::RepeatMode");
qRegisterMetaType<PlaylistSequence::ShuffleMode>("PlaylistSequence::ShuffleMode"); qRegisterMetaType<PlaylistSequence::ShuffleMode>("PlaylistSequence::ShuffleMode");
qRegisterMetaType<AlbumCoverLoaderResult>("AlbumCoverLoaderResult");
qRegisterMetaType<AlbumCoverLoaderResult::Type>("AlbumCoverLoaderResult::Type");
qRegisterMetaType<CoverSearchResult>("CoverSearchResult"); qRegisterMetaType<CoverSearchResult>("CoverSearchResult");
qRegisterMetaType<QList<CoverSearchResult> >("QList<CoverSearchResult>"); qRegisterMetaType<QList<CoverSearchResult> >("QList<CoverSearchResult>");
qRegisterMetaType<CoverSearchResults>("CoverSearchResults"); qRegisterMetaType<CoverSearchResults>("CoverSearchResults");

View File

@@ -58,14 +58,13 @@
#include "playlist/playlistmanager.h" #include "playlist/playlistmanager.h"
#include "playlist/playlistsequence.h" #include "playlist/playlistsequence.h"
#include "covermanager/currentalbumcoverloader.h" #include "covermanager/currentalbumcoverloader.h"
#include "covermanager/albumcoverloaderresult.h"
#include <core/mpris2_player.h> #include <core/mpris2_player.h>
#include <core/mpris2_playlists.h> #include <core/mpris2_playlists.h>
#include <core/mpris2_root.h> #include <core/mpris2_root.h>
#include <core/mpris2_tracklist.h> #include <core/mpris2_tracklist.h>
using std::reverse;
QDBusArgument &operator<<(QDBusArgument &arg, const MprisPlaylist &playlist) { QDBusArgument &operator<<(QDBusArgument &arg, const MprisPlaylist &playlist) {
arg.beginStructure(); arg.beginStructure();
arg << playlist.id << playlist.name << playlist.icon; arg << playlist.id << playlist.name << playlist.icon;
@@ -122,7 +121,7 @@ Mpris2::Mpris2(Application *app, QObject *parent)
return; return;
} }
connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, QUrl, QImage)), SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)));
connect(app_->player()->engine(), SIGNAL(StateChanged(Engine::State)), SLOT(EngineStateChanged(Engine::State))); connect(app_->player()->engine(), SIGNAL(StateChanged(Engine::State)), SLOT(EngineStateChanged(Engine::State)));
connect(app_->player(), SIGNAL(VolumeChanged(int)), SLOT(VolumeChanged())); connect(app_->player(), SIGNAL(VolumeChanged(int)), SLOT(VolumeChanged()));
@@ -378,7 +377,7 @@ QString Mpris2::current_track_id() const {
// We send Metadata change notification as soon as the process of changing song starts... // We send Metadata change notification as soon as the process of changing song starts...
void Mpris2::CurrentSongChanged(const Song &song) { void Mpris2::CurrentSongChanged(const Song &song) {
AlbumCoverLoaded(song, QUrl(), QImage()); AlbumCoverLoaded(song);
EmitNotification("CanPlay"); EmitNotification("CanPlay");
EmitNotification("CanPause"); EmitNotification("CanPause");
EmitNotification("CanGoNext", CanGoNext()); EmitNotification("CanGoNext", CanGoNext());
@@ -388,9 +387,7 @@ void Mpris2::CurrentSongChanged(const Song &song) {
} }
// ... and we add the cover information later, when it's available. // ... and we add the cover information later, when it's available.
void Mpris2::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { void Mpris2::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
Q_UNUSED(image);
last_metadata_ = QVariantMap(); last_metadata_ = QVariantMap();
song.ToXesam(&last_metadata_); song.ToXesam(&last_metadata_);
@@ -398,9 +395,14 @@ void Mpris2::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QIm
using mpris::AddMetadata; using mpris::AddMetadata;
AddMetadata("mpris:trackid", current_track_id(), &last_metadata_); AddMetadata("mpris:trackid", current_track_id(), &last_metadata_);
if (cover_url.isValid()) { QUrl cover_url;
AddMetadata("mpris:artUrl", cover_url.toString(), &last_metadata_); if (result.cover_url.isValid() && result.cover_url.isLocalFile()) {
cover_url = result.cover_url;
} }
else if (result.temp_cover_url.isValid() && result.temp_cover_url.isLocalFile()) {
cover_url = result.temp_cover_url;
}
if (cover_url.isValid()) AddMetadata("mpris:artUrl", result.cover_url.toString(), &last_metadata_);
AddMetadata("year", song.year(), &last_metadata_); AddMetadata("year", song.year(), &last_metadata_);
AddMetadata("bitrate", song.bitrate(), &last_metadata_); AddMetadata("bitrate", song.bitrate(), &last_metadata_);

View File

@@ -39,6 +39,7 @@
#include <QJsonObject> #include <QJsonObject>
#include "engine/engine_fwd.h" #include "engine/engine_fwd.h"
#include "covermanager/albumcoverloaderresult.h"
class Application; class Application;
class Song; class Song;
@@ -204,7 +205,7 @@ signals:
void PlaylistChanged(const MprisPlaylist &playlist); void PlaylistChanged(const MprisPlaylist &playlist);
private slots: private slots:
void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result = AlbumCoverLoaderResult());
void EngineStateChanged(Engine::State newState); void EngineStateChanged(Engine::State newState);
void VolumeChanged(); void VolumeChanged();

View File

@@ -30,6 +30,7 @@
#include <QObject> #include <QObject>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QDir>
#include <QSharedData> #include <QSharedData>
#include <QHash> #include <QHash>
#include <QByteArray> #include <QByteArray>
@@ -42,6 +43,7 @@
#include <QIcon> #include <QIcon>
#include <QTextCodec> #include <QTextCodec>
#include <QSqlQuery> #include <QSqlQuery>
#include <QStandardPaths>
#include <QtDebug> #include <QtDebug>
#ifdef HAVE_LIBGPOD #ifdef HAVE_LIBGPOD
@@ -66,7 +68,6 @@
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "tagreadermessages.pb.h" #include "tagreadermessages.pb.h"
using std::sort;
#ifndef USE_SYSTEM_TAGLIB #ifndef USE_SYSTEM_TAGLIB
using namespace Strawberry_TagLib; using namespace Strawberry_TagLib;
#endif #endif
@@ -353,7 +354,8 @@ bool Song::is_cdda() const { return d->source_ == Source_CDDA; }
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; } bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
bool Song::art_automatic_is_valid() const { bool Song::art_automatic_is_valid() const {
return ( return !d->art_automatic_.isEmpty() &&
(
(d->art_automatic_.path() == kManuallyUnsetCover) || (d->art_automatic_.path() == kManuallyUnsetCover) ||
(d->art_automatic_.path() == kEmbeddedCover) || (d->art_automatic_.path() == kEmbeddedCover) ||
(d->art_automatic_.isValid() && !d->art_automatic_.isLocalFile()) || (d->art_automatic_.isValid() && !d->art_automatic_.isLocalFile()) ||
@@ -363,7 +365,8 @@ bool Song::art_automatic_is_valid() const {
} }
bool Song::art_manual_is_valid() const { bool Song::art_manual_is_valid() const {
return ( return !d->art_manual_.isEmpty() &&
(
(d->art_manual_.path() == kManuallyUnsetCover) || (d->art_manual_.path() == kManuallyUnsetCover) ||
(d->art_manual_.path() == kEmbeddedCover) || (d->art_manual_.path() == kEmbeddedCover) ||
(d->art_manual_.isValid() && !d->art_manual_.isLocalFile()) || (d->art_manual_.isValid() && !d->art_manual_.isLocalFile()) ||
@@ -655,6 +658,29 @@ Song::FileType Song::FiletypeByExtension(const QString &ext) {
} }
QString Song::ImageCacheDir(const Song::Source source) {
switch (source) {
case Song::Source_Collection:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/collectionalbumcovers";
case Song::Source_Subsonic:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/subsonicalbumcovers";
case Song::Source_Tidal:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/tidalalbumcovers";
case Song::Source_Qobuz:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/qobuzalbumcovers";
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/albumcovers";
}
return QString();
}
int CompareSongsName(const Song &song1, const Song &song2) { int CompareSongsName(const Song &song1, const Song &song2) {
return song1.PrettyTitleWithArtist().localeAwareCompare(song2.PrettyTitleWithArtist()) < 0; return song1.PrettyTitleWithArtist().localeAwareCompare(song2.PrettyTitleWithArtist()) < 0;
} }
@@ -997,16 +1023,24 @@ void Song::InitFromFilePartial(const QString &filename) {
void Song::InitArtManual() { void Song::InitArtManual() {
QString album = d->album_; QString album = effective_album();
album.remove(Song::kAlbumRemoveDisc); album.remove(Song::kAlbumRemoveDisc);
// If we don't have an art, check if we have one in the cache // If we don't have an art, check if we have one in the cache
if (d->art_manual_.isEmpty() && d->art_automatic_.isEmpty()) { if (d->art_manual_.isEmpty() && d->art_automatic_.isEmpty() && !effective_albumartist().isEmpty() && !album.isEmpty()) {
QString filename(Utilities::Sha1CoverHash(effective_albumartist(), album).toHex() + ".jpg"); QString filename(Utilities::Sha1CoverHash(effective_albumartist(), album).toHex() + ".jpg");
QString path(AlbumCoverLoader::ImageCacheDir(d->source_) + "/" + filename); QString path(ImageCacheDir(d->source_) + "/" + filename);
if (QFile::exists(path)) { if (QFile::exists(path)) {
d->art_manual_ = QUrl::fromLocalFile(path); d->art_manual_ = QUrl::fromLocalFile(path);
} }
else if (d->url_.isLocalFile()) { // Pick the first image file in the album directory.
QFileInfo file(d->url_.toLocalFile());
QDir dir(file.path());
QStringList files = dir.entryList(QStringList() << "*.jpg" << "*.png" << "*.gif" << "*.jpeg", QDir::Files|QDir::Readable, QDir::Name);
if (files.count() > 0) {
d->art_manual_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
}
}
} }
} }

View File

@@ -147,6 +147,7 @@ class Song {
static FileType FiletypeByMimetype(const QString &mimetype); static FileType FiletypeByMimetype(const QString &mimetype);
static FileType FiletypeByDescription(const QString &text); static FileType FiletypeByDescription(const QString &text);
static FileType FiletypeByExtension(const QString &ext); static FileType FiletypeByExtension(const QString &ext);
static QString ImageCacheDir(const Song::Source source);
// Sort songs alphabetically using their pretty title // Sort songs alphabetically using their pretty title
static void SortSongsListAlphabetically(QList<Song> *songs); static void SortSongsListAlphabetically(QList<Song> *songs);

View File

@@ -40,7 +40,7 @@ StandardItemIconLoader::StandardItemIconLoader(AlbumCoverLoader *cover_loader, Q
cover_options_.desired_height_ = 16; cover_options_.desired_height_ = 16;
connect(cover_loader_, SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(ImageLoaded(quint64, QUrl, QImage))); connect(cover_loader_, SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
} }
void StandardItemIconLoader::SetModel(QAbstractItemModel *model) { void StandardItemIconLoader::SetModel(QAbstractItemModel *model) {
@@ -96,15 +96,13 @@ void StandardItemIconLoader::ModelReset() {
} }
void StandardItemIconLoader::ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { void StandardItemIconLoader::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult result) {
Q_UNUSED(cover_url);
QStandardItem *item = pending_covers_.take(id); QStandardItem *item = pending_covers_.take(id);
if (!item) return; if (!item) return;
if (!image.isNull()) { if (!result.image_scaled.isNull()) {
item->setIcon(QIcon(QPixmap::fromImage(image))); item->setIcon(QIcon(QPixmap::fromImage(result.image_scaled)));
} }
} }

View File

@@ -26,9 +26,11 @@
#include <QtGlobal> #include <QtGlobal>
#include <QObject> #include <QObject>
#include <QMap> #include <QMap>
#include <QUrl>
#include <QImage> #include <QImage>
#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
class QAbstractItemModel; class QAbstractItemModel;
class QStandardItem; class QStandardItem;
@@ -52,12 +54,12 @@ class StandardItemIconLoader : public QObject {
void LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item); void LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item);
void LoadIcon(const Song &song, QStandardItem *for_item); void LoadIcon(const Song &song, QStandardItem *for_item);
private slots: private slots:
void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult result);
void RowsAboutToBeRemoved(const QModelIndex &parent, int begin, int end); void RowsAboutToBeRemoved(const QModelIndex &parent, int begin, int end);
void ModelReset(); void ModelReset();
private: private:
AlbumCoverLoader *cover_loader_; AlbumCoverLoader *cover_loader_;
AlbumCoverLoaderOptions cover_options_; AlbumCoverLoaderOptions cover_options_;

View File

@@ -312,12 +312,14 @@ void AlbumCoverChoiceController::ShowCover(const Song &song, const QPixmap &pixm
} }
void AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) { qint64 AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) {
qint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(), song.effective_album(), true); qint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(), song.album(), song.title(), true);
cover_fetching_tasks_[id] = song; cover_fetching_tasks_[id] = song;
return id;
} }
void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) { void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) {
@@ -371,7 +373,7 @@ void AlbumCoverChoiceController::SaveCoverToSong(Song *song, const QUrl &cover_u
} }
if (song->url() == app_->current_albumcover_loader()->last_song().url()) { if (*song == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(*song); app_->current_albumcover_loader()->LoadAlbumCover(*song);
} }

View File

@@ -110,7 +110,7 @@ class AlbumCoverChoiceController : public QWidget {
void ShowCover(const Song &song, const QPixmap &pixmap); void ShowCover(const Song &song, const QPixmap &pixmap);
// Search for covers automatically // Search for covers automatically
void SearchCoverAutomatically(const Song &song); qint64 SearchCoverAutomatically(const Song &song);
// Saves the chosen cover as manual cover path of this song in collection. // Saves the chosen cover as manual cover path of this song in collection.
void SaveCoverToSong(Song *song, const QUrl &cover_url); void SaveCoverToSong(Song *song, const QUrl &cover_url);
@@ -124,7 +124,7 @@ class AlbumCoverChoiceController : public QWidget {
static bool CanAcceptDrag(const QDragEnterEvent *e); static bool CanAcceptDrag(const QDragEnterEvent *e);
signals: signals:
void AutomaticCoverSearchDone(); void AutomaticCoverSearchDone();
private slots: private slots:

View File

@@ -44,14 +44,15 @@ AlbumCoverFetcher::AlbumCoverFetcher(CoverProviders *cover_providers, QObject *p
connect(request_starter_, SIGNAL(timeout()), SLOT(StartRequests())); connect(request_starter_, SIGNAL(timeout()), SLOT(StartRequests()));
} }
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, bool fetchall) { quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, const QString &title, bool fetchall) {
CoverSearchRequest request; CoverSearchRequest request;
request.id = next_id_++; request.id = next_id_++;
request.artist = artist; request.artist = artist;
request.album = album; request.album = album;
request.album.remove(Song::kAlbumRemoveDisc); request.album = request.album.remove(Song::kAlbumRemoveDisc);
request.album.remove(Song::kAlbumRemoveMisc); request.album = request.album.remove(Song::kAlbumRemoveMisc);
request.title = title;
request.search = false; request.search = false;
request.fetchall = fetchall; request.fetchall = fetchall;
@@ -60,14 +61,15 @@ quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString
} }
quint64 AlbumCoverFetcher::SearchForCovers(const QString &artist, const QString &album) { quint64 AlbumCoverFetcher::SearchForCovers(const QString &artist, const QString &album, const QString &title) {
CoverSearchRequest request; CoverSearchRequest request;
request.id = next_id_++; request.id = next_id_++;
request.artist = artist; request.artist = artist;
request.album = album; request.album = album;
request.album.remove(Song::kAlbumRemoveDisc); request.album = request.album.remove(Song::kAlbumRemoveDisc);
request.album.remove(Song::kAlbumRemoveMisc); request.album = request.album.remove(Song::kAlbumRemoveMisc);
request.title = title;
request.search = true; request.search = true;
request.fetchall = false; request.fetchall = false;

View File

@@ -50,6 +50,7 @@ struct CoverSearchRequest {
// A search query // A search query
QString artist; QString artist;
QString album; QString album;
QString title;
// Is this only a search request or should we also fetch the first cover that's found? // Is this only a search request or should we also fetch the first cover that's found?
bool search; bool search;
@@ -92,12 +93,12 @@ class AlbumCoverFetcher : public QObject {
static const int kMaxConcurrentRequests; static const int kMaxConcurrentRequests;
quint64 SearchForCovers(const QString &artist, const QString &album); quint64 SearchForCovers(const QString &artist, const QString &album, const QString &title = QString());
quint64 FetchAlbumCover(const QString &artist, const QString &album, const bool fetchall); quint64 FetchAlbumCover(const QString &artist, const QString &album, const QString &title, const bool fetchall);
void Clear(); void Clear();
signals: signals:
void AlbumCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &cover, const CoverSearchStatistics &statistics); void AlbumCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &cover, const CoverSearchStatistics &statistics);
void SearchFinished(const quint64 request_id, const CoverSearchResults &results, const CoverSearchStatistics &statistics); void SearchFinished(const quint64 request_id, const CoverSearchResults &results, const CoverSearchStatistics &statistics);

View File

@@ -42,11 +42,6 @@
#include "coverprovider.h" #include "coverprovider.h"
#include "coverproviders.h" #include "coverproviders.h"
using std::min;
using std::max;
using std::stable_sort;
using std::sqrt;
const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 25000; const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 25000;
const int AlbumCoverFetcherSearch::kImageLoadTimeoutMs = 3000; const int AlbumCoverFetcherSearch::kImageLoadTimeoutMs = 3000;
const int AlbumCoverFetcherSearch::kTargetSize = 500; const int AlbumCoverFetcherSearch::kTargetSize = 500;
@@ -81,13 +76,16 @@ void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
// Skip provider if it does not have fetchall set, and we are doing fetchall - "Fetch Missing Covers". // Skip provider if it does not have fetchall set, and we are doing fetchall - "Fetch Missing Covers".
if (!provider->fetchall() && request_.fetchall) { if (!provider->fetchall() && request_.fetchall) {
//qLog(Debug) << "Skipping provider" << provider->name(); continue;
}
// If album is missing, check if we can still use this provider by searching using artist + title.
if (!provider->allow_missing_album() && request_.album.isEmpty()) {
continue; continue;
} }
connect(provider, SIGNAL(SearchFinished(int, CoverSearchResults)), SLOT(ProviderSearchFinished(int, CoverSearchResults))); connect(provider, SIGNAL(SearchFinished(int, CoverSearchResults)), SLOT(ProviderSearchFinished(int, CoverSearchResults)));
const int id = cover_providers->NextId(); const int id = cover_providers->NextId();
const bool success = provider->StartSearch(request_.artist, request_.album, id); const bool success = provider->StartSearch(request_.artist, request_.album, request_.title, id);
if (success) { if (success) {
pending_requests_[id] = provider; pending_requests_[id] = provider;
@@ -112,7 +110,7 @@ void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverSe
CoverProvider *provider = pending_requests_.take(id); CoverProvider *provider = pending_requests_.take(id);
CoverSearchResults results_copy(results); CoverSearchResults results_copy(results);
for (int i = 0; i < results_copy.count(); ++i) { for (int i = 0 ; i < results_copy.count() ; ++i) {
results_copy[i].provider = provider->name(); results_copy[i].provider = provider->name();
results_copy[i].score = provider->quality(); results_copy[i].score = provider->quality();
if (results_copy[i].artist.toLower() == request_.artist.toLower()) { if (results_copy[i].artist.toLower() == request_.artist.toLower()) {
@@ -170,7 +168,7 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
// Try the first one in each category. // Try the first one in each category.
QString last_provider; QString last_provider;
for (int i = 0; i < results_.count(); ++i) { for (int i = 0 ; i < results_.count() ; ++i) {
if (results_[i].provider == last_provider) { if (results_[i].provider == last_provider) {
continue; continue;
} }

View File

@@ -50,6 +50,7 @@
#include "organise/organiseformat.h" #include "organise/organiseformat.h"
#include "albumcoverloader.h" #include "albumcoverloader.h"
#include "albumcoverloaderoptions.h" #include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
AlbumCoverLoader::AlbumCoverLoader(QObject *parent) AlbumCoverLoader::AlbumCoverLoader(QObject *parent)
: QObject(parent), : QObject(parent),
@@ -98,89 +99,7 @@ void AlbumCoverLoader::ReloadSettings() {
} }
QString AlbumCoverLoader::ImageCacheDir(const Song::Source source) { QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album) {
switch (source) {
case Song::Source_LocalFile:
case Song::Source_Collection:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/albumcovers";
case Song::Source_Tidal:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/tidalalbumcovers";
case Song::Source_Qobuz:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/qobuzalbumcovers";
case Song::Source_Subsonic:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/subsonicalbumcovers";
}
return QString();
}
QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url) {
album.remove(Song::kAlbumRemoveDisc);
QString path;
if (source == Song::Source_Collection && cover_album_dir_ && !album_dir.isEmpty()) {
path = album_dir;
}
else {
path = AlbumCoverLoader::ImageCacheDir(source);
}
if (path.right(1) == QDir::separator()) {
path.chop(1);
}
QDir dir;
if (!dir.mkpath(path)) {
qLog(Error) << "Unable to create directory" << path;
return QString();
}
QString filename;
if (source == Song::Source_Collection && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) {
filename = CreateCoverFilename(artist, album) + ".jpg";
filename.remove(OrganiseFormat::kInvalidFatCharacters);
if (cover_lowercase_) filename = filename.toLower();
if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-");
}
else {
switch (source) {
case Song::Source_Tidal:
filename = album_id + "-" + cover_url.fileName();
break;
case Song::Source_Subsonic:
case Song::Source_Qobuz:
filename = AlbumCoverFileName(artist, album);
if (filename.length() > 8 && (filename.length() - 5) >= (artist.length() + album.length() - 2)) {
break;
}
// fallthrough
case Song::Source_Collection:
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
filename = Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg";
break;
}
}
if (filename.isEmpty()) return QString();
QString filepath(path + "/" + filename);
return filepath;
}
QString AlbumCoverLoader::AlbumCoverFileName(QString artist, QString album) {
artist.remove('/'); artist.remove('/');
album.remove('/'); album.remove('/');
@@ -196,7 +115,79 @@ QString AlbumCoverLoader::AlbumCoverFileName(QString artist, QString album) {
} }
QString AlbumCoverLoader::CreateCoverFilename(const QString &artist, const QString &album) { QString AlbumCoverLoader::CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url) {
return CoverFilePath(song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url);
}
QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url) {
album.remove(Song::kAlbumRemoveDisc);
QString path;
if (source == Song::Source_Collection && cover_album_dir_ && !album_dir.isEmpty()) {
path = album_dir;
}
else {
path = Song::ImageCacheDir(source);
}
if (path.right(1) == QDir::separator()) {
path.chop(1);
}
QDir dir;
if (!dir.mkpath(path)) {
qLog(Error) << "Unable to create directory" << path;
path = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
}
QString filename;
if (source == Song::Source_Collection && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) {
filename = CoverFilenameFromVariable(artist, album) + ".jpg";
filename.remove(OrganiseFormat::kInvalidFatCharacters);
if (cover_lowercase_) filename = filename.toLower();
if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-");
}
else {
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id);
}
QString filepath(path + "/" + filename);
return filepath;
}
QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id) {
QString filename;
switch (source) {
case Song::Source_Tidal:
filename = album_id + "-" + cover_url.fileName();
break;
case Song::Source_Subsonic:
case Song::Source_Qobuz:
filename = AlbumCoverFilename(artist, album);
if (filename.length() > 8 && (filename.length() - 5) >= (artist.length() + album.length() - 2)) {
break;
}
// fallthrough
case Song::Source_Collection:
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
filename = Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg";
break;
}
return filename;
}
QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, const QString &album) {
QString filename(cover_pattern_); QString filename(cover_pattern_);
filename.replace("%albumartist", artist); filename.replace("%albumartist", artist);
@@ -215,6 +206,7 @@ void AlbumCoverLoader::CancelTask(const quint64 id) {
break; break;
} }
} }
} }
void AlbumCoverLoader::CancelTasks(const QSet<quint64> &ids) { void AlbumCoverLoader::CancelTasks(const QSet<quint64> &ids) {
@@ -228,21 +220,25 @@ void AlbumCoverLoader::CancelTasks(const QSet<quint64> &ids) {
++it; ++it;
} }
} }
} }
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options, const Song &song) { quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options, const Song &song) {
return LoadImageAsync(options, song.art_automatic(), song.art_manual(), song.url().toLocalFile(), song.image()); return LoadImageAsync(options, song.art_automatic(), song.art_manual(), song.url(), song, song.image());
} }
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QString &song_filename, const QImage &embedded_image) { quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url, const Song song, const QImage &embedded_image) {
Task task; Task task;
task.options = options; task.options = options;
task.art_automatic = art_automatic; task.song = song;
task.song_url = song_url;
task.art_manual = art_manual; task.art_manual = art_manual;
task.song_filename = song_filename; task.art_automatic = art_automatic;
task.art_updated = false;
task.embedded_image = embedded_image; task.embedded_image = embedded_image;
task.state = State_TryingManual; task.type = AlbumCoverLoaderResult::Type_None;
task.state = State_Manual;
{ {
QMutexLocker l(&mutex_); QMutexLocker l(&mutex_);
@@ -269,20 +265,20 @@ void AlbumCoverLoader::ProcessTasks() {
ProcessTask(&task); ProcessTask(&task);
} }
} }
void AlbumCoverLoader::ProcessTask(Task *task) { void AlbumCoverLoader::ProcessTask(Task *task) {
TryLoadResult result = TryLoadImage(*task); TryLoadResult result = TryLoadImage(task);
if (result.started_async) { if (result.started_async) {
// The image is being loaded from a remote URL, we'll carry on later when it's done // The image is being loaded from a remote URL, we'll carry on later when it's done
return; return;
} }
if (result.loaded_success) { if (result.loaded_success) {
QImage scaled = ScaleAndPad(task->options, result.image); QPair<QImage, QImage> images = ScaleAndPad(task->options, result.image);
emit ImageLoaded(task->id, result.cover_url, scaled); emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(result.type, result.cover_url, result.image, images.first, images.second, task->art_updated));
emit ImageLoaded(task->id, result.cover_url, scaled, result.image);
return; return;
} }
@@ -292,64 +288,85 @@ void AlbumCoverLoader::ProcessTask(Task *task) {
void AlbumCoverLoader::NextState(Task *task) { void AlbumCoverLoader::NextState(Task *task) {
if (task->state == State_TryingManual) { if (task->state == State_Manual) {
// Try the automatic one next // Try the automatic one next
task->state = State_TryingAuto; task->state = State_Automatic;
ProcessTask(task); ProcessTask(task);
} }
else { else {
// Give up // Give up
emit ImageLoaded(task->id, QUrl(), task->options.default_output_image_); emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(AlbumCoverLoaderResult::Type_None, QUrl(), task->options.default_output_image_, task->options.default_output_image_, task->options.default_thumbnail_image_, task->art_updated));
emit ImageLoaded(task->id, QUrl(), task->options.default_output_image_, task->options.default_output_image_);
} }
} }
AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(const Task &task) { AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
// An image embedded in the song itself takes priority // An image embedded in the song itself takes priority
if (!task.embedded_image.isNull()) if (!task->embedded_image.isNull()) {
return TryLoadResult(false, true, QUrl(), ScaleAndPad(task.options, task.embedded_image)); QPair<QImage, QImage> images = ScaleAndPad(task->options, task->embedded_image);
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, QUrl(), images.first);
}
// Use cached album cover if possible.
if (task->state == State_Manual &&
!task->song.art_manual_is_valid() &&
task->art_manual.isEmpty() &&
task->song.source() != Song::Source_Collection &&
!task->options.scale_output_image_ &&
!task->options.pad_output_image_) {
task->song.InitArtManual();
if (task->art_manual != task->song.art_manual()) {
task->art_manual = task->song.art_manual();
task->art_updated = true;
}
}
AlbumCoverLoaderResult::Type type(AlbumCoverLoaderResult::Type_None);
QUrl cover_url; QUrl cover_url;
switch (task->state) {
switch (task.state) { case State_None:
case State_TryingAuto: cover_url = task.art_automatic; break; case State_Automatic:
case State_TryingManual: cover_url = task.art_manual; break; type = AlbumCoverLoaderResult::Type_Automatic;
cover_url = task->art_automatic;
break;
case State_Manual:
type = AlbumCoverLoaderResult::Type_Manual;
cover_url = task->art_manual;
break;
} }
task->type = type;
if (cover_url.path() == Song::kManuallyUnsetCover) if (!cover_url.isEmpty() && !cover_url.path().isEmpty()) {
return TryLoadResult(false, true, QUrl(), task.options.default_output_image_); if (cover_url.path() == Song::kManuallyUnsetCover) {
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_ManuallyUnset, QUrl(), task->options.default_output_image_);
else if (cover_url.path() == Song::kEmbeddedCover && !task.song_filename.isEmpty()) { }
const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task.song_filename); else if (cover_url.path() == Song::kEmbeddedCover && task->song_url.isLocalFile()) {
const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task->song_url.toLocalFile());
if (!taglib_image.isNull()) if (!taglib_image.isNull()) {
return TryLoadResult(false, true, QUrl(), ScaleAndPad(task.options, taglib_image)); return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, QUrl(), ScaleAndPad(task->options, taglib_image).first);
} }
}
if (cover_url.path().isEmpty()) { else if (cover_url.isLocalFile()) {
return TryLoadResult(false, false, cover_url, task.options.default_output_image_);
}
else {
if (cover_url.isLocalFile()) {
QImage image(cover_url.toLocalFile()); QImage image(cover_url.toLocalFile());
return TryLoadResult(false, !image.isNull(), cover_url, image.isNull() ? task.options.default_output_image_ : image); return TryLoadResult(false, !image.isNull(), type, cover_url, image.isNull() ? task->options.default_output_image_ : image);
} }
else if (cover_url.scheme().isEmpty()) { // Assume a local file with no scheme. else if (cover_url.scheme().isEmpty()) { // Assume a local file with no scheme.
QImage image(cover_url.path()); QImage image(cover_url.path());
return TryLoadResult(false, !image.isNull(), cover_url, image.isNull() ? task.options.default_output_image_ : image); return TryLoadResult(false, !image.isNull(), type, cover_url, image.isNull() ? task->options.default_output_image_ : image);
} }
else if (network_->supportedSchemes().contains(cover_url.scheme())) { // Remote URL else if (network_->supportedSchemes().contains(cover_url.scheme())) { // Remote URL
QNetworkReply *reply = network_->get(QNetworkRequest(cover_url)); QNetworkRequest request(cover_url);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
QNetworkReply *reply = network_->get(request);
NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, QUrl)), reply, cover_url); NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, QUrl)), reply, cover_url);
remote_tasks_.insert(reply, task); remote_tasks_.insert(reply, *task);
return TryLoadResult(true, false, cover_url, QImage()); return TryLoadResult(true, false, type, cover_url, QImage());
} }
} }
return TryLoadResult(false, false, cover_url, task.options.default_output_image_); return TryLoadResult(false, false, AlbumCoverLoaderResult::Type_None, cover_url, task->options.default_output_image_);
} }
@@ -367,6 +384,7 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cov
return; // Give up. return; // Give up.
} }
QNetworkRequest request = reply->request(); QNetworkRequest request = reply->request();
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
request.setUrl(redirect.toUrl()); request.setUrl(redirect.toUrl());
QNetworkReply *redirected_reply = network_->get(request); QNetworkReply *redirected_reply = network_->get(request);
NewClosure(redirected_reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, QUrl)), redirected_reply, redirect.toUrl()); NewClosure(redirected_reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, QUrl)), redirected_reply, redirect.toUrl());
@@ -379,41 +397,67 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cov
// Try to load the image // Try to load the image
QImage image; QImage image;
if (image.load(reply, 0)) { if (image.load(reply, 0)) {
QImage scaled = ScaleAndPad(task.options, image); QPair<QImage, QImage> images = ScaleAndPad(task.options, image);
emit ImageLoaded(task.id, cover_url, scaled); emit AlbumCoverLoaded(task.id, AlbumCoverLoaderResult(task.type, cover_url, image, images.first, images.second, task.art_updated));
emit ImageLoaded(task.id, cover_url, scaled, image);
return; return;
} }
else {
qLog(Error) << "Unable to load album cover image" << cover_url;
}
}
else {
qLog(Error) << "Unable to get album cover" << cover_url << reply->error() << reply->errorString();
} }
NextState(&task); NextState(&task);
} }
QImage AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image) { QPair<QImage, QImage> AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image) {
if (image.isNull()) return image; if (image.isNull()) return qMakePair(image, image);
// Scale the image down // Scale the image down
QImage copy; QImage image_scaled;
if (options.scale_output_image_) { if (options.scale_output_image_) {
copy = image.scaled(QSize(options.desired_height_, options.desired_height_), Qt::KeepAspectRatio, Qt::SmoothTransformation); image_scaled = image.scaled(QSize(options.desired_height_, options.desired_height_), Qt::KeepAspectRatio, Qt::SmoothTransformation);
} }
else { else {
copy = image; image_scaled = image;
} }
if (!options.pad_output_image_) return copy; // Pad the image to height x height
if (options.pad_output_image_) {
QImage image_padded(options.desired_height_, options.desired_height_, QImage::Format_ARGB32);
image_padded.fill(0);
// Pad the image to height_ x height_ QPainter p(&image_padded);
QImage padded_image(options.desired_height_, options.desired_height_, QImage::Format_ARGB32); p.drawImage((options.desired_height_ - image_scaled.width()) / 2, (options.desired_height_ - image_scaled.height()) / 2, image_scaled);
padded_image.fill(0); p.end();
QPainter p(&padded_image); image_scaled = image_padded;
p.drawImage((options.desired_height_ - copy.width()) / 2, (options.desired_height_ - copy.height()) / 2, copy); }
p.end();
return padded_image; // Create thumbnail
QImage image_thumbnail;
if (options.create_thumbnail_) {
if (options.pad_thumbnail_image_) {
image_thumbnail = image.scaled(options.thumbnail_size_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage image_padded(options.thumbnail_size_, QImage::Format_ARGB32_Premultiplied);
image_padded.fill(0);
QPainter p(&image_padded);
p.drawImage((image_padded.width() - image_thumbnail.width()) / 2, (image_padded.height() - image_thumbnail.height()) / 2, image_thumbnail);
p.end();
image_thumbnail = image_padded;
}
else {
image_thumbnail = image.scaledToHeight(options.thumbnail_size_.height(), Qt::SmoothTransformation);
}
}
return qMakePair(image_scaled, image_thumbnail);
} }

View File

@@ -27,6 +27,7 @@
#include <QtGlobal> #include <QtGlobal>
#include <QObject> #include <QObject>
#include <QMutex> #include <QMutex>
#include <QPair>
#include <QSet> #include <QSet>
#include <QMap> #include <QMap>
#include <QQueue> #include <QQueue>
@@ -37,6 +38,7 @@
#include "core/song.h" #include "core/song.h"
#include "settings/collectionsettingspage.h" #include "settings/collectionsettingspage.h"
#include "albumcoverloaderoptions.h" #include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
class QThread; class QThread;
class QNetworkReply; class QNetworkReply;
@@ -48,29 +50,36 @@ class AlbumCoverLoader : public QObject {
public: public:
explicit AlbumCoverLoader(QObject *parent = nullptr); explicit AlbumCoverLoader(QObject *parent = nullptr);
enum State {
State_None,
State_Manual,
State_Automatic,
};
void ReloadSettings(); void ReloadSettings();
void ExitAsync(); void ExitAsync();
void Stop() { stop_requested_ = true; } void Stop() { stop_requested_ = true; }
static QString ImageCacheDir(const Song::Source source); static QString AlbumCoverFilename(QString artist, QString album);
QString CreateCoverFilename(const QString &artist, const QString &album);
QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id);
QString CoverFilenameFromVariable(const QString &artist, const QString &album);
QString CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url);
QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url); QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url);
QString AlbumCoverFileName(QString artist, QString album);
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song); quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song);
virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QString &song_filename = QString(), const QImage &embedded_image = QImage()); virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url = QUrl(), const Song song = Song(), const QImage &embedded_image = QImage());
void CancelTask(const quint64 id); void CancelTask(const quint64 id);
void CancelTasks(const QSet<quint64> &ids); void CancelTasks(const QSet<quint64> &ids);
static QPixmap TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QUrl &url = QUrl()); static QPixmap TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QUrl &url = QUrl());
static QImage ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image); static QPair<QImage, QImage> ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image);
signals: signals:
void ExitFinished(); void ExitFinished();
void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(quint64 id, AlbumCoverLoaderResult result);
void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &scaled, const QImage &original);
protected slots: protected slots:
void Exit(); void Exit();
@@ -78,38 +87,38 @@ class AlbumCoverLoader : public QObject {
void RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url); void RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url);
protected: protected:
enum State {
State_TryingManual,
State_TryingAuto,
};
struct Task { struct Task {
explicit Task() : redirects(0) {} explicit Task() : id(0), state(State_None), type(AlbumCoverLoaderResult::Type_None), art_updated(false), redirects(0) {}
AlbumCoverLoaderOptions options; AlbumCoverLoaderOptions options;
quint64 id; quint64 id;
QUrl art_automatic;
QUrl art_manual; QUrl art_manual;
QString song_filename; QUrl art_automatic;
QUrl song_url;
Song song;
QImage embedded_image; QImage embedded_image;
State state; State state;
AlbumCoverLoaderResult::Type type;
bool art_updated;
int redirects; int redirects;
}; };
struct TryLoadResult { struct TryLoadResult {
explicit TryLoadResult(bool async, bool success, const QUrl &_cover_url, const QImage &_image) : started_async(async), loaded_success(success), cover_url(_cover_url), image(_image) {} explicit TryLoadResult(const bool _started_async = false, const bool _loaded_success = false, const AlbumCoverLoaderResult::Type _type = AlbumCoverLoaderResult::Type_None, const QUrl &_cover_url = QUrl(), const QImage &_image = QImage()) : started_async(_started_async), loaded_success(_loaded_success), type(_type), cover_url(_cover_url), image(_image) {}
bool started_async; bool started_async;
bool loaded_success; bool loaded_success;
AlbumCoverLoaderResult::Type type;
QUrl cover_url; QUrl cover_url;
QImage image; QImage image;
}; };
void ProcessTask(Task *task); void ProcessTask(Task *task);
void NextState(Task *task); void NextState(Task *task);
TryLoadResult TryLoadImage(const Task &task); TryLoadResult TryLoadImage(Task *task);
bool stop_requested_; bool stop_requested_;

View File

@@ -24,17 +24,24 @@
#include "config.h" #include "config.h"
#include <QImage> #include <QImage>
#include <QSize>
struct AlbumCoverLoaderOptions { struct AlbumCoverLoaderOptions {
explicit AlbumCoverLoaderOptions() explicit AlbumCoverLoaderOptions()
: desired_height_(120), : desired_height_(120),
scale_output_image_(true), scale_output_image_(true),
pad_output_image_(true) {} pad_output_image_(true),
create_thumbnail_(false),
pad_thumbnail_image_(false) {}
int desired_height_; int desired_height_;
QSize thumbnail_size_;
bool scale_output_image_; bool scale_output_image_;
bool pad_output_image_; bool pad_output_image_;
bool create_thumbnail_;
bool pad_thumbnail_image_;
QImage default_output_image_; QImage default_output_image_;
QImage default_thumbnail_image_;
}; };
#endif // ALBUMCOVERLOADEROPTIONS_H #endif // ALBUMCOVERLOADEROPTIONS_H

View File

@@ -0,0 +1,52 @@
/*
* Strawberry Music Player
* Copyright 2020, 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 ALBUMCOVERLOADERRESULT_H
#define ALBUMCOVERLOADERRESULT_H
#include "config.h"
#include <QImage>
#include <QUrl>
struct AlbumCoverLoaderResult {
enum Type {
Type_None,
Type_ManuallyUnset,
Type_Embedded,
Type_Automatic,
Type_Manual,
Type_Remote,
};
explicit AlbumCoverLoaderResult(const Type _type = Type_None, const QUrl &_cover_url = QUrl(), const QImage &_image_original = QImage(), const QImage &_image_scaled = QImage(), const QImage &_image_thumbnail = QImage(), const bool _updated = false) : type(_type), cover_url(_cover_url), image_original(_image_original), image_scaled(_image_scaled), image_thumbnail(_image_thumbnail), updated(_updated) {}
Type type;
QUrl cover_url;
QImage image_original;
QImage image_scaled;
QImage image_thumbnail;
bool updated;
QUrl temp_cover_url;
};
#endif // ALBUMCOVERLOADERRESULT_H

View File

@@ -77,6 +77,8 @@
#include "albumcoverexporter.h" #include "albumcoverexporter.h"
#include "albumcoverfetcher.h" #include "albumcoverfetcher.h"
#include "albumcoverloader.h" #include "albumcoverloader.h"
#include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "albumcovermanagerlist.h" #include "albumcovermanagerlist.h"
#include "coversearchstatistics.h" #include "coversearchstatistics.h"
#include "coversearchstatisticsdialog.h" #include "coversearchstatisticsdialog.h"
@@ -216,7 +218,7 @@ void AlbumCoverManager::Init() {
ui_->splitter->setSizes(QList<int>() << 200 << width() - 200); ui_->splitter->setSizes(QList<int>() << 200 << width() - 200);
} }
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(CoverImageLoaded(quint64, QUrl, QImage))); connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
cover_searcher_->Init(cover_fetcher_); cover_searcher_->Init(cover_fetcher_);
@@ -392,7 +394,7 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
} }
if (!info.art_automatic.isEmpty() || !info.art_manual.isEmpty()) { 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()); quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, info.art_automatic, info.art_manual, info.first_url);
item->setData(Role_PathAutomatic, info.art_automatic); item->setData(Role_PathAutomatic, info.art_automatic);
item->setData(Role_PathManual, info.art_manual); item->setData(Role_PathManual, info.art_manual);
cover_loading_tasks_[id] = item; cover_loading_tasks_[id] = item;
@@ -403,17 +405,15 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
} }
void AlbumCoverManager::CoverImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { void AlbumCoverManager::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
Q_UNUSED(cover_url);
if (!cover_loading_tasks_.contains(id)) return; if (!cover_loading_tasks_.contains(id)) return;
QListWidgetItem *item = cover_loading_tasks_.take(id); QListWidgetItem *item = cover_loading_tasks_.take(id);
if (image.isNull()) return; if (result.image_scaled.isNull()) return;
item->setIcon(QPixmap::fromImage(image)); item->setIcon(QPixmap::fromImage(result.image_scaled));
UpdateFilter(); UpdateFilter();
} }
@@ -488,7 +488,7 @@ void AlbumCoverManager::FetchAlbumCovers() {
if (item->isHidden()) continue; if (item->isHidden()) continue;
if (ItemHasCover(*item)) continue; if (ItemHasCover(*item)) continue;
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), true); quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), QString(), true);
cover_fetching_tasks_[id] = item; cover_fetching_tasks_[id] = item;
jobs_++; jobs_++;
} }
@@ -623,7 +623,7 @@ void AlbumCoverManager::ShowCover() {
void AlbumCoverManager::FetchSingleCover() { void AlbumCoverManager::FetchSingleCover() {
for (QListWidgetItem *item : context_menu_items_) { for (QListWidgetItem *item : context_menu_items_) {
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), false); quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), QString(), false);
cover_fetching_tasks_[id] = item; cover_fetching_tasks_[id] = item;
jobs_++; jobs_++;
} }

View File

@@ -36,6 +36,7 @@
#include "core/song.h" #include "core/song.h"
#include "albumcoverloaderoptions.h" #include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "coversearchstatistics.h" #include "coversearchstatistics.h"
class QWidget; class QWidget;
@@ -132,7 +133,7 @@ class AlbumCoverManager : public QMainWindow {
private slots: private slots:
void ArtistChanged(QListWidgetItem *current); void ArtistChanged(QListWidgetItem *current);
void CoverImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
void UpdateFilter(); void UpdateFilter();
void FetchAlbumCovers(); void FetchAlbumCovers();
void ExportCovers(); void ExportCovers();

View File

@@ -47,6 +47,7 @@
#include "core/application.h" #include "core/application.h"
#include "core/utilities.h" #include "core/utilities.h"
#include "core/logging.h"
#include "widgets/busyindicator.h" #include "widgets/busyindicator.h"
#include "widgets/forcescrollperpixel.h" #include "widgets/forcescrollperpixel.h"
#include "widgets/groupediconview.h" #include "widgets/groupediconview.h"
@@ -55,6 +56,7 @@
#include "albumcoverfetcher.h" #include "albumcoverfetcher.h"
#include "albumcoverloader.h" #include "albumcoverloader.h"
#include "albumcoverloaderoptions.h" #include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "ui_albumcoversearcher.h" #include "ui_albumcoversearcher.h"
const int SizeOverlayDelegate::kMargin = 4; const int SizeOverlayDelegate::kMargin = 4;
@@ -129,8 +131,11 @@ AlbumCoverSearcher::AlbumCoverSearcher(const QIcon &no_cover_icon, Application *
options_.scale_output_image_ = false; options_.scale_output_image_ = false;
options_.pad_output_image_ = false; options_.pad_output_image_ = false;
options_.create_thumbnail_ = true;
options_.pad_thumbnail_image_ = true;
options_.thumbnail_size_ = ui_->covers->iconSize();
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(ImageLoaded(quint64, QUrl, QImage))); connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
connect(ui_->search, SIGNAL(clicked()), SLOT(Search())); connect(ui_->search, SIGNAL(clicked()), SLOT(Search()));
connect(ui_->covers, SIGNAL(doubleClicked(QModelIndex)), SLOT(CoverDoubleClicked(QModelIndex))); connect(ui_->covers, SIGNAL(doubleClicked(QModelIndex)), SLOT(CoverDoubleClicked(QModelIndex)));
@@ -235,37 +240,25 @@ void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverSearchResul
} }
void AlbumCoverSearcher::ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) { void AlbumCoverSearcher::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
Q_UNUSED(cover_url);
if (!cover_loading_tasks_.contains(id)) return; if (!cover_loading_tasks_.contains(id)) return;
QStandardItem *item = cover_loading_tasks_.take(id); QStandardItem *item = cover_loading_tasks_.take(id);
if (cover_loading_tasks_.isEmpty()) ui_->busy->hide(); if (cover_loading_tasks_.isEmpty()) ui_->busy->hide();
if (image.isNull()) { if (result.image_original.isNull()) {
model_->removeRow(item->row()); model_->removeRow(item->row());
return; return;
} }
QIcon icon(QPixmap::fromImage(image)); QIcon icon;
icon.addPixmap(QPixmap::fromImage(result.image_original));
// Create a pixmap that's padded and exactly the right size for the icon. icon.addPixmap(QPixmap::fromImage(result.image_thumbnail));
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(true, Role_ImageFetchFinished);
item->setData(image.width() * image.height(), Role_ImageDimensions); item->setData(result.image_original.width() * result.image_original.height(), Role_ImageDimensions);
item->setData(image.size(), Role_ImageSize); item->setData(result.image_original.size(), Role_ImageSize);
item->setIcon(icon); item->setIcon(icon);
} }

View File

@@ -36,6 +36,7 @@
#include "albumcoverfetcher.h" #include "albumcoverfetcher.h"
#include "albumcoverloaderoptions.h" #include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
class QWidget; class QWidget;
class QStandardItem; class QStandardItem;
@@ -88,7 +89,7 @@ class AlbumCoverSearcher : public QDialog {
private slots: private slots:
void Search(); void Search();
void SearchFinished(const quint64 id, const CoverSearchResults &results); void SearchFinished(const quint64 id, const CoverSearchResults &results);
void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
void CoverDoubleClicked(const QModelIndex &index); void CoverDoubleClicked(const QModelIndex &index);

View File

@@ -26,5 +26,5 @@
#include "core/application.h" #include "core/application.h"
#include "coverprovider.h" #include "coverprovider.h"
CoverProvider::CoverProvider(const QString &name, const float &quality, const bool &fetchall, Application *app, QObject *parent) CoverProvider::CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent)
: QObject(parent), app_(app), name_(name), quality_(quality), fetchall_(fetchall) {} : QObject(parent), app_(app), name_(name), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}

View File

@@ -37,17 +37,18 @@ class CoverProvider : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit CoverProvider(const QString &name, const float &quality, const bool &fetchall, Application *app, QObject *parent); explicit CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
// A name (very short description) of this provider, like "last.fm". // A name (very short description) of this provider, like "last.fm".
QString name() const { return name_; } QString name() const { return name_; }
bool quality() const { return quality_; } bool quality() const { return quality_; }
bool fetchall() const { return fetchall_; } bool fetchall() const { return fetchall_; }
bool allow_missing_album() const { return allow_missing_album_; }
// Starts searching for covers matching the given query text. // Starts searching for covers matching the given query text.
// Returns true if the query has been started, or false if an error occurred. // 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. // 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 bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) = 0;
virtual void CancelSearch(int id) { Q_UNUSED(id); } virtual void CancelSearch(int id) { Q_UNUSED(id); }
@@ -59,6 +60,7 @@ class CoverProvider : public QObject {
QString name_; QString name_;
float quality_; float quality_;
bool fetchall_; bool fetchall_;
bool allow_missing_album_;
}; };

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019-2020, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -32,6 +33,7 @@
#include "playlist/playlistmanager.h" #include "playlist/playlistmanager.h"
#include "albumcoverloader.h" #include "albumcoverloader.h"
#include "albumcoverloaderoptions.h" #include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "currentalbumcoverloader.h" #include "currentalbumcoverloader.h"
CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *parent) CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *parent)
@@ -43,56 +45,73 @@ CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *pare
options_.scale_output_image_ = false; options_.scale_output_image_ = false;
options_.pad_output_image_ = false; options_.pad_output_image_ = false;
options_.create_thumbnail_ = true;
options_.thumbnail_size_ = QSize(120, 120);
options_.default_output_image_ = QImage(":/pictures/cdcase.png"); options_.default_output_image_ = QImage(":/pictures/cdcase.png");
options_.default_thumbnail_image_ = options_.default_output_image_.scaledToHeight(120, Qt::SmoothTransformation);
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(TempAlbumCoverLoaded(quint64, QUrl, QImage))); connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(TempAlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(LoadAlbumCover(Song))); connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(LoadAlbumCover(Song)));
} }
CurrentAlbumCoverLoader::~CurrentAlbumCoverLoader() { CurrentAlbumCoverLoader::~CurrentAlbumCoverLoader() {
if (temp_cover_) temp_cover_->remove(); if (temp_cover_) temp_cover_->remove();
if (temp_cover_thumbnail_) temp_cover_thumbnail_->remove(); if (temp_cover_thumbnail_) temp_cover_thumbnail_->remove();
} }
void CurrentAlbumCoverLoader::LoadAlbumCover(const Song &song) { void CurrentAlbumCoverLoader::LoadAlbumCover(const Song &song) {
last_song_ = song; last_song_ = song;
id_ = app_->album_cover_loader()->LoadImageAsync(options_, last_song_); id_ = app_->album_cover_loader()->LoadImageAsync(options_, last_song_);
} }
void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, const QUrl &remote_url, const QImage &image) { void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, AlbumCoverLoaderResult result) {
Q_UNUSED(remote_url);
if (id != id_) return; if (id != id_) return;
id_ = 0; id_ = 0;
QUrl cover_url; if (!result.image_scaled.isNull()) {
QUrl thumbnail_url;
QImage thumbnail;
if (!image.isNull()) {
QString filename;
temp_cover_.reset(new QTemporaryFile(temp_file_pattern_)); temp_cover_.reset(new QTemporaryFile(temp_file_pattern_));
temp_cover_->setAutoRemove(true); temp_cover_->setAutoRemove(true);
temp_cover_->open(); if (temp_cover_->open()) {
if (result.image_scaled.save(temp_cover_->fileName(), "JPEG")) {
image.save(temp_cover_->fileName(), "JPEG"); result.temp_cover_url = QUrl::fromLocalFile(temp_cover_->fileName());
}
// 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. else {
temp_cover_thumbnail_.reset(new QTemporaryFile(temp_file_pattern_)); qLog(Error) << "Unable to save cover image to" << temp_cover_->fileName();
temp_cover_thumbnail_->open(); }
temp_cover_thumbnail_->setAutoRemove(true); }
thumbnail = image.scaledToHeight(120, Qt::SmoothTransformation); else {
thumbnail.save(temp_cover_thumbnail_->fileName(), "JPEG"); qLog(Error) << "Unable to open" << temp_cover_->fileName();
}
cover_url = QUrl::fromLocalFile(temp_cover_->fileName());
thumbnail_url = QUrl::fromLocalFile(temp_cover_thumbnail_->fileName());
} }
emit AlbumCoverLoaded(last_song_, cover_url, image); QUrl thumbnail_url;
emit ThumbnailLoaded(last_song_, thumbnail_url, thumbnail); if (!result.image_thumbnail.isNull()) {
temp_cover_thumbnail_.reset(new QTemporaryFile(temp_file_pattern_));
temp_cover_thumbnail_->setAutoRemove(true);
if (temp_cover_thumbnail_->open()) {
if (result.image_thumbnail.save(temp_cover_thumbnail_->fileName(), "JPEG")) {
thumbnail_url = QUrl::fromLocalFile(temp_cover_thumbnail_->fileName());
}
else {
qLog(Error) << "Unable to save cover thumbnail image to" << temp_cover_thumbnail_->fileName();
}
}
else {
qLog(Error) << "Unable to open" << temp_cover_thumbnail_->fileName();
}
}
if (result.updated) {
last_song_.set_art_manual(result.cover_url);
}
emit AlbumCoverLoaded(last_song_, result);
emit ThumbnailLoaded(last_song_, thumbnail_url, result.image_thumbnail);
} }

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019-2020, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -33,6 +34,7 @@
#include "core/song.h" #include "core/song.h"
#include "albumcoverloaderoptions.h" #include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
class Application; class Application;
@@ -50,11 +52,11 @@ class CurrentAlbumCoverLoader : public QObject {
void LoadAlbumCover(const Song &song); void LoadAlbumCover(const Song &song);
signals: signals:
void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(Song song, AlbumCoverLoaderResult result);
void ThumbnailLoaded(const Song &song, const QUrl &thumbnail_uri, const QImage &image); void ThumbnailLoaded(Song song, QUrl thumbnail_uri, QImage image);
private slots: private slots:
void TempAlbumCoverLoaded(const quint64 id, const QUrl &remote_url, const QImage &image); void TempAlbumCoverLoaded(const quint64 id, AlbumCoverLoaderResult result);
private: private:
Application *app_; Application *app_;

View File

@@ -50,16 +50,27 @@
const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com"; const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com";
const int DeezerCoverProvider::kLimit = 10; const int DeezerCoverProvider::kLimit = 10;
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", 2.0, true, app, parent), network_(new NetworkAccessManager(this)) {} DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) { bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
typedef QPair<QString, QString> Param; typedef QPair<QString, QString> Param;
typedef QList<Param> Params; typedef QList<Param> Params;
typedef QPair<QByteArray, QByteArray> EncodedParam; typedef QPair<QByteArray, QByteArray> EncodedParam;
QUrl url(kApiUrl);
QString search;
if (album.isEmpty()) {
url.setPath("/search/track");
search = artist + " " + title;
}
else {
url.setPath("/search/album");
search = artist + " " + album;
}
const Params params = Params() << Param("output", "json") const Params params = Params() << Param("output", "json")
<< Param("q", QString(artist + " " + album)) << Param("q", search)
<< Param("limit", QString::number(kLimit)); << Param("limit", QString::number(kLimit));
QUrlQuery url_query; QUrlQuery url_query;
@@ -68,7 +79,6 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu
url_query.addQueryItem(encoded_param.first, encoded_param.second); url_query.addQueryItem(encoded_param.first, encoded_param.second);
} }
QUrl url(kApiUrl + QString("/search/album"));
url.setQuery(url_query); url.setQuery(url_query);
QNetworkRequest req(url); QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
@@ -220,19 +230,27 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
for (const QJsonValue &value : json_data) { for (const QJsonValue &value : json_data) {
if (!value.isObject()) { if (!value.isObject()) {
Error("Invalid Json reply, data is not an object.", value); Error("Invalid Json reply, data in array is not a object.", value);
continue; continue;
} }
QJsonObject json_obj = value.toObject(); QJsonObject json_obj = value.toObject();
QJsonObject json_album = json_obj;
if (json_obj.contains("album") && json_obj["album"].isObject()) { // Song search, so extract the album.
json_album = json_obj["album"].toObject();
}
if (!json_obj.contains("id") || !json_obj.contains("type")) { if (!json_obj.contains("id") || !json_album.contains("id")) {
Error("Invalid Json reply, item is missing ID or type.", json_obj); Error("Invalid Json reply, object is missing ID.", json_obj);
continue; continue;
} }
QString type = json_obj["type"].toString(); if (!json_album.contains("type")) {
Error("Invalid Json reply, album object is missing type.", json_album);
continue;
}
QString type = json_album["type"].toString();
if (type != "album") { if (type != "album") {
Error("Invalid Json reply, incorrect type returned", json_obj); Error("Invalid Json reply, incorrect type returned", json_album);
continue; continue;
} }
@@ -242,7 +260,7 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
} }
QJsonValue json_value_artist = json_obj["artist"]; QJsonValue json_value_artist = json_obj["artist"];
if (!json_value_artist.isObject()) { if (!json_value_artist.isObject()) {
Error("Invalid Json reply, item artist is not a object.", json_value_artist); Error("Invalid Json reply, artist is not a object.", json_value_artist);
continue; continue;
} }
QJsonObject json_artist = json_value_artist.toObject(); QJsonObject json_artist = json_value_artist.toObject();
@@ -253,27 +271,27 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
} }
QString artist = json_artist["name"].toString(); QString artist = json_artist["name"].toString();
if (!json_obj.contains("title")) { if (!json_album.contains("title")) {
Error("Invalid Json reply, data missing title.", json_obj); Error("Invalid Json reply, data missing title.", json_album);
continue; continue;
} }
QString album = json_obj["title"].toString(); QString album = json_album["title"].toString();
QString cover; QString cover;
if (json_obj.contains("cover_xl")) { if (json_album.contains("cover_xl")) {
cover = json_obj["cover_xl"].toString(); cover = json_album["cover_xl"].toString();
} }
else if (json_obj.contains("cover_big")) { else if (json_album.contains("cover_big")) {
cover = json_obj["cover_big"].toString(); cover = json_album["cover_big"].toString();
} }
else if (json_obj.contains("cover_medium")) { else if (json_album.contains("cover_medium")) {
cover = json_obj["cover_medium"].toString(); cover = json_album["cover_medium"].toString();
} }
else if (json_obj.contains("cover_small")) { else if (json_album.contains("cover_small")) {
cover = json_obj["cover_small"].toString(); cover = json_album["cover_small"].toString();
} }
else { else {
Error("Invalid Json reply, data missing cover.", json_obj); Error("Invalid Json reply, album missing cover.", json_album);
continue; continue;
} }
QUrl url(cover); QUrl url(cover);

View File

@@ -40,7 +40,7 @@ class DeezerCoverProvider : public CoverProvider {
public: public:
explicit DeezerCoverProvider(Application *app, QObject *parent = nullptr); explicit DeezerCoverProvider(Application *app, QObject *parent = nullptr);
bool StartSearch(const QString &artist, const QString &album, const int id); bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
void CancelSearch(const int id); void CancelSearch(const int id);
private slots: private slots:

View File

@@ -59,9 +59,11 @@ const char *DiscogsCoverProvider::kUrlReleases = "https://api.discogs.com/releas
const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk="; const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI="; const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", 0.0, false, app, parent), network_(new NetworkAccessManager(this)) {} DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", 0.0, false, false, app, parent), network_(new NetworkAccessManager(this)) {}
bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, const int s_id) { bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int s_id) {
Q_UNUSED(title);
DiscogsCoverSearchContext *s_ctx = new DiscogsCoverSearchContext; DiscogsCoverSearchContext *s_ctx = new DiscogsCoverSearchContext;

View File

@@ -73,7 +73,7 @@ class DiscogsCoverProvider : public CoverProvider {
public: public:
explicit DiscogsCoverProvider(Application *app, QObject *parent = nullptr); explicit DiscogsCoverProvider(Application *app, QObject *parent = nullptr);
bool StartSearch(const QString &artist, const QString &album, const int s_id); bool StartSearch(const QString &artist, const QString &album, const QString &title, const int s_id);
void CancelSearch(const int id); void CancelSearch(const int id);

View File

@@ -52,9 +52,11 @@ const char *LastFmCoverProvider::kUrl = "https://ws.audioscrobbler.com/2.0/";
const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e"; const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e";
const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8"; const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8";
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", 1.0, true, app, parent), network_(new NetworkAccessManager(this)) {} LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) { bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
Q_UNUSED(title);
typedef QPair<QString, QString> Param; typedef QPair<QString, QString> Param;
typedef QPair<QByteArray, QByteArray> EncodedParam; typedef QPair<QByteArray, QByteArray> EncodedParam;

View File

@@ -40,7 +40,7 @@ class LastFmCoverProvider : public CoverProvider {
public: public:
explicit LastFmCoverProvider(Application *app, QObject *parent = nullptr); explicit LastFmCoverProvider(Application *app, QObject *parent = nullptr);
bool StartSearch(const QString &artist, const QString &album, const int id); bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
private slots: private slots:
void QueryFinished(QNetworkReply *reply, const int id); void QueryFinished(QNetworkReply *reply, const int id);

View File

@@ -47,9 +47,11 @@ const char *MusicbrainzCoverProvider::kReleaseSearchUrl = "https://musicbrainz.o
const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front"; const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front";
const int MusicbrainzCoverProvider::kLimit = 8; const int MusicbrainzCoverProvider::kLimit = 8;
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", 1.5, true, app, parent), network_(new NetworkAccessManager(this)) {} MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", 1.5, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) { bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
Q_UNUSED(title);
QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(album.trimmed().replace('"', "\\\"")).arg(artist.trimmed().replace('"', "\\\"")); QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(album.trimmed().replace('"', "\\\"")).arg(artist.trimmed().replace('"', "\\\""));

View File

@@ -39,7 +39,7 @@ class MusicbrainzCoverProvider : public CoverProvider {
public: public:
explicit MusicbrainzCoverProvider(Application *app, QObject *parent = nullptr); explicit MusicbrainzCoverProvider(Application *app, QObject *parent = nullptr);
bool StartSearch(const QString &artist, const QString &album, const int id); bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
void CancelSearch(const int id); void CancelSearch(const int id);
private slots: private slots:

View File

@@ -52,13 +52,15 @@ const char *TidalCoverProvider::kResourcesUrl = "https://resources.tidal.com";
const int TidalCoverProvider::kLimit = 10; const int TidalCoverProvider::kLimit = 10;
TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) : TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) :
CoverProvider("Tidal", 2.0, true, app, parent), CoverProvider("Tidal", 2.0, true, false, app, parent),
service_(app->internet_services()->Service<TidalService>()), service_(app->internet_services()->Service<TidalService>()),
network_(new NetworkAccessManager(this)) { network_(new NetworkAccessManager(this)) {
} }
bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) { bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
Q_UNUSED(title);
if (!service_ || !service_->authenticated()) return false; if (!service_ || !service_->authenticated()) return false;

View File

@@ -44,7 +44,7 @@ class TidalCoverProvider : public CoverProvider {
public: public:
explicit TidalCoverProvider(Application *app, QObject *parent = nullptr); explicit TidalCoverProvider(Application *app, QObject *parent = nullptr);
bool StartSearch(const QString &artist, const QString &album, const int id); bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
void CancelSearch(int id); void CancelSearch(int id);
private slots: private slots:

View File

@@ -0,0 +1,54 @@
/*
* Strawberry Music Player
* Copyright 2020, 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 "addstreamdialog.h"
#include "ui_addstreamdialog.h"
#include <QSettings>
#include <QUrl>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QDialogButtonBox>
#include <QShowEvent>
AddStreamDialog::AddStreamDialog(QWidget* parent) : QDialog(parent), ui_(new Ui_AddStreamDialog) {
ui_->setupUi(this);
connect(ui_->url, SIGNAL(textChanged(QString)), SLOT(TextChanged(QString)));
TextChanged(QString());
}
AddStreamDialog::~AddStreamDialog() { delete ui_; }
void AddStreamDialog::showEvent(QShowEvent*) {
ui_->url->setFocus();
ui_->url->selectAll();
}
void AddStreamDialog::TextChanged(const QString &text) {
QUrl url(text);
ui_->button_box->button(QDialogButtonBox::Ok)->setEnabled(url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty());
}

View File

@@ -0,0 +1,51 @@
/*
* Strawberry Music Player
* Copyright 2020, 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 ADDSTREAMDIALOG_H
#define ADDSTREAMDIALOG_H
#include <QDialog>
#include <QString>
#include <QUrl>
#include <QLineEdit>
#include "ui_addstreamdialog.h"
class AddStreamDialog : public QDialog {
Q_OBJECT
public:
AddStreamDialog(QWidget *parent = nullptr);
~AddStreamDialog();
QUrl url() const { return QUrl(ui_->url->text()); }
void set_url(const QUrl &url) { ui_->url->setText(url.toString());}
protected:
void showEvent(QShowEvent*);
private slots:
void TextChanged(const QString &text);
private:
Ui_AddStreamDialog *ui_;
};
#endif // ADDSTREAMDIALOG_H

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddStreamDialog</class>
<widget class="QDialog" name="AddStreamDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>120</height>
</rect>
</property>
<property name="windowTitle">
<string>Add Stream</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/icons.qrc">
<normaloff>:/icons/48x48/document-open-remote.png</normaloff>:/icons/48x48/document-open-remote.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enter the URL of a stream:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="url"/>
</item>
<item>
<spacer name="spacer_bottom">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>url</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources>
<include location="../../data/data.qrc"/>
<include location="../../data/icons.qrc"/>
</resources>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>AddStreamDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>257</x>
<y>158</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>167</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>AddStreamDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>325</x>
<y>158</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>167</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -81,6 +81,8 @@
#endif #endif
#include "covermanager/albumcoverchoicecontroller.h" #include "covermanager/albumcoverchoicecontroller.h"
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/coverproviders.h" #include "covermanager/coverproviders.h"
#include "edittagdialog.h" #include "edittagdialog.h"
#include "trackselectiondialog.h" #include "trackselectiondialog.h"
@@ -106,9 +108,9 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent)
pending_(0) pending_(0)
{ {
cover_options_.default_output_image_ = AlbumCoverLoader::ScaleAndPad(cover_options_, QImage(":/pictures/cdcase.png")); cover_options_.default_output_image_ = AlbumCoverLoader::ScaleAndPad(cover_options_, QImage(":/pictures/cdcase.png")).first;
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage, QImage)), SLOT(AlbumCoverLoaded(quint64, QUrl, QImage, QImage))); connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
connect(tag_fetcher_, SIGNAL(ResultAvailable(Song, SongList)), results_dialog_, SLOT(FetchTagFinished(Song, SongList)), Qt::QueuedConnection); connect(tag_fetcher_, SIGNAL(ResultAvailable(Song, SongList)), results_dialog_, SLOT(FetchTagFinished(Song, SongList)), Qt::QueuedConnection);
@@ -562,13 +564,11 @@ void EditTagDialog::UpdateStatisticsTab(const Song &song) {
} }
void EditTagDialog::AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &scaled, const QImage &original) { void EditTagDialog::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
Q_UNUSED(cover_url);
if (id == cover_art_id_) { if (id == cover_art_id_) {
ui_->art->setPixmap(QPixmap::fromImage(scaled)); ui_->art->setPixmap(QPixmap::fromImage(result.image_scaled));
original_ = original; original_ = result.image_original;
} }
} }

View File

@@ -38,6 +38,7 @@
#include "core/tagreaderclient.h" #include "core/tagreaderclient.h"
#include "playlist/playlistitem.h" #include "playlist/playlistitem.h"
#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
class QWidget; class QWidget;
class QMenu; class QMenu;
@@ -110,7 +111,7 @@ class EditTagDialog : public QDialog {
void FetchTag(); void FetchTag();
void FetchTagSongChosen(const Song &original_song, const Song &new_metadata); void FetchTagSongChosen(const Song &original_song, const Song &new_metadata);
void AlbumCoverLoaded(const quint64 id, const QUrl &cover_url, const QImage &scaled, const QImage &original); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
void LoadCoverFromFile(); void LoadCoverFromFile();
void SaveCoverToFile(); void SaveCoverToFile();

View File

@@ -839,6 +839,16 @@ void GstEnginePipeline::TagMessageReceived(GstMessage *msg) {
bundle.bitrate = ParseUIntTag(taglist, GST_TAG_BITRATE) / 1000; bundle.bitrate = ParseUIntTag(taglist, GST_TAG_BITRATE) / 1000;
bundle.lyrics = ParseStrTag(taglist, GST_TAG_LYRICS); bundle.lyrics = ParseStrTag(taglist, GST_TAG_LYRICS);
if (!bundle.title.isEmpty() && bundle.artist.isEmpty() && bundle.album.isEmpty() && bundle.title.contains(" - ")) {
QStringList title_splitted = bundle.title.split(" - ");
if (title_splitted.count() == 2) {
bundle.artist = title_splitted.first();
bundle.title = title_splitted.last();
bundle.artist = bundle.artist.trimmed();
bundle.title = bundle.title.trimmed();
}
}
gst_tag_list_free(taglist); gst_tag_list_free(taglist);
emit MetadataFound(id(), bundle); emit MetadataFound(id(), bundle);

View File

@@ -46,9 +46,11 @@ InternetPlaylistItem::InternetPlaylistItem(InternetService *service, const Song
} }
bool InternetPlaylistItem::InitFromQuery(const SqlRow &query) { bool InternetPlaylistItem::InitFromQuery(const SqlRow &query) {
metadata_.InitFromQuery(query, false, (Song::kColumns.count() + 1) * 1); metadata_.InitFromQuery(query, false, (Song::kColumns.count() + 1) * 1);
InitMetadata(); InitMetadata();
return true; return true;
} }
QVariant InternetPlaylistItem::DatabaseValue(DatabaseColumn column) const { QVariant InternetPlaylistItem::DatabaseValue(DatabaseColumn column) const {
@@ -56,15 +58,26 @@ QVariant InternetPlaylistItem::DatabaseValue(DatabaseColumn column) const {
} }
void InternetPlaylistItem::InitMetadata() { void InternetPlaylistItem::InitMetadata() {
if (metadata_.title().isEmpty()) metadata_.set_title(metadata_.url().toString()); if (metadata_.title().isEmpty()) metadata_.set_title(metadata_.url().toString());
if (metadata_.source() == Song::Source_Unknown) metadata_.set_source(Song::Source_Stream); if (metadata_.source() == Song::Source_Unknown) metadata_.set_source(Song::Source_Stream);
if (metadata_.filetype() == Song::FileType_Unknown) metadata_.set_filetype(Song::FileType_Stream); if (metadata_.filetype() == Song::FileType_Unknown) metadata_.set_filetype(Song::FileType_Stream);
metadata_.set_valid(true); metadata_.set_valid(true);
} }
Song InternetPlaylistItem::Metadata() const { Song InternetPlaylistItem::Metadata() const {
if (HasTemporaryMetadata()) return temp_metadata_; if (HasTemporaryMetadata()) return temp_metadata_;
return metadata_; return metadata_;
} }
QUrl InternetPlaylistItem::Url() const { return metadata_.url(); } QUrl InternetPlaylistItem::Url() const { return metadata_.url(); }
void InternetPlaylistItem::SetArtManual(const QUrl &cover_url) {
metadata_.set_art_manual(cover_url);
temp_metadata_.set_art_manual(cover_url);
}

View File

@@ -36,10 +36,11 @@ class InternetPlaylistItem : public PlaylistItem {
public: public:
explicit InternetPlaylistItem(const Song::Source &type); explicit InternetPlaylistItem(const Song::Source &type);
InternetPlaylistItem(InternetService *service, const Song &metadata); explicit InternetPlaylistItem(InternetService *service, const Song &metadata);
bool InitFromQuery(const SqlRow &query); bool InitFromQuery(const SqlRow &query);
Song Metadata() const; Song Metadata() const;
QUrl Url() const; QUrl Url() const;
void SetArtManual(const QUrl &cover_url);
protected: protected:
QVariant DatabaseValue(DatabaseColumn) const; QVariant DatabaseValue(DatabaseColumn) const;

View File

@@ -27,11 +27,11 @@
InternetSearchItemDelegate::InternetSearchItemDelegate(InternetSearchView *view) InternetSearchItemDelegate::InternetSearchItemDelegate(InternetSearchView *view)
: CollectionItemDelegate(view), view_(view) {} : CollectionItemDelegate(view), view_(view) {}
void InternetSearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { void InternetSearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const {
// Tell the view we painted this item so it can lazy load some art. // Tell the view we painted this item so it can lazy load some art.
const_cast<InternetSearchView*>(view_)->LazyLoadAlbumCover(index); const_cast<InternetSearchView*>(view_)->LazyLoadAlbumCover(idx);
CollectionItemDelegate::paint(painter, option, index); CollectionItemDelegate::paint(painter, option, idx);
} }

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This code was part of Clementine (GlobalSearch) * This code was part of Clementine (GlobalSearch)
* Copyright 2012, David Sansome <me@davidsansome.com> * Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -56,21 +57,17 @@ InternetSearchModel::InternetSearchModel(InternetService *service, QObject *pare
void InternetSearchModel::AddResults(const InternetSearchView::ResultList &results) { void InternetSearchModel::AddResults(const InternetSearchView::ResultList &results) {
int sort_index = 0;
for (const InternetSearchView::Result &result : results) { for (const InternetSearchView::Result &result : results) {
QStandardItem *parent = invisibleRootItem(); QStandardItem *parent = invisibleRootItem();
// Find (or create) the container nodes for this result if we can. // Find (or create) the container nodes for this result if we can.
ContainerKey key; ContainerKey key;
key.provider_index_ = sort_index;
parent = BuildContainers(result.metadata_, parent, &key); parent = BuildContainers(result.metadata_, parent, &key);
// Create the item // Create the item
QStandardItem *item = new QStandardItem; QStandardItem *item = new QStandardItem;
item->setText(result.metadata_.TitleWithCompilationArtist()); item->setText(result.metadata_.TitleWithCompilationArtist());
item->setData(QVariant::fromValue(result), Role_Result); item->setData(QVariant::fromValue(result), Role_Result);
item->setData(sort_index, Role_ProviderIndex);
parent->appendRow(item); parent->appendRow(item);
@@ -78,7 +75,7 @@ void InternetSearchModel::AddResults(const InternetSearchView::ResultList &resul
} }
QStandardItem *InternetSearchModel::BuildContainers(const Song &s, QStandardItem *parent, ContainerKey *key, int level) { QStandardItem *InternetSearchModel::BuildContainers(const Song &s, QStandardItem *parent, ContainerKey *key, const int level) {
if (level >= 3) { if (level >= 3) {
return parent; return parent;
@@ -249,7 +246,6 @@ QStandardItem *InternetSearchModel::BuildContainers(const Song &s, QStandardItem
QStandardItem *container = containers_[*key]; QStandardItem *container = containers_[*key];
if (!container) { if (!container) {
container = new QStandardItem(display_text); container = new QStandardItem(display_text);
container->setData(key->provider_index_, Role_ProviderIndex);
container->setData(sort_text, CollectionModel::Role_SortText); container->setData(sort_text, CollectionModel::Role_SortText);
container->setData(group_by_[level], CollectionModel::Role_ContainerType); container->setData(group_by_[level], CollectionModel::Role_ContainerType);
@@ -275,8 +271,10 @@ QStandardItem *InternetSearchModel::BuildContainers(const Song &s, QStandardItem
} }
void InternetSearchModel::Clear() { void InternetSearchModel::Clear() {
containers_.clear(); containers_.clear();
clear(); clear();
} }
InternetSearchView::ResultList InternetSearchModel::GetChildResults(const QModelIndexList &indexes) const { InternetSearchView::ResultList InternetSearchModel::GetChildResults(const QModelIndexList &indexes) const {
@@ -314,7 +312,7 @@ void InternetSearchModel::GetChildResults(const QStandardItem *item, InternetSea
const QModelIndex parent_proxy_index = proxy_->mapFromSource(item->index()); const QModelIndex parent_proxy_index = proxy_->mapFromSource(item->index());
// Yes - visit all the children, but do so through the proxy so we get them in the right order. // Yes - visit all the children, but do so through the proxy so we get them in the right order.
for (int i = 0; i < item->rowCount(); ++i) { for (int i = 0 ; i < item->rowCount() ; ++i) {
const QModelIndex proxy_index = parent_proxy_index.model()->index(i, 0, parent_proxy_index); const QModelIndex proxy_index = parent_proxy_index.model()->index(i, 0, parent_proxy_index);
const QModelIndex index = proxy_->mapToSource(proxy_index); const QModelIndex index = proxy_->mapToSource(proxy_index);
GetChildResults(itemFromIndex(index), results, visited); GetChildResults(itemFromIndex(index), results, visited);
@@ -326,21 +324,6 @@ void InternetSearchModel::GetChildResults(const QStandardItem *item, InternetSea
if (result.isValid()) { if (result.isValid()) {
results->append(result.value<InternetSearchView::Result>()); results->append(result.value<InternetSearchView::Result>());
} }
else {
// Maybe it's a provider then?
bool is_provider;
const int sort_index = item->data(Role_ProviderIndex).toInt(&is_provider);
if (is_provider) {
// Go through all the items (through the proxy to keep them ordered) and add the ones belonging to this provider to our list
for (int i = 0; i < proxy_->rowCount(invisibleRootItem()->index()); ++i) {
QModelIndex child_index = proxy_->index(i, 0, invisibleRootItem()->index());
const QStandardItem *child_item = itemFromIndex(proxy_->mapToSource(child_index));
if (child_item->data(Role_ProviderIndex).toInt() == sort_index) {
GetChildResults(child_item, results, visited);
}
}
}
}
} }
} }
@@ -360,13 +343,13 @@ void GatherResults(const QStandardItem *parent, InternetSearchView::ResultList *
(*results).append(result); (*results).append(result);
} }
for (int i = 0; i < parent->rowCount(); ++i) { for (int i = 0 ; i < parent->rowCount() ; ++i) {
GatherResults(parent->child(i), results); GatherResults(parent->child(i), results);
} }
} }
} }
void InternetSearchModel::SetGroupBy(const CollectionModel::Grouping &grouping, bool regroup_now) { void InternetSearchModel::SetGroupBy(const CollectionModel::Grouping &grouping, const bool regroup_now) {
const CollectionModel::Grouping old_group_by = group_by_; const CollectionModel::Grouping old_group_by = group_by_;
group_by_ = grouping; group_by_ = grouping;
@@ -389,24 +372,17 @@ MimeData *InternetSearchModel::LoadTracks(const InternetSearchView::ResultList &
return nullptr; return nullptr;
} }
InternetSearchView::ResultList results_copy;
for (const InternetSearchView::Result &result : results) {
results_copy << result;
}
SongList songs; SongList songs;
QList<QUrl> urls;
for (const InternetSearchView::Result &result : results) { for (const InternetSearchView::Result &result : results) {
songs << result.metadata_; songs << result.metadata_;
urls << result.metadata_.url();
} }
InternetSongMimeData *internet_song_mime_data = new InternetSongMimeData(service_); InternetSongMimeData *internet_song_mime_data = new InternetSongMimeData(service_);
internet_song_mime_data->songs = songs; internet_song_mime_data->songs = songs;
MimeData *mime_data = internet_song_mime_data; MimeData *mime_data = internet_song_mime_data;
QList<QUrl> urls;
for (const InternetSearchView::Result &result : results) {
urls << result.metadata_.url();
}
mime_data->setUrls(urls); mime_data->setUrls(urls);
return mime_data; return mime_data;

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This code was part of Clementine (GlobalSearch) * This code was part of Clementine (GlobalSearch)
* Copyright 2012, David Sansome <me@davidsansome.com> * Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -54,18 +55,16 @@ class InternetSearchModel : public QStandardItemModel {
enum Role { enum Role {
Role_Result = CollectionModel::LastRole, Role_Result = CollectionModel::LastRole,
Role_LazyLoadingArt, Role_LazyLoadingArt,
Role_ProviderIndex,
LastRole LastRole
}; };
struct ContainerKey { struct ContainerKey {
int provider_index_;
QString group_[3]; QString group_[3];
}; };
void set_proxy(QSortFilterProxyModel *proxy) { proxy_ = proxy; } void set_proxy(QSortFilterProxyModel *proxy) { proxy_ = proxy; }
void set_use_pretty_covers(const bool pretty) { use_pretty_covers_ = pretty; } void set_use_pretty_covers(const bool pretty) { use_pretty_covers_ = pretty; }
void SetGroupBy(const CollectionModel::Grouping &grouping, bool regroup_now); void SetGroupBy(const CollectionModel::Grouping &grouping, const bool regroup_now);
void Clear(); void Clear();
@@ -82,7 +81,7 @@ class InternetSearchModel : public QStandardItemModel {
void AddResults(const InternetSearchView::ResultList &results); void AddResults(const InternetSearchView::ResultList &results);
private: private:
QStandardItem *BuildContainers(const Song &metadata, QStandardItem *parent, ContainerKey *key, int level = 0); QStandardItem *BuildContainers(const Song &s, QStandardItem *parent, ContainerKey *key, const int level = 0);
void GetChildResults(const QStandardItem *item, InternetSearchView::ResultList *results, QSet<const QStandardItem*> *visited) const; void GetChildResults(const QStandardItem *item, InternetSearchView::ResultList *results, QSet<const QStandardItem*> *visited) const;
private: private:
@@ -98,7 +97,7 @@ class InternetSearchModel : public QStandardItemModel {
}; };
inline uint qHash(const InternetSearchModel::ContainerKey &key) { inline uint qHash(const InternetSearchModel::ContainerKey &key) {
return qHash(key.provider_index_) ^ qHash(key.group_[0]) ^ qHash(key.group_[1]) ^ qHash(key.group_[2]); return qHash(key.group_[0]) ^ qHash(key.group_[1]) ^ qHash(key.group_[2]);
} }
inline bool operator<(const InternetSearchModel::ContainerKey &left, const InternetSearchModel::ContainerKey &right) { inline bool operator<(const InternetSearchModel::ContainerKey &left, const InternetSearchModel::ContainerKey &right) {
@@ -106,7 +105,6 @@ inline bool operator<(const InternetSearchModel::ContainerKey &left, const Inter
if (left.field < right.field) return true; \ if (left.field < right.field) return true; \
if (left.field > right.field) return false if (left.field > right.field) return false
CMP(provider_index_);
CMP(group_[0]); CMP(group_[0]);
CMP(group_[1]); CMP(group_[1]);
CMP(group_[2]); CMP(group_[2]);

View File

@@ -32,15 +32,9 @@
#include "internetsearchsortmodel.h" #include "internetsearchsortmodel.h"
#include "internetsearchview.h" #include "internetsearchview.h"
InternetSearchSortModel::InternetSearchSortModel(QObject *parent) InternetSearchSortModel::InternetSearchSortModel(QObject *parent) : QSortFilterProxyModel(parent) {}
: QSortFilterProxyModel(parent) {}
bool InternetSearchSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { bool InternetSearchSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const {
// Compare the provider sort index first.
const int index_left = left.data(InternetSearchModel::Role_ProviderIndex).toInt();
const int index_right = right.data(InternetSearchModel::Role_ProviderIndex).toInt();
if (index_left < index_right) return true;
if (index_left > index_right) return false;
// Dividers always go first // Dividers always go first
if (left.data(CollectionModel::Role_IsDivider).toBool()) return true; if (left.data(CollectionModel::Role_IsDivider).toBool()) return true;

View File

@@ -21,6 +21,7 @@
#include "config.h" #include "config.h"
#include <memory>
#include <utility> #include <utility>
#include <QtGlobal> #include <QtGlobal>
@@ -32,6 +33,7 @@
#include <QApplication> #include <QApplication>
#include <QWidget> #include <QWidget>
#include <QTimer> #include <QTimer>
#include <QPair>
#include <QList> #include <QList>
#include <QMap> #include <QMap>
#include <QVariant> #include <QVariant>
@@ -69,10 +71,13 @@
#include "core/mimedata.h" #include "core/mimedata.h"
#include "core/iconloader.h" #include "core/iconloader.h"
#include "core/song.h" #include "core/song.h"
#include "core/logging.h"
#include "collection/collectionfilterwidget.h" #include "collection/collectionfilterwidget.h"
#include "collection/collectionmodel.h" #include "collection/collectionmodel.h"
#include "collection/groupbydialog.h" #include "collection/groupbydialog.h"
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "internetsongmimedata.h" #include "internetsongmimedata.h"
#include "internetservice.h" #include "internetservice.h"
#include "internetsearchitemdelegate.h" #include "internetsearchitemdelegate.h"
@@ -102,11 +107,11 @@ InternetSearchView::InternetSearchView(QWidget *parent)
back_proxy_(new InternetSearchSortModel(this)), back_proxy_(new InternetSearchSortModel(this)),
current_proxy_(front_proxy_), current_proxy_(front_proxy_),
swap_models_timer_(new QTimer(this)), swap_models_timer_(new QTimer(this)),
use_pretty_covers_(true),
search_type_(InternetSearchView::SearchType_Artists), search_type_(InternetSearchView::SearchType_Artists),
search_error_(false), search_error_(false),
last_search_id_(0), last_search_id_(0),
searches_next_id_(1), searches_next_id_(1) {
art_searches_next_id_(1) {
ui_->setupUi(this); ui_->setupUi(this);
@@ -190,6 +195,7 @@ void InternetSearchView::Init(Application *app, InternetService *service) {
connect(ui_->radiobutton_search_albums, SIGNAL(clicked(bool)), SLOT(SearchAlbumsClicked(bool))); connect(ui_->radiobutton_search_albums, SIGNAL(clicked(bool)), SLOT(SearchAlbumsClicked(bool)));
connect(ui_->radiobutton_search_songs, SIGNAL(clicked(bool)), SLOT(SearchSongsClicked(bool))); connect(ui_->radiobutton_search_songs, SIGNAL(clicked(bool)), SLOT(SearchSongsClicked(bool)));
connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*))); connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString))); connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString)));
connect(ui_->results, SIGNAL(AddToPlaylistSignal(QMimeData*)), SIGNAL(AddToPlaylist(QMimeData*))); connect(ui_->results, SIGNAL(AddToPlaylistSignal(QMimeData*)), SIGNAL(AddToPlaylist(QMimeData*)));
@@ -201,7 +207,7 @@ void InternetSearchView::Init(Application *app, InternetService *service) {
connect(service_, SIGNAL(SearchResults(int, SongList, QString)), SLOT(SearchDone(int, SongList, QString))); connect(service_, SIGNAL(SearchResults(int, SongList, QString)), SLOT(SearchDone(int, SongList, QString)));
connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings())); connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()));
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(AlbumCoverLoaded(quint64, QUrl, QImage))); connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
ReloadSettings(); ReloadSettings();
@@ -214,9 +220,9 @@ void InternetSearchView::ReloadSettings() {
// Collection settings // Collection settings
s.beginGroup(service_->settings_group()); s.beginGroup(service_->settings_group());
const bool pretty = s.value("pretty_covers", true).toBool(); use_pretty_covers_ = s.value("pretty_covers", true).toBool();
front_model_->set_use_pretty_covers(pretty); front_model_->set_use_pretty_covers(use_pretty_covers_);
back_model_->set_use_pretty_covers(pretty); back_model_->set_use_pretty_covers(use_pretty_covers_);
// Internet search settings // Internet search settings
@@ -248,12 +254,6 @@ void InternetSearchView::showEvent(QShowEvent *e) {
} }
void InternetSearchView::hideEvent(QHideEvent *e) {
QWidget::hideEvent(e);
}
bool InternetSearchView::eventFilter(QObject *object, QEvent *e) { bool InternetSearchView::eventFilter(QObject *object, QEvent *e) {
if (object == ui_->search && e->type() == QEvent::KeyRelease) { if (object == ui_->search && e->type() == QEvent::KeyRelease) {
@@ -374,6 +374,7 @@ void InternetSearchView::TextEdited(const QString &text) {
const QString trimmed(text.trimmed()); const QString trimmed(text.trimmed());
search_error_ = false; search_error_ = false;
cover_loader_tasks_.clear();
// Add results to the back model, switch models after some delay. // Add results to the back model, switch models after some delay.
back_model_->Clear(); back_model_->Clear();
@@ -387,7 +388,7 @@ void InternetSearchView::TextEdited(const QString &text) {
// If text query is empty, don't start a new search // If text query is empty, don't start a new search
if (trimmed.isEmpty()) { if (trimmed.isEmpty()) {
last_search_id_ = -1; last_search_id_ = -1;
ui_->label_helptext->setText("Enter search terms above to find music"); ui_->label_helptext->setText(tr("Enter search terms above to find music"));
ui_->label_status->clear(); ui_->label_status->clear();
ui_->progressbar->hide(); ui_->progressbar->hide();
ui_->progressbar->reset(); ui_->progressbar->reset();
@@ -401,7 +402,7 @@ void InternetSearchView::TextEdited(const QString &text) {
void InternetSearchView::SwapModels() { void InternetSearchView::SwapModels() {
art_requests_.clear(); cover_loader_tasks_.clear();
std::swap(front_model_, back_model_); std::swap(front_model_, back_model_);
std::swap(front_proxy_, back_proxy_); std::swap(front_proxy_, back_proxy_);
@@ -488,10 +489,8 @@ void InternetSearchView::SearchDone(const int service_id, const SongList &songs,
results << result; results << result;
} }
if (results.isEmpty()) return;
// Load cached pixmaps into the results // Load cached pixmaps into the results
for (InternetSearchView::ResultList::iterator it = results.begin(); it != results.end(); ++it) { for (InternetSearchView::ResultList::iterator it = results.begin() ; it != results.end() ; ++it) {
it->pixmap_cache_key_ = PixmapCacheKey(*it); it->pixmap_cache_key_ = PixmapCacheKey(*it);
} }
@@ -576,7 +575,7 @@ MimeData *InternetSearchView::SelectedMimeData() {
QModelIndexList indexes = ui_->results->selectionModel()->selectedRows(); QModelIndexList indexes = ui_->results->selectionModel()->selectedRows();
if (indexes.isEmpty()) { if (indexes.isEmpty()) {
// There's nothing selected - take the first thing in the model that isn't a divider. // There's nothing selected - take the first thing in the model that isn't a divider.
for (int i = 0; i < front_proxy_->rowCount(); ++i) { for (int i = 0 ; i < front_proxy_->rowCount() ; ++i) {
QModelIndex index = front_proxy_->index(i, 0); QModelIndex index = front_proxy_->index(i, 0);
if (!index.data(CollectionModel::Role_IsDivider).toBool()) { if (!index.data(CollectionModel::Role_IsDivider).toBool()) {
indexes << index; indexes << index;
@@ -663,7 +662,7 @@ void InternetSearchView::GroupByClicked(QAction *action) {
if (action->property("group_by").isNull()) { if (action->property("group_by").isNull()) {
if (!group_by_dialog_) { if (!group_by_dialog_) {
group_by_dialog_.reset(new GroupByDialog); group_by_dialog_.reset(new GroupByDialog);
connect(group_by_dialog_.data(), SIGNAL(Accepted(CollectionModel::Grouping)), SLOT(SetGroupBy(CollectionModel::Grouping))); connect(group_by_dialog_.get(), SIGNAL(Accepted(CollectionModel::Grouping)), SLOT(SetGroupBy(CollectionModel::Grouping)));
} }
group_by_dialog_->show(); group_by_dialog_->show();
@@ -679,7 +678,8 @@ void InternetSearchView::SetGroupBy(const CollectionModel::Grouping &g) {
// Clear requests: changing "group by" on the models will cause all the items to be removed/added again, // Clear requests: changing "group by" on the models will cause all the items to be removed/added again,
// so all the QModelIndex here will become invalid. New requests will be created for those // so all the QModelIndex here will become invalid. New requests will be created for those
// songs when they will be displayed again anyway (when InternetSearchItemDelegate::paint will call LazyLoadAlbumCover) // songs when they will be displayed again anyway (when InternetSearchItemDelegate::paint will call LazyLoadAlbumCover)
art_requests_.clear(); cover_loader_tasks_.clear();
// Update the models // Update the models
front_model_->SetGroupBy(g, true); front_model_->SetGroupBy(g, true);
back_model_->SetGroupBy(g, false); back_model_->SetGroupBy(g, false);
@@ -722,10 +722,12 @@ void InternetSearchView::SearchSongsClicked(const bool) {
void InternetSearchView::SetSearchType(const InternetSearchView::SearchType type) { void InternetSearchView::SetSearchType(const InternetSearchView::SearchType type) {
search_type_ = type; search_type_ = type;
QSettings s; QSettings s;
s.beginGroup(service_->settings_group()); s.beginGroup(service_->settings_group());
s.setValue("type", int(search_type_)); s.setValue("type", int(search_type_));
s.endGroup(); s.endGroup();
TextEdited(ui_->search->text()); TextEdited(ui_->search->text());
} }
@@ -761,110 +763,101 @@ void InternetSearchView::AddSongs() {
} }
QString InternetSearchView::PixmapCacheKey(const InternetSearchView::Result &result) const { QString InternetSearchView::PixmapCacheKey(const InternetSearchView::Result &result) const {
return "internet:" % result.metadata_.url().toString();
if (result.metadata_.art_automatic_is_valid()) {
return Song::TextForSource(service_->source()) + "/" + result.metadata_.art_automatic().toString();
}
else if (!result.metadata_.effective_albumartist().isEmpty() && !result.metadata_.album().isEmpty()) {
return Song::TextForSource(service_->source()) + "/" + result.metadata_.effective_albumartist() + "/" + result.metadata_.album();
}
else {
return Song::TextForSource(service_->source()) + "/" + result.metadata_.url().toString();
}
} }
bool InternetSearchView::FindCachedPixmap(const InternetSearchView::Result &result, QPixmap *pixmap) const { bool InternetSearchView::FindCachedPixmap(const InternetSearchView::Result &result, QPixmap *pixmap) const {
return pixmap_cache_.find(result.pixmap_cache_key_, pixmap); return pixmap_cache_.find(result.pixmap_cache_key_, pixmap);
} }
int InternetSearchView::LoadAlbumCoverAsync(const InternetSearchView::Result &result) {
const int id = art_searches_next_id_++;
pending_art_searches_[id] = result.pixmap_cache_key_;
quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, result.metadata_);
cover_loader_tasks_[loader_id] = id;
return id;
}
void InternetSearchView::LazyLoadAlbumCover(const QModelIndex &proxy_index) { void InternetSearchView::LazyLoadAlbumCover(const QModelIndex &proxy_index) {
if (!proxy_index.isValid() || proxy_index.model() != front_proxy_) { if (!proxy_index.isValid() || proxy_index.model() != front_proxy_) {
return; return;
} }
const QModelIndex source_index = front_proxy_->mapToSource(proxy_index);
if (!source_index.isValid()) {
return;
}
// Already loading art for this item? // Already loading art for this item?
if (proxy_index.data(InternetSearchModel::Role_LazyLoadingArt).isValid()) { if (source_index.data(InternetSearchModel::Role_LazyLoadingArt).isValid()) {
return; return;
} }
// Should we even load art at all? // Should we even load art at all?
if (!app_->collection_model()->use_pretty_covers()) { if (!use_pretty_covers_) {
return; return;
} }
// Is this an album? // Is this an album?
const CollectionModel::GroupBy container_type = CollectionModel::GroupBy(proxy_index.data(CollectionModel::Role_ContainerType).toInt()); const CollectionModel::GroupBy container_type = CollectionModel::GroupBy(proxy_index.data(CollectionModel::Role_ContainerType).toInt());
if (container_type != CollectionModel::GroupBy_Album && if (container_type != CollectionModel::GroupBy_Album &&
container_type != CollectionModel::GroupBy_AlbumDisc &&
container_type != CollectionModel::GroupBy_YearAlbum && container_type != CollectionModel::GroupBy_YearAlbum &&
container_type != CollectionModel::GroupBy_YearAlbumDisc &&
container_type != CollectionModel::GroupBy_OriginalYearAlbum) { container_type != CollectionModel::GroupBy_OriginalYearAlbum) {
return; return;
} }
// Mark the item as loading art // Mark the item as loading art
const QModelIndex source_index = front_proxy_->mapToSource(proxy_index);
QStandardItem *item = front_model_->itemFromIndex(source_index); QStandardItem *item_album = front_model_->itemFromIndex(source_index);
item->setData(true, InternetSearchModel::Role_LazyLoadingArt); if (!item_album) {
return;
}
item_album->setData(true, InternetSearchModel::Role_LazyLoadingArt);
// Walk down the item's children until we find a track // Walk down the item's children until we find a track
while (item->rowCount()) { QStandardItem *item_song = item_album;
item = item->child(0); while (item_song->rowCount()) {
item_song = item_song->child(0);
} }
// Get the track's Result // Get the track's Result
const InternetSearchView::Result result = item->data(InternetSearchModel::Role_Result).value<InternetSearchView::Result>(); const InternetSearchView::Result result = item_song->data(InternetSearchModel::Role_Result).value<InternetSearchView::Result>();
// Load the art. QPixmap cached_pixmap;
int id = LoadAlbumCoverAsync(result); if (pixmap_cache_.find(result.pixmap_cache_key_, &cached_pixmap)) {
art_requests_[id] = source_index; item_album->setData(cached_pixmap, Qt::DecorationRole);
}
} else {
quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, result.metadata_);
void InternetSearchView::AlbumCoverLoaded(const quint64 id, const QUrl&, const QImage &image) { cover_loader_tasks_[loader_id] = qMakePair(source_index, result.pixmap_cache_key_);
if (!cover_loader_tasks_.contains(id)) return;
int orig_id = cover_loader_tasks_.take(id);
const QString key = pending_art_searches_.take(orig_id);
QPixmap pixmap = QPixmap::fromImage(image);
pixmap_cache_.insert(key, pixmap);
if (!art_requests_.contains(id)) return;
QModelIndex idx = art_requests_.take(id);
if (!pixmap.isNull() && idx.isValid()) {
front_model_->itemFromIndex(idx)->setData(pixmap, Qt::DecorationRole);
} }
} }
QImage InternetSearchView::ScaleAndPad(const QImage &image) { void InternetSearchView::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &albumcover_result) {
if (image.isNull()) return QImage(); if (!cover_loader_tasks_.contains(id)) {
return;
}
QPair<QModelIndex, QString> cover_loader_task = cover_loader_tasks_.take(id);
QModelIndex idx = cover_loader_task.first;
QString key = cover_loader_task.second;
const QSize target_size = QSize(kArtHeight, kArtHeight); QPixmap pixmap = QPixmap::fromImage(albumcover_result.image_scaled);
if (!pixmap.isNull()) {
pixmap_cache_.insert(key, pixmap);
}
if (image.size() == target_size) return image; if (idx.isValid()) {
QStandardItem *item = front_model_->itemFromIndex(idx);
// Scale the image down if (item) {
QImage copy; item->setData(albumcover_result.image_scaled, Qt::DecorationRole);
copy = image.scaled(target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation); }
}
// Pad the image to kHeight x kHeight
if (copy.size() == target_size) return copy;
QImage padded_image(kArtHeight, kArtHeight, QImage::Format_ARGB32);
padded_image.fill(0);
QPainter p(&padded_image);
p.drawImage((kArtHeight - copy.width()) / 2, (kArtHeight - copy.height()) / 2, copy);
p.end();
return padded_image;
} }

View File

@@ -24,10 +24,13 @@
#include "config.h" #include "config.h"
#include <memory>
#include <QtGlobal> #include <QtGlobal>
#include <QObject> #include <QObject>
#include <QWidget> #include <QWidget>
#include <QSet> #include <QSet>
#include <QPair>
#include <QList> #include <QList>
#include <QMap> #include <QMap>
#include <QString> #include <QString>
@@ -36,12 +39,12 @@
#include <QImage> #include <QImage>
#include <QPixmap> #include <QPixmap>
#include <QPixmapCache> #include <QPixmapCache>
#include <QScopedPointer>
#include <QMetaType> #include <QMetaType>
#include "core/song.h" #include "core/song.h"
#include "collection/collectionmodel.h" #include "collection/collectionmodel.h"
#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "settings/settingsdialog.h" #include "settings/settingsdialog.h"
class QSortFilterProxyModel; class QSortFilterProxyModel;
@@ -53,14 +56,12 @@ class QActionGroup;
class QEvent; class QEvent;
class QKeyEvent; class QKeyEvent;
class QShowEvent; class QShowEvent;
class QHideEvent;
class QContextMenuEvent; class QContextMenuEvent;
class QTimerEvent; class QTimerEvent;
class Application; class Application;
class MimeData; class MimeData;
class GroupByDialog; class GroupByDialog;
class AlbumCoverLoader;
class InternetService; class InternetService;
class InternetSearchModel; class InternetSearchModel;
class Ui_InternetSearchView; class Ui_InternetSearchView;
@@ -104,7 +105,6 @@ class InternetSearchView : public QWidget {
}; };
void showEvent(QShowEvent *e); void showEvent(QShowEvent *e);
void hideEvent(QHideEvent *e);
bool eventFilter(QObject *object, QEvent *e); bool eventFilter(QObject *object, QEvent *e);
void timerEvent(QTimerEvent *e); void timerEvent(QTimerEvent *e);
@@ -135,7 +135,6 @@ class InternetSearchView : public QWidget {
QString PixmapCacheKey(const Result &result) const; QString PixmapCacheKey(const Result &result) const;
bool FindCachedPixmap(const Result &result, QPixmap *pixmap) const; bool FindCachedPixmap(const Result &result, QPixmap *pixmap) const;
static QImage ScaleAndPad(const QImage &image);
int LoadAlbumCoverAsync(const Result &result); int LoadAlbumCoverAsync(const Result &result);
signals: signals:
@@ -173,7 +172,7 @@ class InternetSearchView : public QWidget {
void GroupByClicked(QAction *action); void GroupByClicked(QAction *action);
void SetGroupBy(const CollectionModel::Grouping &g); void SetGroupBy(const CollectionModel::Grouping &g);
void AlbumCoverLoaded(const quint64 id, const QUrl&, const QImage &image); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &albumcover_result);
public slots: public slots:
void ReloadSettings(); void ReloadSettings();
@@ -187,7 +186,7 @@ class InternetSearchView : public QWidget {
Application *app_; Application *app_;
InternetService *service_; InternetService *service_;
Ui_InternetSearchView *ui_; Ui_InternetSearchView *ui_;
QScopedPointer<GroupByDialog> group_by_dialog_; std::unique_ptr<GroupByDialog> group_by_dialog_;
QMenu *context_menu_; QMenu *context_menu_;
QList<QAction*> context_actions_; QList<QAction*> context_actions_;
@@ -206,18 +205,17 @@ class InternetSearchView : public QWidget {
QTimer *swap_models_timer_; QTimer *swap_models_timer_;
bool use_pretty_covers_;
SearchType search_type_; SearchType search_type_;
bool search_error_; bool search_error_;
int last_search_id_; int last_search_id_;
int searches_next_id_; int searches_next_id_;
int art_searches_next_id_;
QMap<int, DelayedSearch> delayed_searches_; QMap<int, DelayedSearch> delayed_searches_;
QMap<int, PendingState> pending_searches_; QMap<int, PendingState> pending_searches_;
QMap<int, QString> pending_art_searches_;
QMap<int, QModelIndex> art_requests_;
AlbumCoverLoaderOptions cover_loader_options_; AlbumCoverLoaderOptions cover_loader_options_;
QMap<quint64, quint64> cover_loader_tasks_; QMap<quint64, QPair<QModelIndex, QString>> cover_loader_tasks_;
QPixmapCache pixmap_cache_; QPixmapCache pixmap_cache_;
}; };

View File

@@ -69,6 +69,7 @@
#include "collection/collection.h" #include "collection/collection.h"
#include "collection/collectionbackend.h" #include "collection/collectionbackend.h"
#include "collection/collectionplaylistitem.h" #include "collection/collectionplaylistitem.h"
#include "covermanager/albumcoverloader.h"
#include "queue/queue.h" #include "queue/queue.h"
#include "playlist.h" #include "playlist.h"
#include "playlistitem.h" #include "playlistitem.h"
@@ -88,12 +89,6 @@
using std::placeholders::_1; using std::placeholders::_1;
using std::placeholders::_2; using std::placeholders::_2;
using std::shared_ptr;
using std::unordered_map;
using std::sort;
using std::stable_sort;
using std::greater;
using std::swap;
const char *Playlist::kCddaMimeType = "x-content/audio-cdda"; const char *Playlist::kCddaMimeType = "x-content/audio-cdda";
const char *Playlist::kRowsMimetype = "application/x-strawberry-playlist-rows"; const char *Playlist::kRowsMimetype = "application/x-strawberry-playlist-rows";
@@ -256,6 +251,7 @@ bool Playlist::set_column_value(Song &song, Playlist::Column column, const QVari
default: default:
break; break;
} }
return true; return true;
} }
@@ -392,7 +388,9 @@ void Playlist::SongSaveComplete(TagReaderReply *reply, const QPersistentModelInd
if (reply->is_successful() && index.isValid()) { if (reply->is_successful() && index.isValid()) {
if (reply->message().save_file_response().success()) { if (reply->message().save_file_response().success()) {
QFuture<void> future = item_at(index.row())->BackgroundReload(); PlaylistItemPtr item = item_at(index.row());
if (!item) return;
QFuture<void> future = item->BackgroundReload();
NewClosure(future, this, SLOT(ItemReloadComplete(QPersistentModelIndex)), index); NewClosure(future, this, SLOT(ItemReloadComplete(QPersistentModelIndex)), index);
} }
else { else {
@@ -406,6 +404,20 @@ void Playlist::SongSaveComplete(TagReaderReply *reply, const QPersistentModelInd
void Playlist::ItemReloadComplete(const QPersistentModelIndex &index) { void Playlist::ItemReloadComplete(const QPersistentModelIndex &index) {
if (index.isValid()) { if (index.isValid()) {
PlaylistItemPtr item = item_at(index.row());
if (item) {
// Update temporary metadata for songs that are not in the collection.
// Songs that are in the collection is updated through the collection watcher/backend in playlist manager.
if (item->Metadata().source() != Song::Source_Collection) {
SongPlaylistItem *song_item = static_cast<SongPlaylistItem*>(item.get());
if (song_item) {
song_item->UpdateTemporaryMetadata(song_item->DatabaseSongMetadata());
}
}
}
emit dataChanged(index, index); emit dataChanged(index, index);
emit EditingFinished(index); emit EditingFinished(index);
} }
@@ -879,7 +891,7 @@ void Playlist::InsertItems(const PlaylistItemList &itemsIn, int pos, bool play_n
// exercise vetoes // exercise vetoes
SongList songs; SongList songs;
for (const PlaylistItemPtr item : items) { for (PlaylistItemPtr item : items) {
songs << item->Metadata(); songs << item->Metadata();
} }
@@ -1004,7 +1016,7 @@ void Playlist::InsertInternetItems(InternetService *service, const SongList &son
PlaylistItemList playlist_items; PlaylistItemList playlist_items;
for (const Song &song : songs) { for (const Song &song : songs) {
playlist_items << shared_ptr<PlaylistItem>(new InternetPlaylistItem(service, song)); playlist_items << std::shared_ptr<PlaylistItem>(new InternetPlaylistItem(service, song));
} }
InsertItems(playlist_items, pos, play_now, enqueue, enqueue_next); InsertItems(playlist_items, pos, play_now, enqueue, enqueue_next);
@@ -1014,6 +1026,7 @@ void Playlist::InsertInternetItems(InternetService *service, const SongList &son
void Playlist::UpdateItems(const SongList &songs) { void Playlist::UpdateItems(const SongList &songs) {
qLog(Debug) << "Updating playlist with new tracks' info"; qLog(Debug) << "Updating playlist with new tracks' info";
// We first convert our songs list into a linked list (a 'real' list), because removals are faster with QLinkedList. // We first convert our songs list into a linked list (a 'real' list), because removals are faster with QLinkedList.
// Next, we walk through the list of playlist's items then the list of songs // Next, we walk through the list of playlist's items then the list of songs
// we want to update: if an item corresponds to the song (we rely on URL for this), we update the item with the new metadata, // we want to update: if an item corresponds to the song (we rely on URL for this), we update the item with the new metadata,
@@ -1027,20 +1040,12 @@ void Playlist::UpdateItems(const SongList &songs) {
QMutableLinkedListIterator<Song> it(songs_list); QMutableLinkedListIterator<Song> it(songs_list);
while (it.hasNext()) { while (it.hasNext()) {
const Song &song = it.next(); const Song &song = it.next();
PlaylistItemPtr &item = items_[i]; const PlaylistItemPtr &item = items_[i];
if (item->Metadata().url() == song.url() && if (item->Metadata().url() == song.url()) {
(
item->Metadata().source() == Song::Source_Unknown ||
item->Metadata().filetype() == Song::FileType_Unknown ||
// Stream may change and may need to be updated too
item->Metadata().is_stream() ||
// And CD tracks as well (tags are loaded in a second step)
item->Metadata().is_cdda()
)
) {
PlaylistItemPtr new_item; PlaylistItemPtr new_item;
if (song.is_collection_song()) { if (song.is_collection_song()) {
new_item = PlaylistItemPtr(new CollectionPlaylistItem(song)); new_item = PlaylistItemPtr(new CollectionPlaylistItem(song));
if (collection_items_by_id_.contains(song.id(), item)) collection_items_by_id_.remove(song.id(), item);
collection_items_by_id_.insertMulti(song.id(), new_item); collection_items_by_id_.insertMulti(song.id(), new_item);
} }
else { else {
@@ -1103,10 +1108,10 @@ QMimeData *Playlist::mimeData(const QModelIndexList &indexes) const {
} }
bool Playlist::CompareItems(int column, Qt::SortOrder order, shared_ptr<PlaylistItem> _a, shared_ptr<PlaylistItem> _b) { bool Playlist::CompareItems(int column, Qt::SortOrder order, std::shared_ptr<PlaylistItem> _a, std::shared_ptr<PlaylistItem> _b) {
shared_ptr<PlaylistItem> a = order == Qt::AscendingOrder ? _a : _b; std::shared_ptr<PlaylistItem> a = order == Qt::AscendingOrder ? _a : _b;
shared_ptr<PlaylistItem> b = order == Qt::AscendingOrder ? _b : _a; std::shared_ptr<PlaylistItem> b = order == Qt::AscendingOrder ? _b : _a;
#define cmp(field) return a->Metadata().field() < b->Metadata().field() #define cmp(field) return a->Metadata().field() < b->Metadata().field()
#define strcmp(field) return QString::localeAwareCompare(a->Metadata().field().toLower(), b->Metadata().field().toLower()) < 0; #define strcmp(field) return QString::localeAwareCompare(a->Metadata().field().toLower(), b->Metadata().field().toLower()) < 0;
@@ -1154,10 +1159,10 @@ bool Playlist::CompareItems(int column, Qt::SortOrder order, shared_ptr<Playlist
} }
bool Playlist::ComparePathDepths(Qt::SortOrder order, shared_ptr<PlaylistItem> _a, shared_ptr<PlaylistItem> _b) { bool Playlist::ComparePathDepths(Qt::SortOrder order, std::shared_ptr<PlaylistItem> _a, std::shared_ptr<PlaylistItem> _b) {
shared_ptr<PlaylistItem> a = order == Qt::AscendingOrder ? _a : _b; std::shared_ptr<PlaylistItem> a = order == Qt::AscendingOrder ? _a : _b;
shared_ptr<PlaylistItem> b = order == Qt::AscendingOrder ? _b : _a; std::shared_ptr<PlaylistItem> b = order == Qt::AscendingOrder ? _b : _a;
int a_dir_level = a->Url().path().count('/'); int a_dir_level = a->Url().path().count('/');
int b_dir_level = b->Url().path().count('/'); int b_dir_level = b->Url().path().count('/');
@@ -1293,6 +1298,7 @@ void Playlist::SetCurrentIsPaused(bool paused) {
} }
void Playlist::Save() const { void Playlist::Save() const {
if (!backend_ || is_loading_) return; if (!backend_ || is_loading_) return;
backend_->SavePlaylistAsync(id_, items_, last_played_row()); backend_->SavePlaylistAsync(id_, items_, last_played_row());
@@ -1443,7 +1449,7 @@ PlaylistItemList Playlist::RemoveItemsWithoutUndo(int row, int count) {
if (item->source() == Song::Source_Collection) { if (item->source() == Song::Source_Collection) {
int id = item->Metadata().id(); int id = item->Metadata().id();
if (id != -1) { if (id != -1 && collection_items_by_id_.contains(id, item)) {
collection_items_by_id_.remove(id, item); collection_items_by_id_.remove(id, item);
} }
} }
@@ -1755,22 +1761,26 @@ void Playlist::set_sequence(PlaylistSequence *v) {
QSortFilterProxyModel *Playlist::proxy() const { return proxy_; } QSortFilterProxyModel *Playlist::proxy() const { return proxy_; }
SongList Playlist::GetAllSongs() const { SongList Playlist::GetAllSongs() const {
SongList ret; SongList ret;
for (const PlaylistItemPtr item : items_) { for (PlaylistItemPtr item : items_) {
ret << item->Metadata(); ret << item->Metadata();
} }
return ret; return ret;
} }
PlaylistItemList Playlist::GetAllItems() const { return items_; } PlaylistItemList Playlist::GetAllItems() const { return items_; }
quint64 Playlist::GetTotalLength() const { quint64 Playlist::GetTotalLength() const {
quint64 ret = 0; quint64 ret = 0;
for (const PlaylistItemPtr item : items_) { for (PlaylistItemPtr item : items_) {
quint64 length = item->Metadata().length_nanosec(); quint64 length = item->Metadata().length_nanosec();
if (length > 0) ret += length; if (length > 0) ret += length;
} }
return ret; return ret;
} }
PlaylistItemList Playlist::collection_items_by_id(int id) const { PlaylistItemList Playlist::collection_items_by_id(int id) const {
@@ -1778,30 +1788,38 @@ PlaylistItemList Playlist::collection_items_by_id(int id) const {
} }
void Playlist::TracksAboutToBeDequeued(const QModelIndex&, int begin, int end) { void Playlist::TracksAboutToBeDequeued(const QModelIndex&, int begin, int end) {
for (int i = begin; i <= end; ++i) { for (int i = begin; i <= end; ++i) {
temp_dequeue_change_indexes_ << queue_->mapToSource(queue_->index(i, Column_Title)); temp_dequeue_change_indexes_ << queue_->mapToSource(queue_->index(i, Column_Title));
} }
} }
void Playlist::TracksDequeued() { void Playlist::TracksDequeued() {
for (const QModelIndex &index : temp_dequeue_change_indexes_) { for (const QModelIndex &index : temp_dequeue_change_indexes_) {
emit dataChanged(index, index); emit dataChanged(index, index);
} }
temp_dequeue_change_indexes_.clear(); temp_dequeue_change_indexes_.clear();
emit QueueChanged(); emit QueueChanged();
} }
void Playlist::TracksEnqueued(const QModelIndex&, int begin, int end) { void Playlist::TracksEnqueued(const QModelIndex&, int begin, int end) {
const QModelIndex &b = queue_->mapToSource(queue_->index(begin, Column_Title)); const QModelIndex &b = queue_->mapToSource(queue_->index(begin, Column_Title));
const QModelIndex &e = queue_->mapToSource(queue_->index(end, Column_Title)); const QModelIndex &e = queue_->mapToSource(queue_->index(end, Column_Title));
emit dataChanged(b, e); emit dataChanged(b, e);
} }
void Playlist::QueueLayoutChanged() { void Playlist::QueueLayoutChanged() {
for (int i = 0; i < queue_->rowCount(); ++i) { for (int i = 0; i < queue_->rowCount(); ++i) {
const QModelIndex &index = queue_->mapToSource(queue_->index(i, Column_Title)); const QModelIndex &index = queue_->mapToSource(queue_->index(i, Column_Title));
emit dataChanged(index, index); emit dataChanged(index, index);
} }
} }
void Playlist::ItemChanged(PlaylistItemPtr item) { void Playlist::ItemChanged(PlaylistItemPtr item) {
@@ -1858,6 +1876,7 @@ void Playlist::InvalidateDeletedSongs() {
} }
void Playlist::RemoveDeletedSongs() { void Playlist::RemoveDeletedSongs() {
QList<int> rows_to_remove; QList<int> rows_to_remove;
for (int row = 0; row < items_.count(); ++row) { for (int row = 0; row < items_.count(); ++row) {
@@ -1892,7 +1911,7 @@ struct SongSimilarEqual {
void Playlist::RemoveDuplicateSongs() { void Playlist::RemoveDuplicateSongs() {
QList<int> rows_to_remove; QList<int> rows_to_remove;
unordered_map<Song, int, SongSimilarHash, SongSimilarEqual> unique_songs; std::unordered_map<Song, int, SongSimilarHash, SongSimilarEqual> unique_songs;
for (int row = 0; row < items_.count(); ++row) { for (int row = 0; row < items_.count(); ++row) {
PlaylistItemPtr item = items_[row]; PlaylistItemPtr item = items_[row];
@@ -2007,3 +2026,16 @@ void Playlist::UpdateScrobblePoint(const qint64 seek_point_nanosec) {
} }
void Playlist::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
// Update art_manual for local songs that are not in the collection.
if (result.type == AlbumCoverLoaderResult::Type_Manual && result.cover_url.isLocalFile() && (song.source() == Song::Source_LocalFile || song.source() == Song::Source_CDDA || song.source() == Song::Source_Device)) {
PlaylistItemPtr item = current_item();
if (item && item->Metadata() == song && !item->Metadata().art_manual_is_valid()) {
qLog(Debug) << "Updating art manual for local song" << song.title() << song.album() << song.title() << "to" << result.cover_url << "in playlist.";
item->SetArtManual(result.cover_url);
Save();
}
}
}

View File

@@ -42,6 +42,7 @@
#include "core/song.h" #include "core/song.h"
#include "core/tagreaderclient.h" #include "core/tagreaderclient.h"
#include "covermanager/albumcoverloaderresult.h"
#include "playlistitem.h" #include "playlistitem.h"
#include "playlistsequence.h" #include "playlistsequence.h"
@@ -348,6 +349,7 @@ class Playlist : public QAbstractListModel {
void ItemReloadComplete(const QPersistentModelIndex &index); void ItemReloadComplete(const QPersistentModelIndex &index);
void ItemsLoaded(QFuture<PlaylistItemList> future); void ItemsLoaded(QFuture<PlaylistItemList> future);
void SongInsertVetoListenerDestroyed(); void SongInsertVetoListenerDestroyed();
void AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result);
private: private:
bool is_loading_; bool is_loading_;
@@ -364,8 +366,10 @@ class Playlist : public QAbstractListModel {
bool favorite_; bool favorite_;
PlaylistItemList items_; PlaylistItemList items_;
// Contains the indices into items_ in the order that they will be played. // Contains the indices into items_ in the order that they will be played.
QList<int> virtual_items_; QList<int> virtual_items_;
// A map of collection ID to playlist item - for fast lookups when collection items change. // A map of collection ID to playlist item - for fast lookups when collection items change.
QMultiMap<int, PlaylistItemPtr> collection_items_by_id_; QMultiMap<int, PlaylistItemPtr> collection_items_by_id_;

View File

@@ -43,21 +43,21 @@ PlaylistItem::~PlaylistItem() {}
PlaylistItem *PlaylistItem::NewFromSource(const Song::Source &source) { PlaylistItem *PlaylistItem::NewFromSource(const Song::Source &source) {
switch (source) { switch (source) {
case Song::Source_Collection: return new CollectionPlaylistItem(); case Song::Source_Collection:
return new CollectionPlaylistItem();
case Song::Source_Subsonic:
case Song::Source_Tidal: case Song::Source_Tidal:
case Song::Source_Stream: return new InternetPlaylistItem(source); case Song::Source_Qobuz:
default: return new SongPlaylistItem(source); case Song::Source_Stream:
return new InternetPlaylistItem(source);
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Unknown:
break;
} }
} return new SongPlaylistItem(source);
PlaylistItem *PlaylistItem::NewFromSongsTable(const QString &table, const Song &song) {
if (table == SCollection::kSongsTable)
return new CollectionPlaylistItem(song);
qLog(Warning) << "Invalid PlaylistItem songs table:" << table;
return nullptr;
} }

View File

@@ -49,7 +49,6 @@ class PlaylistItem : public std::enable_shared_from_this<PlaylistItem> {
virtual ~PlaylistItem(); virtual ~PlaylistItem();
static PlaylistItem *NewFromSource(const Song::Source &source); static PlaylistItem *NewFromSource(const Song::Source &source);
static PlaylistItem *NewFromSongsTable(const QString &table, const Song &song);
enum Option { enum Option {
Default = 0x00, Default = 0x00,
@@ -87,6 +86,8 @@ class PlaylistItem : public std::enable_shared_from_this<PlaylistItem> {
qint64 effective_beginning_nanosec() const { return HasTemporaryMetadata() && temp_metadata_.is_valid() && temp_metadata_.beginning_nanosec() != -1 ? temp_metadata_.beginning_nanosec() : Metadata().beginning_nanosec(); } qint64 effective_beginning_nanosec() const { return HasTemporaryMetadata() && temp_metadata_.is_valid() && temp_metadata_.beginning_nanosec() != -1 ? temp_metadata_.beginning_nanosec() : Metadata().beginning_nanosec(); }
qint64 effective_end_nanosec() const { return HasTemporaryMetadata() && temp_metadata_.is_valid() && temp_metadata_.end_nanosec() != -1 ? temp_metadata_.end_nanosec() : Metadata().end_nanosec(); } qint64 effective_end_nanosec() const { return HasTemporaryMetadata() && temp_metadata_.is_valid() && temp_metadata_.end_nanosec() != -1 ? temp_metadata_.end_nanosec() : Metadata().end_nanosec(); }
virtual void SetArtManual(const QUrl &cover_url) = 0;
// Background colors. // Background colors.
void SetBackgroundColor(short priority, const QColor &color); void SetBackgroundColor(short priority, const QColor &color);
bool HasBackgroundColor(short priority) const; bool HasBackgroundColor(short priority) const;

View File

@@ -48,6 +48,8 @@
#include "core/utilities.h" #include "core/utilities.h"
#include "collection/collectionbackend.h" #include "collection/collectionbackend.h"
#include "collection/collectionplaylistitem.h" #include "collection/collectionplaylistitem.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/currentalbumcoverloader.h"
#include "playlist.h" #include "playlist.h"
#include "playlistbackend.h" #include "playlistbackend.h"
#include "playlistcontainer.h" #include "playlistcontainer.h"
@@ -147,6 +149,7 @@ Playlist *PlaylistManager::AddPlaylist(int id, const QString &name, const QStrin
connect(ret, SIGNAL(Error(QString)), SIGNAL(Error(QString))); connect(ret, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
connect(ret, SIGNAL(PlayRequested(QModelIndex)), SIGNAL(PlayRequested(QModelIndex))); connect(ret, SIGNAL(PlayRequested(QModelIndex)), SIGNAL(PlayRequested(QModelIndex)));
connect(playlist_container_->view(), SIGNAL(ColumnAlignmentChanged(ColumnAlignmentMap)), ret, SLOT(SetColumnAlignment(ColumnAlignmentMap))); connect(playlist_container_->view(), SIGNAL(ColumnAlignmentChanged(ColumnAlignmentMap)), ret, SLOT(SetColumnAlignment(ColumnAlignmentMap)));
connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)), ret, SLOT(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)));
playlists_[id] = Data(ret, name); playlists_[id] = Data(ret, name);
@@ -440,7 +443,8 @@ void PlaylistManager::UpdateSummaryText() {
QString summary; QString summary;
if (selected > 1) { if (selected > 1) {
summary += tr("%1 selected of").arg(selected) + " "; summary += tr("%1 selected of").arg(selected) + " ";
} else { }
else {
nanoseconds = current()->GetTotalLength(); nanoseconds = current()->GetTotalLength();
} }
@@ -466,7 +470,7 @@ void PlaylistManager::SongsDiscovered(const SongList &songs) {
for (const Song &song : songs) { for (const Song &song : songs) {
for (const Data &data : playlists_) { for (const Data &data : playlists_) {
PlaylistItemList items = data.p->collection_items_by_id(song.id()); PlaylistItemList items = data.p->collection_items_by_id(song.id());
for (const PlaylistItemPtr item : items) { for (PlaylistItemPtr item : items) {
if (item->Metadata().directory_id() != song.directory_id()) continue; if (item->Metadata().directory_id() != song.directory_id()) continue;
static_cast<CollectionPlaylistItem*>(item.get())->SetMetadata(song); static_cast<CollectionPlaylistItem*>(item.get())->SetMetadata(song);
item->UpdateTemporaryMetadata(song); item->UpdateTemporaryMetadata(song);

View File

@@ -75,6 +75,7 @@
#include "playlistheader.h" #include "playlistheader.h"
#include "playlistview.h" #include "playlistview.h"
#include "covermanager/currentalbumcoverloader.h" #include "covermanager/currentalbumcoverloader.h"
#include "covermanager/albumcoverloaderresult.h"
#include "settings/appearancesettingspage.h" #include "settings/appearancesettingspage.h"
#include "settings/playlistsettingspage.h" #include "settings/playlistsettingspage.h"
@@ -82,8 +83,6 @@
# include "moodbar/moodbaritemdelegate.h" # include "moodbar/moodbaritemdelegate.h"
#endif #endif
using std::sort;
const int PlaylistView::kGlowIntensitySteps = 24; const int PlaylistView::kGlowIntensitySteps = 24;
const int PlaylistView::kAutoscrollGraceTimeout = 30; // seconds const int PlaylistView::kAutoscrollGraceTimeout = 30; // seconds
const int PlaylistView::kDropIndicatorWidth = 2; const int PlaylistView::kDropIndicatorWidth = 2;
@@ -221,7 +220,7 @@ void PlaylistView::SetApplication(Application *app) {
Q_ASSERT(app); Q_ASSERT(app);
app_ = app; app_ = app;
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), this, SLOT(SongChanged(Song))); connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), this, SLOT(SongChanged(Song)));
connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, QUrl, QImage)), SLOT(AlbumCoverLoaded(Song, QUrl, QImage))); connect(app_->current_albumcover_loader(), SIGNAL(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(Song, AlbumCoverLoaderResult)));
connect(app_->player(), SIGNAL(Playing()), SLOT(StartGlowing())); connect(app_->player(), SIGNAL(Playing()), SLOT(StartGlowing()));
connect(app_->player(), SIGNAL(Paused()), SLOT(StopGlowing())); connect(app_->player(), SIGNAL(Paused()), SLOT(StopGlowing()));
connect(app_->player(), SIGNAL(Stopped()), SLOT(Stopped())); connect(app_->player(), SIGNAL(Stopped()), SLOT(Stopped()));
@@ -1261,17 +1260,15 @@ void PlaylistView::Stopped() {
if (song_playing_ == Song()) return; if (song_playing_ == Song()) return;
song_playing_ = Song(); song_playing_ = Song();
StopGlowing(); StopGlowing();
AlbumCoverLoaded(Song(), QUrl(), QImage()); AlbumCoverLoaded(Song());
} }
void PlaylistView::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &song_art) { void PlaylistView::AlbumCoverLoaded(const Song &song, AlbumCoverLoaderResult result) {
Q_UNUSED(cover_url); if ((song != Song() && song_playing_ == Song()) || result.image_original == current_song_cover_art_) return;
if ((song != Song() && song_playing_ == Song()) || song_art == current_song_cover_art_) return; current_song_cover_art_ = result.image_original;
current_song_cover_art_ = song_art;
if (background_image_type_ == AppearanceSettingsPage::BackgroundImageType_Album) { if (background_image_type_ == AppearanceSettingsPage::BackgroundImageType_Album) {
if (song.art_automatic().isEmpty() && song.art_manual().isEmpty()) { if (song.art_automatic().isEmpty() && song.art_manual().isEmpty()) {
set_background_image(QImage()); set_background_image(QImage());

View File

@@ -47,6 +47,7 @@
#include <QCommonStyle> #include <QCommonStyle>
#include "core/song.h" #include "core/song.h"
#include "covermanager/albumcoverloaderresult.h"
#include "settings/appearancesettingspage.h" #include "settings/appearancesettingspage.h"
#include "playlist.h" #include "playlist.h"
@@ -173,7 +174,7 @@ class PlaylistView : public QTreeView {
void Playing(); void Playing();
void Stopped(); void Stopped();
void SongChanged(const Song &song); void SongChanged(const Song &song);
void AlbumCoverLoaded(const Song &new_song, const QUrl &cover_url, const QImage &song_art); void AlbumCoverLoaded(const Song &song, AlbumCoverLoaderResult result = AlbumCoverLoaderResult());
private: private:
void LoadGeometry(); void LoadGeometry();

View File

@@ -48,3 +48,10 @@ Song SongPlaylistItem::Metadata() const {
if (HasTemporaryMetadata()) return temp_metadata_; if (HasTemporaryMetadata()) return temp_metadata_;
return song_; return song_;
} }
void SongPlaylistItem::SetArtManual(const QUrl &cover_url) {
song_.set_art_manual(cover_url);
temp_metadata_.set_art_manual(cover_url);
}

View File

@@ -44,8 +44,8 @@ class SongPlaylistItem : public PlaylistItem {
QUrl Url() const; QUrl Url() const;
protected:
Song DatabaseSongMetadata() const { return song_; } Song DatabaseSongMetadata() const { return song_; }
void SetArtManual(const QUrl &cover_url);
private: private:
Song song_; Song song_;

View File

@@ -48,8 +48,6 @@
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "playingwidget.h" #include "playingwidget.h"
using std::unique_ptr;
const char *PlayingWidget::kSettingsGroup = "PlayingWidget"; const char *PlayingWidget::kSettingsGroup = "PlayingWidget";
// Space between the cover and the details in small mode // Space between the cover and the details in small mode
@@ -269,9 +267,7 @@ void PlayingWidget::SongChanged(const Song &song) {
song_ = song; song_ = song;
} }
void PlayingWidget::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) { void PlayingWidget::AlbumCoverLoaded(const Song &song, const QImage &image) {
Q_UNUSED(cover_url);
if (!playing_ || song != song_playing_ || (timeline_fade_->state() == QTimeLine::Running && image == image_original_)) return; if (!playing_ || song != song_playing_ || (timeline_fade_->state() == QTimeLine::Running && image == image_original_)) return;
@@ -313,7 +309,7 @@ void PlayingWidget::SetImage(const QImage &image) {
} }
void PlayingWidget::ScaleCover() { void PlayingWidget::ScaleCover() {
pixmap_cover_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_)); pixmap_cover_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_).first);
update(); update();
} }

View File

@@ -40,8 +40,6 @@
#include "core/song.h" #include "core/song.h"
#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderoptions.h"
using std::unique_ptr;
class QTimeLine; class QTimeLine;
class QTextDocument; class QTextDocument;
class QPainter; class QPainter;
@@ -98,7 +96,7 @@ class PlayingWidget : public QWidget {
void AutomaticCoverSearchDone(); void AutomaticCoverSearchDone();
void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image); void AlbumCoverLoaded(const Song &song, const QImage &image);
void SetHeight(int height); void SetHeight(int height);
void FadePreviousTrack(qreal value); void FadePreviousTrack(qreal value);