Commit e6b0c721 authored by Stefan Vukanović's avatar Stefan Vukanović Committed by Nate Graham
Browse files

Use up to four latest album or track covers as artist cover

When retrieving artist data, find up to four latest album or track covers
and display them as the artist's cover in a grid layout. If four covers
can't be found, display only the latest one.

This should improve the UX of the Artists view, which right now is a sea
of identical icons.

BUG: 406475
FIXED-IN: 22.12
parent 7670077b
Pipeline #218310 passed with stage
in 5 minutes and 32 seconds
......@@ -128,7 +128,9 @@ public:
mUpdateTrackPriority(mTracksDatabase), mUpdateTrackFileModifiedTime(mTracksDatabase),
mSelectTracksMapping(mTracksDatabase), mSelectTracksMappingPriority(mTracksDatabase),
mSelectRadioIdFromHttpAddress(mTracksDatabase),
mUpdateAlbumArtUriFromAlbumIdQuery(mTracksDatabase), mSelectTracksMappingPriorityByTrackId(mTracksDatabase),
mUpdateAlbumArtUriFromAlbumIdQuery(mTracksDatabase),
mSelectUpToFourLatestCoversFromArtistNameQuery(mTracksDatabase),
mSelectTracksMappingPriorityByTrackId(mTracksDatabase),
mSelectAlbumIdsFromArtist(mTracksDatabase), mSelectAllTrackFilesQuery(mTracksDatabase),
mRemoveTracksMappingFromSource(mTracksDatabase), mRemoveTracksMapping(mTracksDatabase),
mSelectTracksWithoutMappingQuery(mTracksDatabase), mSelectAlbumIdFromTitleAndArtistQuery(mTracksDatabase),
......@@ -229,6 +231,8 @@ public:
QSqlQuery mUpdateAlbumArtUriFromAlbumIdQuery;
QSqlQuery mSelectUpToFourLatestCoversFromArtistNameQuery;
QSqlQuery mSelectTracksMappingPriorityByTrackId;
QSqlQuery mSelectAlbumIdsFromArtist;
......@@ -353,7 +357,7 @@ public:
QSet<qulonglong> mInsertedAlbums;
QSet<QPair<qulonglong, QString>> mInsertedArtists;
QSet<qulonglong> mInsertedArtists;
qulonglong mAlbumId = 1;
......@@ -1191,11 +1195,10 @@ void DatabaseInterface::insertTracksList(const DataTypes::ListTrackDataType &tra
if (!d->mInsertedArtists.isEmpty()) {
DataTypes::ListArtistDataType newArtists;
for (auto newArtistData : std::as_const(d->mInsertedArtists)) {
newArtists.push_back({{DataTypes::DatabaseIdRole, newArtistData.first},
{DataTypes::TitleRole, newArtistData.second},
{DataTypes::ElementTypeRole, ElisaUtils::Artist}});
for (auto newArtistId : std::as_const(d->mInsertedArtists)) {
newArtists.push_back(internalOneArtistPartialData(newArtistId));
}
qCInfo(orgKdeElisaDatabase) << "artistsAdded" << newArtists.size();
Q_EMIT artistsAdded(newArtists);
}
......@@ -1252,22 +1255,20 @@ void DatabaseInterface::removeTracksList(const QList<QUrl> &removedTracks)
internalRemoveTracksList(removedTracks);
if (!d->mInsertedArtists.isEmpty()) {
DataTypes::ListArtistDataType newArtists;
for (auto newArtistData : std::as_const(d->mInsertedArtists)) {
newArtists.push_back({{DataTypes::DatabaseIdRole, newArtistData.first},
{DataTypes::TitleRole, newArtistData.second},
{DataTypes::ElementTypeRole, ElisaUtils::Artist}});
}
Q_EMIT artistsAdded(newArtists);
}
transactionResult = finishTransaction();
if (!transactionResult) {
Q_EMIT finishRemovingTracksList();
return;
}
if (!d->mInsertedArtists.isEmpty()) {
DataTypes::ListArtistDataType newArtists;
for (auto newArtistId : std::as_const(d->mInsertedArtists)) {
newArtists.push_back(internalOneArtistPartialData(newArtistId));
}
Q_EMIT artistsAdded(newArtists);
}
Q_EMIT finishRemovingTracksList();
}
......@@ -6059,6 +6060,38 @@ void DatabaseInterface::initRequest()
}
}
{
auto selectUpToFourLatestCoversFromArtistNameQueryText = QStringLiteral("SELECT "
"(CASE WHEN (album.`CoverFileName` IS NOT NULL AND "
"album.`CoverFileName` IS NOT '') THEN album.`CoverFileName` "
"ELSE track.`FileName` END) AS CoverFileName, "
"(album.`CoverFileName` IS NULL OR "
"album.`CoverFileName` IS '') AS IsTrackCover "
"FROM "
"`Tracks` track LEFT OUTER JOIN `Albums` album ON "
"album.`Title` = track.`AlbumTitle` AND "
"album.`ArtistName` = track.`AlbumArtistName` AND "
"album.`AlbumPath` = track.`AlbumPath` "
"WHERE "
"(track.`HasEmbeddedCover` = 1 OR "
"(album.`CoverFileName` IS NOT NULL AND "
"album.`CoverFileName` IS NOT '')) AND "
"(track.`ArtistName` = :artistName OR "
"track.`AlbumArtistName` = :artistName) "
"GROUP BY track.`AlbumTitle` "
"ORDER BY track.`Year` DESC "
"LIMIT 4 ");
auto result = prepareQuery(d->mSelectUpToFourLatestCoversFromArtistNameQuery, selectUpToFourLatestCoversFromArtistNameQueryText);
if (!result) {
qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectUpToFourLatestCoversFromArtistNameQuery.lastQuery();
qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::initRequest" << d->mSelectUpToFourLatestCoversFromArtistNameQuery.lastError();
Q_EMIT databaseError();
}
}
{
auto selectTracksFromArtistQueryText = QStringLiteral("SELECT "
"tracks.`ID`, "
......@@ -6650,7 +6683,7 @@ qulonglong DatabaseInterface::insertArtist(const QString &name)
++d->mArtistId;
d->mInsertedArtists.insert({result, name});
d->mInsertedArtists.insert(result);
d->mInsertArtistsQuery.finish();
......@@ -8138,6 +8171,11 @@ DataTypes::ListArtistDataType DatabaseInterface::internalAllArtistsPartialData(Q
newData[DataTypes::DatabaseIdRole] = currentRecord.value(0);
newData[DataTypes::TitleRole] = currentRecord.value(1);
newData[DataTypes::GenreRole] = QVariant::fromValue(currentRecord.value(2).toString().split(QStringLiteral(", ")));
const auto covers = internalGetLatestFourCoversForArtist(currentRecord.value(1).toString());
newData[DataTypes::MultipleImageUrlsRole] = covers;
newData[DataTypes::ImageUrlRole] = covers.value(0).toUrl();
newData[DataTypes::ElementTypeRole] = ElisaUtils::Artist;
result.push_back(newData);
......@@ -8295,6 +8333,11 @@ DataTypes::ArtistDataType DatabaseInterface::internalOneArtistPartialData(qulong
result[DataTypes::DatabaseIdRole] = currentRecord.value(0);
result[DataTypes::TitleRole] = currentRecord.value(1);
result[DataTypes::GenreRole] = QVariant::fromValue(currentRecord.value(2).toString().split(QStringLiteral(", ")));
const auto covers = internalGetLatestFourCoversForArtist(currentRecord.value(1).toString());
result[DataTypes::MultipleImageUrlsRole] = covers;
result[DataTypes::ImageUrlRole] = covers.value(0).toUrl();
result[DataTypes::ElementTypeRole] = ElisaUtils::Artist;
}
......@@ -8631,6 +8674,40 @@ bool DatabaseInterface::updateAlbumCover(qulonglong albumId, const QUrl &albumAr
return modifiedAlbum;
}
QVariantList DatabaseInterface::internalGetLatestFourCoversForArtist(const QString& artistName) {
auto covers = QList<QVariant>{};
d->mSelectUpToFourLatestCoversFromArtistNameQuery.bindValue(QStringLiteral(":artistName"), artistName);
auto queryResult = execQuery(d->mSelectUpToFourLatestCoversFromArtistNameQuery);
if (!queryResult || !d->mSelectUpToFourLatestCoversFromArtistNameQuery.isSelect() || !d->mSelectUpToFourLatestCoversFromArtistNameQuery.isActive()) {
Q_EMIT databaseError();
qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalGetLatestFourCoversForArtist" << d->mSelectUpToFourLatestCoversFromArtistNameQuery.lastQuery();
qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalGetLatestFourCoversForArtist" << d->mSelectUpToFourLatestCoversFromArtistNameQuery.boundValues();
qCDebug(orgKdeElisaDatabase) << "DatabaseInterface::internalGetLatestFourCoversForArtist" << d->mSelectUpToFourLatestCoversFromArtistNameQuery.lastError();
d->mSelectUpToFourLatestCoversFromArtistNameQuery.finish();
return covers;
}
while (d->mSelectUpToFourLatestCoversFromArtistNameQuery.next()) {
const auto& cover = d->mSelectUpToFourLatestCoversFromArtistNameQuery.record().value(0).toUrl();
const auto& isTrackCover = d->mSelectUpToFourLatestCoversFromArtistNameQuery.record().value(1).toBool();
if (isTrackCover) {
covers.push_back(QVariant {QLatin1String {"image://cover/"} + cover.toLocalFile()}.toUrl());
} else {
covers.push_back(cover);
}
}
d->mSelectUpToFourLatestCoversFromArtistNameQuery.finish();
return covers;
}
void DatabaseInterface::updateTrackStatistics(const QUrl &fileName, const QDateTime &time)
{
d->mUpdateTrackStatistics.bindValue(QStringLiteral(":fileName"), fileName);
......
......@@ -290,6 +290,8 @@ private:
bool updateAlbumCover(qulonglong albumId, const QUrl &albumArtUri);
QVariantList internalGetLatestFourCoversForArtist(const QString& artistName);
void updateTrackStatistics(const QUrl &fileName, const QDateTime &time);
void createDatabaseV9();
......
......@@ -72,6 +72,7 @@ public:
IsPlayListRole,
FilePathRole,
HasChildrenRole,
MultipleImageUrlsRole,
};
Q_ENUM(ColumnsRoles)
......@@ -388,6 +389,10 @@ public:
return operator[](key_type::DatabaseIdRole).toULongLong();
}
[[nodiscard]] QUrl artistArtURI() const
{
return operator[](key_type::ImageUrlRole).toUrl();
}
};
using ListArtistDataType = QList<ArtistDataType>;
......
......@@ -44,6 +44,8 @@ public:
KFileMetaData::ExtractorCollection ec;
KFileMetaData::SimpleExtractionResult result(mId, fileMimeType, KFileMetaData::ExtractionResult::ExtractImageData);
mErrorMessage = QLatin1String{""};
auto extractors = ec.fetchExtractors(fileMimeType);
for (const auto& ex : extractors) {
ex->extract(&result);
......@@ -51,23 +53,39 @@ public:
auto imageData = result.imageData();
if (!imageData.isEmpty()) {
if (imageData.contains(KFileMetaData::EmbeddedImageData::FrontCover)) {
mCoverImage = QImage::fromData(imageData[KFileMetaData::EmbeddedImageData::FrontCover]);
} else {
mCoverImage = QImage::fromData(imageData.values().first());
}
auto newCoverImage = mCoverImage.scaled(mRequestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (!newCoverImage.isNull()) {
mCoverImage = std::move(newCoverImage);
}
if (imageData.isEmpty()) {
mErrorMessage = QString{QLatin1String{"Unable to load image data from "} + mId};
Q_EMIT finished();
return;
}
if (imageData.contains(KFileMetaData::EmbeddedImageData::FrontCover)) {
mCoverImage = QImage::fromData(imageData[KFileMetaData::EmbeddedImageData::FrontCover]);
} else {
mCoverImage = QImage::fromData(imageData.values().first());
}
if (mCoverImage.isNull()) {
mErrorMessage = QString{QLatin1String{"Invalid embedded cover image in "} + mId};
Q_EMIT finished();
return;
}
auto newCoverImage = mCoverImage.scaled(mRequestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (!newCoverImage.isNull()) {
mCoverImage = std::move(newCoverImage);
}
Q_EMIT finished();
}
QString errorString() const override
{
return mErrorMessage;
}
QString mId;
QString mErrorMessage;
QSize mRequestedSize;
QImage mCoverImage;
};
......
......@@ -76,6 +76,7 @@ QHash<int, QByteArray> DataModel::roleNames() const
roles[static_cast<int>(DataTypes::ColumnsRoles::TitleRole)] = "title";
roles[static_cast<int>(DataTypes::ColumnsRoles::SecondaryTextRole)] = "secondaryText";
roles[static_cast<int>(DataTypes::ColumnsRoles::ImageUrlRole)] = "imageUrl";
roles[static_cast<int>(DataTypes::ColumnsRoles::MultipleImageUrlsRole)] = "multipleImageUrls";
roles[static_cast<int>(DataTypes::ColumnsRoles::DatabaseIdRole)] = "databaseId";
roles[static_cast<int>(DataTypes::ColumnsRoles::ElementTypeRole)] = "dataType";
roles[static_cast<int>(DataTypes::ColumnsRoles::ResourceRole)] = "url";
......
......@@ -45,6 +45,7 @@ AbstractDataView {
fileUrl: model.url ? model.url : ""
secondaryText: gridView.delegateDisplaySecondaryText && model.secondaryText ? model.secondaryText : ""
imageUrl: model.imageUrl ? model.imageUrl : ''
multipleImageUrls: model.multipleImageUrls
imageFallbackUrl: defaultIcon
databaseId: model.databaseId
delegateDisplaySecondaryText: gridView.delegateDisplaySecondaryText
......
......@@ -21,6 +21,7 @@ FocusScope {
property url imageUrl
property url imageFallbackUrl
property var multipleImageUrls
property url fileUrl
property var entryType
property string mainText
......@@ -139,6 +140,58 @@ FocusScope {
text: mainLabel.text
}
component CoverImage: ImageWithFallback {
id: coverImage
property var imageSource
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: imageSource ? imageSource : ""
fallback: gridEntry.imageFallbackUrl
asynchronous: true
layer.enabled: !coverImage.usingFallback && !Kirigami.Settings.isMobile // don't use drop shadow on mobile
layer.effect: DropShadow {
source: coverImage
radius: 10
spread: 0.1
samples: 21
color: myPalette.shadow
}
}
Component {
id: quartersCover
Grid {
rows: 2
columns: 2
component QuarterImage: CoverImage {
width: parent.width / 2
height: parent.height / 2
}
QuarterImage {imageSource: gridEntry.multipleImageUrls[0]}
QuarterImage {imageSource: gridEntry.multipleImageUrls[1]}
QuarterImage {imageSource: gridEntry.multipleImageUrls[2]}
QuarterImage {imageSource: gridEntry.multipleImageUrls[3]}
}
}
Component {
id: singleCover
CoverImage {
width: parent.width
height: parent.height
imageSource: gridEntry.imageUrl
}
}
// cover image
Loader {
id: coverImageLoader
......@@ -151,29 +204,8 @@ FocusScope {
active: gridEntry.delegateLoaded && !isPartial
sourceComponent: ImageWithFallback {
id: coverImage
sourceSize.width: parent.width
sourceSize.height: parent.height
fillMode: Image.PreserveAspectFit
source: gridEntry.imageUrl
fallback: gridEntry.imageFallbackUrl
asynchronous: true
layer.enabled: !coverImage.usingFallback && !Kirigami.Settings.isMobile // don't use drop shadow on mobile
layer.effect: DropShadow {
source: coverImage
radius: 10
spread: 0.1
samples: 21
color: myPalette.shadow
}
}
sourceComponent: gridEntry.multipleImageUrls && gridEntry.multipleImageUrls.length == 4
? quartersCover : singleCover
}
// ========== desktop hover actions ==========
......
......@@ -322,7 +322,7 @@ void ViewsListData::artistsAdded(const DataTypes::ListArtistDataType &newData)
Q_EMIT dataAboutToBeAdded(d->mViewsParameters.size(), d->mViewsParameters.size() + newData.size() - 1);
for (const auto &oneArtist : newData) {
d->mViewsParameters.push_back({oneArtist.name(),
QUrl{QStringLiteral("image://icon/view-media-artist")},
oneArtist.artistArtURI(),
ViewManager::GridView,
ViewManager::GenericDataModel,
ElisaUtils::FilterByArtist,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment