/* * Strawberry Music Player * This code was part of Clementine. * Copyright 2010, David Sansome * Copyright 2013-2021, Jonas Kvinge * * 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 . * */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/application.h" #include "core/database.h" #include "core/iconloader.h" #include "collection/collectionquery.h" #include "collection/collectionbackend.h" #include "collection/collectionitem.h" #include "collection/sqlrow.h" #include "playlist/playlistmanager.h" #include "playlist/songmimedata.h" #include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderresult.h" #include "contextalbumsmodel.h" const int ContextAlbumsModel::kPrettyCoverSize = 32; ContextAlbumsModel::ContextAlbumsModel(CollectionBackend *backend, Application *app, QObject *parent) : SimpleTreeModel(new CollectionItem(this), parent), backend_(backend), app_(app), album_icon_(IconLoader::Load("cdcase")) { root_->lazy_loaded = true; cover_loader_options_.get_image_data_ = false; cover_loader_options_.scale_output_image_ = true; cover_loader_options_.pad_output_image_ = true; cover_loader_options_.desired_height_ = kPrettyCoverSize; QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &ContextAlbumsModel::AlbumCoverLoaded); QIcon nocover = IconLoader::Load("cdcase"); QList nocover_sizes = nocover.availableSizes(); no_cover_icon_ = nocover.pixmap(nocover_sizes.last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } ContextAlbumsModel::~ContextAlbumsModel() { delete root_; } void ContextAlbumsModel::AddSongs(const SongList &songs) { for (const Song &song : songs) { if (song_nodes_.contains(song.id())) continue; // Before we can add each song we need to make sure the required container items already exist in the tree. // Find parent containers in the tree CollectionItem *container = root_; // Does it exist already? if (!container_nodes_.contains(song.album())) { // Create the container container_nodes_[song.album()] = ItemFromSong(CollectionItem::Type_Container, true, container, song, 0); } container = container_nodes_[song.album()]; if (!container->lazy_loaded) continue; // We've gone all the way down to the deepest level and everything was already lazy loaded, so now we have to create the song in the container. song_nodes_[song.id()] = ItemFromSong(CollectionItem::Type_Song, true, container, song, -1); } } QString ContextAlbumsModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const { QStringList path; QModelIndex index_copy(idx); while (index_copy.isValid()) { path.prepend(index_copy.data().toString()); index_copy = index_copy.parent(); } return "contextalbumsart:" + path.join("/"); } QVariant ContextAlbumsModel::AlbumIcon(const QModelIndex &idx) { CollectionItem *item = IndexToItem(idx); if (!item) return no_cover_icon_; // Check the cache for a pixmap we already loaded. const QString cache_key = AlbumIconPixmapCacheKey(idx); QPixmap cached_pixmap; if (QPixmapCache::find(cache_key, &cached_pixmap)) { return cached_pixmap; } // Maybe we're loading a pixmap already? if (pending_cache_keys_.contains(cache_key)) { return no_cover_icon_; } // No art is cached and we're not loading it already. Load art for the first song in the album. SongList songs = GetChildSongs(idx); if (!songs.isEmpty()) { const quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, songs.first()); pending_art_[id] = ItemAndCacheKey(item, cache_key); pending_cache_keys_.insert(cache_key); } return no_cover_icon_; } void ContextAlbumsModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) { if (!pending_art_.contains(id)) return; ItemAndCacheKey item_and_cache_key = pending_art_.take(id); CollectionItem *item = item_and_cache_key.first; if (!item) return; const QString &cache_key = item_and_cache_key.second; pending_cache_keys_.remove(cache_key); // Insert this image in the cache. if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) { // Set the no_cover image so we don't continually try to load art. QPixmapCache::insert(cache_key, no_cover_icon_); } else { QPixmap image_pixmap; image_pixmap = QPixmap::fromImage(result.image_scaled); QPixmapCache::insert(cache_key, image_pixmap); } const QModelIndex idx = ItemToIndex(item); emit dataChanged(idx, idx); } QVariant ContextAlbumsModel::data(const QModelIndex &idx, int role) const { const CollectionItem *item = IndexToItem(idx); if (role == Qt::DecorationRole && item->type == CollectionItem::Type_Container && item->container_level == 0) { return const_cast(this)->AlbumIcon(idx); } return data(item, role); } QVariant ContextAlbumsModel::data(const CollectionItem *item, int role) const { switch (role) { case Qt::DisplayRole: case Qt::ToolTipRole: return item->DisplayText(); case Qt::DecorationRole: switch (item->type) { case CollectionItem::Type_Container: if (item->type == CollectionItem::Type_Container && item->container_level == 0) { return album_icon_; } break; default: break; } break; case Role_Type: return item->type; case Role_ContainerType: return item->type; case Role_Key: return item->key; case Role_Artist: return item->metadata.artist(); case Role_Editable: if (!item->lazy_loaded) { const_cast(this)->LazyPopulate(const_cast(item), true); } if (item->type == CollectionItem::Type_Container) { // if we have even one non editable item as a child, we ourselves are not available for edit if (!item->children.isEmpty()) { for (CollectionItem *child : item->children) { if (!data(child, role).toBool()) { return false; } } return true; } else { return false; } } else if (item->type == CollectionItem::Type_Song) { return item->metadata.IsEditable(); } else { return false; } case Role_SortText: return item->SortText(); } return QVariant(); } ContextAlbumsModel::QueryResult ContextAlbumsModel::RunQuery(CollectionItem *parent) { QMutexLocker l(backend_->db()->Mutex()); QSqlDatabase db(backend_->db()->Connect()); QueryResult result; CollectionQuery q(db, backend_->songs_table(), backend_->fts_table(), query_options_); q.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec); // Walk up through the item's parents adding filters as necessary CollectionItem *p = parent; while (p && p->type == CollectionItem::Type_Container) { if (p->container_level == 0) { q.AddWhere("album", p->key); } p = p->parent; } // Execute the query if (!q.Exec()) return result; while (q.Next()) { result.rows << SqlRow(q); } return result; } void ContextAlbumsModel::PostQuery(CollectionItem *parent, const ContextAlbumsModel::QueryResult &result, bool signal) { int child_level = (parent == root_ ? 0 : parent->container_level + 1); for (const SqlRow &row : result.rows) { CollectionItem::Type item_type = (parent == root_ ? CollectionItem::Type_Container : CollectionItem::Type_Song); if (signal) beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count()); CollectionItem *item = new CollectionItem(item_type, parent); item->container_level = child_level; item->metadata.InitFromQuery(row, true); item->key = item->metadata.title(); item->display_text = item->metadata.TitleWithCompilationArtist(); item->sort_text = SortTextForSong(item->metadata); if (parent != root_) item->lazy_loaded = true; if (signal) endInsertRows(); if (parent == root_) container_nodes_[item->key] = item; else song_nodes_[item->metadata.id()] = item; } } void ContextAlbumsModel::LazyPopulate(CollectionItem *parent, const bool signal) { if (parent->lazy_loaded) return; parent->lazy_loaded = true; QueryResult result = RunQuery(parent); PostQuery(parent, result, signal); } void ContextAlbumsModel::Reset() { QMap::iterator i = container_nodes_.begin(); while (i != container_nodes_.end()) { const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(i.value())); QPixmapCache::remove(cache_key); ++i; } beginResetModel(); delete root_; song_nodes_.clear(); container_nodes_.clear(); pending_art_.clear(); pending_cache_keys_.clear(); root_ = new CollectionItem(this); root_->lazy_loaded = false; endResetModel(); } CollectionItem *ContextAlbumsModel::ItemFromSong(CollectionItem::Type item_type, bool signal, CollectionItem *parent, const Song &s, int container_level) { if (signal) beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count()); CollectionItem *item = new CollectionItem(item_type, parent); item->container_level = container_level; if (item->key.isNull()) item->key = s.album(); //if (item->key.isNull()) item->key = s.effective_albumartist(); item->display_text = TextOrUnknown(item->key); item->sort_text = SortTextForArtist(item->key); if (item_type == CollectionItem::Type_Song) item->lazy_loaded = true; if (signal) endInsertRows(); return item; } QString ContextAlbumsModel::TextOrUnknown(const QString &text) { if (text.isEmpty()) return tr("Unknown"); return text; } QString ContextAlbumsModel::SortText(QString text) { if (text.isEmpty()) { text = " unknown"; } else { text = text.toLower(); } text = text.remove(QRegularExpression("[^\\w ]", QRegularExpression::UseUnicodePropertiesOption)); return text; } QString ContextAlbumsModel::SortTextForArtist(QString artist) { artist = SortText(artist); if (artist.startsWith("the ")) { artist = artist.right(artist.length() - 4) + ", the"; } else if (artist.startsWith("a ")) { artist = artist.right(artist.length() - 2) + ", a"; } else if (artist.startsWith("an ")) { artist = artist.right(artist.length() - 3) + ", an"; } return artist; } QString ContextAlbumsModel::SortTextForSong(const Song &song) { QString ret = QString::number(qMax(0, song.disc()) * 1000 + qMax(0, song.track())); ret.prepend(QString("0").repeated(6 - ret.length())); ret.append(song.url().toString()); return ret; } Qt::ItemFlags ContextAlbumsModel::flags(const QModelIndex &idx) const { switch (IndexToItem(idx)->type) { case CollectionItem::Type_Song: case CollectionItem::Type_Container: return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled; case CollectionItem::Type_Root: case CollectionItem::Type_LoadingIndicator: default: return Qt::ItemIsEnabled; } } QStringList ContextAlbumsModel::mimeTypes() const { return QStringList() << "text/uri-list"; } QMimeData *ContextAlbumsModel::mimeData(const QModelIndexList &indexes) const { if (indexes.isEmpty()) return nullptr; SongMimeData *data = new SongMimeData; QList urls; QSet song_ids; data->backend = backend_; for (const QModelIndex &idx : indexes) { GetChildSongs(IndexToItem(idx), &urls, &data->songs, &song_ids); } data->setUrls(urls); data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs); return data; } bool ContextAlbumsModel::CompareItems(const CollectionItem *a, const CollectionItem *b) const { QVariant left(data(a, ContextAlbumsModel::Role_SortText)); QVariant right(data(b, ContextAlbumsModel::Role_SortText)); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) if (left.metaType().id() == QMetaType::Int) #else if (left.type() == QVariant::Int) #endif return left.toInt() < right.toInt(); else return left.toString() < right.toString(); } void ContextAlbumsModel::GetChildSongs(CollectionItem *item, QList *urls, SongList *songs, QSet *song_ids) const { switch (item->type) { case CollectionItem::Type_Container: { const_cast(this)->LazyPopulate(item); QList children = item->children; std::sort(children.begin(), children.end(), std::bind(&ContextAlbumsModel::CompareItems, this, std::placeholders::_1, std::placeholders::_2)); for (CollectionItem *child : children) GetChildSongs(child, urls, songs, song_ids); break; } case CollectionItem::Type_Song: urls->append(item->metadata.url()); if (!song_ids->contains(item->metadata.id())) { songs->append(item->metadata); song_ids->insert(item->metadata.id()); } break; default: break; } } SongList ContextAlbumsModel::GetChildSongs(const QModelIndexList &indexes) const { QList dontcare; SongList ret; QSet song_ids; for (const QModelIndex &idx : indexes) { GetChildSongs(IndexToItem(idx), &dontcare, &ret, &song_ids); } return ret; } SongList ContextAlbumsModel::GetChildSongs(const QModelIndex &idx) const { return GetChildSongs(QModelIndexList() << idx); } bool ContextAlbumsModel::canFetchMore(const QModelIndex &parent) const { if (!parent.isValid()) return false; CollectionItem *item = IndexToItem(parent); return !item->lazy_loaded; }