/* * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome * Copyright 2018-2023, 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/scoped_ptr.h" #include "core/shared_ptr.h" #include "core/application.h" #include "core/database.h" #include "core/iconloader.h" #include "core/logging.h" #include "core/taskmanager.h" #include "core/sqlrow.h" #include "collectionfilteroptions.h" #include "collectionquery.h" #include "collectionqueryoptions.h" #include "collectionbackend.h" #include "collectiondirectorymodel.h" #include "collectionitem.h" #include "collectionmodel.h" #include "playlist/playlistmanager.h" #include "playlist/songmimedata.h" #include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderresult.h" #include "covermanager/albumcoverloader.h" #include "settings/collectionsettingspage.h" const int CollectionModel::kPrettyCoverSize = 32; const char *CollectionModel::kPixmapDiskCacheDir = "pixmapcache"; QNetworkDiskCache *CollectionModel::sIconCache = nullptr; CollectionModel::CollectionModel(SharedPtr backend, Application *app, QObject *parent) : SimpleTreeModel(new CollectionItem(this), parent), backend_(backend), app_(app), dir_model_(new CollectionDirectoryModel(backend, this)), show_various_artists_(true), total_song_count_(0), total_artist_count_(0), total_album_count_(0), separate_albums_by_grouping_(false), artist_icon_(IconLoader::Load("folder-sound")), album_icon_(IconLoader::Load("cdcase")), init_task_id_(-1), use_pretty_covers_(true), show_dividers_(true), use_disk_cache_(false), use_lazy_loading_(true) { root_->lazy_loaded = true; group_by_[0] = GroupBy::AlbumArtist; group_by_[1] = GroupBy::AlbumDisc; group_by_[2] = GroupBy::None; if (app_) { QObject::connect(&*app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &CollectionModel::AlbumCoverLoaded); } QIcon nocover = IconLoader::Load("cdcase"); if (!nocover.isNull()) { QList nocover_sizes = nocover.availableSizes(); no_cover_icon_ = nocover.pixmap(nocover_sizes.last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } if (app_ && !sIconCache) { sIconCache = new QNetworkDiskCache(this); sIconCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + kPixmapDiskCacheDir); QObject::connect(app_, &Application::ClearPixmapDiskCache, this, &CollectionModel::ClearDiskCache); } QObject::connect(&*backend_, &CollectionBackend::SongsDiscovered, this, &CollectionModel::SongsDiscovered); QObject::connect(&*backend_, &CollectionBackend::SongsDeleted, this, &CollectionModel::SongsDeleted); QObject::connect(&*backend_, &CollectionBackend::DatabaseReset, this, &CollectionModel::Reset); QObject::connect(&*backend_, &CollectionBackend::TotalSongCountUpdated, this, &CollectionModel::TotalSongCountUpdatedSlot); QObject::connect(&*backend_, &CollectionBackend::TotalArtistCountUpdated, this, &CollectionModel::TotalArtistCountUpdatedSlot); QObject::connect(&*backend_, &CollectionBackend::TotalAlbumCountUpdated, this, &CollectionModel::TotalAlbumCountUpdatedSlot); QObject::connect(&*backend_, &CollectionBackend::SongsStatisticsChanged, this, &CollectionModel::SongsSlightlyChanged); QObject::connect(&*backend_, &CollectionBackend::SongsRatingChanged, this, &CollectionModel::SongsSlightlyChanged); backend_->UpdateTotalSongCountAsync(); backend_->UpdateTotalArtistCountAsync(); backend_->UpdateTotalAlbumCountAsync(); ReloadSettings(); } CollectionModel::~CollectionModel() { qLog(Debug) << "Collection model" << this << "for" << Song::TextForSource(backend_->source()) << "deleted"; delete root_; } void CollectionModel::set_pretty_covers(const bool use_pretty_covers) { if (use_pretty_covers != use_pretty_covers_) { use_pretty_covers_ = use_pretty_covers; Reset(); } } void CollectionModel::set_show_dividers(const bool show_dividers) { if (show_dividers != show_dividers_) { show_dividers_ = show_dividers; Reset(); } } void CollectionModel::ReloadSettings() { QSettings s; s.beginGroup(CollectionSettingsPage::kSettingsGroup); use_disk_cache_ = s.value(CollectionSettingsPage::kSettingsDiskCacheEnable, false).toBool(); QPixmapCache::setCacheLimit(static_cast(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsCacheSize, CollectionSettingsPage::kSettingsCacheSizeUnit, CollectionSettingsPage::kSettingsCacheSizeDefault) / 1024)); if (sIconCache) { sIconCache->setMaximumCacheSize(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsDiskCacheSize, CollectionSettingsPage::kSettingsDiskCacheSizeUnit, CollectionSettingsPage::kSettingsDiskCacheSizeDefault)); } s.endGroup(); cover_types_ = AlbumCoverLoaderOptions::LoadTypes(); if (!use_disk_cache_) { ClearDiskCache(); } } void CollectionModel::Init(const bool async) { if (async) { // Show a loading indicator in the model. CollectionItem *loading = new CollectionItem(CollectionItem::Type_LoadingIndicator, root_); loading->display_text = tr("Loading..."); loading->lazy_loaded = true; beginResetModel(); endResetModel(); // Show a loading indicator in the status bar too. if (app_) { init_task_id_ = app_->task_manager()->StartTask(tr("Loading songs")); } ResetAsync(); } else { Reset(); } } void CollectionModel::SongsDiscovered(const SongList &songs) { for (const Song &song : songs) { // Sanity check to make sure we don't add songs that are outside the user's filter if (!filter_options_.Matches(song)) continue; // Hey, we've already got that one! 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. // These depend on which "group by" settings the user has on the collection. // Eg. if the user grouped by artist and album, we would need to make sure nodes for the song's artist and album were already in the tree. // Find parent containers in the tree CollectionItem *container = root_; QString key; for (int i = 0; i < 3; ++i) { GroupBy group_by = group_by_[i]; if (group_by == GroupBy::None) break; if (!key.isEmpty()) key.append("-"); // Special case: if the song is a compilation and the current GroupBy level is Artists, then we want the Various Artists node :( if (IsArtistGroupBy(group_by) && song.is_compilation()) { if (container->compilation_artist_node_ == nullptr) { CreateCompilationArtistNode(true, container); } container = container->compilation_artist_node_; key = container->key; } else { // Otherwise find the proper container at this level based on the item's key key.append(ContainerKey(group_by, separate_albums_by_grouping_, song)); // Does it exist already? if (container_nodes_[i].contains(key)) { container = container_nodes_[i][key]; } else { // Create the container container = ItemFromSong(group_by, separate_albums_by_grouping_, true, i == 0, container, song, i); container_nodes_[i].insert(key, container); } } // If we just created the damn thing then we don't need to continue into it any further because it'll get lazy-loaded properly later. if (!container->lazy_loaded && use_lazy_loading_) break; } if (!container->lazy_loaded && use_lazy_loading_) 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_.insert(song.id(), ItemFromSong(GroupBy::None, separate_albums_by_grouping_, true, false, container, song, -1)); } } void CollectionModel::SongsSlightlyChanged(const SongList &songs) { // This is called if there was a minor change to the songs that will not normally require the collection to be restructured. // We can just update our internal cache of Song objects without worrying about resetting the model. for (const Song &song : songs) { if (song_nodes_.contains(song.id())) { song_nodes_[song.id()]->metadata = song; } } } CollectionItem *CollectionModel::CreateCompilationArtistNode(const bool signal, CollectionItem *parent) { Q_ASSERT(parent->compilation_artist_node_ == nullptr); if (signal) beginInsertRows(ItemToIndex(parent), static_cast(parent->children.count()), static_cast(parent->children.count())); parent->compilation_artist_node_ = new CollectionItem(CollectionItem::Type_Container, parent); parent->compilation_artist_node_->compilation_artist_node_ = nullptr; if (parent != root_ && !parent->key.isEmpty()) parent->compilation_artist_node_->key.append(parent->key); parent->compilation_artist_node_->key.append(tr("Various artists")); parent->compilation_artist_node_->display_text = tr("Various artists"); parent->compilation_artist_node_->sort_text = " various"; parent->compilation_artist_node_->container_level = parent->container_level + 1; if (signal) endInsertRows(); return parent->compilation_artist_node_; } QString CollectionModel::ContainerKey(const GroupBy group_by, const bool separate_albums_by_grouping, const Song &song) { QString key; switch (group_by) { case GroupBy::AlbumArtist: key = TextOrUnknown(song.effective_albumartist()); break; case GroupBy::Artist: key = TextOrUnknown(song.artist()); break; case GroupBy::Album: key = TextOrUnknown(song.album()); if (!song.album_id().isEmpty()) key.append("-" + song.album_id()); if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append("-" + song.grouping()); break; case GroupBy::AlbumDisc: key = PrettyAlbumDisc(song.album(), song.disc()); if (!song.album_id().isEmpty()) key.append("-" + song.album_id()); if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append("-" + song.grouping()); break; case GroupBy::YearAlbum: key = PrettyYearAlbum(song.year(), song.album()); if (!song.album_id().isEmpty()) key.append("-" + song.album_id()); if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append("-" + song.grouping()); break; case GroupBy::YearAlbumDisc: key = PrettyYearAlbumDisc(song.year(), song.album(), song.disc()); if (!song.album_id().isEmpty()) key.append("-" + song.album_id()); if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append("-" + song.grouping()); break; case GroupBy::OriginalYearAlbum: key = PrettyYearAlbum(song.effective_originalyear(), song.album()); if (!song.album_id().isEmpty()) key.append("-" + song.album_id()); if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append("-" + song.grouping()); break; case GroupBy::OriginalYearAlbumDisc: key = PrettyYearAlbumDisc(song.effective_originalyear(), song.album(), song.disc()); if (!song.album_id().isEmpty()) key.append("-" + song.album_id()); if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append("-" + song.grouping()); break; case GroupBy::Disc: key = PrettyDisc(song.disc()); break; case GroupBy::Year: key = QString::number(std::max(0, song.year())); break; case GroupBy::OriginalYear: key = QString::number(std::max(0, song.effective_originalyear())); break; case GroupBy::Genre: key = TextOrUnknown(song.genre()); break; case GroupBy::Composer: key = TextOrUnknown(song.composer()); break; case GroupBy::Performer: key = TextOrUnknown(song.performer()); break; case GroupBy::Grouping: key = TextOrUnknown(song.grouping()); break; case GroupBy::FileType: key = song.TextForFiletype(); break; case GroupBy::Samplerate: key = QString::number(std::max(0, song.samplerate())); break; case GroupBy::Bitdepth: key = QString::number(std::max(0, song.bitdepth())); break; case GroupBy::Bitrate: key = QString::number(std::max(0, song.bitrate())); break; case GroupBy::Format: if (song.samplerate() <= 0) { key = song.TextForFiletype(); } else { if (song.bitdepth() <= 0) { key = QString("%1 (%2)").arg(song.TextForFiletype(), QString::number(song.samplerate() / 1000.0, 'G', 5)); } else { key = QString("%1 (%2/%3)").arg(song.TextForFiletype(), QString::number(song.samplerate() / 1000.0, 'G', 5)).arg(song.bitdepth()); } } break; case GroupBy::None: case GroupBy::GroupByCount: qLog(Error) << "GroupBy::None"; break; } return key; } QString CollectionModel::DividerKey(const GroupBy group_by, CollectionItem *item) { // Items which are to be grouped under the same divider must produce the same divider key. This will only get called for top-level items. if (item->sort_text.isEmpty()) return QString(); switch (group_by) { case GroupBy::AlbumArtist: case GroupBy::Artist: case GroupBy::Album: case GroupBy::AlbumDisc: case GroupBy::Composer: case GroupBy::Performer: case GroupBy::Grouping: case GroupBy::Disc: case GroupBy::Genre: case GroupBy::Format: case GroupBy::FileType: { QChar c = item->sort_text[0]; if (c.isDigit()) return "0"; if (c == ' ') return QString(); if (c.decompositionTag() != QChar::NoDecomposition) { QString decomposition = c.decomposition(); return QChar(decomposition[0]); } return c; } case GroupBy::Year: case GroupBy::OriginalYear: return SortTextForNumber(item->sort_text.toInt() / 10 * 10); case GroupBy::YearAlbum: case GroupBy::YearAlbumDisc: return SortTextForNumber(item->metadata.year()); case GroupBy::OriginalYearAlbum: case GroupBy::OriginalYearAlbumDisc: return SortTextForNumber(item->metadata.effective_originalyear()); case GroupBy::Samplerate: return SortTextForNumber(item->metadata.samplerate()); case GroupBy::Bitdepth: return SortTextForNumber(item->metadata.bitdepth()); case GroupBy::Bitrate: return SortTextForNumber(item->metadata.bitrate()); case GroupBy::None: case GroupBy::GroupByCount: return QString(); } qLog(Error) << "Unknown GroupBy" << group_by << "for item" << item->display_text; return QString(); } QString CollectionModel::DividerDisplayText(const GroupBy group_by, const QString &key) { // Pretty display text for the dividers. switch (group_by) { case GroupBy::AlbumArtist: case GroupBy::Artist: case GroupBy::Album: case GroupBy::AlbumDisc: case GroupBy::Composer: case GroupBy::Performer: case GroupBy::Disc: case GroupBy::Grouping: case GroupBy::Genre: case GroupBy::FileType: case GroupBy::Format: if (key == "0") return "0-9"; return key.toUpper(); case GroupBy::YearAlbum: case GroupBy::YearAlbumDisc: case GroupBy::OriginalYearAlbum: case GroupBy::OriginalYearAlbumDisc: if (key == "0000") return tr("Unknown"); return key.toUpper(); case GroupBy::Year: case GroupBy::OriginalYear: if (key == "0000") return tr("Unknown"); return QString::number(key.toInt()); // To remove leading 0s case GroupBy::Samplerate: if (key == "000") return tr("Unknown"); return QString::number(key.toInt()); // To remove leading 0s case GroupBy::Bitdepth: if (key == "000") return tr("Unknown"); return QString::number(key.toInt()); // To remove leading 0s case GroupBy::Bitrate: if (key == "000") return tr("Unknown"); return QString::number(key.toInt()); // To remove leading 0s case GroupBy::None: case GroupBy::GroupByCount: break; } qLog(Error) << "Unknown GroupBy" << group_by << "for divider key" << key; return QString(); } void CollectionModel::SongsDeleted(const SongList &songs) { // Delete the actual song nodes first, keeping track of each parent so we might check to see if they're empty later. QSet parents; for (const Song &song : songs) { if (song_nodes_.contains(song.id())) { CollectionItem *node = song_nodes_[song.id()]; if (node->parent != root_) parents << node->parent; beginRemoveRows(ItemToIndex(node->parent), node->row, node->row); node->parent->Delete(node->row); song_nodes_.remove(song.id()); endRemoveRows(); } else { // If we get here it means some of the songs we want to delete haven't been lazy-loaded yet. // This is bad, because it would mean that to clean up empty parents we would need to lazy-load them all individually to see if they're empty. // This can take a very long time, so better to just reset the model and be done with it. Reset(); return; } } // Now delete empty parents QSet divider_keys; while (!parents.isEmpty()) { // Since we are going to remove elements from the container, we need a copy to iterate over. // If we iterate over the original, the behavior will be undefined. QSet parents_copy = parents; for (CollectionItem *node : parents_copy) { parents.remove(node); if (node->children.count() != 0) continue; // Consider its parent for the next round if (node->parent != root_) parents << node->parent; // Maybe consider its divider node if (node->container_level == 0) { divider_keys << DividerKey(group_by_[0], node); } // Special case the Various Artists node if (IsCompilationArtistNode(node)) { node->parent->compilation_artist_node_ = nullptr; } else if (container_nodes_[node->container_level].contains(node->key)) { container_nodes_[node->container_level].remove(node->key); } // Remove from pixmap cache const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(node)); QPixmapCache::remove(cache_key); if (use_disk_cache_ && sIconCache) sIconCache->remove(AlbumIconPixmapDiskCacheKey(cache_key)); if (pending_cache_keys_.contains(cache_key)) { pending_cache_keys_.remove(cache_key); } // Remove from pending art loading for (QMap::iterator it = pending_art_.begin(); it != pending_art_.end();) { if (it.value().first == node) { it = pending_art_.erase(it); // clazy:exclude=strict-iterators } else { ++it; } } // It was empty - delete it beginRemoveRows(ItemToIndex(node->parent), node->row, node->row); node->parent->Delete(node->row); endRemoveRows(); } } // Delete empty dividers for (const QString ÷r_key : std::as_const(divider_keys)) { if (!divider_nodes_.contains(divider_key)) continue; // Look to see if there are any other items still under this divider QList container_nodes = container_nodes_[0].values(); if (std::any_of(container_nodes.begin(), container_nodes.end(), [this, divider_key](CollectionItem *node){ return DividerKey(group_by_[0], node) == divider_key; })) { continue; } // Remove the divider int row = divider_nodes_[divider_key]->row; beginRemoveRows(ItemToIndex(root_), row, row); root_->Delete(row); endRemoveRows(); divider_nodes_.remove(divider_key); } } QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const { QStringList path; QModelIndex idx_copy(idx); while (idx_copy.isValid()) { path.prepend(idx_copy.data().toString()); idx_copy = idx_copy.parent(); } return Song::TextForSource(backend_->source()) + "/" + path.join("/"); } QUrl CollectionModel::AlbumIconPixmapDiskCacheKey(const QString &cache_key) const { return QUrl(QUrl::toPercentEncoding(cache_key)); } QVariant CollectionModel::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; } // Try to load it from the disk cache if (use_disk_cache_ && sIconCache) { ScopedPtr disk_cache_img(sIconCache->data(AlbumIconPixmapDiskCacheKey(cache_key))); if (disk_cache_img) { QImage cached_image; if (cached_image.load(&*disk_cache_img, "XPM")) { QPixmapCache::insert(cache_key, QPixmap::fromImage(cached_image)); return QPixmap::fromImage(cached_image); } } } // 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()) { AlbumCoverLoaderOptions cover_loader_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage); cover_loader_options.desired_scaled_size = QSize(kPrettyCoverSize, kPrettyCoverSize); cover_loader_options.types = cover_types_; 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 CollectionModel::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::Unset) { // 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); } // If we have a valid cover not already in the disk cache if (use_disk_cache_ && sIconCache && result.success && !result.image_scaled.isNull()) { const QUrl disk_cache_key = AlbumIconPixmapDiskCacheKey(cache_key); ScopedPtr disk_cache_img(sIconCache->data(disk_cache_key)); if (!disk_cache_img) { QNetworkCacheMetaData disk_cache_metadata; disk_cache_metadata.setSaveToDisk(true); disk_cache_metadata.setUrl(disk_cache_key); // Qt 6 now ignores any entry without headers, so add a fake header. disk_cache_metadata.setRawHeaders(QNetworkCacheMetaData::RawHeaderList() << qMakePair(QByteArray(), QByteArray())); QIODevice *device_iconcache = sIconCache->prepare(disk_cache_metadata); if (device_iconcache) { result.image_scaled.save(device_iconcache, "XPM"); sIconCache->insert(device_iconcache); } } } const QModelIndex idx = ItemToIndex(item); if (!idx.isValid()) return; emit dataChanged(idx, idx); } QVariant CollectionModel::data(const QModelIndex &idx, const int role) const { const CollectionItem *item = IndexToItem(idx); // Handle a special case for returning album artwork instead of a generic CD icon. // this is here instead of in the other data() function to let us use the // QModelIndex& version of GetChildSongs, which satisfies const-ness, instead // of the CollectionItem *version, which doesn't. if (use_pretty_covers_) { bool is_album_node = false; if (role == Qt::DecorationRole && item->type == CollectionItem::Type_Container) { GroupBy container_group_by = group_by_[item->container_level]; is_album_node = IsAlbumGroupBy(container_group_by); } if (is_album_node) { // It has const behaviour some of the time - that's ok right? return const_cast(this)->AlbumIcon(idx); } } return data(item, role); } QVariant CollectionModel::data(const CollectionItem *item, const int role) const { GroupBy container_group_by = item->type == CollectionItem::Type_Container ? group_by_[item->container_level] : GroupBy::None; switch (role) { case Qt::DisplayRole: case Qt::ToolTipRole: return item->DisplayText(); case Qt::DecorationRole: switch (item->type) { case CollectionItem::Type_Container: switch (container_group_by) { case GroupBy::Album: case GroupBy::AlbumDisc: case GroupBy::YearAlbum: case GroupBy::YearAlbumDisc: case GroupBy::OriginalYearAlbum: case GroupBy::OriginalYearAlbumDisc: return album_icon_; case GroupBy::Artist: case GroupBy::AlbumArtist: return artist_icon_; default: break; } break; default: break; } break; case Role_Type: return item->type; case Role_IsDivider: return item->type == CollectionItem::Type_Divider; case Role_ContainerType: return static_cast(container_group_by); 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()) { return false; } else if (std::any_of(item->children.begin(), item->children.end(), [this, role](CollectionItem *child) { return !data(child, role).toBool(); })) { return false; } else { return true; } } else if (item->type == CollectionItem::Type_Song) { return item->metadata.IsEditable(); } else { return false; } } case Role_SortText: return item->SortText(); default: return QVariant(); } return QVariant(); } bool CollectionModel::HasCompilations(const QSqlDatabase &db, const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options) { CollectionQuery q(db, backend_->songs_table(), backend_->fts_table(), filter_options); q.SetColumnSpec(query_options.column_spec()); for (const CollectionQueryOptions::Where &where_clauses : query_options.where_clauses()) { q.AddWhere(where_clauses.column, where_clauses.value, where_clauses.op); } q.AddCompilationRequirement(true); q.SetLimit(1); if (!q.Exec()) { backend_->ReportErrors(q); return false; } return q.Next(); } CollectionQueryOptions CollectionModel::PrepareQuery(CollectionItem *parent) { // Information about what we want the children to be const int child_level = parent == root_ ? 0 : parent->container_level + 1; const GroupBy child_group_by = child_level >= 3 ? GroupBy::None : group_by_[child_level]; CollectionQueryOptions query_options; // Initialize the query. child_group_by says what type of thing we want (artists, songs, etc.) SetQueryColumnSpec(child_group_by, separate_albums_by_grouping_, &query_options); // Walk up through the item's parents adding filters as necessary for (CollectionItem *p = parent; p && p->type == CollectionItem::Type_Container; p = p->parent) { AddQueryWhere(group_by_[p->container_level], separate_albums_by_grouping_, p, &query_options); } // Artists GroupBy is special - we don't want compilation albums appearing if (show_various_artists_ && IsArtistGroupBy(child_group_by)) { query_options.set_query_have_compilations(true); } return query_options; } CollectionModel::QueryResult CollectionModel::RunQuery(const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options) { QMutexLocker l(backend_->db()->Mutex()); QueryResult result; { QSqlDatabase db(backend_->db()->Connect()); // Add the special Various artists node if (query_options.query_have_compilations() && HasCompilations(db, filter_options, query_options)) { result.create_va = true; } CollectionQuery q(db, backend_->songs_table(), backend_->fts_table(), filter_options); q.SetColumnSpec(query_options.column_spec()); for (const CollectionQueryOptions::Where &where_clauses : query_options.where_clauses()) { q.AddWhere(where_clauses.column, where_clauses.value, where_clauses.op); } if (result.create_va) { q.AddCompilationRequirement(false); } else if (query_options.compilation_requirement() != CollectionQueryOptions::CompilationRequirement::None) { q.AddCompilationRequirement(query_options.compilation_requirement() == CollectionQueryOptions::CompilationRequirement::On); } if (q.Exec()) { while (q.Next()) { result.rows << SqlRow(q); } } else { backend_->ReportErrors(q); } } if (QThread::currentThread() != thread() && QThread::currentThread() != backend_->thread()) { backend_->db()->Close(); } return result; } void CollectionModel::PostQuery(CollectionItem *parent, const CollectionModel::QueryResult &result, const bool signal) { // Information about what we want the children to be int child_level = parent == root_ ? 0 : parent->container_level + 1; GroupBy child_group_by = child_level >= 3 ? GroupBy::None : group_by_[child_level]; if (result.create_va && parent->compilation_artist_node_ == nullptr) { CreateCompilationArtistNode(signal, parent); } // Step through the results for (const SqlRow &row : result.rows) { // Create the item - it will get inserted into the model here CollectionItem *item = ItemFromQuery(child_group_by, separate_albums_by_grouping_, signal, child_level == 0, parent, row, child_level); // Save a pointer to it for later if (child_group_by == GroupBy::None) { song_nodes_.insert(item->metadata.id(), item); } else { container_nodes_[child_level].insert(item->key, item); } } } void CollectionModel::LazyPopulate(CollectionItem *parent, const bool signal) { if (parent->lazy_loaded) return; parent->lazy_loaded = true; CollectionQueryOptions query_options = PrepareQuery(parent); QueryResult result = RunQuery(filter_options_, query_options); PostQuery(parent, result, signal); } void CollectionModel::ResetAsync() { CollectionQueryOptions query_options = PrepareQuery(root_); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QFuture future = QtConcurrent::run(&CollectionModel::RunQuery, this, filter_options_, query_options); #else QFuture future = QtConcurrent::run(this, &CollectionModel::RunQuery, filter_options_, query_options); #endif QFutureWatcher *watcher = new QFutureWatcher(); QObject::connect(watcher, &QFutureWatcher::finished, this, &CollectionModel::ResetAsyncQueryFinished); watcher->setFuture(future); } void CollectionModel::ResetAsyncQueryFinished() { QFutureWatcher *watcher = static_cast*>(sender()); const struct QueryResult result = watcher->result(); watcher->deleteLater(); BeginReset(); root_->lazy_loaded = true; PostQuery(root_, result, false); if (init_task_id_ != -1) { if (app_) { app_->task_manager()->SetTaskFinished(init_task_id_); } init_task_id_ = -1; } endResetModel(); } void CollectionModel::BeginReset() { beginResetModel(); delete root_; song_nodes_.clear(); container_nodes_[0].clear(); container_nodes_[1].clear(); container_nodes_[2].clear(); divider_nodes_.clear(); pending_art_.clear(); pending_cache_keys_.clear(); root_ = new CollectionItem(this); root_->compilation_artist_node_ = nullptr; root_->lazy_loaded = false; } void CollectionModel::Reset() { BeginReset(); // Populate top level LazyPopulate(root_, false); endResetModel(); } void CollectionModel::SetQueryColumnSpec(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQueryOptions *query_options) { // Say what group_by of thing we want to get back from the database. switch (group_by) { case GroupBy::AlbumArtist: query_options->set_column_spec("DISTINCT effective_albumartist"); break; case GroupBy::Artist: query_options->set_column_spec("DISTINCT artist"); break; case GroupBy::Album:{ QString query("DISTINCT album, album_id"); if (separate_albums_by_grouping) query.append(", grouping"); query_options->set_column_spec(query); break; } case GroupBy::AlbumDisc:{ QString query("DISTINCT album, album_id, disc"); if (separate_albums_by_grouping) query.append(", grouping"); query_options->set_column_spec(query); break; } case GroupBy::YearAlbum:{ QString query("DISTINCT year, album, album_id"); if (separate_albums_by_grouping) query.append(", grouping"); query_options->set_column_spec(query); break; } case GroupBy::YearAlbumDisc:{ QString query("DISTINCT year, album, album_id, disc"); if (separate_albums_by_grouping) query.append(", grouping"); query_options->set_column_spec(query); break; } case GroupBy::OriginalYearAlbum:{ QString query("DISTINCT year, originalyear, album, album_id"); if (separate_albums_by_grouping) query.append(", grouping"); query_options->set_column_spec(query); break; } case GroupBy::OriginalYearAlbumDisc:{ QString query("DISTINCT year, originalyear, album, album_id, disc"); if (separate_albums_by_grouping) query.append(", grouping"); query_options->set_column_spec(query); break; } case GroupBy::Disc: query_options->set_column_spec("DISTINCT disc"); break; case GroupBy::Year: query_options->set_column_spec("DISTINCT year"); break; case GroupBy::OriginalYear: query_options->set_column_spec("DISTINCT effective_originalyear"); break; case GroupBy::Genre: query_options->set_column_spec("DISTINCT genre"); break; case GroupBy::Composer: query_options->set_column_spec("DISTINCT composer"); break; case GroupBy::Performer: query_options->set_column_spec("DISTINCT performer"); break; case GroupBy::Grouping: query_options->set_column_spec("DISTINCT grouping"); break; case GroupBy::FileType: query_options->set_column_spec("DISTINCT filetype"); break; case GroupBy::Format: query_options->set_column_spec("DISTINCT filetype, samplerate, bitdepth"); break; case GroupBy::Samplerate: query_options->set_column_spec("DISTINCT samplerate"); break; case GroupBy::Bitdepth: query_options->set_column_spec("DISTINCT bitdepth"); break; case GroupBy::Bitrate: query_options->set_column_spec("DISTINCT bitrate"); break; case GroupBy::None: case GroupBy::GroupByCount: query_options->set_column_spec("%songs_table.ROWID, " + Song::kColumnSpec); break; } } void CollectionModel::AddQueryWhere(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQueryOptions *query_options) { // Say how we want the query to be filtered. This is done once for each parent going up the tree. switch (group_by) { case GroupBy::AlbumArtist: if (IsCompilationArtistNode(item)) { query_options->set_compilation_requirement(CollectionQueryOptions::CompilationRequirement::On); } else { // Don't duplicate compilations outside the Various artists node query_options->set_compilation_requirement(CollectionQueryOptions::CompilationRequirement::Off); query_options->AddWhere("effective_albumartist", item->metadata.effective_albumartist()); } break; case GroupBy::Artist: if (IsCompilationArtistNode(item)) { query_options->set_compilation_requirement(CollectionQueryOptions::CompilationRequirement::On); } else { // Don't duplicate compilations outside the Various artists node query_options->set_compilation_requirement(CollectionQueryOptions::CompilationRequirement::Off); query_options->AddWhere("artist", item->metadata.artist()); } break; case GroupBy::Album: query_options->AddWhere("album", item->metadata.album()); query_options->AddWhere("album_id", item->metadata.album_id()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy::AlbumDisc: query_options->AddWhere("album", item->metadata.album()); query_options->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("disc", item->metadata.disc()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy::YearAlbum: query_options->AddWhere("year", item->metadata.year()); query_options->AddWhere("album", item->metadata.album()); query_options->AddWhere("album_id", item->metadata.album_id()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy::YearAlbumDisc: query_options->AddWhere("year", item->metadata.year()); query_options->AddWhere("album", item->metadata.album()); query_options->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("disc", item->metadata.disc()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy::OriginalYearAlbum: query_options->AddWhere("year", item->metadata.year()); query_options->AddWhere("originalyear", item->metadata.originalyear()); query_options->AddWhere("album", item->metadata.album()); query_options->AddWhere("album_id", item->metadata.album_id()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy::OriginalYearAlbumDisc: query_options->AddWhere("year", item->metadata.year()); query_options->AddWhere("originalyear", item->metadata.originalyear()); query_options->AddWhere("album", item->metadata.album()); query_options->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("disc", item->metadata.disc()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy::Disc: query_options->AddWhere("disc", item->metadata.disc()); break; case GroupBy::Year: query_options->AddWhere("year", item->metadata.year()); break; case GroupBy::OriginalYear: query_options->AddWhere("effective_originalyear", item->metadata.effective_originalyear()); break; case GroupBy::Genre: query_options->AddWhere("genre", item->metadata.genre()); break; case GroupBy::Composer: query_options->AddWhere("composer", item->metadata.composer()); break; case GroupBy::Performer: query_options->AddWhere("performer", item->metadata.performer()); break; case GroupBy::Grouping: query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy::FileType: query_options->AddWhere("filetype", static_cast(item->metadata.filetype())); break; case GroupBy::Format: query_options->AddWhere("filetype", static_cast(item->metadata.filetype())); query_options->AddWhere("samplerate", item->metadata.samplerate()); query_options->AddWhere("bitdepth", item->metadata.bitdepth()); break; case GroupBy::Samplerate: query_options->AddWhere("samplerate", item->metadata.samplerate()); break; case GroupBy::Bitdepth: query_options->AddWhere("bitdepth", item->metadata.bitdepth()); break; case GroupBy::Bitrate: query_options->AddWhere("bitrate", item->metadata.bitrate()); break; case GroupBy::None: case GroupBy::GroupByCount: qLog(Error) << "Unknown GroupBy" << group_by << "used in filter"; break; } } CollectionItem *CollectionModel::InitItem(const GroupBy group_by, const bool signal, CollectionItem *parent, const int container_level) { CollectionItem::Type item_type = group_by == GroupBy::None ? CollectionItem::Type_Song : CollectionItem::Type_Container; if (signal) beginInsertRows(ItemToIndex(parent), static_cast(parent->children.count()), static_cast(parent->children.count())); // Initialize the item depending on what type it's meant to be CollectionItem *item = new CollectionItem(item_type, parent); item->compilation_artist_node_ = nullptr; item->container_level = container_level; return item; } CollectionItem *CollectionModel::ItemFromQuery(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level) { CollectionItem *item = InitItem(group_by, signal, parent, container_level); if (parent != root_ && !parent->key.isEmpty()) { item->key = parent->key + "-"; } switch (group_by) { case GroupBy::AlbumArtist:{ item->metadata.set_albumartist(row.value(0).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = TextOrUnknown(item->metadata.albumartist()); item->sort_text = SortTextForArtist(item->metadata.albumartist()); break; } case GroupBy::Artist:{ item->metadata.set_artist(row.value(0).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = TextOrUnknown(item->metadata.artist()); item->sort_text = SortTextForArtist(item->metadata.artist()); break; } case GroupBy::Album:{ item->metadata.set_album(row.value(0).toString()); item->metadata.set_album_id(row.value(1).toString()); item->metadata.set_grouping(row.value(2).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = TextOrUnknown(item->metadata.album()); item->sort_text = SortTextForArtist(item->metadata.album()); break; } case GroupBy::AlbumDisc:{ item->metadata.set_album(row.value(0).toString()); item->metadata.set_album_id(row.value(1).toString()); item->metadata.set_disc(row.value(2).toInt()); item->metadata.set_grouping(row.value(3).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = PrettyAlbumDisc(item->metadata.album(), item->metadata.disc()); item->sort_text = item->metadata.album() + SortTextForNumber(std::max(0, item->metadata.disc())); break; } case GroupBy::YearAlbum:{ item->metadata.set_year(row.value(0).toInt()); item->metadata.set_album(row.value(1).toString()); item->metadata.set_album_id(row.value(2).toString()); item->metadata.set_grouping(row.value(3).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = PrettyYearAlbum(item->metadata.year(), item->metadata.album()); item->sort_text = SortTextForNumber(std::max(0, item->metadata.year())) + item->metadata.grouping() + item->metadata.album(); break; } case GroupBy::YearAlbumDisc:{ item->metadata.set_year(row.value(0).toInt()); item->metadata.set_album(row.value(1).toString()); item->metadata.set_album_id(row.value(2).toString()); item->metadata.set_disc(row.value(3).toInt()); item->metadata.set_grouping(row.value(4).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = PrettyYearAlbumDisc(item->metadata.year(), item->metadata.album(), item->metadata.disc()); item->sort_text = SortTextForNumber(std::max(0, item->metadata.year())) + item->metadata.album() + SortTextForNumber(std::max(0, item->metadata.disc())); break; } case GroupBy::OriginalYearAlbum:{ item->metadata.set_year(row.value(0).toInt()); item->metadata.set_originalyear(row.value(1).toInt()); item->metadata.set_album(row.value(2).toString()); item->metadata.set_album_id(row.value(3).toString()); item->metadata.set_grouping(row.value(4).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = PrettyYearAlbum(item->metadata.effective_originalyear(), item->metadata.album()); item->sort_text = SortTextForNumber(std::max(0, item->metadata.effective_originalyear())) + item->metadata.grouping() + item->metadata.album(); break; } case GroupBy::OriginalYearAlbumDisc:{ item->metadata.set_year(row.value(0).toInt()); item->metadata.set_originalyear(row.value(1).toInt()); item->metadata.set_album(row.value(2).toString()); item->metadata.set_album_id(row.value(3).toString()); item->metadata.set_disc(row.value(4).toInt()); item->metadata.set_grouping(row.value(5).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = PrettyYearAlbumDisc(item->metadata.effective_originalyear(), item->metadata.album(), item->metadata.disc()); item->sort_text = SortTextForNumber(std::max(0, item->metadata.effective_originalyear())) + item->metadata.album() + SortTextForNumber(std::max(0, item->metadata.disc())); break; } case GroupBy::Disc:{ item->metadata.set_disc(row.value(0).toInt()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); const int disc = std::max(0, row.value(0).toInt()); item->display_text = PrettyDisc(disc); item->sort_text = SortTextForNumber(disc); break; } case GroupBy::Year:{ item->metadata.set_year(row.value(0).toInt()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); const int year = std::max(0, item->metadata.year()); item->display_text = QString::number(year); item->sort_text = SortTextForNumber(year) + " "; break; } case GroupBy::OriginalYear:{ item->metadata.set_originalyear(row.value(0).toInt()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); const int year = std::max(0, item->metadata.originalyear()); item->display_text = QString::number(year); item->sort_text = SortTextForNumber(year) + " "; break; } case GroupBy::Genre:{ item->metadata.set_genre(row.value(0).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = TextOrUnknown(item->metadata.genre()); item->sort_text = SortTextForArtist(item->metadata.genre()); break; } case GroupBy::Composer:{ item->metadata.set_composer(row.value(0).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = TextOrUnknown(item->metadata.composer()); item->sort_text = SortTextForArtist(item->metadata.composer()); break; } case GroupBy::Performer:{ item->metadata.set_performer(row.value(0).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = TextOrUnknown(item->metadata.performer()); item->sort_text = SortTextForArtist(item->metadata.performer()); break; } case GroupBy::Grouping:{ item->metadata.set_grouping(row.value(0).toString()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = TextOrUnknown(item->metadata.grouping()); item->sort_text = SortTextForArtist(item->metadata.grouping()); break; } case GroupBy::FileType:{ item->metadata.set_filetype(static_cast(row.value(0).toInt())); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); item->display_text = item->metadata.TextForFiletype(); item->sort_text = item->metadata.TextForFiletype(); break; } case GroupBy::Format:{ item->metadata.set_filetype(static_cast(row.value(0).toInt())); item->metadata.set_samplerate(row.value(1).toInt()); item->metadata.set_bitdepth(row.value(2).toInt()); QString key = ContainerKey(group_by, separate_albums_by_grouping, item->metadata); item->key.append(key); item->display_text = key; item->sort_text = key; break; } case GroupBy::Samplerate:{ item->metadata.set_samplerate(row.value(0).toInt()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); const int samplerate = std::max(0, item->metadata.samplerate()); item->display_text = QString::number(samplerate); item->sort_text = SortTextForNumber(samplerate) + " "; break; } case GroupBy::Bitdepth:{ item->metadata.set_bitdepth(row.value(0).toInt()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); const int bitdepth = std::max(0, item->metadata.bitdepth()); item->display_text = QString::number(bitdepth); item->sort_text = SortTextForNumber(bitdepth) + " "; break; } case GroupBy::Bitrate:{ item->metadata.set_bitrate(row.value(0).toInt()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, item->metadata)); const int bitrate = std::max(0, item->metadata.bitrate()); item->display_text = QString::number(bitrate); item->sort_text = SortTextForNumber(bitrate) + " "; break; } case GroupBy::None: case GroupBy::GroupByCount: item->metadata.InitFromQuery(row, true); item->key.append(TextOrUnknown(item->metadata.title())); item->display_text = item->metadata.TitleWithCompilationArtist(); if (item->container_level == 1 && !IsAlbumGroupBy(group_by_[0])) { item->sort_text = SortText(item->metadata.title()); } else { item->sort_text = SortTextForSong(item->metadata); } break; } FinishItem(group_by, signal, create_divider, parent, item); return item; } CollectionItem *CollectionModel::ItemFromSong(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const Song &s, const int container_level) { CollectionItem *item = InitItem(group_by, signal, parent, container_level); if (parent != root_ && !parent->key.isEmpty()) { item->key = parent->key + "-"; } switch (group_by) { case GroupBy::AlbumArtist:{ item->metadata.set_albumartist(s.effective_albumartist()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = TextOrUnknown(s.effective_albumartist()); item->sort_text = SortTextForArtist(s.effective_albumartist()); break; } case GroupBy::Artist:{ item->metadata.set_artist(s.artist()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = TextOrUnknown(s.artist()); item->sort_text = SortTextForArtist(s.artist()); break; } case GroupBy::Album:{ item->metadata.set_album(s.album()); item->metadata.set_album_id(s.album_id()); item->metadata.set_grouping(s.grouping()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = TextOrUnknown(s.album()); item->sort_text = SortTextForArtist(s.album()); break; } case GroupBy::AlbumDisc:{ item->metadata.set_album(s.album()); item->metadata.set_album_id(s.album_id()); item->metadata.set_disc(s.disc() <= 0 ? -1 : s.disc()); item->metadata.set_grouping(s.grouping()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = PrettyAlbumDisc(s.album(), s.disc()); item->sort_text = s.album() + SortTextForNumber(std::max(0, s.disc())); break; } case GroupBy::YearAlbum:{ item->metadata.set_year(s.year() <= 0 ? -1 : s.year()); item->metadata.set_album(s.album()); item->metadata.set_album_id(s.album_id()); item->metadata.set_grouping(s.grouping()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = PrettyYearAlbum(s.year(), s.album()); item->sort_text = SortTextForNumber(std::max(0, s.year())) + s.grouping() + s.album(); break; } case GroupBy::YearAlbumDisc:{ item->metadata.set_year(s.year() <= 0 ? -1 : s.year()); item->metadata.set_album(s.album()); item->metadata.set_album_id(s.album_id()); item->metadata.set_disc(s.disc() <= 0 ? -1 : s.disc()); item->metadata.set_grouping(s.grouping()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = PrettyYearAlbumDisc(s.year(), s.album(), s.disc()); item->sort_text = SortTextForNumber(std::max(0, s.year())) + s.album() + SortTextForNumber(std::max(0, s.disc())); break; } case GroupBy::OriginalYearAlbum:{ item->metadata.set_year(s.year() <= 0 ? -1 : s.year()); item->metadata.set_originalyear(s.originalyear() <= 0 ? -1 : s.originalyear()); item->metadata.set_album(s.album()); item->metadata.set_album_id(s.album_id()); item->metadata.set_grouping(s.grouping()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = PrettyYearAlbum(s.effective_originalyear(), s.album()); item->sort_text = SortTextForNumber(std::max(0, s.effective_originalyear())) + s.grouping() + s.album(); break; } case GroupBy::OriginalYearAlbumDisc:{ item->metadata.set_year(s.year() <= 0 ? -1 : s.year()); item->metadata.set_originalyear(s.originalyear() <= 0 ? -1 : s.originalyear()); item->metadata.set_album(s.album()); item->metadata.set_album_id(s.album_id()); item->metadata.set_disc(s.disc() <= 0 ? -1 : s.disc()); item->metadata.set_grouping(s.grouping()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = PrettyYearAlbumDisc(s.effective_originalyear(), s.album(), s.disc()); item->sort_text = SortTextForNumber(std::max(0, s.effective_originalyear())) + s.album() + SortTextForNumber(std::max(0, s.disc())); break; } case GroupBy::Disc:{ item->metadata.set_disc(s.disc() <= 0 ? -1 : s.disc()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); const int disc = std::max(0, s.disc()); item->display_text = PrettyDisc(disc); item->sort_text = SortTextForNumber(disc); break; } case GroupBy::Year:{ item->metadata.set_year(s.year() <= 0 ? -1 : s.year()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); const int year = std::max(0, s.year()); item->display_text = QString::number(year); item->sort_text = SortTextForNumber(year) + " "; break; } case GroupBy::OriginalYear:{ item->metadata.set_originalyear(s.effective_originalyear() <= 0 ? -1 : s.effective_originalyear()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); const int year = std::max(0, s.effective_originalyear()); item->display_text = QString::number(year); item->sort_text = SortTextForNumber(year) + " "; break; } case GroupBy::Genre:{ item->metadata.set_genre(s.genre()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = TextOrUnknown(s.genre()); item->sort_text = SortTextForArtist(s.genre()); break; } case GroupBy::Composer:{ item->metadata.set_composer(s.composer()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = TextOrUnknown(s.composer()); item->sort_text = SortTextForArtist(s.composer()); break; } case GroupBy::Performer:{ item->metadata.set_performer(s.performer()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = TextOrUnknown(s.performer()); item->sort_text = SortTextForArtist(s.performer()); break; } case GroupBy::Grouping:{ item->metadata.set_grouping(s.grouping()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = TextOrUnknown(s.grouping()); item->sort_text = SortTextForArtist(s.grouping()); break; } case GroupBy::FileType:{ item->metadata.set_filetype(s.filetype()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); item->display_text = s.TextForFiletype(); item->sort_text = s.TextForFiletype(); break; } case GroupBy::Format:{ item->metadata.set_filetype(s.filetype()); item->metadata.set_samplerate(s.samplerate()); item->metadata.set_bitdepth(s.bitdepth()); QString key = ContainerKey(group_by, separate_albums_by_grouping, s); item->key.append(key); item->display_text = key; item->sort_text = key; break; } case GroupBy::Samplerate:{ item->metadata.set_samplerate(s.samplerate()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); const int samplerate = std::max(0, s.samplerate()); item->display_text = QString::number(samplerate); item->sort_text = SortTextForNumber(samplerate) + " "; break; } case GroupBy::Bitdepth:{ item->metadata.set_bitdepth(s.bitdepth()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); const int bitdepth = std::max(0, s.bitdepth()); item->display_text = QString::number(bitdepth); item->sort_text = SortTextForNumber(bitdepth) + " "; break; } case GroupBy::Bitrate:{ item->metadata.set_bitrate(s.bitrate()); item->key.append(ContainerKey(group_by, separate_albums_by_grouping, s)); const int bitrate = std::max(0, s.bitrate()); item->display_text = QString::number(bitrate); item->sort_text = SortTextForNumber(bitrate) + " "; break; } case GroupBy::None: case GroupBy::GroupByCount:{ item->metadata = s; item->key.append(TextOrUnknown(s.title())); item->display_text = s.TitleWithCompilationArtist(); if (item->container_level == 1 && !IsAlbumGroupBy(group_by_[0])) { item->sort_text = SortText(s.title()); } else { item->sort_text = SortTextForSong(s); } break; } } FinishItem(group_by, signal, create_divider, parent, item); if (s.url().scheme() == "cdda") item->lazy_loaded = true; return item; } void CollectionModel::FinishItem(const GroupBy group_by, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item) { if (group_by == GroupBy::None) item->lazy_loaded = true; if (signal) { endInsertRows(); } // Create the divider entry if we're supposed to if (create_divider && show_dividers_) { QString divider_key = DividerKey(group_by, item); if (!divider_key.isEmpty()) { item->sort_text.prepend(divider_key + " "); } if (!divider_key.isEmpty() && !divider_nodes_.contains(divider_key)) { if (signal) { beginInsertRows(ItemToIndex(parent), static_cast(parent->children.count()), static_cast(parent->children.count())); } CollectionItem *divider = new CollectionItem(CollectionItem::Type_Divider, root_); divider->key = divider_key; divider->display_text = DividerDisplayText(group_by, divider_key); divider->sort_text = divider_key + " "; divider->lazy_loaded = true; divider_nodes_[divider_key] = divider; if (signal) { endInsertRows(); } } } } QString CollectionModel::TextOrUnknown(const QString &text) { if (text.isEmpty()) return tr("Unknown"); return text; } QString CollectionModel::PrettyYearAlbum(const int year, const QString &album) { if (year <= 0) return TextOrUnknown(album); return QString::number(year) + " - " + TextOrUnknown(album); } QString CollectionModel::PrettyAlbumDisc(const QString &album, const int disc) { if (disc <= 0 || album.contains(Song::kAlbumRemoveDisc)) return TextOrUnknown(album); else return TextOrUnknown(album) + " - (Disc " + QString::number(disc) + ")"; } QString CollectionModel::PrettyYearAlbumDisc(const int year, const QString &album, const int disc) { QString str; if (year <= 0) str = TextOrUnknown(album); else str = QString::number(year) + " - " + TextOrUnknown(album); if (!album.contains(Song::kAlbumRemoveDisc) && disc > 0) str += " - (Disc " + QString::number(disc) + ")"; return str; } QString CollectionModel::PrettyDisc(const int disc) { return "Disc " + QString::number(std::max(1, disc)); } QString CollectionModel::SortText(QString text) { if (text.isEmpty()) { text = " unknown"; } else { text = text.toLower(); } text = text.remove(QRegularExpression("[^\\w ]", QRegularExpression::UseUnicodePropertiesOption)); return text; } QString CollectionModel::SortTextForArtist(QString artist) { artist = SortText(artist); for (const auto &i : Song::kArticles) { if (artist.startsWith(i)) { qint64 ilen = i.length(); artist = artist.right(artist.length() - ilen) + ", " + i.left(ilen - 1); break; } } return artist; } QString CollectionModel::SortTextForNumber(const int number) { return QString("%1").arg(number, 4, 10, QChar('0')); } QString CollectionModel::SortTextForYear(const int year) { QString str = QString::number(year); return QString("0").repeated(qMax(0, 4 - str.length())) + str; } QString CollectionModel::SortTextForBitrate(const int bitrate) { QString str = QString::number(bitrate); return QString("0").repeated(qMax(0, 3 - str.length())) + str; } QString CollectionModel::SortTextForSong(const Song &song) { QString ret = QString::number(std::max(0, song.disc()) * 1000 + std::max(0, song.track())); ret.prepend(QString("0").repeated(6 - ret.length())); ret.append(song.url().toString()); return ret; } Qt::ItemFlags CollectionModel::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_Divider: case CollectionItem::Type_Root: case CollectionItem::Type_LoadingIndicator: default: return Qt::ItemIsEnabled; } } QStringList CollectionModel::mimeTypes() const { return QStringList() << "text/uri-list"; } QMimeData *CollectionModel::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 CollectionModel::CompareItems(const CollectionItem *a, const CollectionItem *b) const { QVariant left(data(a, CollectionModel::Role_SortText)); QVariant right(data(b, CollectionModel::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(); } qint64 CollectionModel::MaximumCacheSize(QSettings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default) { qint64 size = s->value(size_id, cache_size_default).toInt(); int unit = s->value(size_unit_id, static_cast(CollectionSettingsPage::CacheSizeUnit::MB)).toInt() + 1; do { size *= 1024; unit -= 1; } while (unit > 0); return size; } void CollectionModel::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(&CollectionModel::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 CollectionModel::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 CollectionModel::GetChildSongs(const QModelIndex &idx) const { return GetChildSongs(QModelIndexList() << idx); } void CollectionModel::SetFilterMode(CollectionFilterOptions::FilterMode filter_mode) { filter_options_.set_filter_mode(filter_mode); ResetAsync(); } void CollectionModel::SetFilterAge(const int filter_age) { filter_options_.set_max_age(filter_age); ResetAsync(); } void CollectionModel::SetFilterText(const QString &filter_text) { filter_options_.set_filter_text(filter_text); ResetAsync(); } bool CollectionModel::canFetchMore(const QModelIndex &parent) const { if (!parent.isValid()) return false; CollectionItem *item = IndexToItem(parent); return !item->lazy_loaded; } void CollectionModel::SetGroupBy(const Grouping g, const std::optional separate_albums_by_grouping) { group_by_ = g; if (separate_albums_by_grouping) { separate_albums_by_grouping_ = separate_albums_by_grouping.value(); } ResetAsync(); emit GroupingChanged(g, separate_albums_by_grouping_); } const CollectionModel::GroupBy &CollectionModel::Grouping::operator[](const int i) const { switch (i) { case 0: return first; case 1: return second; case 2: return third; default: break; } qLog(Error) << "CollectionModel::Grouping[] index out of range" << i; return first; } CollectionModel::GroupBy &CollectionModel::Grouping::operator[](const int i) { switch (i) { case 0: return first; case 1: return second; case 2: return third; default: break; } qLog(Error) << "CollectionModel::Grouping[] index out of range" << i; return first; } void CollectionModel::TotalSongCountUpdatedSlot(const int count) { total_song_count_ = count; emit TotalSongCountUpdated(count); } void CollectionModel::TotalArtistCountUpdatedSlot(const int count) { total_artist_count_ = count; emit TotalArtistCountUpdated(count); } void CollectionModel::TotalAlbumCountUpdatedSlot(const int count) { total_album_count_ = count; emit TotalAlbumCountUpdated(count); } void CollectionModel::ClearDiskCache() { if (sIconCache) sIconCache->clear(); } void CollectionModel::ExpandAll(CollectionItem *item) const { if (!item) item = root_; const_cast(this)->LazyPopulate(const_cast(item), false); for (CollectionItem *child : item->children) { ExpandAll(child); } } QDataStream &operator<<(QDataStream &s, const CollectionModel::Grouping g) { s << static_cast(g.first) << static_cast(g.second) << static_cast(g.third); return s; } QDataStream &operator>>(QDataStream &s, CollectionModel::Grouping &g) { quint32 buf = 0; s >> buf; g.first = static_cast(buf); s >> buf; g.second = static_cast(buf); s >> buf; g.third = static_cast(buf); return s; }