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

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* 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
* it under the terms of the GNU General Public License as published by
@@ -96,6 +96,7 @@
#include "dialogs/console.h"
#include "dialogs/trackselectiondialog.h"
#include "dialogs/edittagdialog.h"
#include "dialogs/addstreamdialog.h"
#include "organise/organisedialog.h"
#include "widgets/fancytabwidget.h"
#include "widgets/playingwidget.h"
@@ -131,7 +132,7 @@
#endif
#include "covermanager/albumcovermanager.h"
#include "covermanager/albumcoverchoicecontroller.h"
#include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/currentalbumcoverloader.h"
#ifndef Q_OS_WIN
# include "device/devicemanager.h"
@@ -178,10 +179,6 @@
# include "windows7thumbbar.h"
#endif
using std::bind;
using std::floor;
using std::stable_sort;
const char *MainWindow::kSettingsGroup = "MainWindow";
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);
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
subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source_Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, this)),
#endif
@@ -260,7 +262,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
// Initialise the UI
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);
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()));
@@ -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_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_repeat_mode->setIcon(IconLoader::Load("media-playlist-repeat"));
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_add_file, SIGNAL(triggered()), SLOT(AddFile()));
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_equalizer, SIGNAL(triggered()), equalizer_.get(), SLOT(show()));
#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(Stopped()), context_view_, SLOT(Stopped()));
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(context_view_, SIGNAL(AlbumEnabledChanged()), SLOT(TabSwitched()));
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(Error()), ui_->widget_playing, SLOT(Error()));
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(ui_->action_console, SIGNAL(triggered()), SLOT(ShowConsole()));
@@ -1777,7 +1781,7 @@ void MainWindow::EditTracks() {
void MainWindow::EditTagDialogAccepted() {
for (const PlaylistItemPtr item : edit_tag_dialog_->playlist_items()) {
for (PlaylistItemPtr item : edit_tag_dialog_->playlist_items()) {
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() {
// Show the first valid selected track artist/album in CollectionView
@@ -2512,7 +2526,7 @@ void MainWindow::AutoCompleteTags() {
void MainWindow::AutoCompleteTagsAccepted() {
for (const PlaylistItemPtr item : autocomplete_tag_items_) {
for (PlaylistItemPtr item : autocomplete_tag_items_) {
item->Reload();
}
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;
image_original_ = image;
image_original_ = result.image_original;
emit AlbumCoverReady(song, cover_url, image);
emit AlbumCoverReady(song, result.image_original);
GetCoverAutomatically();
@@ -2617,13 +2631,12 @@ void MainWindow::GetCoverAutomatically() {
// Search for cover automatically?
bool search =
song_.source() == Song::Source_Collection &&
album_cover_choice_controller_->search_cover_auto_action()->isChecked() &&
!song_.has_manually_unset_cover() &&
!song_.art_automatic_is_valid() &&
!song_.art_manual_is_valid() &&
!song_.effective_albumartist().isEmpty() &&
!song_.effective_album().isEmpty();
album_cover_choice_controller_->search_cover_auto_action()->isChecked() &&
!song_.has_manually_unset_cover() &&
!song_.art_automatic_is_valid() &&
!song_.art_manual_is_valid() &&
!song_.effective_albumartist().isEmpty() &&
!song_.effective_album().isEmpty();
if (search) {
album_cover_choice_controller_->SearchCoverAutomatically(song_);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* 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
* it under the terms of the GNU General Public License as published by
@@ -59,8 +59,7 @@
#include "playlist/playlistitem.h"
#include "settings/settingsdialog.h"
#include "settings/behavioursettingspage.h"
using std::unique_ptr;
#include "covermanager/albumcoverloaderresult.h"
class About;
class AlbumCoverManager;
@@ -95,6 +94,7 @@ class InternetTabsView;
#ifdef Q_OS_WIN
class Windows7ThumbBar;
#endif
class AddStreamDialog;
class MainWindow : public QMainWindow, public PlatformInterface {
Q_OBJECT
@@ -126,7 +126,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
bool LoadUrl(const QString& url);
signals:
void AlbumCoverReady(const Song &song, const QUrl &cover_url, const QImage &image);
void AlbumCoverReady(const Song &song, const QImage &image);
void SearchCoverInProgress();
// Signals that stop playing after track was toggled.
void StopAfterToggled(bool stop);
@@ -210,6 +210,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void AddFile();
void AddFolder();
void AddCDTracks();
void AddStream();
void AddStreamAccepted();
void CommandlineOptionsReceived(const quint32 instanceId, const QByteArray &string_options);
@@ -251,7 +253,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void UnsetCover();
void ShowCover();
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 ScrobbleButtonVisibilityChanged(const bool value);
@@ -308,6 +310,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
#ifdef HAVE_GSTREAMER
Lazy<TranscodeDialog> transcode_dialog_;
#endif
Lazy<AddStreamDialog> add_stream_dialog_;
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
std::unique_ptr<TagFetcher> tag_fetcher_;

View File

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

View File

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

View File

@@ -58,14 +58,13 @@
#include "playlist/playlistmanager.h"
#include "playlist/playlistsequence.h"
#include "covermanager/currentalbumcoverloader.h"
#include "covermanager/albumcoverloaderresult.h"
#include <core/mpris2_player.h>
#include <core/mpris2_playlists.h>
#include <core/mpris2_root.h>
#include <core/mpris2_tracklist.h>
using std::reverse;
QDBusArgument &operator<<(QDBusArgument &arg, const MprisPlaylist &playlist) {
arg.beginStructure();
arg << playlist.id << playlist.name << playlist.icon;
@@ -122,7 +121,7 @@ Mpris2::Mpris2(Application *app, QObject *parent)
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(), 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...
void Mpris2::CurrentSongChanged(const Song &song) {
AlbumCoverLoaded(song, QUrl(), QImage());
AlbumCoverLoaded(song);
EmitNotification("CanPlay");
EmitNotification("CanPause");
EmitNotification("CanGoNext", CanGoNext());
@@ -388,9 +387,7 @@ void Mpris2::CurrentSongChanged(const Song &song) {
}
// ... and we add the cover information later, when it's available.
void Mpris2::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image) {
Q_UNUSED(image);
void Mpris2::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
last_metadata_ = QVariantMap();
song.ToXesam(&last_metadata_);
@@ -398,9 +395,14 @@ void Mpris2::AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QIm
using mpris::AddMetadata;
AddMetadata("mpris:trackid", current_track_id(), &last_metadata_);
if (cover_url.isValid()) {
AddMetadata("mpris:artUrl", cover_url.toString(), &last_metadata_);
QUrl cover_url;
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("bitrate", song.bitrate(), &last_metadata_);

View File

@@ -39,6 +39,7 @@
#include <QJsonObject>
#include "engine/engine_fwd.h"
#include "covermanager/albumcoverloaderresult.h"
class Application;
class Song;
@@ -204,7 +205,7 @@ signals:
void PlaylistChanged(const MprisPlaylist &playlist);
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 VolumeChanged();

View File

@@ -30,6 +30,7 @@
#include <QObject>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QSharedData>
#include <QHash>
#include <QByteArray>
@@ -42,6 +43,7 @@
#include <QIcon>
#include <QTextCodec>
#include <QSqlQuery>
#include <QStandardPaths>
#include <QtDebug>
#ifdef HAVE_LIBGPOD
@@ -66,7 +68,6 @@
#include "covermanager/albumcoverloader.h"
#include "tagreadermessages.pb.h"
using std::sort;
#ifndef USE_SYSTEM_TAGLIB
using namespace Strawberry_TagLib;
#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::art_automatic_is_valid() const {
return (
return !d->art_automatic_.isEmpty() &&
(
(d->art_automatic_.path() == kManuallyUnsetCover) ||
(d->art_automatic_.path() == kEmbeddedCover) ||
(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 {
return (
return !d->art_manual_.isEmpty() &&
(
(d->art_manual_.path() == kManuallyUnsetCover) ||
(d->art_manual_.path() == kEmbeddedCover) ||
(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) {
return song1.PrettyTitleWithArtist().localeAwareCompare(song2.PrettyTitleWithArtist()) < 0;
}
@@ -997,16 +1023,24 @@ void Song::InitFromFilePartial(const QString &filename) {
void Song::InitArtManual() {
QString album = d->album_;
QString album = effective_album();
album.remove(Song::kAlbumRemoveDisc);
// 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 path(AlbumCoverLoader::ImageCacheDir(d->source_) + "/" + filename);
QString path(ImageCacheDir(d->source_) + "/" + filename);
if (QFile::exists(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 FiletypeByDescription(const QString &text);
static FileType FiletypeByExtension(const QString &ext);
static QString ImageCacheDir(const Song::Source source);
// Sort songs alphabetically using their pretty title
static void SortSongsListAlphabetically(QList<Song> *songs);

View File

@@ -40,7 +40,7 @@ StandardItemIconLoader::StandardItemIconLoader(AlbumCoverLoader *cover_loader, Q
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) {
@@ -96,15 +96,13 @@ void StandardItemIconLoader::ModelReset() {
}
void StandardItemIconLoader::ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) {
Q_UNUSED(cover_url);
void StandardItemIconLoader::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult result) {
QStandardItem *item = pending_covers_.take(id);
if (!item) return;
if (!image.isNull()) {
item->setIcon(QIcon(QPixmap::fromImage(image)));
if (!result.image_scaled.isNull()) {
item->setIcon(QIcon(QPixmap::fromImage(result.image_scaled)));
}
}

View File

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