diff --git a/CMakeLists.txt b/CMakeLists.txt index e6105ab90..143cf2a6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -467,25 +467,6 @@ if(NOT CMAKE_CROSSCOMPILING) " QT_SQLITE_TEST ) - if(QT_SQLITE_TEST) - # Check that we have sqlite3 with FTS5 - check_cxx_source_runs(" - #include - #include - int main() { - QSqlDatabase db = QSqlDatabase::addDatabase(\"QSQLITE\"); - db.setDatabaseName(\":memory:\"); - if (!db.open()) { return 1; } - QSqlQuery q(db); - q.prepare(\"CREATE VIRTUAL TABLE test_fts USING fts5(test, tokenize = 'unicode61 remove_diacritics 0');\"); - if (!q.exec()) return 1; - } - " - SQLITE_FTS5_TEST - ) - endif() - unset(CMAKE_REQUIRED_FLAGS) - unset(CMAKE_REQUIRED_LIBRARIES) endif() # Set up definitions @@ -546,11 +527,7 @@ if(QT_VERSION_MAJOR EQUAL 5) endif() if(NOT CMAKE_CROSSCOMPILING) - if(QT_SQLITE_TEST) - if(NOT SQLITE_FTS5_TEST) - message(WARNING "sqlite must be enabled with FTS5. See: https://www.sqlite.org/fts5.html") - endif() - else() + if(NOT QT_SQLITE_TEST) message(WARNING "The Qt sqlite driver test failed.") endif() endif() diff --git a/README.md b/README.md index 38a9e0de2..3bfd081ec 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ To build Strawberry from source you need the following installed on your system * [Boost](https://www.boost.org/) * [GLib](https://developer.gnome.org/glib/) * [Qt 6 or Qt 5.12 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/) -* [SQLite 3.9 or newer with FTS5](https://www.sqlite.org) +* [SQLite 3.9 or newer](https://www.sqlite.org) * [Protobuf](https://developers.google.com/protocol-buffers/) * [ALSA (Required on Linux)](https://www.alsa-project.org/) * [D-Bus (Required on Linux)](https://www.freedesktop.org/wiki/Software/dbus/) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9e49616fe..f17c149b8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -92,12 +92,14 @@ set(SOURCES collection/collectiondirectorymodel.cpp collection/collectionfilteroptions.cpp collection/collectionfilterwidget.cpp + collection/collectionfilter.cpp collection/collectionplaylistitem.cpp collection/collectionquery.cpp collection/collectionqueryoptions.cpp collection/savedgroupingmanager.cpp collection/groupbydialog.cpp collection/collectiontask.cpp + collection/collectionmodelupdate.cpp playlist/playlist.cpp playlist/playlistbackend.cpp @@ -349,6 +351,7 @@ set(HEADERS collection/collectionviewcontainer.h collection/collectiondirectorymodel.h collection/collectionfilterwidget.h + collection/collectionfilter.h collection/savedgroupingmanager.h collection/groupbydialog.h diff --git a/src/collection/collection.cpp b/src/collection/collection.cpp index b6a46e8bc..4b7916447 100644 --- a/src/collection/collection.cpp +++ b/src/collection/collection.cpp @@ -49,7 +49,6 @@ using std::make_shared; const char *SCollection::kSongsTable = "songs"; -const char *SCollection::kFtsTable = "songs_fts"; const char *SCollection::kDirsTable = "directories"; const char *SCollection::kSubdirsTable = "subdirectories"; @@ -70,7 +69,7 @@ SCollection::SCollection(Application *app, QObject *parent) backend()->moveToThread(app->database()->thread()); qLog(Debug) << &*backend_ << "moved to thread" << app->database()->thread(); - backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, QLatin1String(kSongsTable), QLatin1String(kFtsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable)); + backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, QLatin1String(kSongsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable)); model_ = new CollectionModel(backend_, app_, this); diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index ddbb0cb80..275df111f 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -76,16 +76,13 @@ CollectionBackend::~CollectionBackend() { } -void CollectionBackend::Init(SharedPtr db, SharedPtr task_manager, const Song::Source source, const QString &songs_table, const QString &fts_table, const QString &dirs_table, const QString &subdirs_table) { - +void CollectionBackend::Init(SharedPtr db, SharedPtr task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table, const QString &subdirs_table) { db_ = db; task_manager_ = task_manager; source_ = source; songs_table_ = songs_table; dirs_table_ = dirs_table; subdirs_table_ = subdirs_table; - fts_table_ = fts_table; - } void CollectionBackend::Close() { @@ -123,6 +120,35 @@ void CollectionBackend::ReportErrors(const CollectionQuery &query) { } +void CollectionBackend::GetAllSongsAsync(const int id) { + metaObject()->invokeMethod(this, "GetAllSongs", Qt::QueuedConnection, Q_ARG(int, id)); +} + +void CollectionBackend::GetAllSongs(const int id) { + + QMutexLocker l(db_->Mutex()); + QSqlDatabase db(db_->Connect()); + + SqlQuery q(db); + q.setForwardOnly(true); + q.prepare(QStringLiteral("SELECT %1 FROM %2").arg(Song::kRowIdColumnSpec, songs_table_)); + if (!q.exec()) { + db_->ReportErrors(q); + emit GotSongs(SongList(), id); + return; + } + + SongList songs; + while (q.next()) { + Song song(source_); + song.InitFromQuery(q, true); + songs << song; + } + + emit GotSongs(songs, id); + +} + void CollectionBackend::LoadDirectoriesAsync() { QMetaObject::invokeMethod(this, &CollectionBackend::LoadDirectories, Qt::QueuedConnection); } @@ -596,7 +622,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) { ScopedTransaction transaction(&db); SongList added_songs; - SongList deleted_songs; + SongList changed_songs; for (const Song &song : songs) { @@ -633,19 +659,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) { } } - { - SqlQuery q(db); - q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec)); - song.BindToFtsQuery(&q); - q.BindValue(QStringLiteral(":id"), song.id()); - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - } - - deleted_songs << old_song; - added_songs << song; + changed_songs << song; continue; @@ -672,19 +686,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) { } } - { - SqlQuery q(db); - q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec)); - new_song.BindToFtsQuery(&q); - q.BindValue(QStringLiteral(":id"), new_song.id()); - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - } - - deleted_songs << old_song; - added_songs << new_song; + changed_songs << new_song; continue; } @@ -707,17 +709,6 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) { if (id == -1) return; - { // Add to the FTS index - SqlQuery q(db); - q.prepare(QStringLiteral("INSERT INTO %1 (ROWID, %2) VALUES (:id, %3)").arg(fts_table_, Song::kFtsColumnSpec, Song::kFtsBindSpec)); - q.BindValue(QStringLiteral(":id"), id); - song.BindToFtsQuery(&q); - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - } - Song song_copy(song); song_copy.set_id(id); added_songs << song_copy; @@ -726,8 +717,8 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) { transaction.Commit(); - if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs); - if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs); + if (!added_songs.isEmpty()) emit SongsAdded(added_songs); + if (!changed_songs.isEmpty()) emit SongsChanged(changed_songs); UpdateTotalSongCountAsync(); UpdateTotalArtistCountAsync(); @@ -748,11 +739,12 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) { ScopedTransaction transaction(&db); SongList added_songs; + SongList changed_songs; SongList deleted_songs; SongMap old_songs; { - CollectionQuery query(db, songs_table_, fts_table_); + CollectionQuery query(db, songs_table_); if (!ExecCollectionQuery(&query, old_songs)) { ReportErrors(query); return; @@ -778,21 +770,10 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) { return; } } - { - SqlQuery q(db); - q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec)); - new_song.BindToFtsQuery(&q); - q.BindValue(QStringLiteral(":id"), old_song.id()); - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - } - deleted_songs << old_song; Song new_song_copy(new_song); new_song_copy.set_id(old_song.id()); - added_songs << new_song_copy; + changed_songs << new_song_copy; } @@ -813,17 +794,6 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) { if (id == -1) return; - { // Add to the FTS index - SqlQuery q(db); - q.prepare(QStringLiteral("INSERT INTO %1 (ROWID, %2) VALUES (:id, %3)").arg(fts_table_, Song::kFtsColumnSpec, Song::kFtsBindSpec)); - q.BindValue(QStringLiteral(":id"), id); - new_song.BindToFtsQuery(&q); - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - } - Song new_song_copy(new_song); new_song_copy.set_id(id); added_songs << new_song_copy; @@ -843,15 +813,6 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) { return; } } - { - SqlQuery q(db); - q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_)); - q.BindValue(QStringLiteral(":id"), old_song.id()); - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - } deleted_songs << old_song; } } @@ -859,7 +820,8 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) { transaction.Commit(); if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs); - if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs); + if (!added_songs.isEmpty()) emit SongsAdded(added_songs); + if (!changed_songs.isEmpty()) emit SongsChanged(changed_songs); UpdateTotalSongCountAsync(); UpdateTotalArtistCountAsync(); @@ -872,11 +834,10 @@ void CollectionBackend::UpdateMTimesOnly(const SongList &songs) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - SqlQuery q(db); - q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id").arg(songs_table_)); - ScopedTransaction transaction(&db); for (const Song &song : songs) { + SqlQuery q(db); + q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id").arg(songs_table_)); q.BindValue(QStringLiteral(":mtime"), song.mtime()); q.BindValue(QStringLiteral(":id"), song.id()); if (!q.Exec()) { @@ -893,25 +854,17 @@ void CollectionBackend::DeleteSongs(const SongList &songs) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - SqlQuery remove(db); - remove.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_)); - SqlQuery remove_fts(db); - remove_fts.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_)); - ScopedTransaction transaction(&db); for (const Song &song : songs) { - remove.BindValue(QStringLiteral(":id"), song.id()); - if (!remove.Exec()) { - db_->ReportErrors(remove); - return; - } - - remove_fts.BindValue(QStringLiteral(":id"), song.id()); - if (!remove_fts.Exec()) { - db_->ReportErrors(remove_fts); + SqlQuery q(db); + q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_)); + q.BindValue(QStringLiteral(":id"), song.id()); + if (!q.Exec()) { + db_->ReportErrors(q); return; } } + transaction.Commit(); emit SongsDeleted(songs); @@ -944,7 +897,7 @@ void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool u emit SongsDeleted(songs); } else { - emit SongsDiscovered(songs); + emit SongsAdded(songs); } UpdateTotalSongCountAsync(); @@ -958,7 +911,7 @@ QStringList CollectionBackend::GetAll(const QString &column, const CollectionFil QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - CollectionQuery query(db, songs_table_, fts_table_, filter_options); + CollectionQuery query(db, songs_table_, filter_options); query.SetColumnSpec(QStringLiteral("DISTINCT ") + column); query.AddCompilationRequirement(false); @@ -986,13 +939,13 @@ QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOpt QSqlDatabase db(db_->Connect()); // Albums with 'albumartist' field set: - CollectionQuery query(db, songs_table_, fts_table_, opt); + CollectionQuery query(db, songs_table_, opt); query.SetColumnSpec(QStringLiteral("DISTINCT albumartist")); query.AddCompilationRequirement(false); query.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!=")); // Albums with no 'albumartist' (extract 'artist'): - CollectionQuery query2(db, songs_table_, fts_table_, opt); + CollectionQuery query2(db, songs_table_, opt); query2.SetColumnSpec(QStringLiteral("DISTINCT artist")); query2.AddCompilationRequirement(false); query2.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!=")); @@ -1033,7 +986,7 @@ SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); - CollectionQuery query(db, songs_table_, fts_table_, opt); + CollectionQuery query(db, songs_table_, opt); query.AddCompilationRequirement(false); query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); @@ -1051,7 +1004,7 @@ SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); - CollectionQuery query(db, songs_table_, fts_table_, opt); + CollectionQuery query(db, songs_table_, opt); query.AddCompilationRequirement(false); query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); query.AddWhere(QStringLiteral("album"), album); @@ -1070,7 +1023,7 @@ SongList CollectionBackend::GetSongsByAlbum(const QString &album, const Collecti QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); - CollectionQuery query(db, songs_table_, fts_table_, opt); + CollectionQuery query(db, songs_table_, opt); query.AddCompilationRequirement(false); query.AddWhere(QStringLiteral("album"), album); @@ -1207,11 +1160,10 @@ Song CollectionBackend::GetSongByUrl(const QUrl &url, const qint64 beginning) { SqlQuery q(db); q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND beginning = :beginning AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_)); - - q.BindValue(QStringLiteral(":url1"), url); - q.BindValue(QStringLiteral(":url2"), url.toString()); - q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded)); - q.BindValue(QStringLiteral(":url4"), url.toEncoded()); + q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded)); + q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded)); q.BindValue(QStringLiteral(":beginning"), beginning); if (!q.Exec()) { @@ -1237,11 +1189,10 @@ Song CollectionBackend::GetSongByUrlAndTrack(const QUrl &url, const int track) { SqlQuery q(db); q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND track = :track AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_)); - - q.BindValue(QStringLiteral(":url1"), url); - q.BindValue(QStringLiteral(":url2"), url.toString()); - q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded)); - q.BindValue(QStringLiteral(":url4"), url.toEncoded()); + q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded)); + q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded)); q.BindValue(QStringLiteral(":track"), track); if (!q.Exec()) { @@ -1267,23 +1218,21 @@ SongList CollectionBackend::GetSongsByUrl(const QUrl &url, const bool unavailabl SqlQuery q(db); q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = :unavailable").arg(Song::kRowIdColumnSpec, songs_table_)); - - q.BindValue(QStringLiteral(":url1"), url); - q.BindValue(QStringLiteral(":url2"), url.toString()); - q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded)); - q.BindValue(QStringLiteral(":url4"), url.toEncoded()); + q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded)); + q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded)); q.BindValue(QStringLiteral(":unavailable"), (unavailable ? 1 : 0)); SongList songs; - if (q.Exec()) { - while (q.next()) { - Song song(source_); - song.InitFromQuery(q, true); - songs << song; - } - } - else { + if (!q.Exec()) { db_->ReportErrors(q); + return SongList(); + } + while (q.next()) { + Song song(source_); + song.InitFromQuery(q, true); + songs << song; } return songs; @@ -1377,7 +1326,7 @@ SongList CollectionBackend::GetCompilationSongs(const QString &album, const Coll QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - CollectionQuery query(db, songs_table_, fts_table_, opt); + CollectionQuery query(db, songs_table_, opt); query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec); query.AddCompilationRequirement(true); query.AddWhere(QStringLiteral("album"), album); @@ -1434,8 +1383,7 @@ void CollectionBackend::CompilationsNeedUpdating() { } // Now mark the songs that we think are in compilations - SongList deleted_songs; - SongList added_songs; + SongList changed_songs; ScopedTransaction transaction(&db); @@ -1448,12 +1396,12 @@ void CollectionBackend::CompilationsNeedUpdating() { for (const QUrl &url : info.urls) { if (info.artists.count() > 1) { // This directory+album is a compilation. if (info.has_not_compilation_detected > 0) { // Run updates if any of the songs is not marked as compilations. - UpdateCompilations(db, deleted_songs, added_songs, url, true); + UpdateCompilations(db, changed_songs, url, true); } } else { if (info.has_compilation_detected > 0) { - UpdateCompilations(db, deleted_songs, added_songs, url, false); + UpdateCompilations(db, changed_songs, url, false); } } } @@ -1461,29 +1409,27 @@ void CollectionBackend::CompilationsNeedUpdating() { transaction.Commit(); - if (!deleted_songs.isEmpty()) { - emit SongsDeleted(deleted_songs); - emit SongsDiscovered(added_songs); + if (!changed_songs.isEmpty()) { + emit SongsChanged(changed_songs); } } -bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_songs, const QUrl &url, const bool compilation_detected) { +bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &changed_songs, const QUrl &url, const bool compilation_detected) { { // Get song, so we can tell the model its updated SqlQuery q(db); q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_)); - q.BindValue(QStringLiteral(":url1"), url); - q.BindValue(QStringLiteral(":url2"), url.toString()); - q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded)); - q.BindValue(QStringLiteral(":url4"), url.toEncoded()); + q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded)); + q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded)); if (q.Exec()) { while (q.next()) { Song song(source_); song.InitFromQuery(q, true); - deleted_songs << song; song.set_compilation_detected(compilation_detected); - added_songs << song; + changed_songs << song; } } else { @@ -1496,10 +1442,10 @@ bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &del SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET compilation_detected = :compilation_detected, compilation_effective = ((compilation OR :compilation_detected OR compilation_on) AND NOT compilation_off) + 0 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(songs_table_)); q.BindValue(QStringLiteral(":compilation_detected"), static_cast(compilation_detected)); - q.BindValue(QStringLiteral(":url1"), url); - q.BindValue(QStringLiteral(":url2"), url.toString()); - q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded)); - q.BindValue(QStringLiteral(":url4"), url.toEncoded()); + q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded)); + q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded)); + q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded)); if (!q.Exec()) { db_->ReportErrors(q); return false; @@ -1514,7 +1460,7 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - CollectionQuery query(db, songs_table_, fts_table_, opt); + CollectionQuery query(db, songs_table_, opt); query.SetColumnSpec(QStringLiteral("url, filetype, cue_path, effective_albumartist, album, compilation_effective, art_embedded, art_automatic, art_manual, art_unset")); query.SetOrderBy(QStringLiteral("effective_albumartist, album, url")); @@ -1605,7 +1551,7 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective ret.album = album; ret.album_artist = effective_albumartist; - CollectionQuery query(db, songs_table_, fts_table_); + CollectionQuery query(db, songs_table_); query.SetColumnSpec(QStringLiteral("url, art_embedded, art_automatic, art_manual, art_unset")); if (!effective_albumartist.isEmpty()) { query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); @@ -1640,54 +1586,37 @@ void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumart QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - // Get the songs before they're updated - CollectionQuery query(db, songs_table_, fts_table_); - query.SetColumnSpec(Song::kRowIdColumnSpec); - query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); - query.AddWhere(QStringLiteral("album"), album); - - if (!query.Exec()) { - ReportErrors(query); - return; + { + SqlQuery q(db); + q.prepare(QStringLiteral("UPDATE %1 SET art_embedded = :art_embedded, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); + q.BindValue(QStringLiteral(":art_embedded"), art_embedded ? 1 : 0); + q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist); + q.BindValue(QStringLiteral(":album"), album); + if (!q.Exec()) { + db_->ReportErrors(q); + return; + } } - SongList deleted_songs; - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - deleted_songs << song; + SongList songs; + { + CollectionQuery q(db, songs_table_); + q.SetColumnSpec(Song::kRowIdColumnSpec); + q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); + q.AddWhere(QStringLiteral("album"), album); + if (!q.Exec()) { + ReportErrors(q); + return; + } + while (q.Next()) { + Song song(source_); + song.InitFromQuery(q, true); + songs << song; + } } - // Update the songs - QString sql = QStringLiteral("UPDATE %1 SET art_embedded = :art_embedded, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_); - - SqlQuery q(db); - q.prepare(sql); - q.BindValue(QStringLiteral(":art_embedded"), art_embedded ? 1 : 0); - q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist); - q.BindValue(QStringLiteral(":album"), album); - - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - - // Now get the updated songs - if (!query.Exec()) { - ReportErrors(query); - return; - } - - SongList added_songs; - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - added_songs << song; - } - - if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) { - emit SongsDeleted(deleted_songs); - emit SongsDiscovered(added_songs); + if (!songs.isEmpty()) { + emit SongsChanged(songs); } } @@ -1703,49 +1632,37 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - CollectionQuery query(db, songs_table_, fts_table_); - query.SetColumnSpec(Song::kRowIdColumnSpec); - query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); - query.AddWhere(QStringLiteral("album"), album); - - if (!query.Exec()) { - ReportErrors(query); - return; + { + SqlQuery q(db); + q.prepare(QStringLiteral("UPDATE %1 SET art_manual = :art_manual, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); + q.BindValue(QStringLiteral(":art_manual"), art_manual.isValid() ? art_manual.toString(QUrl::FullyEncoded) : QLatin1String("")); + q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist); + q.BindValue(QStringLiteral(":album"), album); + if (!q.Exec()) { + db_->ReportErrors(q); + return; + } } - SongList deleted_songs; - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - deleted_songs << song; + SongList songs; + { + CollectionQuery q(db, songs_table_); + q.SetColumnSpec(Song::kRowIdColumnSpec); + q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); + q.AddWhere(QStringLiteral("album"), album); + if (!q.Exec()) { + ReportErrors(q); + return; + } + while (q.Next()) { + Song song(source_); + song.InitFromQuery(q, true); + songs << song; + } } - SqlQuery q(db); - q.prepare(QStringLiteral("UPDATE %1 SET art_manual = :art_manual, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); - q.BindValue(QStringLiteral(":art_manual"), art_manual.isValid() ? art_manual.toString(QUrl::FullyEncoded) : QLatin1String("")); - q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist); - q.BindValue(QStringLiteral(":album"), album); - - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - - if (!query.Exec()) { - ReportErrors(query); - return; - } - - SongList added_songs; - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - added_songs << song; - } - - if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) { - emit SongsDeleted(deleted_songs); - emit SongsDiscovered(added_songs); + if (!songs.isEmpty()) { + emit SongsChanged(songs); } } @@ -1761,48 +1678,36 @@ void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, cons QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - CollectionQuery query(db, songs_table_, fts_table_); - query.SetColumnSpec(Song::kRowIdColumnSpec); - query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); - query.AddWhere(QStringLiteral("album"), album); - - if (!query.Exec()) { - ReportErrors(query); - return; + { + SqlQuery q(db); + q.prepare(QStringLiteral("UPDATE %1 SET art_unset = 1, art_manual = '', art_automatic = '', art_embedded = '' WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); + q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist); + q.BindValue(QStringLiteral(":album"), album); + if (!q.Exec()) { + db_->ReportErrors(q); + return; + } } - SongList deleted_songs; - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - deleted_songs << song; + SongList songs; + { + CollectionQuery q(db, songs_table_); + q.SetColumnSpec(Song::kRowIdColumnSpec); + q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); + q.AddWhere(QStringLiteral("album"), album); + if (!q.Exec()) { + ReportErrors(q); + return; + } + while (q.Next()) { + Song song(source_); + song.InitFromQuery(q, true); + songs << song; + } } - SqlQuery q(db); - q.prepare(QStringLiteral("UPDATE %1 SET art_unset = 1, art_manual = '', art_automatic = '', art_embedded = '' WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); - q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist); - q.BindValue(QStringLiteral(":album"), album); - - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - - if (!query.Exec()) { - ReportErrors(query); - return; - } - - SongList added_songs; - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - added_songs << song; - } - - if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) { - emit SongsDeleted(deleted_songs); - emit SongsDiscovered(added_songs); + if (!songs.isEmpty()) { + emit SongsChanged(songs); } } @@ -1818,49 +1723,37 @@ void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, cons QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - CollectionQuery query(db, songs_table_, fts_table_); - query.SetColumnSpec(Song::kRowIdColumnSpec); - query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); - query.AddWhere(QStringLiteral("album"), album); - - if (!query.Exec()) { - ReportErrors(query); - return; + { + SqlQuery q(db); + q.prepare(QStringLiteral("UPDATE %1 SET art_embedded = 0, art_automatic = '', art_manual = '', art_unset = :art_unset WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); + q.BindValue(QStringLiteral(":art_unset"), art_unset ? 1 : 0); + q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist); + q.BindValue(QStringLiteral(":album"), album); + if (!q.Exec()) { + db_->ReportErrors(q); + return; + } } - SongList deleted_songs; - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - deleted_songs << song; + SongList songs; + { + CollectionQuery q(db, songs_table_); + q.SetColumnSpec(Song::kRowIdColumnSpec); + q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); + q.AddWhere(QStringLiteral("album"), album); + if (!q.Exec()) { + ReportErrors(q); + return; + } + while (q.Next()) { + Song song(source_); + song.InitFromQuery(q, true); + songs << song; + } } - SqlQuery q(db); - q.prepare(QStringLiteral("UPDATE %1 SET art_embedded = 0, art_automatic = '', art_manual = '', art_unset = :art_unset WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); - q.BindValue(QStringLiteral(":art_unset"), art_unset ? 1 : 0); - q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist); - q.BindValue(QStringLiteral(":album"), album); - - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - - if (!query.Exec()) { - ReportErrors(query); - return; - } - - SongList added_songs; - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - added_songs << song; - } - - if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) { - emit SongsDeleted(deleted_songs); - emit SongsDiscovered(added_songs); + if (!songs.isEmpty()) { + emit SongsChanged(songs); } } @@ -1869,25 +1762,9 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - SongList deleted_songs, added_songs; + SongList songs; for (const QString &artist : artists) { - // Get the songs before they're updated - CollectionQuery query(db, songs_table_, fts_table_); - query.SetColumnSpec(Song::kRowIdColumnSpec); - query.AddWhere(QStringLiteral("album"), album); - if (!artist.isEmpty()) query.AddWhere(QStringLiteral("artist"), artist); - - if (!query.Exec()) { - ReportErrors(query); - return; - } - - while (query.Next()) { - Song song(source_); - song.InitFromQuery(query, true); - deleted_songs << song; - } // Update the songs QString sql(QStringLiteral("UPDATE %1 SET compilation_on = :compilation_on, compilation_off = :compilation_off, compilation_effective = ((compilation OR compilation_detected OR :compilation_on) AND NOT :compilation_off) + 0 WHERE album = :album AND unavailable = 0").arg(songs_table_)); @@ -1905,7 +1782,13 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList return; } - // Now get the updated songs + // Get the updated songs + + CollectionQuery query(db, songs_table_); + query.SetColumnSpec(Song::kRowIdColumnSpec); + query.AddWhere(QStringLiteral("album"), album); + if (!artist.isEmpty()) query.AddWhere(QStringLiteral("artist"), artist); + if (!query.Exec()) { ReportErrors(query); return; @@ -1914,13 +1797,12 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList while (query.Next()) { Song song(source_); song.InitFromQuery(query, true); - added_songs << song; + songs << song; } } - if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) { - emit SongsDeleted(deleted_songs); - emit SongsDiscovered(added_songs); + if (!songs.isEmpty()) { + emit SongsChanged(songs); } } @@ -2035,15 +1917,6 @@ void CollectionBackend::DeleteAll() { } } - { - SqlQuery q(db); - q.prepare(QStringLiteral("DELETE FROM ") + fts_table_); - if (!q.Exec()) { - db_->ReportErrors(q); - return; - } - } - t.Commit(); } diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index 9b987954e..27b760bf7 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -80,12 +80,13 @@ class CollectionBackendInterface : public QObject { using AlbumList = QList; virtual QString songs_table() const = 0; - virtual QString fts_table() const = 0; virtual Song::Source source() const = 0; virtual SharedPtr db() const = 0; + virtual void GetAllSongsAsync(const int id = 0) = 0; + // Get a list of directories in the collection. Emits DirectoriesDiscovered. virtual void LoadDirectoriesAsync() = 0; @@ -145,7 +146,8 @@ class CollectionBackend : public CollectionBackendInterface { ~CollectionBackend(); - void Init(SharedPtr db, SharedPtr task_manager, const Song::Source source, const QString &songs_table, const QString &fts_table, const QString &dirs_table = QString(), const QString &subdirs_table = QString()); + void Init(SharedPtr db, SharedPtr task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table = QString(), const QString &subdirs_table = QString()); + void Close(); void ExitAsync(); @@ -157,10 +159,11 @@ class CollectionBackend : public CollectionBackendInterface { SharedPtr db() const override { return db_; } QString songs_table() const override { return songs_table_; } - QString fts_table() const override { return fts_table_; } QString dirs_table() const { return dirs_table_; } QString subdirs_table() const { return subdirs_table_; } + void GetAllSongsAsync(const int id = 0) override; + // Get a list of directories in the collection. Emits DirectoriesDiscovered. void LoadDirectoriesAsync() override; @@ -235,6 +238,7 @@ class CollectionBackend : public CollectionBackendInterface { public slots: void Exit(); + void GetAllSongs(const int id); void LoadDirectories(); void UpdateTotalSongCount(); void UpdateTotalArtistCount(); @@ -275,8 +279,10 @@ class CollectionBackend : public CollectionBackendInterface { void DirectoryAdded(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdir); void DirectoryDeleted(const CollectionDirectory &dir); - void SongsDiscovered(const SongList &songs); + void GotSongs(const SongList &songs, const int id); + void SongsAdded(const SongList &songs); void SongsDeleted(const SongList &songs); + void SongsChanged(const SongList &songs); void SongsStatisticsChanged(const SongList &songs, const bool save_tags = false); void DatabaseReset(); @@ -301,7 +307,7 @@ class CollectionBackend : public CollectionBackendInterface { int has_not_compilation_detected; }; - bool UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_songs, const QUrl &url, const bool compilation_detected); + bool UpdateCompilations(const QSqlDatabase &db, SongList &changed_songs, const QUrl &url, const bool compilation_detected); AlbumList GetAlbums(const QString &artist, const QString &album_artist, const bool compilation_required = false, const CollectionFilterOptions &opt = CollectionFilterOptions()); AlbumList GetAlbums(const QString &artist, const bool compilation_required, const CollectionFilterOptions &opt = CollectionFilterOptions()); CollectionSubdirectoryList SubdirsInDirectory(const int id, QSqlDatabase &db); @@ -319,7 +325,6 @@ class CollectionBackend : public CollectionBackendInterface { QString songs_table_; QString dirs_table_; QString subdirs_table_; - QString fts_table_; QThread *original_thread_; }; diff --git a/src/collection/collectionfilter.cpp b/src/collection/collectionfilter.cpp new file mode 100644 index 000000000..da1ea12c6 --- /dev/null +++ b/src/collection/collectionfilter.cpp @@ -0,0 +1,335 @@ +/* + * Strawberry Music Player + * Copyright 2021-2024, 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 "core/logging.h" +#include "utilities/timeconstants.h" +#include "utilities/searchparserutils.h" + +#include "collectionfilter.h" +#include "collectionmodel.h" +#include "collectionitem.h" + +const QStringList CollectionFilter::Operators = QStringList() << QStringLiteral(":") + << QStringLiteral("=") + << QStringLiteral("==") + << QStringLiteral("<>") + << QStringLiteral("<") + << QStringLiteral("<=") + << QStringLiteral(">") + << QStringLiteral(">="); + +CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent) {} + +bool CollectionFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const { + + CollectionModel *model = qobject_cast(sourceModel()); + if (!model) return false; + const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + if (!idx.isValid()) return false; + CollectionItem *item = model->IndexToItem(idx); + if (!item) return false; + + if (item->type == CollectionItem::Type::LoadingIndicator) return true; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QString filter_text = filterRegularExpression().pattern().remove(QLatin1Char('\\')); +#else + QString filter_text = filterRegExp().pattern(); +#endif + + if (filter_text.isEmpty()) return true; + + filter_text = filter_text.replace(QRegularExpression(QStringLiteral("\\s*:\\s*")), QStringLiteral(":")) + .replace(QRegularExpression(QStringLiteral("\\s*=\\s*")), QStringLiteral("=")) + .replace(QRegularExpression(QStringLiteral("\\s*==\\s*")), QStringLiteral("==")) + .replace(QRegularExpression(QStringLiteral("\\s*<>\\s*")), QStringLiteral("<>")) + .replace(QRegularExpression(QStringLiteral("\\s*<\\s*")), QStringLiteral("<")) + .replace(QRegularExpression(QStringLiteral("\\s*>\\s*")), QStringLiteral(">")) + .replace(QRegularExpression(QStringLiteral("\\s*<=\\s*")), QStringLiteral("<=")) + .replace(QRegularExpression(QStringLiteral("\\s*>=\\s*")), QStringLiteral(">=")); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts); +#else + const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts); +#endif + + filter_text.clear(); + + FilterList filters; + static QRegularExpression operator_regex(QStringLiteral("(=|<[>=]?|>=?|!=)")); + for (int i = 0; i < tokens.count(); ++i) { + const QString &token = tokens[i]; + if (token.contains(QLatin1Char(':'))) { + QString field = token.section(QLatin1Char(':'), 0, 0).remove(QLatin1Char(':')).trimmed(); + QString value = token.section(QLatin1Char(':'), 1, -1).remove(QLatin1Char(':')).trimmed(); + if (field.isEmpty() || value.isEmpty()) continue; + if (Song::kTextSearchColumns.contains(field, Qt::CaseInsensitive) && value.count(QLatin1Char('"')) <= 2) { + bool quotation_mark_start = false; + bool quotation_mark_end = false; + if (value.left(1) == QLatin1Char('"')) { + value.remove(0, 1); + quotation_mark_start = true; + if (value.length() >= 1 && value.count(QLatin1Char('"')) == 1) { + value = value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed(); + quotation_mark_end = true; + } + } + for (int y = i + 1; y < tokens.count() && !quotation_mark_end; ++y) { + QString next_value = tokens[y]; + if (!quotation_mark_start && ContainsOperators(next_value)) { + break; + } + if (quotation_mark_start && next_value.contains(QLatin1Char('"'))) { + next_value = next_value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed(); + quotation_mark_end = true; + } + value.append(QLatin1Char(' ') + next_value); + i = y; + } + if (!field.isEmpty() && !value.isEmpty()) { + filters.insert(field, Filter(field, value)); + } + continue; + } + } + else if (token.contains(operator_regex)) { + QRegularExpressionMatch re_match = operator_regex.match(token); + if (re_match.hasMatch()) { + const QString foperator = re_match.captured(0); + const QString field = token.section(foperator, 0, 0).remove(foperator).trimmed(); + const QString value = token.section(foperator, 1, -1).remove(foperator).trimmed(); + if (value.isEmpty()) continue; + if (Song::kNumericalSearchColumns.contains(field, Qt::CaseInsensitive)) { + if (Song::kIntSearchColumns.contains(field, Qt::CaseInsensitive)) { + bool ok = false; + const int value_int = value.toInt(&ok); + if (ok) { + filters.insert(field, Filter(field, value_int, foperator)); + continue; + } + } + else if (Song::kUIntSearchColumns.contains(field, Qt::CaseInsensitive)) { + bool ok = false; + const uint value_uint = value.toUInt(&ok); + if (ok) { + filters.insert(field, Filter(field, value_uint, foperator)); + continue; + } + } + else if (field.compare(QStringLiteral("length"), Qt::CaseInsensitive) == 0) { + filters.insert(field, Filter(field, static_cast(Utilities::ParseSearchTime(value)) * kNsecPerSec, foperator)); + continue; + } + else if (field.compare(QStringLiteral("rating"), Qt::CaseInsensitive) == 0) { + filters.insert(field, Filter(field, Utilities::ParseSearchRating(value), foperator)); + } + } + } + } + if (!filter_text.isEmpty()) filter_text.append(QLatin1Char(' ')); + filter_text += token; + } + + if (filter_text.isEmpty() && filters.isEmpty()) return true; + + return ItemMatchesFilters(item, filters, filter_text); + +} + +bool CollectionFilter::ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text) { + + if (item->type == CollectionItem::Type::Song && + item->metadata.is_valid() && + ItemMetadataMatchesFilters(item->metadata, filters, filter_text)) { + return true; + } + + for (CollectionItem *child : std::as_const(item->children)) { + if (ItemMatchesFilters(child, filters, filter_text)) return true; + } + + return false; + +} + +bool CollectionFilter::ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QString &filter_text) { + + for (FilterList::const_iterator it = filters.begin() ; it != filters.end() ; ++it) { + const QString &field = it.key(); + const Filter &filter = it.value(); + const QVariant &value = filter.value; + const QString &foperator = filter.foperator; + if (field.isEmpty() || !value.isValid()) { + continue; + } + const QVariant data = DataFromField(field, metadata); + if ( +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + value.metaType() != data.metaType() +#else + value.type() != data.type() +#endif + || !FieldValueMatchesData(value, data, foperator)) { + return false; + } + } + + return filter_text.isEmpty() || ItemMetadataMatchesFilterText(metadata, filter_text); + +} + +bool CollectionFilter::ItemMetadataMatchesFilterText(const Song &metadata, const QString &filter_text) { + + return metadata.effective_albumartist().contains(filter_text, Qt::CaseInsensitive) || + metadata.artist().contains(filter_text, Qt::CaseInsensitive) || + metadata.album().contains(filter_text, Qt::CaseInsensitive) || + metadata.title().contains(filter_text, Qt::CaseInsensitive) || + metadata.composer().contains(filter_text, Qt::CaseInsensitive) || + metadata.performer().contains(filter_text, Qt::CaseInsensitive) || + metadata.grouping().contains(filter_text, Qt::CaseInsensitive) || + metadata.genre().contains(filter_text, Qt::CaseInsensitive) || + metadata.comment().contains(filter_text, Qt::CaseInsensitive); + +} + +QVariant CollectionFilter::DataFromField(const QString &field, const Song &metadata) { + + if (field == QStringLiteral("albumartist")) return metadata.effective_albumartist(); + if (field == QStringLiteral("artist")) return metadata.artist(); + if (field == QStringLiteral("album")) return metadata.album(); + if (field == QStringLiteral("title")) return metadata.title(); + if (field == QStringLiteral("composer")) return metadata.composer(); + if (field == QStringLiteral("performer")) return metadata.performer(); + if (field == QStringLiteral("grouping")) return metadata.grouping(); + if (field == QStringLiteral("genre")) return metadata.genre(); + if (field == QStringLiteral("comment")) return metadata.comment(); + if (field == QStringLiteral("track")) return metadata.track(); + if (field == QStringLiteral("year")) return metadata.year(); + if (field == QStringLiteral("length")) return metadata.length_nanosec(); + if (field == QStringLiteral("samplerate")) return metadata.samplerate(); + if (field == QStringLiteral("bitdepth")) return metadata.bitdepth(); + if (field == QStringLiteral("bitrate")) return metadata.bitrate(); + if (field == QStringLiteral("rating")) return metadata.rating(); + if (field == QStringLiteral("playcount")) return metadata.playcount(); + if (field == QStringLiteral("skipcount")) return metadata.skipcount(); + + return QVariant(); + +} + +bool CollectionFilter::FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator) { + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + switch (value.metaType().id()) { +#else + switch (value.userType()) { +#endif + case QMetaType::QString:{ + const QString str_value = value.toString(); + const QString str_data = data.toString(); + return str_data.contains(str_value, Qt::CaseInsensitive); + } + case QMetaType::Int:{ + return FieldIntValueMatchesData(value.toInt(), foperator, data.toInt()); + } + case QMetaType::UInt:{ + return FieldUIntValueMatchesData(value.toUInt(), foperator, data.toUInt()); + } + case QMetaType::LongLong:{ + return FieldLongLongValueMatchesData(value.toLongLong(), foperator, data.toLongLong()); + } + case QMetaType::Float:{ + return FieldFloatValueMatchesData(value.toFloat(), foperator, data.toFloat()); + } + default:{ + return false; + } + } + + return false; + +} + +template +bool CollectionFilter::FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data) { + + if (foperator == QStringLiteral("=") || foperator == QStringLiteral("==")) { + return data == value; + } + else if (foperator == QStringLiteral("!=") || foperator == QStringLiteral("<>")) { + return data != value; + } + else if (foperator == QStringLiteral("<")) { + return data < value; + } + else if (foperator == QStringLiteral(">")) { + return data > value; + } + else if (foperator == QStringLiteral(">=")) { + return data >= value; + } + else if (foperator == QStringLiteral("<=")) { + return data <= value; + } + + return false; + +} + +bool CollectionFilter::FieldIntValueMatchesData(const int value, const QString &foperator, const int data) { + + return FieldNumericalValueMatchesData(value, foperator, data); + +} + +bool CollectionFilter::FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data) { + + return FieldNumericalValueMatchesData(value, foperator, data); + +} + +bool CollectionFilter::FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data) { + + return FieldNumericalValueMatchesData(value, foperator, data); + +} + +bool CollectionFilter::FieldFloatValueMatchesData(const float value, const QString &foperator, const float data) { + + return FieldNumericalValueMatchesData(value, foperator, data); + +} + +bool CollectionFilter::ContainsOperators(const QString &token) { + + for (const QString &foperator : Operators) { + if (token.contains(foperator, Qt::CaseInsensitive)) return true; + } + + return false; + +} diff --git a/src/collection/collectionfilter.h b/src/collection/collectionfilter.h new file mode 100644 index 000000000..14bb0d28c --- /dev/null +++ b/src/collection/collectionfilter.h @@ -0,0 +1,69 @@ +/* + * Strawberry Music Player + * Copyright 2021-2024, 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 . + * + */ + +#ifndef COLLECTIONFILTER_H +#define COLLECTIONFILTER_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "core/song.h" + +class CollectionItem; + +class CollectionFilter : public QSortFilterProxyModel { + Q_OBJECT + + public: + explicit CollectionFilter(QObject *parent = nullptr); + + protected: + bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override; + + private: + static const QStringList Operators; + struct Filter { + public: + Filter(const QString &_field = QString(), const QVariant &_value = QVariant(), const QString &_foperator = QString()) : field(_field), value(_value), foperator(_foperator) {} + QString field; + QVariant value; + QString foperator; + }; + using FilterList = QMap; + static bool ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text); + static bool ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QString &filter_text); + static bool ItemMetadataMatchesFilterText(const Song &metadata, const QString &filter_text); + static QVariant DataFromField(const QString &field, const Song &metadata); + static bool FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator); + template + static bool FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data); + static bool FieldIntValueMatchesData(const int value, const QString &foperator, const int data); + static bool FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data); + static bool FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data); + static bool FieldFloatValueMatchesData(const float value, const QString &foperator, const float data); + static bool ContainsOperators(const QString &token); +}; + +#endif // COLLECTIONFILTER_H diff --git a/src/collection/collectionfilterwidget.cpp b/src/collection/collectionfilterwidget.cpp index 94a5f742b..f9f6e5b2e 100644 --- a/src/collection/collectionfilterwidget.cpp +++ b/src/collection/collectionfilterwidget.cpp @@ -50,6 +50,8 @@ #include "core/settings.h" #include "collectionfilteroptions.h" #include "collectionmodel.h" +#include "collectionfilter.h" +#include "collectionquery.h" #include "savedgroupingmanager.h" #include "collectionfilterwidget.h" #include "groupbydialog.h" @@ -62,6 +64,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent) : QWidget(parent), ui_(new Ui_CollectionFilterWidget), model_(nullptr), + filter_(nullptr), group_by_dialog_(new GroupByDialog(this)), groupings_manager_(nullptr), filter_age_menu_(nullptr), @@ -74,8 +77,8 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent) ui_->setupUi(this); - QString available_fields = Song::kFtsColumns.join(QStringLiteral(", ")).replace(QRegularExpression(QStringLiteral("\\bfts")), QLatin1String("")); - available_fields += QStringLiteral(", ") + Song::kNumericalColumns.join(QStringLiteral(", ")); + QString available_fields = Song::kTextSearchColumns.join(QStringLiteral(", ")); + available_fields += QStringLiteral(", ") + Song::kNumericalSearchColumns.join(QStringLiteral(", ")); ui_->search_field->setToolTip( QStringLiteral("

") + @@ -125,12 +128,12 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent) filter_age_menu_ = new QMenu(tr("Show"), this); filter_age_menu_->addActions(filter_age_group->actions()); - filter_ages_[ui_->filter_age_all] = -1; - filter_ages_[ui_->filter_age_today] = 60 * 60 * 24; - filter_ages_[ui_->filter_age_week] = 60 * 60 * 24 * 7; - filter_ages_[ui_->filter_age_month] = 60 * 60 * 24 * 30; - filter_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3; - filter_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365; + filter_max_ages_[ui_->filter_age_all] = -1; + filter_max_ages_[ui_->filter_age_today] = 60 * 60 * 24; + filter_max_ages_[ui_->filter_age_week] = 60 * 60 * 24 * 7; + filter_max_ages_[ui_->filter_age_month] = 60 * 60 * 24 * 30; + filter_max_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3; + filter_max_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365; group_by_menu_ = new QMenu(tr("Group by"), this); @@ -156,29 +159,30 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent) CollectionFilterWidget::~CollectionFilterWidget() { delete ui_; } -void CollectionFilterWidget::Init(CollectionModel *model) { +void CollectionFilterWidget::Init(CollectionModel *model, CollectionFilter *filter) { if (model_) { QObject::disconnect(model_, nullptr, this, nullptr); QObject::disconnect(model_, nullptr, group_by_dialog_, nullptr); QObject::disconnect(group_by_dialog_, nullptr, model_, nullptr); - const QList filter_ages = filter_ages_.keys(); - for (QAction *action : filter_ages) { + const QList actions = filter_max_ages_.keys(); + for (QAction *action : actions) { QObject::disconnect(action, &QAction::triggered, model_, nullptr); } } model_ = model; + filter_ = filter; // Connect signals QObject::connect(model_, &CollectionModel::GroupingChanged, group_by_dialog_, &GroupByDialog::CollectionGroupingChanged); QObject::connect(model_, &CollectionModel::GroupingChanged, this, &CollectionFilterWidget::GroupingChanged); QObject::connect(group_by_dialog_, &GroupByDialog::Accepted, model_, &CollectionModel::SetGroupBy); - const QList filter_ages = filter_ages_.keys(); - for (QAction *action : filter_ages) { - int age = filter_ages_[action]; - QObject::connect(action, &QAction::triggered, this, [this, age]() { model_->SetFilterAge(age); } ); + const QList actions = filter_max_ages_.keys(); + for (QAction *action : actions) { + int filter_max_age = filter_max_ages_[action]; + QObject::connect(action, &QAction::triggered, this, [this, filter_max_age]() { model_->SetFilterMaxAge(filter_max_age); } ); } // Load settings @@ -217,6 +221,10 @@ void CollectionFilterWidget::SetSettingsPrefix(const QString &prefix) { } +void CollectionFilterWidget::setFilter(CollectionFilter *filter) { + filter_ = filter; +} + void CollectionFilterWidget::ReloadSettings() { Settings s; @@ -518,9 +526,6 @@ void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) { void CollectionFilterWidget::FilterTextChanged(const QString &text) { - // Searching with one or two characters can be very expensive on the database even with FTS, - // so if there are a large number of songs in the database introduce a small delay before actually filtering the model, - // so if the user is typing the first few characters of something it will be quicker. const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000); if (delay) { @@ -535,9 +540,8 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) { void CollectionFilterWidget::FilterDelayTimeout() { - emit Filter(ui_->search_field->text()); if (filter_applies_to_model_) { - model_->SetFilterText(ui_->search_field->text()); + filter_->setFilterFixedString(ui_->search_field->text()); } } diff --git a/src/collection/collectionfilterwidget.h b/src/collection/collectionfilterwidget.h index fd07b84de..729571a02 100644 --- a/src/collection/collectionfilterwidget.h +++ b/src/collection/collectionfilterwidget.h @@ -41,6 +41,7 @@ class QKeyEvent; class GroupByDialog; class SavedGroupingManager; +class CollectionFilter; class Ui_CollectionFilterWidget; class CollectionFilterWidget : public QWidget { @@ -58,7 +59,9 @@ class CollectionFilterWidget : public QWidget { AlwaysDelayed, }; - void Init(CollectionModel *model); + void Init(CollectionModel *model, CollectionFilter *filter); + + void setFilter(CollectionFilter *filter); static QActionGroup *CreateGroupByActions(const QString &saved_groupings_settings_group, QObject *parent); @@ -94,7 +97,6 @@ class CollectionFilterWidget : public QWidget { void UpPressed(); void DownPressed(); void ReturnPressed(); - void Filter(const QString &text); protected: void keyReleaseEvent(QKeyEvent *e) override; @@ -115,6 +117,7 @@ class CollectionFilterWidget : public QWidget { private: Ui_CollectionFilterWidget *ui_; CollectionModel *model_; + CollectionFilter *filter_; GroupByDialog *group_by_dialog_; SavedGroupingManager *groupings_manager_; @@ -123,7 +126,7 @@ class CollectionFilterWidget : public QWidget { QMenu *group_by_menu_; QMenu *collection_menu_; QActionGroup *group_by_group_; - QHash filter_ages_; + QHash filter_max_ages_; QTimer *filter_delay_; diff --git a/src/collection/collectionitem.h b/src/collection/collectionitem.h index 3cb9e19c9..14655ee96 100644 --- a/src/collection/collectionitem.h +++ b/src/collection/collectionitem.h @@ -29,24 +29,27 @@ class CollectionItem : public SimpleTreeItem { public: - enum Type { - Type_Root, - Type_Divider, - Type_Container, - Type_Song, - Type_LoadingIndicator, + enum class Type { + Root, + Divider, + Container, + Song, + LoadingIndicator, }; explicit CollectionItem(SimpleTreeModel *_model) - : SimpleTreeItem(Type_Root, _model), + : SimpleTreeItem(_model), + type(Type::Root), container_level(-1), compilation_artist_node_(nullptr) {} - explicit CollectionItem(Type _type, CollectionItem *_parent = nullptr) - : SimpleTreeItem(_type, _parent), + explicit CollectionItem(const Type _type, CollectionItem *_parent = nullptr) + : SimpleTreeItem(_parent), + type(_type), container_level(-1), compilation_artist_node_(nullptr) {} + Type type; int container_level; Song metadata; CollectionItem *compilation_artist_node_; @@ -55,4 +58,6 @@ class CollectionItem : public SimpleTreeItem { Q_DISABLE_COPY(CollectionItem) }; +Q_DECLARE_METATYPE(CollectionItem::Type) + #endif // COLLECTIONITEM_H diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp index 386eb9323..f9bf1243b 100644 --- a/src/collection/collectionmodel.cpp +++ b/src/collection/collectionmodel.cpp @@ -1,7 +1,5 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2010, David Sansome * Copyright 2018-2024, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify @@ -52,6 +50,7 @@ #include #include #include +#include #include "core/scoped_ptr.h" #include "core/shared_ptr.h" @@ -59,16 +58,16 @@ #include "core/database.h" #include "core/iconloader.h" #include "core/logging.h" -#include "core/taskmanager.h" #include "core/sqlrow.h" #include "core/settings.h" #include "collectionfilteroptions.h" #include "collectionquery.h" -#include "collectionqueryoptions.h" #include "collectionbackend.h" #include "collectiondirectorymodel.h" #include "collectionitem.h" #include "collectionmodel.h" +#include "collectionmodelupdate.h" +#include "collectionfilter.h" #include "playlist/playlistmanager.h" #include "playlist/songmimedata.h" #include "covermanager/albumcoverloaderoptions.h" @@ -77,7 +76,10 @@ #include "settings/collectionsettingspage.h" const int CollectionModel::kPrettyCoverSize = 32; -const char *CollectionModel::kPixmapDiskCacheDir = "pixmapcache"; +namespace { +constexpr char kPixmapDiskCacheDir[] = "pixmapcache"; +constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists"); +} // namespace QNetworkDiskCache *CollectionModel::sIconCache = nullptr; @@ -86,25 +88,20 @@ CollectionModel::CollectionModel(SharedPtr backend, Applicati backend_(backend), app_(app), dir_model_(new CollectionDirectoryModel(backend, this)), - show_various_artists_(true), - sort_skips_articles_(true), + filter_(new CollectionFilter(this)), + timer_reload_(new QTimer(this)), + timer_update_(new QTimer(this)), + icon_artist_(IconLoader::Load(QStringLiteral("folder-sound"))), + use_disk_cache_(false), total_song_count_(0), total_artist_count_(0), - total_album_count_(0), - separate_albums_by_grouping_(false), - artist_icon_(IconLoader::Load(QStringLiteral("folder-sound"))), - album_icon_(IconLoader::Load(QStringLiteral("cdcase"))), - init_task_id_(-1), - use_pretty_covers_(true), - show_dividers_(true), - use_disk_cache_(false), - use_lazy_loading_(true) { + total_album_count_(0) { - root_->lazy_loaded = true; - - group_by_[0] = GroupBy::AlbumArtist; - group_by_[1] = GroupBy::AlbumDisc; - group_by_[2] = GroupBy::None; + filter_->setSourceModel(this); + filter_->setSortRole(Role_SortText); + filter_->setDynamicSortFilter(true); + filter_->setSortLocaleAware(true); + filter_->sort(0); if (app_) { QObject::connect(&*app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &CollectionModel::AlbumCoverLoaded); @@ -113,7 +110,7 @@ CollectionModel::CollectionModel(SharedPtr backend, Applicati QIcon nocover = IconLoader::Load(QStringLiteral("cdcase")); if (!nocover.isNull()) { QList nocover_sizes = nocover.availableSizes(); - no_cover_icon_ = nocover.pixmap(nocover_sizes.last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + pixmap_no_cover_ = nocover.pixmap(nocover_sizes.last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } if (app_ && !sIconCache) { @@ -122,19 +119,28 @@ CollectionModel::CollectionModel(SharedPtr backend, Applicati 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::SongsAdded, this, &CollectionModel::AddReAddOrUpdate); + QObject::connect(&*backend_, &CollectionBackend::SongsChanged, this, &CollectionModel::AddReAddOrUpdate); + QObject::connect(&*backend_, &CollectionBackend::SongsDeleted, this, &CollectionModel::RemoveSongs); + QObject::connect(&*backend_, &CollectionBackend::DatabaseReset, this, &CollectionModel::ScheduleReset); 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); + QObject::connect(&*backend_, &CollectionBackend::SongsStatisticsChanged, this, &CollectionModel::AddReAddOrUpdate); + QObject::connect(&*backend_, &CollectionBackend::SongsRatingChanged, this, &CollectionModel::AddReAddOrUpdate); backend_->UpdateTotalSongCountAsync(); backend_->UpdateTotalArtistCountAsync(); backend_->UpdateTotalAlbumCountAsync(); + timer_reload_->setSingleShot(true); + timer_reload_->setInterval(300); + QObject::connect(timer_reload_, &QTimer::timeout, this, &CollectionModel::Reload); + + timer_update_->setSingleShot(false); + timer_update_->setInterval(20); + QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate); + ReloadSettings(); } @@ -149,384 +155,472 @@ CollectionModel::~CollectionModel() { } -void CollectionModel::set_pretty_covers(const bool use_pretty_covers) { +void CollectionModel::Init() { + ScheduleReset(); +} - if (use_pretty_covers != use_pretty_covers_) { - use_pretty_covers_ = use_pretty_covers; - Reset(); +void CollectionModel::Reset() { + ScheduleReset(); +} + +void CollectionModel::Clear() { + + if (root_) { + delete root_; + root_ = nullptr; } + 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(); } -void CollectionModel::set_show_dividers(const bool show_dividers) { +void CollectionModel::BeginReset() { - if (show_dividers != show_dividers_) { - show_dividers_ = show_dividers; - Reset(); - } + beginResetModel(); + Clear(); + Q_ASSERT(root_ == nullptr); + root_ = new CollectionItem(this); } -void CollectionModel::set_sort_skips_articles(const bool sort_skips_articles) { +void CollectionModel::EndReset() { - if (sort_skips_articles != sort_skips_articles_) { - sort_skips_articles_ = sort_skips_articles; - Reset(); + endResetModel(); + +} + +void CollectionModel::Reload() { + + options_active_ = options_current_; + + BeginReset(); + // Show a loading indicator in the model. + CollectionItem *loading = new CollectionItem(CollectionItem::Type::LoadingIndicator, root_); + loading->display_text = tr("Loading..."); + EndReset(); + + LoadSongsFromSqlAsync(); + +} + +void CollectionModel::ScheduleReset() { + + if (!timer_reload_->isActive()) { + timer_reload_->start(); } } void CollectionModel::ReloadSettings() { - Settings 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)); + Settings settings; + settings.beginGroup(CollectionSettingsPage::kSettingsGroup); + const bool show_pretty_covers = settings.value("pretty_covers", true).toBool(); + const bool show_dividers= settings.value("show_dividers", true).toBool(); + const bool show_various_artists = settings.value("various_artists", true).toBool(); + const bool sort_skips_articles = settings.value("sort_skips_articles", true).toBool(); + use_disk_cache_ = settings.value(CollectionSettingsPage::kSettingsDiskCacheEnable, false).toBool(); + QPixmapCache::setCacheLimit(static_cast(MaximumCacheSize(&settings, CollectionSettingsPage::kSettingsCacheSize, CollectionSettingsPage::kSettingsCacheSizeUnit, CollectionSettingsPage::kSettingsCacheSizeDefault) / 1024)); if (sIconCache) { - sIconCache->setMaximumCacheSize(MaximumCacheSize(&s, CollectionSettingsPage::kSettingsDiskCacheSize, CollectionSettingsPage::kSettingsDiskCacheSizeUnit, CollectionSettingsPage::kSettingsDiskCacheSizeDefault)); + sIconCache->setMaximumCacheSize(MaximumCacheSize(&settings, CollectionSettingsPage::kSettingsDiskCacheSize, CollectionSettingsPage::kSettingsDiskCacheSizeUnit, CollectionSettingsPage::kSettingsDiskCacheSizeDefault)); } - s.endGroup(); + settings.endGroup(); cover_types_ = AlbumCoverLoaderOptions::LoadTypes(); + if (show_pretty_covers != options_current_.show_pretty_covers || + show_dividers != options_current_.show_dividers || + show_various_artists != options_current_.show_various_artists || + sort_skips_articles != options_current_.sort_skips_articles) { + options_current_.show_pretty_covers = show_pretty_covers; + options_current_.show_dividers = show_dividers; + options_current_.show_various_artists = show_various_artists; + options_current_.sort_skips_articles = sort_skips_articles; + ScheduleReset(); + } + if (!use_disk_cache_) { ClearDiskCache(); } } -void CollectionModel::Init(const bool async) { +void CollectionModel::SetGroupBy(const Grouping g, const std::optional separate_albums_by_grouping) { - if (!root_) return; - - 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(); + options_current_.group_by = g; + if (separate_albums_by_grouping) { + options_current_.separate_albums_by_grouping = separate_albums_by_grouping.value(); } - else { - Reset(); + + ScheduleReset(); + + emit GroupingChanged(g, options_current_.separate_albums_by_grouping); + +} + +void CollectionModel::SetFilterMode(const CollectionFilterOptions::FilterMode filter_mode) { + + if (options_current_.filter_options.filter_mode() != filter_mode) { + options_current_.filter_options.set_filter_mode(filter_mode); + ScheduleReset(); } } -void CollectionModel::SongsDiscovered(const SongList &songs) { +void CollectionModel::SetFilterMaxAge(const int filter_max_age) { - if (!root_) return; + if (options_current_.filter_options.max_age() != filter_max_age) { + options_current_.filter_options.set_max_age(filter_max_age); + ScheduleReset(); + } + +} + +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 (options_active_.show_pretty_covers) { + bool is_album_node = false; + if (role == Qt::DecorationRole && item->type == CollectionItem::Type::Container) { + GroupBy container_group_by = options_active_.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 ? options_active_.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 QVariant(); + case GroupBy::Artist: + case GroupBy::AlbumArtist: + return icon_artist_; + default: + break; + } + break; + default: + break; + } + break; + + case Role_Type: + return QVariant::fromValue(item->type); + + case Role_IsDivider: + return item->type == CollectionItem::Type::Divider; + + case Role_ContainerType: + return static_cast(container_group_by); + + case Role_ContainerKey: + return item->container_key; + + case Role_Artist: + return item->metadata.artist(); + + case Role_Editable:{ + 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(); + +} + +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() << QStringLiteral("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; + +} + +void CollectionModel::AddReAddOrUpdate(const SongList &songs) { + + ScheduleUpdate(CollectionModelUpdate::Type::AddReAddOrUpdate, songs); + +} + +void CollectionModel::RemoveSongs(const SongList &songs) { + + ScheduleRemoveSongs(songs); + +} + +void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) { + + for (qint64 i = 0; i < songs.count(); i += 400LL) { + const qint64 number = std::min(songs.count() - i, 400LL); + const SongList songs_to_queue = songs.mid(i, number); + updates_.enqueue(CollectionModelUpdate(type, songs_to_queue)); + } + + if (!timer_update_->isActive()) { + timer_update_->start(); + } + +} + +void CollectionModel::ScheduleAddSongs(const SongList &songs) { + + ScheduleUpdate(CollectionModelUpdate::Type::Add, songs); + +} + +void CollectionModel::ScheduleUpdateSongs(const SongList &songs) { + + ScheduleUpdate(CollectionModelUpdate::Type::Update, songs); + +} + +void CollectionModel::ScheduleRemoveSongs(const SongList &songs) { + + ScheduleUpdate(CollectionModelUpdate::Type::Remove, songs); + +} + +void CollectionModel::ProcessUpdate() { + + if (updates_.isEmpty()) { + timer_update_->stop(); + return; + } + + const CollectionModelUpdate update = updates_.dequeue(); + + if (updates_.isEmpty()) { + timer_update_->stop(); + } + + switch (update.type) { + case CollectionModelUpdate::Type::AddReAddOrUpdate: + AddReAddOrUpdateSongsInternal(update.songs); + break; + case CollectionModelUpdate::Type::Add: + AddSongsInternal(update.songs); + break; + case CollectionModelUpdate::Type::Update: + UpdateSongsInternal(update.songs); + break; + case CollectionModelUpdate::Type::Remove: + RemoveSongsInternal(update.songs); + break; + } + +} + +void CollectionModel::AddReAddOrUpdateSongsInternal(const SongList &songs) { + + SongList songs_added; + SongList songs_removed; + SongList songs_updated; + + for (const Song &new_song : songs) { + if (!song_nodes_.contains(new_song.id())) { + songs_added << new_song; + continue; + } + const Song &old_song = song_nodes_[new_song.id()]->metadata; + bool container_key_changed = false; + for (int i = 0; i < 3; ++i) { + if (IsArtistGroupBy(options_active_.group_by[i]) && new_song.is_compilation() != old_song.is_compilation()) { + container_key_changed = true; + } + if (options_active_.group_by[i] != GroupBy::None && ContainerKey(options_active_.group_by[i], new_song) != ContainerKey(options_active_.group_by[i], old_song)) { + container_key_changed = true; + } + } + if (container_key_changed) { + qLog(Debug) << "Container key for" << new_song.id() << new_song.PrettyTitleWithArtist() << "was changed, re-adding song."; + songs_removed << old_song; + songs_added << new_song; + } + else { + qLog(Debug) << "Container key for" << new_song.id() << new_song.PrettyTitleWithArtist() << "was not changed, only updating song metadata."; + songs_updated << new_song; + } + } + + ScheduleRemoveSongs(songs_removed); + ScheduleUpdateSongs(songs_updated); + ScheduleAddSongs(songs_added); + +} + +void CollectionModel::AddSongsInternal(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; + if (!options_active_.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; + QString container_key; for (int i = 0; i < 3; ++i) { - GroupBy group_by = group_by_[i]; + const GroupBy group_by = options_active_.group_by[i]; if (group_by == GroupBy::None) break; - - if (!key.isEmpty()) key.append(QLatin1Char('-')); - - // 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_key.isEmpty()) container_key.append(QLatin1Char('-')); + if (IsArtistGroupBy(group_by) && song.is_compilation() && options_active_.show_various_artists) { if (container->compilation_artist_node_ == nullptr) { - CreateCompilationArtistNode(true, container); + CreateCompilationArtistNode(container); } container = container->compilation_artist_node_; - key = container->key; + container_key = container->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]; + container_key.append(ContainerKey(group_by, song)); + if (container_nodes_[i].contains(container_key)) { + container = container_nodes_[i][container_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); + container = CreateContainerItem(group_by, i, container_key, song, 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)); + CreateSongItem(song, container); } } -void CollectionModel::SongsSlightlyChanged(const SongList &songs) { +void CollectionModel::UpdateSongsInternal(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; + QList album_parents; + + for (const Song &new_song : songs) { + if (!song_nodes_.contains(new_song.id())) { + qLog(Error) << "Song does not exist in model" << new_song.id() << new_song.PrettyTitleWithArtist(); + continue; + } + CollectionItem *item = song_nodes_[new_song.id()]; + const Song &old_song = item->metadata; + const bool song_title_data_changed = IsSongTitleDataChanged(old_song, new_song); + const bool art_changed = !old_song.IsArtEqual(new_song); + SetSongItemData(item, new_song); + if (art_changed) { + for (CollectionItem *parent = item->parent; parent != root_; parent = parent->parent) { + if (IsAlbumGroupBy(options_active_.group_by[parent->container_level])) { + album_parents << parent; + } + } + } + if (song_title_data_changed) { + qLog(Debug) << "Song metadata and title for" << new_song.id() << new_song.PrettyTitleWithArtist() << "changed, informing model"; + const QModelIndex idx = ItemToIndex(item); + if (!idx.isValid()) continue; + emit dataChanged(idx, idx); + } + else { + qLog(Debug) << "Song metadata for" << new_song.id() << new_song.PrettyTitleWithArtist() << "changed"; + } + } + + for (CollectionItem *item : album_parents) { + ClearItemPixmapCache(item); + const QModelIndex idx = ItemToIndex(item); + if (idx.isValid()) { + emit dataChanged(idx, idx); } } } -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 = QStringLiteral(" 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(QLatin1Char('-') + song.album_id()); - if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); - break; - case GroupBy::AlbumDisc: - key = PrettyAlbumDisc(song.album(), song.disc()); - if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); - if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); - break; - case GroupBy::YearAlbum: - key = PrettyYearAlbum(song.year(), song.album()); - if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); - if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); - break; - case GroupBy::YearAlbumDisc: - key = PrettyYearAlbumDisc(song.year(), song.album(), song.disc()); - if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); - if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); - break; - case GroupBy::OriginalYearAlbum: - key = PrettyYearAlbum(song.effective_originalyear(), song.album()); - if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); - if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); - break; - case GroupBy::OriginalYearAlbumDisc: - key = PrettyYearAlbumDisc(song.effective_originalyear(), song.album(), song.disc()); - if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); - if (separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + 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 = QStringLiteral("%1 (%2)").arg(song.TextForFiletype(), QString::number(song.samplerate() / 1000.0, 'G', 5)); - } - else { - key = QStringLiteral("%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 QStringLiteral("0"); - if (c == QLatin1Char(' ')) 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 == QLatin1String("0")) return QStringLiteral("0-9"); - return key.toUpper(); - - case GroupBy::YearAlbum: - case GroupBy::YearAlbumDisc: - case GroupBy::OriginalYearAlbum: - case GroupBy::OriginalYearAlbumDisc: - if (key == QStringLiteral("0000")) return tr("Unknown"); - return key.toUpper(); - - case GroupBy::Year: - case GroupBy::OriginalYear: - if (key == QStringLiteral("0000")) return tr("Unknown"); - return QString::number(key.toInt()); // To remove leading 0s - - case GroupBy::Samplerate: - if (key == QStringLiteral("000")) return tr("Unknown"); - return QString::number(key.toInt()); // To remove leading 0s - - case GroupBy::Bitdepth: - if (key == QStringLiteral("000")) return tr("Unknown"); - return QString::number(key.toInt()); // To remove leading 0s - - case GroupBy::Bitrate: - if (key == QStringLiteral("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) { - - if (!root_) return; +void CollectionModel::RemoveSongsInternal(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; @@ -543,13 +637,6 @@ void CollectionModel::SongsDeleted(const SongList &songs) { 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 @@ -567,34 +654,18 @@ void CollectionModel::SongsDeleted(const SongList &songs) { // Maybe consider its divider node if (node->container_level == 0) { - divider_keys << DividerKey(group_by_[0], node); + divider_keys << DividerKey(options_active_.group_by[0], node->metadata, node->sort_text); } // 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); + else if (container_nodes_[node->container_level].contains(node->container_key)) { + container_nodes_[node->container_level].remove(node->container_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; - } - } + ClearItemPixmapCache(node); // It was empty - delete it beginRemoveRows(ItemToIndex(node->parent), node->row, node->row); @@ -609,7 +680,7 @@ void CollectionModel::SongsDeleted(const SongList &songs) { // 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; })) { + if (std::any_of(container_nodes.begin(), container_nodes.end(), [this, divider_key](CollectionItem *node){ return DividerKey(options_active_.group_by[0], node->metadata, node->sort_text) == divider_key; })) { continue; } @@ -623,6 +694,151 @@ void CollectionModel::SongsDeleted(const SongList &songs) { } +CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, const int container_level, const QString &container_key, const Song &song, CollectionItem *parent) { + + QString divider_key; + if (options_active_.show_dividers && container_level == 0) { + divider_key = DividerKey(group_by, song, SortText(group_by, container_level, song, options_active_.sort_skips_articles)); + if (!divider_key.isEmpty()) { + if (!divider_nodes_.contains(divider_key)) { + CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent); + } + } + } + + beginInsertRows(ItemToIndex(parent), static_cast(parent->children.count()), static_cast(parent->children.count())); + + CollectionItem *item = new CollectionItem(CollectionItem::Type::Container, parent); + item->container_level = container_level; + item->container_key = container_key; + item->display_text = DisplayText(group_by, song); + item->sort_text = SortText(group_by, container_level, song, options_active_.sort_skips_articles); + if (!divider_key.isEmpty()) { + item->sort_text.prepend(divider_key + QLatin1Char(' ')); + } + + container_nodes_[container_level].insert(item->container_key, item); + + endInsertRows(); + + return item; + +} + +void CollectionModel::CreateDividerItem(const QString ÷r_key, const QString &display_text, CollectionItem *parent) { + + beginInsertRows(ItemToIndex(parent), static_cast(parent->children.count()), static_cast(parent->children.count())); + + CollectionItem *divider = new CollectionItem(CollectionItem::Type::Divider, root_); + divider->container_key = divider_key; + divider->display_text = display_text; + divider->sort_text = divider_key + QStringLiteral(" "); + divider_nodes_[divider_key] = divider; + + endInsertRows(); + +} + +void CollectionModel::CreateSongItem(const Song &song, CollectionItem *parent) { + + beginInsertRows(ItemToIndex(parent), static_cast(parent->children.count()), static_cast(parent->children.count())); + + CollectionItem *item = new CollectionItem(CollectionItem::Type::Song, parent); + SetSongItemData(item, song); + song_nodes_.insert(song.id(), item); + + endInsertRows(); + +} + +void CollectionModel::SetSongItemData(CollectionItem *item, const Song &song) { + + item->display_text = song.TitleWithCompilationArtist(); + if (item->container_level == 1 && !IsAlbumGroupBy(options_active_.group_by[0])) { + item->sort_text = SortText(song.title()); + } + else { + item->sort_text = SortTextForSong(song); + } + + item->metadata = song; + +} + +CollectionItem *CollectionModel::CreateCompilationArtistNode(CollectionItem *parent) { + + Q_ASSERT(parent->compilation_artist_node_ == nullptr); + + 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->container_key.isEmpty()) parent->compilation_artist_node_->container_key.append(parent->container_key); + parent->compilation_artist_node_->container_key.append(QLatin1String(kVariousArtists)); + parent->compilation_artist_node_->display_text = QLatin1String(kVariousArtists); + parent->compilation_artist_node_->sort_text = QStringLiteral(" various"); + parent->compilation_artist_node_->container_level = parent->container_level + 1; + + endInsertRows(); + + return parent->compilation_artist_node_; + +} + +void CollectionModel::LoadSongsFromSqlAsync() { + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QFuture future = QtConcurrent::run(&CollectionModel::LoadSongsFromSql, this, options_active_.filter_options); +#else + QFuture future = QtConcurrent::run(this, &CollectionModel::LoadSongsFromSql, options_active_.filter_options); +#endif + QFutureWatcher *watcher = new QFutureWatcher(); + QObject::connect(watcher, &QFutureWatcher::finished, this, &CollectionModel::LoadSongsFromSqlAsyncFinished); + watcher->setFuture(future); + +} + +SongList CollectionModel::LoadSongsFromSql(const CollectionFilterOptions &filter_options) { + + SongList songs; + + { + QMutexLocker l(backend_->db()->Mutex()); + QSqlDatabase db(backend_->db()->Connect()); + CollectionQuery q(db, backend_->songs_table(), filter_options); + q.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec); + if (q.Exec()) { + while (q.Next()) { + Song song; + song.InitFromQuery(q, true); + songs << song; + } + } + else { + backend_->ReportErrors(q); + } + } + + if (QThread::currentThread() != thread() && QThread::currentThread() != backend_->thread()) { + backend_->db()->Close(); + } + + return songs; + +} + +void CollectionModel::LoadSongsFromSqlAsyncFinished() { + + QFutureWatcher *watcher = static_cast*>(sender()); + const SongList songs = watcher->result(); + watcher->deleteLater(); + + BeginReset(); + ScheduleAddSongs(songs); + EndReset(); + +} + QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const { QStringList path; @@ -642,10 +858,32 @@ QUrl CollectionModel::AlbumIconPixmapDiskCacheKey(const QString &cache_key) cons } +void CollectionModel::ClearItemPixmapCache(CollectionItem *item) { + + // Remove from pixmap cache + const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(item)); + 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 == item) { + it = pending_art_.erase(it); + } + else { + ++it; + } + } + +} + QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) { CollectionItem *item = IndexToItem(idx); - if (!item) return no_cover_icon_; + if (!item) return pixmap_no_cover_; // Check the cache for a pixmap we already loaded. const QString cache_key = AlbumIconPixmapCacheKey(idx); @@ -669,7 +907,7 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) { // Maybe we're loading a pixmap already? if (pending_cache_keys_.contains(cache_key)) { - return no_cover_icon_; + return pixmap_no_cover_; } // No art is cached and we're not loading it already. Load art for the first song in the album. @@ -683,7 +921,7 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) { pending_cache_keys_.insert(cache_key); } - return no_cover_icon_; + return pixmap_no_cover_; } @@ -702,7 +940,7 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderR // 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_); + QPixmapCache::insert(cache_key, pixmap_no_cover_); } else { QPixmap image_pixmap; @@ -735,971 +973,55 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderR } -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) { - - if (!root_) return; - - // 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 (!root_) return; - - 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() { - - if (!root_) return; - - 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() { - - if (!root_) return; - - 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::Clear() { - - if (root_) { - delete root_; - root_ = nullptr; - } - 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(); - -} - -void CollectionModel::BeginReset() { - - beginResetModel(); - 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(QStringLiteral("DISTINCT effective_albumartist")); - break; - case GroupBy::Artist: - query_options->set_column_spec(QStringLiteral("DISTINCT artist")); - break; - case GroupBy::Album:{ - QString query(QStringLiteral("DISTINCT album, album_id")); - if (separate_albums_by_grouping) query.append(QStringLiteral(", grouping")); - query_options->set_column_spec(query); - break; - } - case GroupBy::AlbumDisc:{ - QString query(QStringLiteral("DISTINCT album, album_id, disc")); - if (separate_albums_by_grouping) query.append(QStringLiteral(", grouping")); - query_options->set_column_spec(query); - break; - } - case GroupBy::YearAlbum:{ - QString query(QStringLiteral("DISTINCT year, album, album_id")); - if (separate_albums_by_grouping) query.append(QStringLiteral(", grouping")); - query_options->set_column_spec(query); - break; - } - case GroupBy::YearAlbumDisc:{ - QString query(QStringLiteral("DISTINCT year, album, album_id, disc")); - if (separate_albums_by_grouping) query.append(QStringLiteral(", grouping")); - query_options->set_column_spec(query); - break; - } - case GroupBy::OriginalYearAlbum:{ - QString query(QStringLiteral("DISTINCT year, originalyear, album, album_id")); - if (separate_albums_by_grouping) query.append(QStringLiteral(", grouping")); - query_options->set_column_spec(query); - break; - } - case GroupBy::OriginalYearAlbumDisc:{ - QString query(QStringLiteral("DISTINCT year, originalyear, album, album_id, disc")); - if (separate_albums_by_grouping) query.append(QStringLiteral(", grouping")); - query_options->set_column_spec(query); - break; - } - case GroupBy::Disc: - query_options->set_column_spec(QStringLiteral("DISTINCT disc")); - break; - case GroupBy::Year: - query_options->set_column_spec(QStringLiteral("DISTINCT year")); - break; - case GroupBy::OriginalYear: - query_options->set_column_spec(QStringLiteral("DISTINCT effective_originalyear")); - break; - case GroupBy::Genre: - query_options->set_column_spec(QStringLiteral("DISTINCT genre")); - break; - case GroupBy::Composer: - query_options->set_column_spec(QStringLiteral("DISTINCT composer")); - break; - case GroupBy::Performer: - query_options->set_column_spec(QStringLiteral("DISTINCT performer")); - break; - case GroupBy::Grouping: - query_options->set_column_spec(QStringLiteral("DISTINCT grouping")); - break; - case GroupBy::FileType: - query_options->set_column_spec(QStringLiteral("DISTINCT filetype")); - break; - case GroupBy::Format: - query_options->set_column_spec(QStringLiteral("DISTINCT filetype, samplerate, bitdepth")); - break; - case GroupBy::Samplerate: - query_options->set_column_spec(QStringLiteral("DISTINCT samplerate")); - break; - case GroupBy::Bitdepth: - query_options->set_column_spec(QStringLiteral("DISTINCT bitdepth")); - break; - case GroupBy::Bitrate: - query_options->set_column_spec(QStringLiteral("DISTINCT bitrate")); - break; - case GroupBy::None: - case GroupBy::GroupByCount: - query_options->set_column_spec(QStringLiteral("%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. +QString CollectionModel::DisplayText(const GroupBy group_by, const Song &song) { 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(QStringLiteral("effective_albumartist"), item->metadata.effective_albumartist()); - } - break; + return TextOrUnknown(song.effective_albumartist()); 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(QStringLiteral("artist"), item->metadata.artist()); - } - break; + return TextOrUnknown(song.artist()); case GroupBy::Album: - query_options->AddWhere(QStringLiteral("album"), item->metadata.album()); - query_options->AddWhere(QStringLiteral("album_id"), item->metadata.album_id()); - if (separate_albums_by_grouping) query_options->AddWhere(QStringLiteral("grouping"), item->metadata.grouping()); - break; + return TextOrUnknown(song.album()); case GroupBy::AlbumDisc: - query_options->AddWhere(QStringLiteral("album"), item->metadata.album()); - query_options->AddWhere(QStringLiteral("album_id"), item->metadata.album_id()); - query_options->AddWhere(QStringLiteral("disc"), item->metadata.disc()); - if (separate_albums_by_grouping) query_options->AddWhere(QStringLiteral("grouping"), item->metadata.grouping()); - break; + return PrettyAlbumDisc(song.album(), song.disc()); case GroupBy::YearAlbum: - query_options->AddWhere(QStringLiteral("year"), item->metadata.year()); - query_options->AddWhere(QStringLiteral("album"), item->metadata.album()); - query_options->AddWhere(QStringLiteral("album_id"), item->metadata.album_id()); - if (separate_albums_by_grouping) query_options->AddWhere(QStringLiteral("grouping"), item->metadata.grouping()); - break; + return PrettyYearAlbum(song.year(), song.album()); case GroupBy::YearAlbumDisc: - query_options->AddWhere(QStringLiteral("year"), item->metadata.year()); - query_options->AddWhere(QStringLiteral("album"), item->metadata.album()); - query_options->AddWhere(QStringLiteral("album_id"), item->metadata.album_id()); - query_options->AddWhere(QStringLiteral("disc"), item->metadata.disc()); - if (separate_albums_by_grouping) query_options->AddWhere(QStringLiteral("grouping"), item->metadata.grouping()); - break; + return PrettyYearAlbumDisc(song.year(), song.album(), song.disc()); case GroupBy::OriginalYearAlbum: - query_options->AddWhere(QStringLiteral("year"), item->metadata.year()); - query_options->AddWhere(QStringLiteral("originalyear"), item->metadata.originalyear()); - query_options->AddWhere(QStringLiteral("album"), item->metadata.album()); - query_options->AddWhere(QStringLiteral("album_id"), item->metadata.album_id()); - if (separate_albums_by_grouping) query_options->AddWhere(QStringLiteral("grouping"), item->metadata.grouping()); - break; + return PrettyYearAlbum(song.effective_originalyear(), song.album()); case GroupBy::OriginalYearAlbumDisc: - query_options->AddWhere(QStringLiteral("year"), item->metadata.year()); - query_options->AddWhere(QStringLiteral("originalyear"), item->metadata.originalyear()); - query_options->AddWhere(QStringLiteral("album"), item->metadata.album()); - query_options->AddWhere(QStringLiteral("album_id"), item->metadata.album_id()); - query_options->AddWhere(QStringLiteral("disc"), item->metadata.disc()); - if (separate_albums_by_grouping) query_options->AddWhere(QStringLiteral("grouping"), item->metadata.grouping()); - break; + return PrettyYearAlbumDisc(song.effective_originalyear(), song.album(), song.disc()); case GroupBy::Disc: - query_options->AddWhere(QStringLiteral("disc"), item->metadata.disc()); - break; + return PrettyDisc(std::max(0, song.disc())); case GroupBy::Year: - query_options->AddWhere(QStringLiteral("year"), item->metadata.year()); - break; + return QString::number(std::max(0, song.year())); case GroupBy::OriginalYear: - query_options->AddWhere(QStringLiteral("effective_originalyear"), item->metadata.effective_originalyear()); - break; + return QString::number(std::max(0, song.effective_originalyear())); case GroupBy::Genre: - query_options->AddWhere(QStringLiteral("genre"), item->metadata.genre()); - break; + return TextOrUnknown(song.genre()); case GroupBy::Composer: - query_options->AddWhere(QStringLiteral("composer"), item->metadata.composer()); - break; + return TextOrUnknown(song.composer()); case GroupBy::Performer: - query_options->AddWhere(QStringLiteral("performer"), item->metadata.performer()); - break; + return TextOrUnknown(song.performer()); case GroupBy::Grouping: - query_options->AddWhere(QStringLiteral("grouping"), item->metadata.grouping()); - break; + return TextOrUnknown(song.grouping()); case GroupBy::FileType: - query_options->AddWhere(QStringLiteral("filetype"), static_cast(item->metadata.filetype())); - break; + return song.TextForFiletype(); case GroupBy::Format: - query_options->AddWhere(QStringLiteral("filetype"), static_cast(item->metadata.filetype())); - query_options->AddWhere(QStringLiteral("samplerate"), item->metadata.samplerate()); - query_options->AddWhere(QStringLiteral("bitdepth"), item->metadata.bitdepth()); - break; + return PrettyFormat(song); case GroupBy::Samplerate: - query_options->AddWhere(QStringLiteral("samplerate"), item->metadata.samplerate()); - break; + return QString::number(std::max(0, song.samplerate())); case GroupBy::Bitdepth: - query_options->AddWhere(QStringLiteral("bitdepth"), item->metadata.bitdepth()); - break; + return QString::number(std::max(0, song.bitdepth())); case GroupBy::Bitrate: - query_options->AddWhere(QStringLiteral("bitrate"), item->metadata.bitrate()); - break; + return QString::number(std::max(0, song.bitrate())); case GroupBy::None: case GroupBy::GroupByCount: - qLog(Error) << "Unknown GroupBy" << group_by << "used in filter"; - break; + return song.TitleWithCompilationArtist(); } -} - -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 + QLatin1Char('-'); - } - - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - break; - } - case GroupBy::Album:{ - item->metadata.set_album(row.value(0).toString()); - item->metadata.set_album_id(row.value(1).toString()); - if (separate_albums_by_grouping) 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(), sort_skips_articles_); - 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()); - if (separate_albums_by_grouping) 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()); - if (separate_albums_by_grouping) 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()); - if (separate_albums_by_grouping) 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()); - if (separate_albums_by_grouping) 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()); - if (separate_albums_by_grouping) 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) + QLatin1Char(' '); - 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) + QLatin1Char(' '); - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - 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) + QLatin1Char(' '); - 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) + QLatin1Char(' '); - 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) + QLatin1Char(' '); - 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 + QLatin1Char('-'); - } - - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - 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) + QLatin1Char(' '); - 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) + QLatin1Char(' '); - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - 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(), sort_skips_articles_); - 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) + QLatin1Char(' '); - 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) + QLatin1Char(' '); - 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) + QLatin1Char(' '); - 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() == QStringLiteral("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 (!root_) return; - - 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 + QLatin1Char(' ')); - } - - 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 + QStringLiteral(" "); - divider->lazy_loaded = true; - - divider_nodes_[divider_key] = divider; - - if (signal) { - endInsertRows(); - } - } - } + return QString(); } @@ -1743,6 +1065,81 @@ QString CollectionModel::PrettyDisc(const int disc) { } +QString CollectionModel::PrettyFormat(const Song &song) { + + if (song.samplerate() <= 0) { + return song.TextForFiletype(); + } + else { + if (song.bitdepth() <= 0) { + return QStringLiteral("%1 (%2)").arg(song.TextForFiletype(), QString::number(song.samplerate() / 1000.0, 'G', 5)); + } + else { + return QStringLiteral("%1 (%2/%3)").arg(song.TextForFiletype(), QString::number(song.samplerate() / 1000.0, 'G', 5)).arg(song.bitdepth()); + } + } + +} + +QString CollectionModel::SortText(const GroupBy group_by, const int container_level, const Song &song, const bool sort_skips_articles) { + + switch (group_by) { + case GroupBy::AlbumArtist: + return SortTextForArtist(song.effective_albumartist(), sort_skips_articles); + case GroupBy::Artist: + return SortTextForArtist(song.artist(), sort_skips_articles); + case GroupBy::Album: + return SortTextForArtist(song.album(), sort_skips_articles); + case GroupBy::AlbumDisc: + return song.album() + SortTextForNumber(std::max(0, song.disc())); + case GroupBy::YearAlbum: + return SortTextForNumber(std::max(0, song.year())) + song.grouping() + song.album(); + case GroupBy::YearAlbumDisc: + return SortTextForNumber(std::max(0, song.year())) + song.album() + SortTextForNumber(std::max(0, song.disc())); + case GroupBy::OriginalYearAlbum: + return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + song.album(); + case GroupBy::OriginalYearAlbumDisc: + return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.album() + SortTextForNumber(std::max(0, song.disc())); + case GroupBy::Disc: + return SortTextForNumber(std::max(0, song.disc())); + case GroupBy::Year: + return SortTextForNumber(std::max(0, song.year())) + QLatin1Char(' '); + case GroupBy::OriginalYear: + return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' '); + case GroupBy::Genre: + return SortTextForArtist(song.genre(), sort_skips_articles); + case GroupBy::Composer: + return SortTextForArtist(song.composer(), sort_skips_articles); + case GroupBy::Performer: + return SortTextForArtist(song.performer(), sort_skips_articles); + case GroupBy::Grouping: + return SortTextForArtist(song.grouping(), sort_skips_articles); + case GroupBy::FileType: + return song.TextForFiletype(); + case GroupBy::Format: + return PrettyFormat(song); + case GroupBy::Samplerate: + return SortTextForNumber(std::max(0, song.samplerate())) + QLatin1Char(' '); + case GroupBy::Bitdepth: + return SortTextForNumber(std::max(0, song.bitdepth())) + QLatin1Char(' '); + case GroupBy::Bitrate: + return SortTextForNumber(std::max(0, song.bitrate())) + QLatin1Char(' '); + case GroupBy::None: + case GroupBy::GroupByCount:{ + if (container_level == 1 && !IsAlbumGroupBy(options_active_.group_by[0])) { + return SortText(song.title()); + } + else { + return SortTextForSong(song); + } + break; + } + } + + return QString(); + +} + QString CollectionModel::SortText(QString text) { if (text.isEmpty()) { @@ -1780,6 +1177,15 @@ QString CollectionModel::SortTextForNumber(const int number) { return QStringLiteral("%1").arg(number, 4, 10, QLatin1Char('0')); } +QString CollectionModel::SortTextForSong(const Song &song) { + + QString ret = QString::number(std::max(0, song.disc()) * 1000 + std::max(0, song.track())); + ret.prepend(QStringLiteral("0").repeated(6 - ret.length())); + ret.append(song.url().toString()); + return ret; + +} + QString CollectionModel::SortTextForYear(const int year) { QString str = QString::number(year); @@ -1794,52 +1200,208 @@ QString CollectionModel::SortTextForBitrate(const int bitrate) { } -QString CollectionModel::SortTextForSong(const Song &song) { +bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song2) { - QString ret = QString::number(std::max(0, song.disc()) * 1000 + std::max(0, song.track())); - ret.prepend(QStringLiteral("0").repeated(6 - ret.length())); - ret.append(song.url().toString()); - return ret; + return song1.url() != song2.url() || + song1.track() != song2.track() || + song1.title() != song2.title() || + song1.compilation() != song2.compilation() || + (song1.compilation() && song1.artist() != song2.artist()); } -Qt::ItemFlags CollectionModel::flags(const QModelIndex &idx) const { +QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song) 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; + 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(QLatin1Char('-') + song.album_id()); + if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); + break; + case GroupBy::AlbumDisc: + key = PrettyAlbumDisc(song.album(), song.disc()); + if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); + if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); + break; + case GroupBy::YearAlbum: + key = PrettyYearAlbum(song.year(), song.album()); + if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); + if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); + break; + case GroupBy::YearAlbumDisc: + key = PrettyYearAlbumDisc(song.year(), song.album(), song.disc()); + if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); + if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); + break; + case GroupBy::OriginalYearAlbum: + key = PrettyYearAlbum(song.effective_originalyear(), song.album()); + if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); + if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping()); + break; + case GroupBy::OriginalYearAlbumDisc: + key = PrettyYearAlbumDisc(song.effective_originalyear(), song.album(), song.disc()); + if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id()); + if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + 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: + key = PrettyFormat(song); + break; + case GroupBy::None: + case GroupBy::GroupByCount: + qLog(Error) << "GroupBy::None"; + break; } + return key; + } -QStringList CollectionModel::mimeTypes() const { - return QStringList() << QStringLiteral("text/uri-list"); -} +QString CollectionModel::DividerKey(const GroupBy group_by, const Song &song, const QString &sort_text) { -QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const { + // 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 (indexes.isEmpty()) return nullptr; + if (sort_text.isEmpty()) return QString(); - 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); + 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 = sort_text[0]; + if (c.isDigit()) return QStringLiteral("0"); + if (c == QLatin1Char(' ')) 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(sort_text.toInt() / 10 * 10); + case GroupBy::YearAlbum: + case GroupBy::YearAlbumDisc: + return SortTextForNumber(song.year()); + case GroupBy::OriginalYearAlbum: + case GroupBy::OriginalYearAlbumDisc: + return SortTextForNumber(song.effective_originalyear()); + case GroupBy::Samplerate: + return SortTextForNumber(song.samplerate()); + case GroupBy::Bitdepth: + return SortTextForNumber(song.bitdepth()); + case GroupBy::Bitrate: + return SortTextForNumber(song.bitrate()); + case GroupBy::None: + case GroupBy::GroupByCount: + return QString(); } - data->setUrls(urls); - data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs); + qLog(Error) << "Unknown GroupBy" << group_by << "for item" << sort_text; - return data; + 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 == QStringLiteral("0")) return QStringLiteral("0-9"); + return key.toUpper(); + + case GroupBy::YearAlbum: + case GroupBy::YearAlbumDisc: + case GroupBy::OriginalYearAlbum: + case GroupBy::OriginalYearAlbumDisc: + if (key == QStringLiteral("0000")) return tr("Unknown"); + return key.toUpper(); + + case GroupBy::Year: + case GroupBy::OriginalYear: + if (key == QStringLiteral("0000")) return tr("Unknown"); + return QString::number(key.toInt()); // To remove leading 0s + + case GroupBy::Samplerate: + if (key == QStringLiteral("000")) return tr("Unknown"); + return QString::number(key.toInt()); // To remove leading 0s + + case GroupBy::Bitdepth: + if (key == QStringLiteral("000")) return tr("Unknown"); + return QString::number(key.toInt()); // To remove leading 0s + + case GroupBy::Bitrate: + if (key == QStringLiteral("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(); } @@ -1861,8 +1423,8 @@ bool CollectionModel::CompareItems(const CollectionItem *a, const CollectionItem qint64 CollectionModel::MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default) { - qint64 size = s->value(QLatin1String(size_id), cache_size_default).toInt(); - int unit = s->value(QLatin1String(size_unit_id), static_cast(CollectionSettingsPage::CacheSizeUnit::MB)).toInt() + 1; + 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; @@ -1876,9 +1438,7 @@ qint64 CollectionModel::MaximumCacheSize(Settings *s, const char *size_id, const 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); - + case CollectionItem::Type::Container: { QList children = item->children; std::sort(children.begin(), children.end(), std::bind(&CollectionModel::CompareItems, this, std::placeholders::_1, std::placeholders::_2)); @@ -1888,7 +1448,7 @@ void CollectionModel::GetChildSongs(CollectionItem *item, QList *urls, Son break; } - case CollectionItem::Type_Song: + case CollectionItem::Type::Song: urls->append(item->metadata.url()); if (!song_ids->contains(item->metadata.id())) { songs->append(item->metadata); @@ -1919,42 +1479,6 @@ 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) { @@ -1982,7 +1506,6 @@ CollectionModel::GroupBy &CollectionModel::Grouping::operator[](const int i) { } - void CollectionModel::TotalSongCountUpdatedSlot(const int count) { total_song_count_ = count; @@ -2013,13 +1536,45 @@ void CollectionModel::ExpandAll(CollectionItem *item) const { if (!root_) return; if (!item) item = root_; - const_cast(this)->LazyPopulate(item, false); + for (CollectionItem *child : item->children) { ExpandAll(child); } } +void CollectionModel::RowsInserted(const QModelIndex &parent, const int first, const int last) { + + SongList songs; + for (int i = first; i <= last; i++) { + const QModelIndex idx = index(i, 0, parent); + if (!idx.isValid()) continue; + CollectionItem *item = IndexToItem(idx); + if (!item || item->type != CollectionItem::Type::Song) continue; + songs << item->metadata; + } + + if (!songs.isEmpty()) { + emit SongsAdded(songs); + } + +} + +void CollectionModel::RowsRemoved(const QModelIndex &parent, const int first, const int last) { + + SongList songs; + for (int i = first; i <= last; i++) { + const QModelIndex idx = index(i, 0, parent); + if (!idx.isValid()) continue; + CollectionItem *item = IndexToItem(idx); + if (!item || item->type != CollectionItem::Type::Song) continue; + songs << item->metadata; + } + + emit SongsRemoved(songs); + +} + QDataStream &operator<<(QDataStream &s, const CollectionModel::Grouping g) { s << static_cast(g.first) << static_cast(g.second) << static_cast(g.third); return s; diff --git a/src/collection/collectionmodel.h b/src/collection/collectionmodel.h index e1d4066d5..acada19fe 100644 --- a/src/collection/collectionmodel.h +++ b/src/collection/collectionmodel.h @@ -1,7 +1,5 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2010, David Sansome * Copyright 2018-2024, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify @@ -44,6 +42,7 @@ #include #include #include +#include #include "core/shared_ptr.h" #include "core/simpletreemodel.h" @@ -51,15 +50,17 @@ #include "core/sqlrow.h" #include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderresult.h" +#include "collectionmodelupdate.h" #include "collectionfilteroptions.h" -#include "collectionqueryoptions.h" #include "collectionitem.h" +class QTimer; class Settings; class Application; class CollectionBackend; class CollectionDirectoryModel; +class CollectionFilter; class CollectionModel : public SimpleTreeModel { Q_OBJECT @@ -69,13 +70,12 @@ class CollectionModel : public SimpleTreeModel { ~CollectionModel() override; static const int kPrettyCoverSize; - static const char *kPixmapDiskCacheDir; enum Role { Role_Type = Qt::UserRole + 1, Role_ContainerType, Role_SortText, - Role_Key, + Role_ContainerKey, Role_Artist, Role_IsDivider, Role_Editable, @@ -125,147 +125,123 @@ class CollectionModel : public SimpleTreeModel { bool operator!=(const Grouping other) const { return !(*this == other); } }; - struct QueryResult { - QueryResult() : create_va(false) {} + struct Options { + Options() : group_by(GroupBy::AlbumArtist, GroupBy::AlbumDisc, GroupBy::None), + show_dividers(true), + show_pretty_covers(true), + show_various_artists(true), + sort_skips_articles(true), + separate_albums_by_grouping(false) {} - SqlRowList rows; - bool create_va; + Grouping group_by; + bool show_dividers; + bool show_pretty_covers; + bool show_various_artists; + bool sort_skips_articles; + bool separate_albums_by_grouping; + CollectionFilterOptions filter_options; }; - SharedPtr backend() const { return backend_; } + CollectionFilter *filter() const { return filter_; } + + void Init(); + void Reset(); + + void ReloadSettings(); + CollectionDirectoryModel *directory_model() const { return dir_model_; } - // Call before Init() - void set_show_various_artists(const bool show_various_artists) { show_various_artists_ = show_various_artists; } - - // Get information about the collection - void GetChildSongs(CollectionItem *item, QList *urls, SongList *songs, QSet *song_ids) const; - SongList GetChildSongs(const QModelIndex &idx) const; - SongList GetChildSongs(const QModelIndexList &indexes) const; - - // Might be accurate int total_song_count() const { return total_song_count_; } int total_artist_count() const { return total_artist_count_; } int total_album_count() const { return total_album_count_; } - // QAbstractItemModel - QVariant data(const QModelIndex &idx, const int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &idx) const override; - QStringList mimeTypes() const override; - QMimeData *mimeData(const QModelIndexList &indexes) const override; - bool canFetchMore(const QModelIndex &parent) const override; - - // Whether or not to use album cover art, if it exists, in the collection view - void set_pretty_covers(const bool use_pretty_covers); - bool use_pretty_covers() const { return use_pretty_covers_; } - - // Whether or not to show letters heading in the collection view - void set_show_dividers(const bool show_dividers); - - // Whether to skip articles such as “The” when sorting artist names - void set_sort_skips_articles(const bool sort_skips_articles); - - // Reload settings. - void ReloadSettings(); - - // Utility functions for manipulating text - static QString TextOrUnknown(const QString &text); - static QString PrettyYearAlbum(const int year, const QString &album); - static QString PrettyAlbumDisc(const QString &album, const int disc); - static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc); - static QString PrettyDisc(const int disc); - static QString SortText(QString text); - static QString SortTextForNumber(const int number); - static QString SortTextForArtist(QString artist, const bool skip_articles); - static QString SortTextForSong(const Song &song); - static QString SortTextForYear(const int year); - static QString SortTextForBitrate(const int bitrate); - quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); } + const CollectionModel::Grouping GetGroupBy() const { return options_current_.group_by; } + void SetGroupBy(const CollectionModel::Grouping g, const std::optional separate_albums_by_grouping = std::optional()); + static bool IsArtistGroupBy(const GroupBy group_by) { return group_by == CollectionModel::GroupBy::Artist || group_by == CollectionModel::GroupBy::AlbumArtist; } static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy::Album || group_by == GroupBy::YearAlbum || group_by == GroupBy::AlbumDisc || group_by == GroupBy::YearAlbumDisc || group_by == GroupBy::OriginalYearAlbum || group_by == GroupBy::OriginalYearAlbumDisc; } - void set_use_lazy_loading(const bool value) { use_lazy_loading_ = value; } - QMap container_nodes(const int i) { return container_nodes_[i]; } QList song_nodes() const { return song_nodes_.values(); } int divider_nodes_count() const { return divider_nodes_.count(); } + // QAbstractItemModel + QVariant data(const QModelIndex &idx, const int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &idx) const override; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + + // Utility functions for manipulating text + static QString DisplayText(const GroupBy group_by, const Song &song); + static QString TextOrUnknown(const QString &text); + static QString PrettyYearAlbum(const int year, const QString &album); + static QString PrettyAlbumDisc(const QString &album, const int disc); + static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc); + static QString PrettyDisc(const int disc); + static QString PrettyFormat(const Song &song); + QString SortText(const GroupBy group_by, const int container_level, const Song &song, const bool sort_skips_articles); + static QString SortText(QString text); + static QString SortTextForNumber(const int number); + static QString SortTextForArtist(QString artist, const bool skip_articles); + static QString SortTextForSong(const Song &song); + static QString SortTextForYear(const int year); + static QString SortTextForBitrate(const int bitrate); + static bool IsSongTitleDataChanged(const Song &song1, const Song &song2); + QString ContainerKey(const GroupBy group_by, const Song &song) const; + + // Get information about the collection + void GetChildSongs(CollectionItem *item, QList *urls, SongList *songs, QSet *song_ids) const; + SongList GetChildSongs(const QModelIndex &idx) const; + SongList GetChildSongs(const QModelIndexList &indexes) const; + void ExpandAll(CollectionItem *item = nullptr) const; - const CollectionModel::Grouping GetGroupBy() const { return group_by_; } - void SetGroupBy(const CollectionModel::Grouping g, const std::optional separate_albums_by_grouping = std::optional()); - - static QString ContainerKey(const GroupBy group_by, const bool separate_albums_by_grouping, const Song &song); - signals: void TotalSongCountUpdated(const int count); void TotalArtistCountUpdated(const int count); void TotalAlbumCountUpdated(const int count); void GroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping); + void SongsAdded(const SongList &songs); + void SongsRemoved(const SongList &songs); public slots: - void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode); - void SetFilterAge(const int filter_age); - void SetFilterText(const QString &filter_text); + void SetFilterMode(const CollectionFilterOptions::FilterMode filter_mode); + void SetFilterMaxAge(const int filter_max_age); - void Init(const bool async = true); - void Reset(); - void ResetAsync(); - - void SongsDiscovered(const SongList &songs); - - protected: - void LazyPopulate(CollectionItem *item) override { LazyPopulate(item, true); } - void LazyPopulate(CollectionItem *parent, const bool signal); - - private slots: - // From CollectionBackend - void SongsDeleted(const SongList &songs); - void SongsSlightlyChanged(const SongList &songs); - void TotalSongCountUpdatedSlot(const int count); - void TotalArtistCountUpdatedSlot(const int count); - void TotalAlbumCountUpdatedSlot(const int count); - static void ClearDiskCache(); - - // Called after ResetAsync - void ResetAsyncQueryFinished(); - - void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result); + void AddReAddOrUpdate(const SongList &songs); + void RemoveSongs(const SongList &songs); private: - // Provides some optimizations for loading the list of items in the root. - // This gets called a lot when filtering the playlist, so it's nice to be able to do it in a background thread. - CollectionQueryOptions PrepareQuery(CollectionItem *parent); - QueryResult RunQuery(const CollectionFilterOptions &filter_options = CollectionFilterOptions(), const CollectionQueryOptions &query_options = CollectionQueryOptions()); - void PostQuery(CollectionItem *parent, const QueryResult &result, const bool signal); - - bool HasCompilations(const QSqlDatabase &db, const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options); - void Clear(); void BeginReset(); + void EndReset(); - // Functions for working with queries and creating items. - // When the model is reset or when a node is lazy-loaded the Collection constructs a database query to populate the items. - // Filters are added for each parent item, restricting the songs returned to a particular album or artist for example. - static void SetQueryColumnSpec(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQueryOptions *query_options); - static void AddQueryWhere(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQueryOptions *query_options); + QVariant data(const CollectionItem *item, const int role) const; - // Items can be created either from a query that's been run to populate a node, or by a spontaneous SongsDiscovered emission from the backend. - CollectionItem *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 *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); + void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs); + void ScheduleAddSongs(const SongList &songs); + void ScheduleUpdateSongs(const SongList &songs); + void ScheduleRemoveSongs(const SongList &songs); - // The "Various Artists" node is an annoying special case. - CollectionItem *CreateCompilationArtistNode(const bool signal, CollectionItem *parent); + void AddReAddOrUpdateSongsInternal(const SongList &songs); + void AddSongsInternal(const SongList &songs); + void UpdateSongsInternal(const SongList &songs); + void RemoveSongsInternal(const SongList &songs); - // Helpers for ItemFromQuery and ItemFromSong - CollectionItem *InitItem(const GroupBy group_by, const bool signal, CollectionItem *parent, const int container_level); - void FinishItem(const GroupBy group_by, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item); + void CreateDividerItem(const QString ÷r_key, const QString &display_text, CollectionItem *parent); + CollectionItem *CreateContainerItem(const GroupBy group_by, const int container_level, const QString &container_key, const Song &song, CollectionItem *parent); + void CreateSongItem(const Song &song, CollectionItem *parent); + void SetSongItemData(CollectionItem *item, const Song &song); + CollectionItem *CreateCompilationArtistNode(CollectionItem *parent); - static QString DividerKey(const GroupBy group_by, CollectionItem *item); + void LoadSongsFromSqlAsync(); + SongList LoadSongsFromSql(const CollectionFilterOptions &filter_options = CollectionFilterOptions()); + + static QString DividerKey(const GroupBy group_by, const Song &song, const QString &sort_text); static QString DividerDisplayText(const GroupBy group_by, const QString &key); // Helpers @@ -273,24 +249,50 @@ class CollectionModel : public SimpleTreeModel { QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const; QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key) const; QVariant AlbumIcon(const QModelIndex &idx); - QVariant data(const CollectionItem *item, const int role) const; + void ClearItemPixmapCache(CollectionItem *item); bool CompareItems(const CollectionItem *a, const CollectionItem *b) const; static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default); + private slots: + void Reload(); + void ScheduleReset(); + void ProcessUpdate(); + void LoadSongsFromSqlAsyncFinished(); + void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result); + + // From CollectionBackend + void TotalSongCountUpdatedSlot(const int count); + void TotalArtistCountUpdatedSlot(const int count); + void TotalAlbumCountUpdatedSlot(const int count); + + static void ClearDiskCache(); + + void RowsInserted(const QModelIndex &parent, const int first, const int last); + void RowsRemoved(const QModelIndex &parent, const int first, const int last); + private: + static QNetworkDiskCache *sIconCache; SharedPtr backend_; Application *app_; CollectionDirectoryModel *dir_model_; - bool show_various_artists_; - bool sort_skips_articles_; + CollectionFilter *filter_; + QTimer *timer_reload_; + QTimer *timer_update_; + + QPixmap pixmap_no_cover_; + QIcon icon_artist_; + + Options options_current_; + Options options_active_; + + bool use_disk_cache_; + AlbumCoverLoaderOptions::Types cover_types_; int total_song_count_; int total_artist_count_; int total_album_count_; - CollectionFilterOptions filter_options_; - Grouping group_by_; - bool separate_albums_by_grouping_; + QQueue updates_; // Keyed on database ID QMap song_nodes_; @@ -301,22 +303,6 @@ class CollectionModel : public SimpleTreeModel { // Keyed on a letter, a year, a century, etc. QMap divider_nodes_; - QIcon artist_icon_; - QIcon album_icon_; - // Used as a generic icon to show when no cover art is found, fixed to the same size as the artwork (32x32) - QPixmap no_cover_icon_; - - static QNetworkDiskCache *sIconCache; - - int init_task_id_; - - bool use_pretty_covers_; - bool show_dividers_; - bool use_disk_cache_; - bool use_lazy_loading_; - - AlbumCoverLoaderOptions::Types cover_types_; - using ItemAndCacheKey = QPair; QMap pending_art_; QSet pending_cache_keys_; diff --git a/src/collection/collectionmodelupdate.cpp b/src/collection/collectionmodelupdate.cpp new file mode 100644 index 000000000..0a8d60acc --- /dev/null +++ b/src/collection/collectionmodelupdate.cpp @@ -0,0 +1,23 @@ +/* + * Strawberry Music Player + * Copyright 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 "collectionmodelupdate.h" + +CollectionModelUpdate::CollectionModelUpdate(const Type &_type, const SongList &_songs) + : type(_type), songs(_songs) {} diff --git a/src/collection/collectionmodelupdate.h b/src/collection/collectionmodelupdate.h new file mode 100644 index 000000000..2567dd2ac --- /dev/null +++ b/src/collection/collectionmodelupdate.h @@ -0,0 +1,38 @@ +/* + * Strawberry Music Player + * Copyright 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 . + * + */ + +#ifndef COLLECTIONMODELUPDATE_H +#define COLLECTIONMODELUPDATE_H + +#include "core/song.h" + +class CollectionModelUpdate { + public: + enum class Type { + AddReAddOrUpdate, + Add, + Update, + Remove, + }; + explicit CollectionModelUpdate(const Type &_type, const SongList &_songs); + Type type; + SongList songs; +}; + +#endif // COLLECTIONMODELUPDATE_H diff --git a/src/collection/collectionquery.cpp b/src/collection/collectionquery.cpp index bee96b8b7..ecf5d163f 100644 --- a/src/collection/collectionquery.cpp +++ b/src/collection/collectionquery.cpp @@ -38,78 +38,13 @@ #include "collectionfilteroptions.h" #include "utilities/searchparserutils.h" -CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options) +CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options) : SqlQuery(db), songs_table_(songs_table), - fts_table_(fts_table), include_unavailable_(false), - join_with_fts_(false), duplicates_only_(false), limit_(-1) { - if (!filter_options.filter_text().isEmpty()) { - // We need to munge the filter text a little bit to get it to work as expected with sqlite's FTS5: - // 1) Append * to all tokens. - // 2) Prefix "fts" to column names. - // 3) Remove colons which don't correspond to column names. - - // Split on whitespace - QString filter_text = filter_options.filter_text().replace(QRegularExpression(QStringLiteral(":\\s+")), QStringLiteral(":")); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QStringList tokens(filter_text.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts)); -#else - QStringList tokens(filter_text.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts)); -#endif - QString query; - for (QString token : tokens) { - token.remove(QLatin1Char('(')) - .remove(QLatin1Char(')')) - .remove(QLatin1Char('"')) - .replace(QLatin1Char('-'), QLatin1Char(' ')); - - if (token.contains(QLatin1Char(':'))) { - const QString columntoken = token.section(QLatin1Char(':'), 0, 0); - QString subtoken = token.section(QLatin1Char(':'), 1, -1).replace(QLatin1String(":"), QLatin1String(" ")).trimmed(); - if (subtoken.isEmpty()) continue; - if (Song::kFtsColumns.contains(QLatin1String("fts") + columntoken, Qt::CaseInsensitive)) { - if (!query.isEmpty()) query.append(QLatin1String(" ")); - query += QStringLiteral("fts") + columntoken + QStringLiteral(":\"") + subtoken + QStringLiteral("\"*"); - } - else if (Song::kNumericalColumns.contains(columntoken, Qt::CaseInsensitive)) { - QString comparator = RemoveSqlOperator(subtoken); - if (columntoken.compare(QLatin1String("rating"), Qt::CaseInsensitive) == 0) { - AddWhereRating(subtoken, comparator); - } - else if (columntoken.compare(QLatin1String("length"), Qt::CaseInsensitive) == 0) { - // Time is saved in nanoseconds, so add 9 0's - QString parsedTime = QString::number(Utilities::ParseSearchTime(subtoken)) + QStringLiteral("000000000"); - AddWhere(columntoken, parsedTime, comparator); - } - else { - AddWhere(columntoken, subtoken, comparator); - } - } - // Not a valid filter, remove - else { - token = token.replace(QLatin1String(":"), QLatin1String(" ")).trimmed(); - if (!token.isEmpty()) { - if (!query.isEmpty()) query.append(QLatin1Char(' ')); - query += QLatin1Char('\"') + token + QStringLiteral("\"*"); - } - } - } - else { - if (!query.isEmpty()) query.append(QLatin1Char(' ')); - query += QLatin1Char('\"') + token + QStringLiteral("\"*"); - } - } - if (!query.isEmpty()) { - where_clauses_ << QStringLiteral("fts.%fts_table_noprefix MATCH ?"); - bound_values_ << query; - join_with_fts_ = true; - } - } - if (filter_options.max_age() != -1) { qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - filter_options.max_age(); @@ -117,12 +52,6 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta bound_values_ << cutoff; } - // TODO: Currently you cannot use any FilterMode other than All and FTS at the same time. - // Joining songs, duplicated_songs and songs_fts all together takes a huge amount of time. - // The query takes about 20 seconds on my machine then. Why? - // Untagged mode could work with additional filtering but I'm disabling it just to be consistent - // this way filtering is available only in the All mode. - // Remember though that when you fix the Duplicates + FTS cooperation, enable the filtering in both Duplicates and Untagged modes. duplicates_only_ = filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Duplicates; if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Untagged) { @@ -131,28 +60,10 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta } -QString CollectionQuery::RemoveSqlOperator(QString &token) { - - QString op = QStringLiteral("="); - static QRegularExpression rxOp(QStringLiteral("^(=|<[>=]?|>=?|!=)")); - QRegularExpressionMatch match = rxOp.match(token); - if (match.hasMatch()) { - op = match.captured(0); - } - token.remove(rxOp); - - if (op == QStringLiteral("!=")) { - op = QStringLiteral("<>"); - } - - return op; - -} - void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) { // Ignore 'literal' for IN - if (op.compare(QLatin1String("IN"), Qt::CaseInsensitive) == 0) { + if (op.compare(QStringLiteral("IN"), Qt::CaseInsensitive) == 0) { QStringList values = value.toStringList(); QStringList final_values; final_values.reserve(values.count()); @@ -161,7 +72,7 @@ void CollectionQuery::AddWhere(const QString &column, const QVariant &value, con bound_values_ << single_value; } - where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(QStringLiteral(","))); + where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(QLatin1Char(','))); } else { // Do integers inline - sqlite seems to get confused when you pass integers to bound parameters @@ -190,49 +101,8 @@ void CollectionQuery::AddWhere(const QString &column, const QVariant &value, con } -void CollectionQuery::AddWhereArtist(const QVariant &value) { - - where_clauses_ << QStringLiteral("((artist = ? AND albumartist = '') OR albumartist = ?)"); - bound_values_ << value; - bound_values_ << value; - -} - -void CollectionQuery::AddWhereRating(const QVariant &value, const QString &op) { - - float parsed_rating = Utilities::ParseSearchRating(value.toString()); - - // You can't query the database for a float, due to float precision errors, - // So we have to use a certain tolerance, so that the searched value is definetly included. - const float tolerance = 0.001F; - if (op == QStringLiteral("<")) { - AddWhere(QStringLiteral("rating"), parsed_rating-tolerance, QStringLiteral("<")); - } - else if (op == QStringLiteral(">")) { - AddWhere(QStringLiteral("rating"), parsed_rating+tolerance, QStringLiteral(">")); - } - else if (op == QStringLiteral("<=")) { - AddWhere(QStringLiteral("rating"), parsed_rating+tolerance, QStringLiteral("<=")); - } - else if (op == QStringLiteral(">=")) { - AddWhere(QStringLiteral("rating"), parsed_rating-tolerance, QStringLiteral(">=")); - } - else if (op == QStringLiteral("<>")) { - where_clauses_ << QStringLiteral("(rating?)"); - bound_values_ << parsed_rating - tolerance; - bound_values_ << parsed_rating + tolerance; - } - else /* (op == "=") */ { - AddWhere(QStringLiteral("rating"), parsed_rating+tolerance, QStringLiteral("<")); - AddWhere(QStringLiteral("rating"), parsed_rating-tolerance, QStringLiteral(">")); - } - -} - void CollectionQuery::AddCompilationRequirement(const bool compilation) { // The unary + is added to prevent sqlite from using the index idx_comp_artist. - // When joining with fts, sqlite 3.8 has a tendency to use this index and thereby nesting the tables in an order which gives very poor performance - where_clauses_ << QStringLiteral("+compilation_effective = %1").arg(compilation ? 1 : 0); } @@ -248,14 +118,7 @@ QString CollectionQuery::GetInnerQuery() const { bool CollectionQuery::Exec() { - QString sql; - - if (join_with_fts_) { - sql = QStringLiteral("SELECT %1 FROM %2 INNER JOIN %3 AS fts ON %2.ROWID = fts.ROWID").arg(column_spec_, songs_table_, fts_table_); - } - else { - sql = QStringLiteral("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery()); - } + QString sql = QStringLiteral("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery()); QStringList where_clauses(where_clauses_); if (!include_unavailable_) { @@ -268,9 +131,7 @@ bool CollectionQuery::Exec() { if (limit_ != -1) sql += QStringLiteral(" LIMIT ") + QString::number(limit_); - sql.replace(QLatin1String("%songs_table"), songs_table_); - sql.replace(QLatin1String("%fts_table_noprefix"), fts_table_.section(QLatin1Char('.'), -1, -1)); - sql.replace(QLatin1String("%fts_table"), fts_table_); + sql.replace(QStringLiteral("%songs_table"), songs_table_); if (!QSqlQuery::prepare(sql)) return false; diff --git a/src/collection/collectionquery.h b/src/collection/collectionquery.h index ce7053033..15d908d36 100644 --- a/src/collection/collectionquery.h +++ b/src/collection/collectionquery.h @@ -36,7 +36,7 @@ class CollectionQuery : public SqlQuery { public: - explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions()); + explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions()); QVariant Value(const int column) const; QVariant value(const int column) const { return Value(column); } @@ -51,7 +51,6 @@ class CollectionQuery : public SqlQuery { QStringList where_clauses() const { return where_clauses_; } QVariantList bound_values() const { return bound_values_; } bool include_unavailable() const { return include_unavailable_; } - bool join_with_fts() const { return join_with_fts_; } bool duplicates_only() const { return duplicates_only_; } int limit() const { return limit_; } @@ -63,14 +62,9 @@ class CollectionQuery : public SqlQuery { void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; } - // Removes = < > <= >= <> from the beginning of the input string and returns the operator - // If the input String has no operator, returns "=" - QString RemoveSqlOperator(QString &token); // Adds a fragment of WHERE clause. When executed, this Query will connect all the fragments with AND operator. // Please note that IN operator expects a QStringList as value. void AddWhere(const QString &column, const QVariant &value, const QString &op = QStringLiteral("=")); - void AddWhereArtist(const QVariant &value); - void AddWhereRating(const QVariant &value, const QString &op = QStringLiteral("=")); void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; } void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; } @@ -83,7 +77,6 @@ class CollectionQuery : public SqlQuery { QSqlDatabase db_; QString songs_table_; - QString fts_table_; QString column_spec_; QString order_by_; @@ -91,7 +84,6 @@ class CollectionQuery : public SqlQuery { QVariantList bound_values_; bool include_unavailable_; - bool join_with_fts_; bool duplicates_only_; int limit_; }; diff --git a/src/collection/collectionview.cpp b/src/collection/collectionview.cpp index 03644584d..97b3e907f 100644 --- a/src/collection/collectionview.cpp +++ b/src/collection/collectionview.cpp @@ -120,9 +120,14 @@ CollectionView::~CollectionView() = default; void CollectionView::SaveFocus() { - QModelIndex current = currentIndex(); - QVariant type = model()->data(current, CollectionModel::Role_Type); - if (!type.isValid() || (type.toInt() != CollectionItem::Type_Song && type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) { + const QModelIndex current = currentIndex(); + const QVariant role_type = model()->data(current, CollectionModel::Role_Type); + if (!role_type.isValid()) { + return; + } + + const CollectionItem::Type item_type = role_type.value(); + if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) { return; } @@ -130,8 +135,8 @@ void CollectionView::SaveFocus() { last_selected_song_ = Song(); last_selected_container_ = QString(); - switch (type.toInt()) { - case CollectionItem::Type_Song:{ + switch (item_type) { + case CollectionItem::Type::Song:{ QModelIndex index = qobject_cast(model())->mapToSource(current); SongList songs = app_->collection_model()->GetChildSongs(index); if (!songs.isEmpty()) { @@ -140,8 +145,8 @@ void CollectionView::SaveFocus() { break; } - case CollectionItem::Type_Container: - case CollectionItem::Type_Divider:{ + case CollectionItem::Type::Container: + case CollectionItem::Type::Divider:{ QString text = model()->data(current, CollectionModel::Role_SortText).toString(); last_selected_container_ = text; break; @@ -157,9 +162,14 @@ void CollectionView::SaveFocus() { void CollectionView::SaveContainerPath(const QModelIndex &child) { - QModelIndex current = model()->parent(child); - QVariant type = model()->data(current, CollectionModel::Role_Type); - if (!type.isValid() || (type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) { + const QModelIndex current = model()->parent(child); + const QVariant role_type = model()->data(current, CollectionModel::Role_Type); + if (!role_type.isValid()) { + return; + } + + const CollectionItem::Type item_type = role_type.value(); + if (item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) { return; } @@ -183,12 +193,17 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) { if (model()->canFetchMore(parent)) { model()->fetchMore(parent); } - int rows = model()->rowCount(parent); + const int rows = model()->rowCount(parent); for (int i = 0; i < rows; i++) { QModelIndex current = model()->index(i, 0, parent); - QVariant type = model()->data(current, CollectionModel::Role_Type); - switch (type.toInt()) { - case CollectionItem::Type_Song: + const QVariant role_type = model()->data(current, CollectionModel::Role_Type); + if (!role_type.isValid()) return false; + const CollectionItem::Type item_type = role_type.value(); + switch (item_type) { + case CollectionItem::Type::Root: + case CollectionItem::Type::LoadingIndicator: + break; + case CollectionItem::Type::Song: if (!last_selected_song_.url().isEmpty()) { QModelIndex index = qobject_cast(model())->mapToSource(current); const SongList songs = app_->collection_model()->GetChildSongs(index); @@ -199,8 +214,8 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) { } break; - case CollectionItem::Type_Container: - case CollectionItem::Type_Divider:{ + case CollectionItem::Type::Container: + case CollectionItem::Type::Divider:{ QString text = model()->data(current, CollectionModel::Role_SortText).toString(); if (!last_selected_container_.isEmpty() && last_selected_container_ == text) { expand(current); @@ -228,18 +243,9 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) { void CollectionView::ReloadSettings() { Settings settings; - settings.beginGroup(CollectionSettingsPage::kSettingsGroup); SetAutoOpen(settings.value("auto_open", false).toBool()); - - if (app_) { - app_->collection_model()->set_pretty_covers(settings.value("pretty_covers", true).toBool()); - app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool()); - app_->collection_model()->set_sort_skips_articles(settings.value("sort_skips_articles", true).toBool()); - } - delete_files_ = settings.value("delete_files", false).toBool(); - settings.endGroup(); } @@ -573,15 +579,20 @@ void CollectionView::OpenInNewPlaylist() { void CollectionView::SearchForThis() { QModelIndex current = currentIndex(); - QVariant type = model()->data(current, CollectionModel::Role_Type); - if (!type.isValid() || (type.toInt() != CollectionItem::Type_Song && type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) { + const QVariant role_type = model()->data(current, CollectionModel::Role_Type); + if (!role_type.isValid()) { + return; + } + + const CollectionItem::Type item_type = role_type.value(); + if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) { return; } QString search; QModelIndex index = qobject_cast(model())->mapToSource(current); - switch (type.toInt()) { - case CollectionItem::Type_Song:{ + switch (item_type) { + case CollectionItem::Type::Song:{ SongList songs = app_->collection_model()->GetChildSongs(index); if (!songs.isEmpty()) { last_selected_song_ = songs.last(); @@ -590,11 +601,11 @@ void CollectionView::SearchForThis() { break; } - case CollectionItem::Type_Divider:{ + case CollectionItem::Type::Divider:{ break; } - case CollectionItem::Type_Container:{ + case CollectionItem::Type::Container:{ CollectionItem *item = app_->collection_model()->IndexToItem(index); int container_level = item->container_level; @@ -750,8 +761,11 @@ void CollectionView::FilterReturnPressed() { if (!currentIndex().isValid()) { // Pick the first thing that isn't a divider for (int row = 0; row < model()->rowCount(); ++row) { - QModelIndex idx(model()->index(row, 0)); - if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) { + const QModelIndex idx = model()->index(row, 0); + const QVariant role_type = idx.data(CollectionModel::Role_Type); + if (!role_type.isValid()) continue; + const CollectionItem::Type item_type = role_type.value(); + if (item_type != CollectionItem::Type::Divider) { setCurrentIndex(idx); break; } diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index bb61bf4de..bf260e17e 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -131,10 +131,11 @@ #include "collection/collection.h" #include "collection/collectionbackend.h" #include "collection/collectiondirectorymodel.h" +#include "collection/collectionviewcontainer.h" #include "collection/collectionfilterwidget.h" +#include "collection/collectionfilter.h" #include "collection/collectionmodel.h" #include "collection/collectionview.h" -#include "collection/collectionviewcontainer.h" #include "playlist/playlist.h" #include "playlist/playlistbackend.h" #include "playlist/playlistcontainer.h" @@ -335,7 +336,6 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS playlist_add_to_another_(nullptr), playlistitem_actions_separator_(nullptr), playlist_rescan_songs_(nullptr), - collection_sort_model_(new QSortFilterProxyModel(this)), track_position_timer_(new QTimer(this)), track_slider_timer_(new QTimer(this)), keep_running_(false), @@ -416,23 +416,13 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS ui_->volume->SetValue(volume); VolumeChanged(volume); - // Models - qLog(Debug) << "Creating models"; - collection_sort_model_->setSourceModel(app_->collection()->model()); - collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - collection_sort_model_->setDynamicSortFilter(true); - collection_sort_model_->setSortLocaleAware(true); - collection_sort_model_->sort(0); - - qLog(Debug) << "Creating models finished"; - QObject::connect(ui_->playlist, &PlaylistContainer::ViewSelectionModelChanged, this, &MainWindow::PlaylistViewSelectionModelChanged); ui_->playlist->SetManager(app_->playlist_manager()); ui_->playlist->view()->Init(app_); - collection_view_->view()->setModel(collection_sort_model_); + collection_view_->view()->setModel(app_->collection()->model()->filter()); collection_view_->view()->SetApplication(app_); #ifndef Q_OS_WIN device_view_->view()->SetApplication(app_); @@ -692,7 +682,7 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS QAction *collection_config_action = new QAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure collection..."), this); QObject::connect(collection_config_action, &QAction::triggered, this, &MainWindow::ShowCollectionConfig); collection_view_->filter_widget()->SetSettingsGroup(QLatin1String(CollectionSettingsPage::kSettingsGroup)); - collection_view_->filter_widget()->Init(app_->collection()->model()); + collection_view_->filter_widget()->Init(app_->collection()->model(), app_->collection()->model()->filter()); QAction *separator = new QAction(this); separator->setSeparator(true); diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index d8b7336e6..1315b2837 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -68,6 +68,7 @@ class AlbumCoverManager; class Application; class ContextView; class CollectionViewContainer; +class CollectionFilter; class AlbumCoverChoiceController; class CommandlineOptions; #ifndef Q_OS_WIN @@ -376,8 +377,6 @@ class MainWindow : public QMainWindow, public PlatformInterface { QModelIndex playlist_menu_index_; - QSortFilterProxyModel *collection_sort_model_; - QTimer *track_position_timer_; QTimer *track_slider_timer_; Settings settings_; diff --git a/src/core/simpletreeitem.h b/src/core/simpletreeitem.h index 34ff1347c..8ec68f587 100644 --- a/src/core/simpletreeitem.h +++ b/src/core/simpletreeitem.h @@ -33,9 +33,9 @@ template class SimpleTreeItem { public: - explicit SimpleTreeItem(int _type, SimpleTreeModel *_model); // For the root item - explicit SimpleTreeItem(int _type, const QString &_key, T *_parent = nullptr); - explicit SimpleTreeItem(int _type, T *_parent = nullptr); + explicit SimpleTreeItem(SimpleTreeModel *_model); // For the root item + explicit SimpleTreeItem(const QString &_key, T *_parent = nullptr); + explicit SimpleTreeItem(T *_parent = nullptr); virtual ~SimpleTreeItem(); void InsertNotify(T *_parent); @@ -49,13 +49,11 @@ class SimpleTreeItem { QString DisplayText() const { return display_text; } QString SortText() const { return sort_text; } - int type; - QString key; + QString container_key; QString sort_text; QString display_text; int row; - bool lazy_loaded; T *parent; QList children; @@ -65,19 +63,15 @@ class SimpleTreeItem { }; template -SimpleTreeItem::SimpleTreeItem(int _type, SimpleTreeModel *_model) - : type(_type), - row(0), - lazy_loaded(true), +SimpleTreeItem::SimpleTreeItem(SimpleTreeModel *_model) + : row(0), parent(nullptr), child_model(nullptr), model(_model) {} template -SimpleTreeItem::SimpleTreeItem(int _type, const QString &_key, T *_parent) - : type(_type), - key(_key), - lazy_loaded(false), +SimpleTreeItem::SimpleTreeItem(const QString &_container_key, T *_parent) + : container_key(_container_key), parent(_parent), child_model(nullptr), model(_parent ? _parent->model : nullptr) { @@ -88,10 +82,8 @@ SimpleTreeItem::SimpleTreeItem(int _type, const QString &_key, T *_parent) } template -SimpleTreeItem::SimpleTreeItem(int _type, T *_parent) - : type(_type), - lazy_loaded(false), - parent(_parent), +SimpleTreeItem::SimpleTreeItem(T *_parent) + : parent(_parent), child_model(nullptr), model(_parent ? _parent->model : nullptr) { if (parent) { @@ -112,7 +104,7 @@ void SimpleTreeItem::InsertNotify(T *_parent) { } template -void SimpleTreeItem::DeleteNotify(int child_row) { +void SimpleTreeItem::DeleteNotify(const int child_row) { model->BeginDelete(static_cast(this), child_row); delete children.takeAt(child_row); diff --git a/src/core/simpletreemodel.h b/src/core/simpletreemodel.h index 8e1b41617..983669077 100644 --- a/src/core/simpletreemodel.h +++ b/src/core/simpletreemodel.h @@ -35,12 +35,10 @@ class SimpleTreeModel : public QAbstractItemModel { // QAbstractItemModel int columnCount(const QModelIndex &parent) const override; - QModelIndex index(int row, int, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex index(const int row, const int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &idx) const override; int rowCount(const QModelIndex &parent) const override; bool hasChildren(const QModelIndex &parent) const override; - bool canFetchMore(const QModelIndex &parent) const override; - void fetchMore(const QModelIndex &parent) override; T *IndexToItem(const QModelIndex &idx) const; QModelIndex ItemToIndex(T *item) const; @@ -52,9 +50,6 @@ class SimpleTreeModel : public QAbstractItemModel { void EndDelete(); void EmitDataChanged(T *item); - protected: - virtual void LazyPopulate(T *item) { item->lazy_loaded = true; } - protected: T *root_; }; @@ -81,7 +76,9 @@ int SimpleTreeModel::columnCount(const QModelIndex&) const { } template -QModelIndex SimpleTreeModel::index(int row, int, const QModelIndex &parent) const { +QModelIndex SimpleTreeModel::index(const int row, const int column, const QModelIndex &parent) const { + + Q_UNUSED(column); T *parent_item = IndexToItem(parent); if (!parent_item || row < 0 || parent_item->children.count() <= row) @@ -106,25 +103,8 @@ int SimpleTreeModel::rowCount(const QModelIndex &parent) const { template bool SimpleTreeModel::hasChildren(const QModelIndex &parent) const { T *item = IndexToItem(parent); - if (!item) return false; - if (item->lazy_loaded) - return !item->children.isEmpty(); - else - return true; -} - -template -bool SimpleTreeModel::canFetchMore(const QModelIndex &parent) const { - T *item = IndexToItem(parent); - return item && !item->lazy_loaded; -} - -template -void SimpleTreeModel::fetchMore(const QModelIndex &parent) { - T *item = IndexToItem(parent); - if (item && !item->lazy_loaded) { - LazyPopulate(item); - } + if (!item) return 0; + return !item->children.isEmpty(); } template diff --git a/src/core/song.cpp b/src/core/song.cpp index 72f8b8665..ed4f139cf 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -144,35 +144,41 @@ const QStringList Song::kColumns = QStringList() << QStringLiteral("title") const QStringList Song::kRowIdColumns = QStringList() << QStringLiteral("ROWID") << kColumns; -const QString Song::kColumnSpec = Song::kColumns.join(QStringLiteral(", ")); -const QString Song::kRowIdColumnSpec = Song::kRowIdColumns.join(QStringLiteral(", ")); -const QString Song::kBindSpec = Utilities::Prepend(QStringLiteral(":"), Song::kColumns).join(QStringLiteral(", ")); -const QString Song::kUpdateSpec = Utilities::Updateify(Song::kColumns).join(QStringLiteral(", ")); +const QString Song::kColumnSpec = kColumns.join(QStringLiteral(", ")); +const QString Song::kRowIdColumnSpec = kRowIdColumns.join(QStringLiteral(", ")); +const QString Song::kBindSpec = Utilities::Prepend(QStringLiteral(":"), kColumns).join(QStringLiteral(", ")); +const QString Song::kUpdateSpec = Utilities::Updateify(kColumns).join(QStringLiteral(", ")); -// used to indicate, what columns can be filtered numerically. Used by the CollectionQuery. -const QStringList Song::kNumericalColumns = QStringList() << QStringLiteral("year") - << QStringLiteral("length") - << QStringLiteral("samplerate") - << QStringLiteral("bitdepth") - << QStringLiteral("bitrate") - << QStringLiteral("rating") - << QStringLiteral("playcount") - << QStringLiteral("skipcount"); +const QStringList Song::kTextSearchColumns = QStringList() << QStringLiteral("title") + << QStringLiteral("album") + << QStringLiteral("artist") + << QStringLiteral("albumartist") + << QStringLiteral("composer") + << QStringLiteral("performer") + << QStringLiteral("grouping") + << QStringLiteral("genre") + << QStringLiteral("comment"); +const QStringList Song::kIntSearchColumns = QStringList() << QStringLiteral("track") + << QStringLiteral("year") + << QStringLiteral("samplerate") + << QStringLiteral("bitdepth") + << QStringLiteral("bitrate"); -const QStringList Song::kFtsColumns = QStringList() << QStringLiteral("ftstitle") - << QStringLiteral("ftsalbum") - << QStringLiteral("ftsartist") - << QStringLiteral("ftsalbumartist") - << QStringLiteral("ftscomposer") - << QStringLiteral("ftsperformer") - << QStringLiteral("ftsgrouping") - << QStringLiteral("ftsgenre") - << QStringLiteral("ftscomment"); +const QStringList Song::kUIntSearchColumns = QStringList() << QStringLiteral("playcount") + << QStringLiteral("skipcount"); -const QString Song::kFtsColumnSpec = Song::kFtsColumns.join(QStringLiteral(", ")); -const QString Song::kFtsBindSpec = Utilities::Prepend(QStringLiteral(":"), Song::kFtsColumns).join(QStringLiteral(", ")); -const QString Song::kFtsUpdateSpec = Utilities::Updateify(Song::kFtsColumns).join(QStringLiteral(", ")); +const QStringList Song::kInt64SearchColumns = QStringList() << QStringLiteral("length"); + +const QStringList Song::kFloatSearchColumns = QStringList() << QStringLiteral("rating"); + +const QStringList Song::kNumericalSearchColumns = QStringList() << kIntSearchColumns + << kUIntSearchColumns + << kInt64SearchColumns + << kFloatSearchColumns; + +const QStringList Song::kSearchColumns = QStringList() << kTextSearchColumns + << kNumericalSearchColumns; const Song::RegularExpressionList Song::kAlbumDisc = Song::RegularExpressionList() << QRegularExpression(QStringLiteral("\\s+-*\\s*(Disc|CD)\\s*([0-9]{1,2})$"), QRegularExpression::CaseInsensitiveOption) @@ -199,10 +205,33 @@ const Song::RegularExpressionList Song::kTitleMisc = Song::RegularExpressionList const QStringList Song::kArticles = QStringList() << QStringLiteral("the ") << QStringLiteral("a ") << QStringLiteral("an "); -const QStringList Song::kAcceptedExtensions = QStringList() << QStringLiteral("wav") << QStringLiteral("flac") << QStringLiteral("wv") << QStringLiteral("ogg") << QStringLiteral("oga") << QStringLiteral("opus") << QStringLiteral("spx") << QStringLiteral("ape") << QStringLiteral("mpc") - << QStringLiteral("mp2") << QStringLiteral("mp3") << QStringLiteral("m4a") << QStringLiteral("mp4") << QStringLiteral("aac") << QStringLiteral("asf") << QStringLiteral("asx") << QStringLiteral("wma") - << QStringLiteral("aif << aiff") << QStringLiteral("mka") << QStringLiteral("tta") << QStringLiteral("dsf") << QStringLiteral("dsd") - << QStringLiteral("ac3") << QStringLiteral("dts") << QStringLiteral("spc") << QStringLiteral("vgm"); +const QStringList Song::kAcceptedExtensions = QStringList() << QStringLiteral("wav") + << QStringLiteral("flac") + << QStringLiteral("wv") + << QStringLiteral("ogg") + << QStringLiteral("oga") + << QStringLiteral("opus") + << QStringLiteral("spx") + << QStringLiteral("ape") + << QStringLiteral("mpc") + << QStringLiteral("mp2") + << QStringLiteral("mp3") + << QStringLiteral("m4a") + << QStringLiteral("mp4") + << QStringLiteral("aac") + << QStringLiteral("asf") + << QStringLiteral("asx") + << QStringLiteral("wma") + << QStringLiteral("aif") + << QStringLiteral("aiff") + << QStringLiteral("mka") + << QStringLiteral("tta") + << QStringLiteral("dsf") + << QStringLiteral("dsd") + << QStringLiteral("ac3") + << QStringLiteral("dts") + << QStringLiteral("spc") + << QStringLiteral("vgm"); struct Song::Private : public QSharedData { @@ -1781,20 +1810,6 @@ void Song::BindToQuery(SqlQuery *query) const { } -void Song::BindToFtsQuery(SqlQuery *query) const { - - query->BindValue(QStringLiteral(":ftstitle"), d->title_); - query->BindValue(QStringLiteral(":ftsalbum"), d->album_); - query->BindValue(QStringLiteral(":ftsartist"), d->artist_); - query->BindValue(QStringLiteral(":ftsalbumartist"), d->albumartist_); - query->BindValue(QStringLiteral(":ftscomposer"), d->composer_); - query->BindValue(QStringLiteral(":ftsperformer"), d->performer_); - query->BindValue(QStringLiteral(":ftsgrouping"), d->grouping_); - query->BindValue(QStringLiteral(":ftsgenre"), d->genre_); - query->BindValue(QStringLiteral(":ftscomment"), d->comment_); - -} - #ifdef HAVE_DBUS void Song::ToXesam(QVariantMap *map) const { diff --git a/src/core/song.h b/src/core/song.h index d26fbce6c..0b1c150fc 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -119,12 +119,13 @@ class Song { static const QString kBindSpec; static const QString kUpdateSpec; - static const QStringList kNumericalColumns; - - static const QStringList kFtsColumns; - static const QString kFtsColumnSpec; - static const QString kFtsBindSpec; - static const QString kFtsUpdateSpec; + static const QStringList kTextSearchColumns; + static const QStringList kIntSearchColumns; + static const QStringList kUIntSearchColumns; + static const QStringList kInt64SearchColumns; + static const QStringList kFloatSearchColumns; + static const QStringList kNumericalSearchColumns; + static const QStringList kSearchColumns; using RegularExpressionList = QList; static const RegularExpressionList kAlbumDisc; @@ -439,7 +440,6 @@ class Song { // Save void BindToQuery(SqlQuery *query) const; - void BindToFtsQuery(SqlQuery *query) const; #ifdef HAVE_DBUS void ToXesam(QVariantMap *map) const; #endif diff --git a/src/core/songloader.cpp b/src/core/songloader.cpp index 9c7be7155..025e108ab 100644 --- a/src/core/songloader.cpp +++ b/src/core/songloader.cpp @@ -236,7 +236,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString &filename) { QMutexLocker l(collection_backend_->db()->Mutex()); QSqlDatabase db(collection_backend_->db()->Connect()); - CollectionQuery query(db, collection_backend_->songs_table(), collection_backend_->fts_table()); + CollectionQuery query(db, collection_backend_->songs_table()); query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec); query.AddWhere(QStringLiteral("url"), url.toEncoded()); diff --git a/src/covermanager/albumcovermanager.cpp b/src/covermanager/albumcovermanager.cpp index 6a336fc53..23210be2a 100644 --- a/src/covermanager/albumcovermanager.cpp +++ b/src/covermanager/albumcovermanager.cpp @@ -876,7 +876,7 @@ SongList AlbumCoverManager::GetSongsInAlbum(const QModelIndex &idx) const { QMutexLocker l(collection_backend_->db()->Mutex()); QSqlDatabase db(collection_backend_->db()->Connect()); - CollectionQuery q(db, collection_backend_->songs_table(), collection_backend_->fts_table()); + CollectionQuery q(db, collection_backend_->songs_table()); q.SetColumnSpec(Song::kRowIdColumnSpec); q.AddWhere(QStringLiteral("album"), idx.data(Role_Album).toString()); q.SetOrderBy(QStringLiteral("disc, track, title")); diff --git a/src/device/cddadevice.cpp b/src/device/cddadevice.cpp index 6161f54a7..74f9b3615 100644 --- a/src/device/cddadevice.cpp +++ b/src/device/cddadevice.cpp @@ -41,7 +41,7 @@ CddaDevice::CddaDevice(const QUrl &url, DeviceLister *lister, const QString &uni QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsLoaded, this, &CddaDevice::SongsLoaded); QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsDurationLoaded, this, &CddaDevice::SongsLoaded); QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsMetadataLoaded, this, &CddaDevice::SongsLoaded); - QObject::connect(this, &CddaDevice::SongsDiscovered, model_, &CollectionModel::SongsDiscovered); + QObject::connect(this, &CddaDevice::SongsDiscovered, model_, &CollectionModel::AddReAddOrUpdate); } diff --git a/src/device/connecteddevice.cpp b/src/device/connecteddevice.cpp index fdd14403b..45228aa2a 100644 --- a/src/device/connecteddevice.cpp +++ b/src/device/connecteddevice.cpp @@ -68,7 +68,6 @@ ConnectedDevice::ConnectedDevice(const QUrl &url, DeviceLister *lister, const QS app_->task_manager(), Song::Source::Device, QStringLiteral("device_%1_songs").arg(database_id), - QStringLiteral("device_%1_fts").arg(database_id), QStringLiteral("device_%1_directories").arg(database_id), QStringLiteral("device_%1_subdirectories").arg(database_id)); diff --git a/src/device/devicedatabasebackend.cpp b/src/device/devicedatabasebackend.cpp index 5effb6e47..11d544548 100644 --- a/src/device/devicedatabasebackend.cpp +++ b/src/device/devicedatabasebackend.cpp @@ -186,7 +186,7 @@ void DeviceDatabaseBackend::RemoveDevice(const int id) { { SqlQuery q(db); - q.prepare(QStringLiteral("DROP TABLE device_%1_fts").arg(id)); + q.prepare(QStringLiteral("DROP TABLE IF EXISTS device_%1_fts").arg(id)); if (!q.Exec()) { db_->ReportErrors(q); return; diff --git a/src/device/deviceinfo.h b/src/device/deviceinfo.h index 842630cbf..1bd916a54 100644 --- a/src/device/deviceinfo.h +++ b/src/device/deviceinfo.h @@ -55,13 +55,14 @@ class ConnectedDevice; class DeviceInfo : public SimpleTreeItem { public: - enum Type { - Type_Root, - Type_Device, + enum class Type { + Root, + Device, }; explicit DeviceInfo(SimpleTreeModel *_model) - : SimpleTreeItem(Type_Root, _model), + : SimpleTreeItem(_model), + type_(Type::Root), database_id_(-1), size_(0), transcode_mode_(MusicStorage::TranscodeMode::Transcode_Unsupported), @@ -71,7 +72,8 @@ class DeviceInfo : public SimpleTreeItem { forget_(false) {} explicit DeviceInfo(const Type _type, DeviceInfo *_parent = nullptr) - : SimpleTreeItem(_type, _parent), + : SimpleTreeItem(_parent), + type_(_type), database_id_(-1), size_(0), transcode_mode_(MusicStorage::TranscodeMode::Transcode_Unsupported), @@ -101,6 +103,7 @@ class DeviceInfo : public SimpleTreeItem { // Gets the best backend available (the one with the highest priority) const Backend *BestBackend() const; + Type type_; int database_id_; // -1 if not remembered in the database SharedPtr device_; // nullptr if not connected QList backends_; @@ -122,4 +125,6 @@ class DeviceInfo : public SimpleTreeItem { Q_DISABLE_COPY(DeviceInfo) }; +Q_DECLARE_METATYPE(DeviceInfo::Type) + #endif // DEVICEINFO_H diff --git a/src/device/devicemanager.cpp b/src/device/devicemanager.cpp index 298b61c0c..da57dd78d 100644 --- a/src/device/devicemanager.cpp +++ b/src/device/devicemanager.cpp @@ -233,7 +233,7 @@ void DeviceManager::LoadAllDevices() { DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices(); for (const DeviceDatabaseBackend::Device &device : devices) { - DeviceInfo *info = new DeviceInfo(DeviceInfo::Type_Device, root_); + DeviceInfo *info = new DeviceInfo(DeviceInfo::Type::Device, root_); info->InitFromDb(device); emit DeviceCreatedFromDB(info); } @@ -479,7 +479,7 @@ void DeviceManager::PhysicalDeviceAdded(const QString &id) { } else { // It's a completely new device - info = new DeviceInfo(DeviceInfo::Type_Device, root_); + info = new DeviceInfo(DeviceInfo::Type::Device, root_); info->backends_ << DeviceInfo::Backend(lister, id); info->friendly_name_ = lister->MakeFriendlyName(id); info->size_ = lister->DeviceCapacity(id); diff --git a/src/internet/internetcollectionview.cpp b/src/internet/internetcollectionview.cpp index 4057ab427..fe4044ac9 100644 --- a/src/internet/internetcollectionview.cpp +++ b/src/internet/internetcollectionview.cpp @@ -89,10 +89,6 @@ void InternetCollectionView::Init(Application *app, SharedPtr collection_model_ = collection_model; favorite_ = favorite; - collection_model_->set_pretty_covers(true); - collection_model_->set_show_dividers(true); - collection_model_->set_sort_skips_articles(true); - ReloadSettings(); } @@ -105,15 +101,20 @@ void InternetCollectionView::SetFilter(CollectionFilterWidget *filter) { void InternetCollectionView::ReloadSettings() { + if (collection_model_) collection_model_->ReloadSettings(); if (filter_) filter_->ReloadSettings(); } void InternetCollectionView::SaveFocus() { - QModelIndex current = currentIndex(); - QVariant type = model()->data(current, CollectionModel::Role_Type); - if (!type.isValid() || (type.toInt() != CollectionItem::Type_Song && type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) { + const QModelIndex current = currentIndex(); + const QVariant role_type = model()->data(current, CollectionModel::Role_Type); + if (!role_type.isValid()) { + return; + } + const CollectionItem::Type item_type = role_type.value(); + if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) { return; } @@ -121,8 +122,8 @@ void InternetCollectionView::SaveFocus() { last_selected_song_ = Song(); last_selected_container_ = QString(); - switch (type.toInt()) { - case CollectionItem::Type_Song:{ + switch (item_type) { + case CollectionItem::Type::Song:{ QModelIndex idx = qobject_cast(model())->mapToSource(current); SongList songs = collection_model_->GetChildSongs(idx); if (!songs.isEmpty()) { @@ -131,8 +132,8 @@ void InternetCollectionView::SaveFocus() { break; } - case CollectionItem::Type_Container: - case CollectionItem::Type_Divider:{ + case CollectionItem::Type::Container: + case CollectionItem::Type::Divider:{ QString text = model()->data(current, CollectionModel::Role_SortText).toString(); last_selected_container_ = text; break; @@ -148,9 +149,13 @@ void InternetCollectionView::SaveFocus() { void InternetCollectionView::SaveContainerPath(const QModelIndex &child) { - QModelIndex current = model()->parent(child); - QVariant type = model()->data(current, CollectionModel::Role_Type); - if (!type.isValid() || (type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) { + const QModelIndex current = model()->parent(child); + const QVariant role_type = model()->data(current, CollectionModel::Role_Type); + if (!role_type.isValid()) { + return; + } + const CollectionItem::Type item_type = role_type.value(); + if (item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) { return; } @@ -174,12 +179,18 @@ bool InternetCollectionView::RestoreLevelFocus(const QModelIndex &parent) { if (model()->canFetchMore(parent)) { model()->fetchMore(parent); } - int rows = model()->rowCount(parent); + const int rows = model()->rowCount(parent); for (int i = 0; i < rows; i++) { - QModelIndex current = model()->index(i, 0, parent); - QVariant type = model()->data(current, CollectionModel::Role_Type); - switch (type.toInt()) { - case CollectionItem::Type_Song: + const QModelIndex current = model()->index(i, 0, parent); + if (!current.isValid()) continue; + const QVariant role_type = model()->data(current, CollectionModel::Role_Type); + if (!role_type.isValid()) continue; + const CollectionItem::Type item_type = role_type.value(); + switch (item_type) { + case CollectionItem::Type::Root: + case CollectionItem::Type::LoadingIndicator: + break; + case CollectionItem::Type::Song: if (!last_selected_song_.url().isEmpty()) { QModelIndex idx = qobject_cast(model())->mapToSource(current); const SongList songs = collection_model_->GetChildSongs(idx); @@ -192,8 +203,8 @@ bool InternetCollectionView::RestoreLevelFocus(const QModelIndex &parent) { } break; - case CollectionItem::Type_Container: - case CollectionItem::Type_Divider:{ + case CollectionItem::Type::Container: + case CollectionItem::Type::Divider:{ QString text = model()->data(current, CollectionModel::Role_SortText).toString(); if (!last_selected_container_.isEmpty() && last_selected_container_ == text) { expand(current); @@ -435,8 +446,11 @@ void InternetCollectionView::FilterReturnPressed() { if (!currentIndex().isValid()) { // Pick the first thing that isn't a divider for (int row = 0; row < model()->rowCount(); ++row) { - QModelIndex idx(model()->index(row, 0)); - if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) { + QModelIndex idx = model()->index(row, 0); + const QVariant role_type = idx.data(CollectionModel::Role::Role_Type); + if (!role_type.isValid()) continue; + const CollectionItem::Type item_type = role_type.value(); + if (item_type != CollectionItem::Type::Divider) { setCurrentIndex(idx); break; } diff --git a/src/internet/internetservice.h b/src/internet/internetservice.h index da71c9a68..8b2b99aeb 100644 --- a/src/internet/internetservice.h +++ b/src/internet/internetservice.h @@ -33,10 +33,10 @@ #include "settings/settingsdialog.h" #include "internetsearchview.h" -class QSortFilterProxyModel; class Application; class CollectionBackend; class CollectionModel; +class CollectionFilter; class InternetService : public QObject { Q_OBJECT @@ -69,9 +69,9 @@ class InternetService : public QObject { virtual CollectionModel *albums_collection_model() { return nullptr; } virtual CollectionModel *songs_collection_model() { return nullptr; } - virtual QSortFilterProxyModel *artists_collection_sort_model() { return nullptr; } - virtual QSortFilterProxyModel *albums_collection_sort_model() { return nullptr; } - virtual QSortFilterProxyModel *songs_collection_sort_model() { return nullptr; } + virtual CollectionFilter *artists_collection_filter_model() { return nullptr; } + virtual CollectionFilter *albums_collection_filter_model() { return nullptr; } + virtual CollectionFilter *songs_collection_filter_model() { return nullptr; } public slots: virtual void ShowConfig() {} diff --git a/src/internet/internetsongsview.cpp b/src/internet/internetsongsview.cpp index 946210942..0831db10d 100644 --- a/src/internet/internetsongsview.cpp +++ b/src/internet/internetsongsview.cpp @@ -34,6 +34,7 @@ #include "core/iconloader.h" #include "collection/collectionbackend.h" #include "collection/collectionmodel.h" +#include "collection/collectionfilter.h" #include "internetservice.h" #include "internetsongsview.h" #include "internetcollectionview.h" @@ -51,10 +52,10 @@ InternetSongsView::InternetSongsView(Application *app, InternetServicePtr servic ui_->stacked->setCurrentWidget(ui_->internetcollection_page); ui_->view->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model(), false); - ui_->view->setModel(service_->songs_collection_sort_model()); + ui_->view->setModel(service_->songs_collection_filter_model()); ui_->view->SetFilter(ui_->filter_widget); ui_->filter_widget->SetSettingsGroup(settings_group); - ui_->filter_widget->Init(service_->songs_collection_model()); + ui_->filter_widget->Init(service_->songs_collection_model(), service_->songs_collection_filter_model()); QAction *action_configure = new QAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure %1...").arg(Song::DescriptionForSource(service_->source())), this); QObject::connect(action_configure, &QAction::triggered, this, &InternetSongsView::OpenSettingsDialog); diff --git a/src/internet/internettabsview.cpp b/src/internet/internettabsview.cpp index 0825c166b..4525b5a2e 100644 --- a/src/internet/internettabsview.cpp +++ b/src/internet/internettabsview.cpp @@ -38,6 +38,7 @@ #include "core/settings.h" #include "collection/collectionbackend.h" #include "collection/collectionmodel.h" +#include "collection/collectionfilter.h" #include "collection/collectionfilterwidget.h" #include "internetservice.h" #include "internettabsview.h" @@ -66,11 +67,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service, if (service_->artists_collection_model()) { ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->internetcollection_page()); ui_->artists_collection->view()->Init(app_, service_->artists_collection_backend(), service_->artists_collection_model(), true); - ui_->artists_collection->view()->setModel(service_->artists_collection_sort_model()); + ui_->artists_collection->view()->setModel(service_->artists_collection_filter_model()); ui_->artists_collection->view()->SetFilter(ui_->artists_collection->filter_widget()); ui_->artists_collection->filter_widget()->SetSettingsGroup(settings_group); ui_->artists_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("artists")); - ui_->artists_collection->filter_widget()->Init(service_->artists_collection_model()); + ui_->artists_collection->filter_widget()->Init(service_->artists_collection_model(), service_->artists_collection_filter_model()); ui_->artists_collection->filter_widget()->AddMenuAction(action_configure); QObject::connect(ui_->artists_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetArtists); @@ -98,11 +99,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service, if (service_->albums_collection_model()) { ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->internetcollection_page()); ui_->albums_collection->view()->Init(app_, service_->albums_collection_backend(), service_->albums_collection_model(), true); - ui_->albums_collection->view()->setModel(service_->albums_collection_sort_model()); + ui_->albums_collection->view()->setModel(service_->albums_collection_filter_model()); ui_->albums_collection->view()->SetFilter(ui_->albums_collection->filter_widget()); ui_->albums_collection->filter_widget()->SetSettingsGroup(settings_group); ui_->albums_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("albums")); - ui_->albums_collection->filter_widget()->Init(service_->albums_collection_model()); + ui_->albums_collection->filter_widget()->Init(service_->albums_collection_model(), service_->albums_collection_filter_model()); ui_->albums_collection->filter_widget()->AddMenuAction(action_configure); QObject::connect(ui_->albums_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetAlbums); @@ -130,11 +131,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service, if (service_->songs_collection_model()) { ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->internetcollection_page()); ui_->songs_collection->view()->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model(), true); - ui_->songs_collection->view()->setModel(service_->songs_collection_sort_model()); + ui_->songs_collection->view()->setModel(service_->songs_collection_filter_model()); ui_->songs_collection->view()->SetFilter(ui_->songs_collection->filter_widget()); ui_->songs_collection->filter_widget()->SetSettingsGroup(settings_group); ui_->songs_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("songs")); - ui_->songs_collection->filter_widget()->Init(service_->songs_collection_model()); + ui_->songs_collection->filter_widget()->Init(service_->songs_collection_model(), service_->songs_collection_filter_model()); ui_->songs_collection->filter_widget()->AddMenuAction(action_configure); QObject::connect(ui_->songs_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetSongs); diff --git a/src/playlist/playlistmanager.cpp b/src/playlist/playlistmanager.cpp index 1bcfd95fd..8d4638e25 100644 --- a/src/playlist/playlistmanager.cpp +++ b/src/playlist/playlistmanager.cpp @@ -96,9 +96,9 @@ void PlaylistManager::Init(SharedPtr collection_backend, Shar parser_ = new PlaylistParser(collection_backend, this); playlist_container_ = playlist_container; - QObject::connect(&*collection_backend_, &CollectionBackend::SongsDiscovered, this, &PlaylistManager::SongsDiscovered); - QObject::connect(&*collection_backend_, &CollectionBackend::SongsStatisticsChanged, this, &PlaylistManager::SongsDiscovered); - QObject::connect(&*collection_backend_, &CollectionBackend::SongsRatingChanged, this, &PlaylistManager::SongsDiscovered); + QObject::connect(&*collection_backend_, &CollectionBackend::SongsChanged, this, &PlaylistManager::UpdateSongs); + QObject::connect(&*collection_backend_, &CollectionBackend::SongsStatisticsChanged, this, &PlaylistManager::UpdateSongs); + QObject::connect(&*collection_backend_, &CollectionBackend::SongsRatingChanged, this, &PlaylistManager::UpdateSongs); for (const PlaylistBackend::Playlist &p : playlist_backend->GetAllOpenPlaylists()) { ++playlists_loading_; @@ -463,7 +463,7 @@ void PlaylistManager::SelectionChanged(const QItemSelection &selection) { UpdateSummaryText(); } -void PlaylistManager::SongsDiscovered(const SongList &songs) { +void PlaylistManager::UpdateSongs(const SongList &songs) { // Some songs might've changed in the collection, let's update any playlist items we have that match those songs diff --git a/src/playlist/playlistmanager.h b/src/playlist/playlistmanager.h index dfbbb148f..d91a7f183 100644 --- a/src/playlist/playlistmanager.h +++ b/src/playlist/playlistmanager.h @@ -233,7 +233,7 @@ class PlaylistManager : public PlaylistManagerInterface { void OneOfPlaylistsChanged(); void UpdateSummaryText(); - void SongsDiscovered(const SongList &songs); + void UpdateSongs(const SongList &songs); void ItemsLoadedForSavePlaylist(const SongList &songs, const QString &filename, const PlaylistSettingsPage::PathType path_type); void PlaylistLoaded(); diff --git a/src/qobuz/qobuzservice.cpp b/src/qobuz/qobuzservice.cpp index 2db75bbe7..1fc06780c 100644 --- a/src/qobuz/qobuzservice.cpp +++ b/src/qobuz/qobuzservice.cpp @@ -35,7 +35,6 @@ #include #include #include -#include #include #include "core/shared_ptr.h" @@ -50,6 +49,7 @@ #include "internet/internetsearchview.h" #include "collection/collectionbackend.h" #include "collection/collectionmodel.h" +#include "collection/collectionfilter.h" #include "qobuzservice.h" #include "qobuzurlhandler.h" #include "qobuzbaserequest.h" @@ -75,10 +75,6 @@ constexpr char kArtistsSongsTable[] = "qobuz_artists_songs"; constexpr char kAlbumsSongsTable[] = "qobuz_albums_songs"; constexpr char kSongsTable[] = "qobuz_songs"; -constexpr char kArtistsSongsFtsTable[] = "qobuz_artists_songs_fts"; -constexpr char kAlbumsSongsFtsTable[] = "qobuz_albums_songs_fts"; -constexpr char kSongsFtsTable[] = "qobuz_songs_fts"; - } // namespace QobuzService::QobuzService(Application *app, QObject *parent) @@ -92,9 +88,6 @@ QobuzService::QobuzService(Application *app, QObject *parent) artists_collection_model_(nullptr), albums_collection_model_(nullptr), songs_collection_model_(nullptr), - artists_collection_sort_model_(new QSortFilterProxyModel(this)), - albums_collection_sort_model_(new QSortFilterProxyModel(this)), - songs_collection_sort_model_(new QSortFilterProxyModel(this)), timer_search_delay_(new QTimer(this)), timer_login_attempt_(new QTimer(this)), favorite_request_(new QobuzFavoriteRequest(this, network_, this)), @@ -120,38 +113,21 @@ QobuzService::QobuzService(Application *app, QObject *parent) artists_collection_backend_ = make_shared(); artists_collection_backend_->moveToThread(app_->database()->thread()); - artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kArtistsSongsTable), QLatin1String(kArtistsSongsFtsTable)); + artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kArtistsSongsTable)); albums_collection_backend_ = make_shared(); albums_collection_backend_->moveToThread(app_->database()->thread()); - albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kAlbumsSongsTable), QLatin1String(kAlbumsSongsFtsTable)); + albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kAlbumsSongsTable)); songs_collection_backend_ = make_shared(); songs_collection_backend_->moveToThread(app_->database()->thread()); - songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable)); + songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kSongsTable)); + // Models artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this); albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this); songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this); - artists_collection_sort_model_->setSourceModel(artists_collection_model_); - artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - artists_collection_sort_model_->setDynamicSortFilter(true); - artists_collection_sort_model_->setSortLocaleAware(true); - artists_collection_sort_model_->sort(0); - - albums_collection_sort_model_->setSourceModel(albums_collection_model_); - albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - albums_collection_sort_model_->setDynamicSortFilter(true); - albums_collection_sort_model_->setSortLocaleAware(true); - albums_collection_sort_model_->sort(0); - - songs_collection_sort_model_->setSourceModel(songs_collection_model_); - songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - songs_collection_sort_model_->setDynamicSortFilter(true); - songs_collection_sort_model_->setSortLocaleAware(true); - songs_collection_sort_model_->sort(0); - // Search timer_search_delay_->setSingleShot(true); diff --git a/src/qobuz/qobuzservice.h b/src/qobuz/qobuzservice.h index 5fffc6288..18e120e9d 100644 --- a/src/qobuz/qobuzservice.h +++ b/src/qobuz/qobuzservice.h @@ -44,7 +44,6 @@ class QTimer; class QNetworkReply; -class QSortFilterProxyModel; class Application; class NetworkAccessManager; class QobuzUrlHandler; @@ -53,6 +52,7 @@ class QobuzFavoriteRequest; class QobuzStreamURLRequest; class CollectionBackend; class CollectionModel; +class CollectionFilter; class QobuzService : public InternetService { Q_OBJECT @@ -105,9 +105,9 @@ class QobuzService : public InternetService { CollectionModel *albums_collection_model() override { return albums_collection_model_; } CollectionModel *songs_collection_model() override { return songs_collection_model_; } - QSortFilterProxyModel *artists_collection_sort_model() override { return artists_collection_sort_model_; } - QSortFilterProxyModel *albums_collection_sort_model() override { return albums_collection_sort_model_; } - QSortFilterProxyModel *songs_collection_sort_model() override { return songs_collection_sort_model_; } + CollectionFilter *artists_collection_filter_model() override { return artists_collection_model_->filter(); } + CollectionFilter *albums_collection_filter_model() override { return albums_collection_model_->filter(); } + CollectionFilter *songs_collection_filter_model() override { return songs_collection_model_->filter(); } public slots: void ShowConfig() override; @@ -160,10 +160,6 @@ class QobuzService : public InternetService { CollectionModel *albums_collection_model_; CollectionModel *songs_collection_model_; - QSortFilterProxyModel *artists_collection_sort_model_; - QSortFilterProxyModel *albums_collection_sort_model_; - QSortFilterProxyModel *songs_collection_sort_model_; - QTimer *timer_search_delay_; QTimer *timer_login_attempt_; diff --git a/src/radios/radioitem.h b/src/radios/radioitem.h index 55912a54e..3d7c66bef 100644 --- a/src/radios/radioitem.h +++ b/src/radios/radioitem.h @@ -29,16 +29,17 @@ class RadioItem : public SimpleTreeItem { public: - enum Type { - Type_LoadingIndicator, - Type_Root, - Type_Service, - Type_Channel + enum class Type { + LoadingIndicator, + Root, + Service, + Channel }; - explicit RadioItem(SimpleTreeModel *_model) : SimpleTreeItem(Type_Root, _model) {} - explicit RadioItem(Type _type, RadioItem *_parent = nullptr) : SimpleTreeItem(_type, _parent) {} + explicit RadioItem(SimpleTreeModel *_model) : SimpleTreeItem(_model), type(Type::Root) {} + explicit RadioItem(const Type _type, RadioItem *_parent = nullptr) : SimpleTreeItem(_parent), type(_type) {} + Type type; Song::Source source; RadioChannel channel; @@ -46,4 +47,6 @@ class RadioItem : public SimpleTreeItem { Q_DISABLE_COPY(RadioItem) }; +Q_DECLARE_METATYPE(RadioItem::Type) + #endif // RADIOITEM_H diff --git a/src/radios/radiomodel.cpp b/src/radios/radiomodel.cpp index fa15e626c..e36b2a03f 100644 --- a/src/radios/radiomodel.cpp +++ b/src/radios/radiomodel.cpp @@ -45,8 +45,6 @@ RadioModel::RadioModel(Application *app, QObject *parent) : SimpleTreeModel(new RadioItem(this), parent), app_(app) { - root_->lazy_loaded = true; - if (app_) { QObject::connect(&*app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &RadioModel::AlbumCoverLoaded); } @@ -60,11 +58,11 @@ RadioModel::~RadioModel() { Qt::ItemFlags RadioModel::flags(const QModelIndex &idx) const { switch (IndexToItem(idx)->type) { - case RadioItem::Type_Service: - case RadioItem::Type_Channel: + case RadioItem::Type::Service: + case RadioItem::Type::Channel: return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled; - case RadioItem::Type_Root: - case RadioItem::Type_LoadingIndicator: + case RadioItem::Type::Root: + case RadioItem::Type::LoadingIndicator: default: return Qt::ItemIsEnabled; } @@ -78,7 +76,7 @@ QVariant RadioModel::data(const QModelIndex &idx, int role) const { const RadioItem *item = IndexToItem(idx); if (!item) return QVariant(); - if (role == Qt::DecorationRole && item->type == RadioItem::Type_Channel) { + if (role == Qt::DecorationRole && item->type == RadioItem::Type::Channel) { return const_cast(this)->ChannelIcon(idx); } @@ -90,7 +88,7 @@ QVariant RadioModel::data(const RadioItem *item, int role) const { switch (role) { case Qt::DecorationRole: - if (item->type == RadioItem::Type_Service) { + if (item->type == RadioItem::Type::Service) { return Song::IconForSource(item->source); } break; @@ -98,7 +96,7 @@ QVariant RadioModel::data(const RadioItem *item, int role) const { return item->DisplayText(); break; case Role_Type: - return item->type; + return QVariant::fromValue(item->type); break; case Role_SortText: return item->SortText(); @@ -154,7 +152,6 @@ void RadioModel::Reset() { pending_cache_keys_.clear(); delete root_; root_ = new RadioItem(this); - root_->lazy_loaded = true; endResetModel(); } @@ -168,22 +165,20 @@ void RadioModel::AddChannels(const RadioChannelList &channels) { } else { beginInsertRows(ItemToIndex(root_), static_cast(root_->children.count()), static_cast(root_->children.count())); - RadioItem *item = new RadioItem(RadioItem::Type_Service, root_); + RadioItem *item = new RadioItem(RadioItem::Type::Service, root_); item->source = channel.source; item->display_text = Song::DescriptionForSource(channel.source); item->sort_text = SortText(Song::TextForSource(channel.source)); - item->lazy_loaded = true; container_nodes_.insert(channel.source, item); endInsertRows(); container = item; } beginInsertRows(ItemToIndex(container), static_cast(container->children.count()), static_cast(container->children.count())); - RadioItem *item = new RadioItem(RadioItem::Type_Channel, container); + RadioItem *item = new RadioItem(RadioItem::Type::Channel, container); item->source = channel.source; item->display_text = channel.name; item->sort_text = SortText(Song::TextForSource(channel.source) + QStringLiteral(" - ") + channel.name); item->channel = channel; - item->lazy_loaded = true; items_ << item; endInsertRows(); } @@ -192,7 +187,7 @@ void RadioModel::AddChannels(const RadioChannelList &channels) { bool RadioModel::IsPlayable(const QModelIndex &idx) const { - return idx.data(Role_Type) == RadioItem::Type_Channel; + return idx.data(Role_Type).value() == RadioItem::Type::Channel; } @@ -214,7 +209,7 @@ bool RadioModel::CompareItems(const RadioItem *a, const RadioItem *b) const { void RadioModel::GetChildSongs(RadioItem *item, QList *urls, SongList *songs) const { switch (item->type) { - case RadioItem::Type_Service:{ + case RadioItem::Type::Service:{ QList children = item->children; std::sort(children.begin(), children.end(), std::bind(&RadioModel::CompareItems, this, std::placeholders::_1, std::placeholders::_2)); for (RadioItem *child : children) { @@ -222,7 +217,7 @@ void RadioModel::GetChildSongs(RadioItem *item, QList *urls, SongList *son } break; } - case RadioItem::Type_Channel: + case RadioItem::Type::Channel: if (!urls->contains(item->channel.url)) { urls->append(item->channel.url); songs->append(item->channel.ToSong()); diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp index 165a6efcb..5665104b7 100644 --- a/src/settings/collectionsettingspage.cpp +++ b/src/settings/collectionsettingspage.cpp @@ -144,8 +144,9 @@ void CollectionSettingsPage::Load() { s.beginGroup(kSettingsGroup); ui_->auto_open->setChecked(s.value("auto_open", true).toBool()); - ui_->pretty_covers->setChecked(s.value("pretty_covers", true).toBool()); ui_->show_dividers->setChecked(s.value("show_dividers", true).toBool()); + ui_->pretty_covers->setChecked(s.value("pretty_covers", true).toBool()); + ui_->various_artists->setChecked(s.value("various_artists", true).toBool()); ui_->sort_skips_articles->setChecked(s.value("sort_skips_articles", true).toBool()); ui_->startup_scan->setChecked(s.value("startup_scan", true).toBool()); ui_->monitor->setChecked(s.value("monitor", true).toBool()); @@ -192,8 +193,9 @@ void CollectionSettingsPage::Save() { s.beginGroup(kSettingsGroup); s.setValue("auto_open", ui_->auto_open->isChecked()); - s.setValue("pretty_covers", ui_->pretty_covers->isChecked()); s.setValue("show_dividers", ui_->show_dividers->isChecked()); + s.setValue("pretty_covers", ui_->pretty_covers->isChecked()); + s.setValue("various_artists", ui_->various_artists->isChecked()); s.setValue("sort_skips_articles", ui_->sort_skips_articles->isChecked()); s.setValue("startup_scan", ui_->startup_scan->isChecked()); s.setValue("monitor", ui_->monitor->isChecked()); diff --git a/src/settings/collectionsettingspage.ui b/src/settings/collectionsettingspage.ui index d02d747b0..c9d35a5f6 100644 --- a/src/settings/collectionsettingspage.ui +++ b/src/settings/collectionsettingspage.ui @@ -7,7 +7,7 @@ 0 0 565 - 973 + 1003 @@ -211,6 +211,13 @@ If there are no matches then it will use the largest image in the directory. + + + + Show dividers + + + @@ -219,9 +226,9 @@ If there are no matches then it will use the largest image in the directory. - + - Show dividers + Use various artists for compilation albums @@ -508,11 +515,13 @@ If there are no matches then it will use the largest image in the directory.monitor song_tracking mark_songs_unavailable + song_ebur128_loudness_analysis expire_unavailable_songs_days cover_art_patterns auto_open - pretty_covers show_dividers + pretty_covers + various_artists sort_skips_articles spinbox_cache_size combobox_cache_size @@ -520,6 +529,11 @@ If there are no matches then it will use the largest image in the directory.spinbox_disk_cache_size combobox_disk_cache_size button_clear_disk_cache + checkbox_save_playcounts + checkbox_save_ratings + checkbox_overwrite_playcount + checkbox_overwrite_rating + button_save_stats checkbox_delete_files diff --git a/src/smartplaylists/smartplaylistsitem.h b/src/smartplaylists/smartplaylistsitem.h index b6052202c..b65428f1b 100644 --- a/src/smartplaylists/smartplaylistsitem.h +++ b/src/smartplaylists/smartplaylistsitem.h @@ -31,14 +31,15 @@ class SmartPlaylistsItem : public SimpleTreeItem { public: - enum Type { - Type_Root, - Type_SmartPlaylist + enum class Type { + Root, + SmartPlaylist }; - SmartPlaylistsItem(SimpleTreeModel *_model) : SimpleTreeItem(Type_Root, _model) {} - SmartPlaylistsItem(const Type _type, SmartPlaylistsItem *_parent = nullptr) : SimpleTreeItem(_type, _parent) {} + SmartPlaylistsItem(SimpleTreeModel *_model) : SimpleTreeItem(_model), type(Type::Root) {} + SmartPlaylistsItem(const Type _type, SmartPlaylistsItem *_parent = nullptr) : SimpleTreeItem(_parent), type(_type) {} + Type type; PlaylistGenerator::Type smart_playlist_type; QByteArray smart_playlist_data; diff --git a/src/smartplaylists/smartplaylistsmodel.cpp b/src/smartplaylists/smartplaylistsmodel.cpp index 36365d2b2..cc7f8aa7d 100644 --- a/src/smartplaylists/smartplaylistsmodel.cpp +++ b/src/smartplaylists/smartplaylistsmodel.cpp @@ -51,11 +51,7 @@ const int SmartPlaylistsModel::kSmartPlaylistsVersion = 1; SmartPlaylistsModel::SmartPlaylistsModel(SharedPtr collection_backend, QObject *parent) : SimpleTreeModel(new SmartPlaylistsItem(this), parent), collection_backend_(collection_backend), - icon_(IconLoader::Load(QStringLiteral("view-media-playlist"))) { - - root_->lazy_loaded = true; - -} + icon_(IconLoader::Load(QStringLiteral("view-media-playlist"))) {} SmartPlaylistsModel::~SmartPlaylistsModel() { delete root_; } @@ -159,12 +155,11 @@ void SmartPlaylistsModel::Init() { void SmartPlaylistsModel::ItemFromSmartPlaylist(const Settings &s, const bool notify) { - SmartPlaylistsItem *item = new SmartPlaylistsItem(SmartPlaylistsItem::Type_SmartPlaylist, notify ? nullptr : root_); + SmartPlaylistsItem *item = new SmartPlaylistsItem(SmartPlaylistsItem::Type::SmartPlaylist, notify ? nullptr : root_); item->display_text = tr(qUtf8Printable(s.value("name").toString())); item->sort_text = item->display_text; item->smart_playlist_type = PlaylistGenerator::Type(s.value("type").toInt()); item->smart_playlist_data = s.value("data").toByteArray(); - item->lazy_loaded = true; if (notify) item->InsertNotify(root_); @@ -259,7 +254,7 @@ PlaylistGeneratorPtr SmartPlaylistsModel::CreateGenerator(const QModelIndex &idx PlaylistGeneratorPtr ret; const SmartPlaylistsItem *item = IndexToItem(idx); - if (!item || item->type != SmartPlaylistsItem::Type_SmartPlaylist) return ret; + if (!item || item->type != SmartPlaylistsItem::Type::SmartPlaylist) return ret; ret = PlaylistGenerator::Create(item->smart_playlist_type); if (!ret) return ret; diff --git a/src/subsonic/subsonicservice.cpp b/src/subsonic/subsonicservice.cpp index 0c52d41c0..53b710080 100644 --- a/src/subsonic/subsonicservice.cpp +++ b/src/subsonic/subsonicservice.cpp @@ -40,7 +40,6 @@ #include #include #include -#include #include "core/logging.h" #include "core/shared_ptr.h" @@ -52,6 +51,7 @@ #include "utilities/randutils.h" #include "collection/collectionbackend.h" #include "collection/collectionmodel.h" +#include "collection/collectionfilter.h" #include "subsonicservice.h" #include "subsonicurlhandler.h" #include "subsonicrequest.h" @@ -68,7 +68,6 @@ const char *SubsonicService::kApiVersion = "1.11.0"; namespace { constexpr char kSongsTable[] = "subsonic_songs"; -constexpr char kSongsFtsTable[] = "subsonic_songs_fts"; constexpr int kMaxRedirects = 3; } // namespace @@ -78,7 +77,6 @@ SubsonicService::SubsonicService(Application *app, QObject *parent) url_handler_(new SubsonicUrlHandler(app, this)), collection_backend_(nullptr), collection_model_(nullptr), - collection_sort_model_(new QSortFilterProxyModel(this)), http2_(false), verify_certificate_(false), download_album_covers_(true), @@ -87,21 +85,10 @@ SubsonicService::SubsonicService(Application *app, QObject *parent) app->player()->RegisterUrlHandler(url_handler_); - // Backend - - collection_backend_ = make_shared(); collection_backend_->moveToThread(app_->database()->thread()); - collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Subsonic, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable)); - - // Model - + collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Subsonic, QLatin1String(kSongsTable)); collection_model_ = new CollectionModel(collection_backend_, app_, this); - collection_sort_model_->setSourceModel(collection_model_); - collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - collection_sort_model_->setDynamicSortFilter(true); - collection_sort_model_->setSortLocaleAware(true); - collection_sort_model_->sort(0); SubsonicService::ReloadSettings(); diff --git a/src/subsonic/subsonicservice.h b/src/subsonic/subsonicservice.h index 412810576..2618d30cf 100644 --- a/src/subsonic/subsonicservice.h +++ b/src/subsonic/subsonicservice.h @@ -42,7 +42,6 @@ #include "internet/internetservice.h" #include "settings/subsonicsettingspage.h" -class QSortFilterProxyModel; class QNetworkReply; class Application; @@ -51,6 +50,7 @@ class SubsonicRequest; class SubsonicScrobbleRequest; class CollectionBackend; class CollectionModel; +class CollectionFilter; class SubsonicService : public InternetService { Q_OBJECT @@ -78,11 +78,11 @@ class SubsonicService : public InternetService { SharedPtr collection_backend() const { return collection_backend_; } CollectionModel *collection_model() const { return collection_model_; } - QSortFilterProxyModel *collection_sort_model() const { return collection_sort_model_; } + CollectionFilter *collection_filter_model() const { return collection_model_->filter(); } SharedPtr songs_collection_backend() override { return collection_backend_; } CollectionModel *songs_collection_model() override { return collection_model_; } - QSortFilterProxyModel *songs_collection_sort_model() override { return collection_sort_model_; } + CollectionFilter *songs_collection_filter_model() override { return collection_model_->filter(); } void CheckConfiguration(); void Scrobble(const QString &song_id, const bool submission, const QDateTime &time); @@ -109,7 +109,6 @@ class SubsonicService : public InternetService { SharedPtr collection_backend_; CollectionModel *collection_model_; - QSortFilterProxyModel *collection_sort_model_; SharedPtr songs_request_; SharedPtr scrobble_request_; diff --git a/src/tidal/tidalservice.cpp b/src/tidal/tidalservice.cpp index 800b9dc11..9d3b30cd0 100644 --- a/src/tidal/tidalservice.cpp +++ b/src/tidal/tidalservice.cpp @@ -54,6 +54,7 @@ #include "internet/internetsearchview.h" #include "collection/collectionbackend.h" #include "collection/collectionmodel.h" +#include "collection/collectionfilter.h" #include "tidalservice.h" #include "tidalurlhandler.h" #include "tidalbaserequest.h" @@ -85,10 +86,6 @@ constexpr char kArtistsSongsTable[] = "tidal_artists_songs"; constexpr char kAlbumsSongsTable[] = "tidal_albums_songs"; constexpr char kSongsTable[] = "tidal_songs"; -constexpr char kArtistsSongsFtsTable[] = "tidal_artists_songs_fts"; -constexpr char kAlbumsSongsFtsTable[] = "tidal_albums_songs_fts"; -constexpr char kSongsFtsTable[] = "tidal_songs_fts"; - } // namespace TidalService::TidalService(Application *app, QObject *parent) @@ -102,9 +99,6 @@ TidalService::TidalService(Application *app, QObject *parent) artists_collection_model_(nullptr), albums_collection_model_(nullptr), songs_collection_model_(nullptr), - artists_collection_sort_model_(new QSortFilterProxyModel(this)), - albums_collection_sort_model_(new QSortFilterProxyModel(this)), - songs_collection_sort_model_(new QSortFilterProxyModel(this)), timer_search_delay_(new QTimer(this)), timer_login_attempt_(new QTimer(this)), timer_refresh_login_(new QTimer(this)), @@ -135,38 +129,21 @@ TidalService::TidalService(Application *app, QObject *parent) artists_collection_backend_ = make_shared(); artists_collection_backend_->moveToThread(app_->database()->thread()); - artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kArtistsSongsTable), QLatin1String(kArtistsSongsFtsTable)); + artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kArtistsSongsTable)); albums_collection_backend_ = make_shared(); albums_collection_backend_->moveToThread(app_->database()->thread()); - albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kAlbumsSongsTable), QLatin1String(kAlbumsSongsFtsTable)); + albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kAlbumsSongsTable)); songs_collection_backend_ = make_shared(); songs_collection_backend_->moveToThread(app_->database()->thread()); - songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable)); + songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kSongsTable)); + // Models artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this); albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this); songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this); - artists_collection_sort_model_->setSourceModel(artists_collection_model_); - artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - artists_collection_sort_model_->setDynamicSortFilter(true); - artists_collection_sort_model_->setSortLocaleAware(true); - artists_collection_sort_model_->sort(0); - - albums_collection_sort_model_->setSourceModel(albums_collection_model_); - albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - albums_collection_sort_model_->setDynamicSortFilter(true); - albums_collection_sort_model_->setSortLocaleAware(true); - albums_collection_sort_model_->sort(0); - - songs_collection_sort_model_->setSourceModel(songs_collection_model_); - songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - songs_collection_sort_model_->setDynamicSortFilter(true); - songs_collection_sort_model_->setSortLocaleAware(true); - songs_collection_sort_model_->sort(0); - // Search timer_search_delay_->setSingleShot(true); diff --git a/src/tidal/tidalservice.h b/src/tidal/tidalservice.h index 11210086f..ab2e8b16b 100644 --- a/src/tidal/tidalservice.h +++ b/src/tidal/tidalservice.h @@ -42,7 +42,6 @@ #include "internet/internetsearchview.h" #include "settings/tidalsettingspage.h" -class QSortFilterProxyModel; class QNetworkReply; class QTimer; @@ -54,6 +53,7 @@ class TidalFavoriteRequest; class TidalStreamURLRequest; class CollectionBackend; class CollectionModel; +class CollectionFilter; class TidalService : public InternetService { Q_OBJECT @@ -112,9 +112,9 @@ class TidalService : public InternetService { CollectionModel *albums_collection_model() override { return albums_collection_model_; } CollectionModel *songs_collection_model() override { return songs_collection_model_; } - QSortFilterProxyModel *artists_collection_sort_model() override { return artists_collection_sort_model_; } - QSortFilterProxyModel *albums_collection_sort_model() override { return albums_collection_sort_model_; } - QSortFilterProxyModel *songs_collection_sort_model() override { return songs_collection_sort_model_; } + CollectionFilter *artists_collection_filter_model() override { return artists_collection_model_->filter(); } + CollectionFilter *albums_collection_filter_model() override { return albums_collection_model_->filter(); } + CollectionFilter *songs_collection_filter_model() override { return songs_collection_model_->filter(); } public slots: void ShowConfig() override; @@ -172,10 +172,6 @@ class TidalService : public InternetService { CollectionModel *albums_collection_model_; CollectionModel *songs_collection_model_; - QSortFilterProxyModel *artists_collection_sort_model_; - QSortFilterProxyModel *albums_collection_sort_model_; - QSortFilterProxyModel *songs_collection_sort_model_; - QTimer *timer_search_delay_; QTimer *timer_login_attempt_; QTimer *timer_refresh_login_; diff --git a/src/utilities/searchparserutils.cpp b/src/utilities/searchparserutils.cpp index 813f7cc9d..f286eb81e 100644 --- a/src/utilities/searchparserutils.cpp +++ b/src/utilities/searchparserutils.cpp @@ -24,22 +24,19 @@ namespace Utilities { -/** - * @brief Try and parse the string as '[[h:]m:]s' (ignoring all spaces), - * and return the number of seconds if it parses correctly. - * If not, the original string is returned. - * The 'h', 'm' and 's' components can have any length (including 0). - * A few examples: - * "::" is parsed to "0" - * "1::" is parsed to "3600" - * "3:45" is parsed to "225" - * "1:165" is parsed to "225" - * "225" is parsed to "225" (srsly! ^.^) - * "2:3:4:5" is parsed to "2:3:4:5" - * "25m" is parsed to "25m" - * @param time_str - * @return - */ +// Try and parse the string as '[[h:]m:]s' (ignoring all spaces), +// and return the number of seconds if it parses correctly. +// If not, the original string is returned. +// The 'h', 'm' and 's' components can have any length (including 0). +// A few examples: +// "::" is parsed to "0" +// "1::" is parsed to "3600" +// "3:45" is parsed to "225" +// "1:165" is parsed to "225" +// "225" is parsed to "225" (srsly! ^.^) +// "2:3:4:5" is parsed to "2:3:4:5" +// "25m" is parsed to "25m" + int ParseSearchTime(const QString &time_str) { int seconds = 0; @@ -67,38 +64,48 @@ int ParseSearchTime(const QString &time_str) { } -/** - * @brief Parses a rating search term to float. - * If the rating is a number from 0-5, map it to 0-1 - * To use float values directly, the search term can be prefixed with "f" (rating:>f0.2) - * If search str is 0, or by default, uses -1 - * @param rating_str: Rating search 0-5, or "f0.2" - * @return float: rating from 0-1 or -1 if not rated. - */ +// Parses a rating search term to float. +// If the rating is a number from 0-5, map it to 0-1 +// To use float values directly, the search term can be prefixed with "f" (rating:>f0.2) +// If search string is 0, or by default, uses -1 +// @param rating_str: Rating search 0-5, or "f0.2" +// @return float: rating from 0-1 or -1 if not rated. + float ParseSearchRating(const QString &rating_str) { if (rating_str.isEmpty()) { return -1; } + float rating = -1.0F; - bool ok = false; - float rating_input = rating_str.toFloat(&ok); - // is valid int from 0-5: convert to float - if (ok && rating_input >= 0 && rating_input <= 5) { - rating = rating_input / 5.0F; - } - // check if the search is a float - else if (rating_str.at(0) == QLatin1Char('f')) { - QString rating_float = rating_str; - rating_float = rating_float.remove(0, 1); - - ok = false; - rating_float.toFloat(&ok); + // Check if the search is a float + if (rating_str.contains(QLatin1Char('f'), Qt::CaseInsensitive)) { + if (rating_str.count(QLatin1Char('f'), Qt::CaseInsensitive) > 1) { + return rating; + } + QString rating_float_str = rating_str; + if (rating_str.at(0) == QLatin1Char('f') || rating_str.at(0) == QLatin1Char('F')) { + rating_float_str = rating_float_str.remove(0, 1); + } + if (rating_str.right(1) == QLatin1Char('f') || rating_str.right(1) == QLatin1Char('F')) { + rating_float_str.chop(1); + } + bool ok = false; + const float rating_input = rating_float_str.toFloat(&ok); if (ok) { - rating = rating_float.toFloat(&ok); + rating = rating_input; } } + else { + bool ok = false; + const int rating_input = rating_str.toInt(&ok); + // Is valid int from 0-5: convert to float + if (ok && rating_input >= 0 && rating_input <= 5) { + rating = static_cast(rating_input) / 5.0F; + } + } + // Songs with zero rating have -1 in the DB if (rating == 0) { rating = -1; diff --git a/tests/src/collectionbackend_test.cpp b/tests/src/collectionbackend_test.cpp index f81a7e9d5..29d0d404e 100644 --- a/tests/src/collectionbackend_test.cpp +++ b/tests/src/collectionbackend_test.cpp @@ -47,7 +47,7 @@ class CollectionBackendTest : public ::testing::Test { void SetUp() override { database_.reset(new MemoryDatabase(nullptr)); backend_ = make_unique(); - backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable)); + backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable)); } static Song MakeDummySong(int directory_id) { @@ -132,7 +132,7 @@ class SingleSong : public CollectionBackendTest { } void AddDummySong() { - QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsDiscovered); + QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsAdded); QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted); // Add the song @@ -265,22 +265,20 @@ TEST_F(SingleSong, UpdateSong) { new_song.set_id(1); new_song.set_title(QStringLiteral("A different title")); + QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsAdded); + QSignalSpy changed_spy(&*backend_, &CollectionBackend::SongsChanged); QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted); - QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsDiscovered); backend_->AddOrUpdateSongs(SongList() << new_song); - ASSERT_EQ(1, added_spy.size()); - ASSERT_EQ(1, deleted_spy.size()); + ASSERT_EQ(0, added_spy.size()); + ASSERT_EQ(1, changed_spy.size()); + ASSERT_EQ(0, deleted_spy.size()); - SongList songs_added = *(reinterpret_cast(added_spy[0][0].data())); - SongList songs_deleted = *(reinterpret_cast(deleted_spy[0][0].data())); - ASSERT_EQ(1, songs_added.size()); - ASSERT_EQ(1, songs_deleted.size()); - EXPECT_EQ(QStringLiteral("Title"), songs_deleted[0].title()); - EXPECT_EQ(QStringLiteral("A different title"), songs_added[0].title()); - EXPECT_EQ(1, songs_deleted[0].id()); - EXPECT_EQ(1, songs_added[0].id()); + SongList songs_changed = *(reinterpret_cast(changed_spy[0][0].data())); + ASSERT_EQ(1, songs_changed.size()); + EXPECT_EQ(QStringLiteral("A different title"), songs_changed[0].title()); + EXPECT_EQ(1, songs_changed[0].id()); } @@ -389,7 +387,7 @@ TEST_F(TestUrls, TestUrls) { } - QSignalSpy spy(&*backend_, &CollectionBackend::SongsDiscovered); + QSignalSpy spy(&*backend_, &CollectionBackend::SongsAdded); backend_->AddOrUpdateSongs(songs); if (HasFatalFailure()) return; @@ -474,7 +472,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) { } - QSignalSpy spy(&*backend_, &CollectionBackend::SongsDiscovered); + QSignalSpy spy(&*backend_, &CollectionBackend::SongsAdded); backend_->UpdateSongsBySongID(songs); @@ -495,7 +493,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) { SongMap songs; { QSqlDatabase db(database_->Connect()); - CollectionQuery query(db, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable)); + CollectionQuery query(db, QLatin1String(SCollection::kSongsTable)); EXPECT_TRUE(backend_->ExecCollectionQuery(&query, songs)); } @@ -512,7 +510,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) { } { // Remove some songs - QSignalSpy spy1(&*backend_, &CollectionBackend::SongsDiscovered); + QSignalSpy spy1(&*backend_, &CollectionBackend::SongsAdded); QSignalSpy spy2(&*backend_, &CollectionBackend::SongsDeleted); SongMap songs; @@ -558,7 +556,8 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) { { // Update some songs QSignalSpy spy1(&*backend_, &CollectionBackend::SongsDeleted); - QSignalSpy spy2(&*backend_, &CollectionBackend::SongsDiscovered); + QSignalSpy spy2(&*backend_, &CollectionBackend::SongsAdded); + QSignalSpy spy3(&*backend_, &CollectionBackend::SongsChanged); SongMap songs; @@ -595,16 +594,14 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) { backend_->UpdateSongsBySongID(songs); - ASSERT_EQ(1, spy1.count()); - ASSERT_EQ(1, spy2.count()); - SongList deleted_songs = spy1[0][0].value(); - SongList added_songs = spy2[0][0].value(); - EXPECT_EQ(deleted_songs.count(), 2); - EXPECT_EQ(added_songs.count(), 2); - EXPECT_EQ(deleted_songs[0].song_id(), QStringLiteral("song1")); - EXPECT_EQ(deleted_songs[1].song_id(), QStringLiteral("song6")); - EXPECT_EQ(added_songs[0].song_id(), QStringLiteral("song1")); - EXPECT_EQ(added_songs[1].song_id(), QStringLiteral("song6")); + ASSERT_EQ(0, spy1.count()); + ASSERT_EQ(0, spy2.count()); + ASSERT_EQ(1, spy3.count()); + + SongList changed_songs = spy3[0][0].value(); + EXPECT_EQ(changed_songs.count(), 2); + EXPECT_EQ(changed_songs[0].song_id(), QStringLiteral("song1")); + EXPECT_EQ(changed_songs[1].song_id(), QStringLiteral("song6")); } diff --git a/tests/src/collectionmodel_test.cpp b/tests/src/collectionmodel_test.cpp index b59b045c1..beff980f7 100644 --- a/tests/src/collectionmodel_test.cpp +++ b/tests/src/collectionmodel_test.cpp @@ -28,16 +28,16 @@ #include #include #include -#include #include #include "core/logging.h" #include "core/scoped_ptr.h" #include "core/shared_ptr.h" #include "core/database.h" -#include "collection/collectionmodel.h" -#include "collection/collectionbackend.h" #include "collection/collection.h" +#include "collection/collectionbackend.h" +#include "collection/collectionmodel.h" +#include "collection/collectionfilter.h" using std::make_unique; using std::make_shared; @@ -48,23 +48,18 @@ namespace { class CollectionModelTest : public ::testing::Test { public: - CollectionModelTest() : added_dir_(false) {} + CollectionModelTest() : collection_filter_(nullptr), added_dir_(false) {} protected: void SetUp() override { database_ = make_shared(nullptr); backend_ = make_shared(); - backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable)); + backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable)); model_ = make_unique(backend_, nullptr); + collection_filter_ = model_->filter(); added_dir_ = false; - model_sorted_ = make_unique(); - model_sorted_->setSourceModel(&*model_); - model_sorted_->setSortRole(CollectionModel::Role_SortText); - model_sorted_->setDynamicSortFilter(true); - model_sorted_->sort(0); - } Song AddSong(Song &song) { @@ -79,7 +74,11 @@ class CollectionModelTest : public ::testing::Test { added_dir_ = true; } + QEventLoop loop; + QObject::connect(&*model_, &CollectionModel::rowsInserted, &loop, &QEventLoop::quit); backend_->AddOrUpdateSongs(SongList() << song); + loop.exec(); + return song; } @@ -94,7 +93,7 @@ class CollectionModelTest : public ::testing::Test { SharedPtr database_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) SharedPtr backend_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) ScopedPtr model_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) - ScopedPtr model_sorted_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) + CollectionFilter *collection_filter_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) bool added_dir_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) }; @@ -108,14 +107,13 @@ TEST_F(CollectionModelTest, WithInitialArtists) { AddSong(QStringLiteral("Title"), QStringLiteral("Artist 1"), QStringLiteral("Album"), 123); AddSong(QStringLiteral("Title"), QStringLiteral("Artist 2"), QStringLiteral("Album"), 123); AddSong(QStringLiteral("Title"), QStringLiteral("Foo"), QStringLiteral("Album"), 123); - model_->Init(false); - ASSERT_EQ(5, model_sorted_->rowCount(QModelIndex())); - EXPECT_EQ(QStringLiteral("A"), model_sorted_->index(0, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("Artist 1"), model_sorted_->index(1, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("Artist 2"), model_sorted_->index(2, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("F"), model_sorted_->index(3, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("Foo"), model_sorted_->index(4, 0, QModelIndex()).data().toString()); + ASSERT_EQ(5, collection_filter_->rowCount(QModelIndex())); + EXPECT_EQ(QStringLiteral("A"), collection_filter_->index(0, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("Artist 1"), collection_filter_->index(1, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("Artist 2"), collection_filter_->index(2, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("F"), collection_filter_->index(3, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("Foo"), collection_filter_->index(4, 0, QModelIndex()).data().toString()); } @@ -128,8 +126,6 @@ TEST_F(CollectionModelTest, CompilationAlbums) { song.set_ctime(0); AddSong(song); - model_->Init(false); - model_->fetchMore(model_->index(0, 0)); ASSERT_EQ(1, model_->rowCount(QModelIndex())); @@ -150,15 +146,14 @@ TEST_F(CollectionModelTest, NumericHeaders) { AddSong(QStringLiteral("Title"), QStringLiteral("2artist"), QStringLiteral("Album"), 123); AddSong(QStringLiteral("Title"), QStringLiteral("0artist"), QStringLiteral("Album"), 123); AddSong(QStringLiteral("Title"), QStringLiteral("zartist"), QStringLiteral("Album"), 123); - model_->Init(false); - ASSERT_EQ(6, model_sorted_->rowCount(QModelIndex())); - EXPECT_EQ(QStringLiteral("0-9"), model_sorted_->index(0, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("0artist"), model_sorted_->index(1, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("1artist"), model_sorted_->index(2, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("2artist"), model_sorted_->index(3, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("Z"), model_sorted_->index(4, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("zartist"), model_sorted_->index(5, 0, QModelIndex()).data().toString()); + ASSERT_EQ(6, collection_filter_->rowCount(QModelIndex())); + EXPECT_EQ(QStringLiteral("0-9"), collection_filter_->index(0, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("0artist"), collection_filter_->index(1, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("1artist"), collection_filter_->index(2, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("2artist"), collection_filter_->index(3, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("Z"), collection_filter_->index(4, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("zartist"), collection_filter_->index(5, 0, QModelIndex()).data().toString()); } @@ -166,20 +161,17 @@ TEST_F(CollectionModelTest, MixedCaseHeaders) { AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); AddSong(QStringLiteral("Title"), QStringLiteral("artist"), QStringLiteral("Album"), 123); - model_->Init(false); - ASSERT_EQ(3, model_sorted_->rowCount(QModelIndex())); - EXPECT_EQ(QStringLiteral("A"), model_sorted_->index(0, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("Artist"), model_sorted_->index(1, 0, QModelIndex()).data().toString()); - EXPECT_EQ(QStringLiteral("artist"), model_sorted_->index(2, 0, QModelIndex()).data().toString()); + ASSERT_EQ(3, collection_filter_->rowCount(QModelIndex())); + EXPECT_EQ(QStringLiteral("A"), collection_filter_->index(0, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("Artist"), collection_filter_->index(1, 0, QModelIndex()).data().toString()); + EXPECT_EQ(QStringLiteral("artist"), collection_filter_->index(2, 0, QModelIndex()).data().toString()); } TEST_F(CollectionModelTest, UnknownArtists) { AddSong(QStringLiteral("Title"), QLatin1String(""), QStringLiteral("Album"), 123); - model_->Init(false); - model_->fetchMore(model_->index(0, 0)); ASSERT_EQ(1, model_->rowCount(QModelIndex())); QModelIndex unknown_index = model_->index(0, 0, QModelIndex()); @@ -194,10 +186,9 @@ TEST_F(CollectionModelTest, UnknownAlbums) { AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QLatin1String(""), 123); AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); - model_->Init(false); - model_->fetchMore(model_->index(0, 0)); - QModelIndex artist_index = model_->index(0, 0, QModelIndex()); + QModelIndex artist_index = model_->index(1, 0, QModelIndex()); + EXPECT_EQ(artist_index.isValid(), true); ASSERT_EQ(2, model_->rowCount(artist_index)); QModelIndex unknown_album_index = model_->index(0, 0, artist_index); @@ -228,14 +219,11 @@ TEST_F(CollectionModelTest, VariousArtistSongs) { for (int i=0 ; i < 4 ; ++i) AddSong(songs[i]); - model_->Init(false); QModelIndex artist_index = model_->index(0, 0, QModelIndex()); - model_->fetchMore(artist_index); ASSERT_EQ(1, model_->rowCount(artist_index)); QModelIndex album_index = model_->index(0, 0, artist_index); - model_->fetchMore(album_index); ASSERT_EQ(4, model_->rowCount(album_index)); EXPECT_EQ(QStringLiteral("Artist 1 - Title 1"), model_->index(0, 0, album_index).data().toString()); @@ -245,19 +233,16 @@ TEST_F(CollectionModelTest, VariousArtistSongs) { } -TEST_F(CollectionModelTest, RemoveSongsLazyLoaded) { +TEST_F(CollectionModelTest, RemoveSongs) { Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1); Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); two.set_id(2); AddSong(QStringLiteral("Title 3"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); - model_->Init(false); - // Lazy load the items - QModelIndex artist_index = model_->index(0, 0, QModelIndex()); - model_->fetchMore(artist_index); + QModelIndex artist_index = model_->index(1, 0, QModelIndex()); ASSERT_EQ(1, model_->rowCount(artist_index)); + QModelIndex album_index = model_->index(0, 0, artist_index); - model_->fetchMore(album_index); ASSERT_EQ(3, model_->rowCount(album_index)); // Remove the first two songs @@ -265,13 +250,16 @@ TEST_F(CollectionModelTest, RemoveSongsLazyLoaded) { QSignalSpy spy_remove(&*model_, &CollectionModel::rowsRemoved); QSignalSpy spy_reset(&*model_, &CollectionModel::modelReset); + QEventLoop loop; + QObject::connect(&*model_, &CollectionModel::rowsRemoved, &loop, &QEventLoop::quit); backend_->DeleteSongs(SongList() << one << two); + loop.exec(); ASSERT_EQ(2, spy_preremove.count()); ASSERT_EQ(2, spy_remove.count()); ASSERT_EQ(0, spy_reset.count()); - artist_index = model_->index(0, 0, QModelIndex()); + artist_index = model_->index(1, 0, QModelIndex()); ASSERT_EQ(1, model_->rowCount(artist_index)); album_index = model_->index(0, 0, artist_index); ASSERT_EQ(1, model_->rowCount(album_index)); @@ -279,45 +267,26 @@ TEST_F(CollectionModelTest, RemoveSongsLazyLoaded) { } -TEST_F(CollectionModelTest, RemoveSongsNotLazyLoaded) { - - Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1); - Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); two.set_id(2); - model_->Init(false); - - // Remove the first two songs - QSignalSpy spy_preremove(&*model_, &CollectionModel::rowsAboutToBeRemoved); - QSignalSpy spy_remove(&*model_, &CollectionModel::rowsRemoved); - QSignalSpy spy_reset(&*model_, &CollectionModel::modelReset); - - backend_->DeleteSongs(SongList() << one << two); - - ASSERT_EQ(0, spy_preremove.count()); - ASSERT_EQ(0, spy_remove.count()); - ASSERT_EQ(1, spy_reset.count()); - -} - TEST_F(CollectionModelTest, RemoveEmptyAlbums) { Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album 1"), 123); one.set_id(1); Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album 2"), 123); two.set_id(2); Song three = AddSong(QStringLiteral("Title 3"), QStringLiteral("Artist"), QStringLiteral("Album 2"), 123); three.set_id(3); - model_->Init(false); - QModelIndex artist_index = model_->index(0, 0, QModelIndex()); - model_->fetchMore(artist_index); + QModelIndex artist_index = model_->index(1, 0, QModelIndex()); ASSERT_EQ(2, model_->rowCount(artist_index)); // Remove one song from each album + QEventLoop loop; + QObject::connect(&*model_, &CollectionModel::rowsRemoved, &loop, &QEventLoop::quit); backend_->DeleteSongs(SongList() << one << two); + loop.exec(); // Check the model - artist_index = model_->index(0, 0, QModelIndex()); - model_->fetchMore(artist_index); + artist_index = model_->index(1, 0, QModelIndex()); ASSERT_EQ(1, model_->rowCount(artist_index)); + QModelIndex album_index = model_->index(0, 0, artist_index); - model_->fetchMore(album_index); EXPECT_EQ(QStringLiteral("Album 2"), album_index.data().toString()); ASSERT_EQ(1, model_->rowCount(album_index)); @@ -328,21 +297,21 @@ TEST_F(CollectionModelTest, RemoveEmptyAlbums) { TEST_F(CollectionModelTest, RemoveEmptyArtists) { Song one = AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1); - model_->Init(false); - // Lazy load the items - QModelIndex artist_index = model_->index(0, 0, QModelIndex()); - model_->fetchMore(artist_index); + QModelIndex artist_index = model_->index(1, 0, QModelIndex()); ASSERT_EQ(1, model_->rowCount(artist_index)); + QModelIndex album_index = model_->index(0, 0, artist_index); - model_->fetchMore(album_index); ASSERT_EQ(1, model_->rowCount(album_index)); // The artist header is there too right? ASSERT_EQ(2, model_->rowCount(QModelIndex())); // Remove the song + QEventLoop loop; + QObject::connect(&*model_, &CollectionModel::rowsRemoved, &loop, &QEventLoop::quit); backend_->DeleteSongs(SongList() << one); + loop.exec(); // Everything should be gone - even the artist header ASSERT_EQ(0, model_->rowCount(QModelIndex())); @@ -351,8 +320,8 @@ TEST_F(CollectionModelTest, RemoveEmptyArtists) { // Test to check that the container nodes are created identical and unique all through the model with all possible collection groupings. // model1 - Nodes are created from a complete reset done through lazy-loading. -// model2 - Initial container nodes are created in SongsDiscovered. -// model3 - All container nodes are created in SongsDiscovered. +// model2 - Initial container nodes are created in SongsAdded. +// model3 - All container nodes are created in SongsAdded. // WARNING: This test can take up to 30 minutes to complete. #if 0 @@ -363,23 +332,23 @@ TEST_F(CollectionModelTest, TestContainerNodes) { // Add some normal albums. for (int artist_number = 1; artist_number <= 3 ; ++artist_number) { Song song(Song::Source::Collection); - song.set_artist(QString("Artist %1").arg(artist_number)); - song.set_composer(QString("Composer %1").arg(artist_number)); - song.set_performer(QString("Performer %1").arg(artist_number)); + song.set_artist(QStringLiteral("Artist %1").arg(artist_number)); + song.set_composer(QStringLiteral("Composer %1").arg(artist_number)); + song.set_performer(QStringLiteral("Performer %1").arg(artist_number)); song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); for (int album_number = 1; album_number <= 3 ; ++album_number) { if (year > 2020) year = 1960; - song.set_album(QString("Artist %1 - Album %2").arg(artist_number).arg(album_number)); + song.set_album(QStringLiteral("Artist %1 - Album %2").arg(artist_number).arg(album_number)); song.set_album_id(QString::number(album_number)); song.set_year(year++); - song.set_genre("Rock"); + song.set_genre(QStringLiteral("Rock")); for (int song_number = 1; song_number <= 5 ; ++song_number) { - song.set_url(QUrl(QString("file:///mnt/music/Artist %1/Album %2/%3 - artist song-n-%3").arg(artist_number).arg(album_number).arg(song_number))); - song.set_title(QString("Title %1").arg(song_number)); + song.set_url(QUrl(QStringLiteral("file:///mnt/music/Artist %1/Album %2/%3 - artist song-n-%3").arg(artist_number).arg(album_number).arg(song_number))); + song.set_title(QStringLiteral("Title %1").arg(song_number)); song.set_track(song_number); songs << song; } @@ -389,26 +358,26 @@ TEST_F(CollectionModelTest, TestContainerNodes) { // Add some albums with 'album artist'. for (int album_artist_number = 1; album_artist_number <= 3 ; ++album_artist_number) { Song song(Song::Source::Collection); - song.set_albumartist(QString("Album Artist %1").arg(album_artist_number)); - song.set_composer(QString("Composer %1").arg(album_artist_number)); - song.set_performer(QString("Performer %1").arg(album_artist_number)); + song.set_albumartist(QStringLiteral("Album Artist %1").arg(album_artist_number)); + song.set_composer(QStringLiteral("Composer %1").arg(album_artist_number)); + song.set_performer(QStringLiteral("Performer %1").arg(album_artist_number)); song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); for (int album_number = 1; album_number <= 3 ; ++album_number) { if (year > 2020) year = 1960; - song.set_album(QString("Album Artist %1 - Album %2").arg(album_artist_number).arg(album_number)); + song.set_album(QStringLiteral("Album Artist %1 - Album %2").arg(album_artist_number).arg(album_number)); song.set_album_id(QString::number(album_number)); song.set_year(year++); - song.set_genre("Rock"); + song.set_genre(QStringLiteral("Rock")); int artist_number = 1; for (int song_number = 1; song_number <= 5 ; ++song_number) { - song.set_url(QUrl(QString("file:///mnt/music/Album Artist %1/Album %2/%3 - album artist song-n-%3").arg(album_artist_number).arg(album_number).arg(QString::number(song_number)))); - song.set_title("Title " + QString::number(song_number)); + song.set_url(QUrl(QStringLiteral("file:///mnt/music/Album Artist %1/Album %2/%3 - album artist song-n-%3").arg(album_artist_number).arg(album_number).arg(QString::number(song_number)))); + song.set_title(QStringLiteral("Title ") + QString::number(song_number)); song.set_track(song_number); - song.set_artist("Artist " + QString::number(artist_number)); + song.set_artist(QStringLiteral("Artist ") + QString::number(artist_number)); songs << song; ++artist_number; } @@ -422,20 +391,20 @@ TEST_F(CollectionModelTest, TestContainerNodes) { song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); - song.set_album(QString("Compilation Album %1").arg(album_number)); + song.set_album(QStringLiteral("Compilation Album %1").arg(album_number)); song.set_album_id(QString::number(album_number)); song.set_year(year++); - song.set_genre("Pop"); + song.set_genre(QStringLiteral("Pop")); song.set_compilation(true); int artist_number = 1; for (int song_number = 1; song_number <= 4 ; ++song_number) { - song.set_url(QUrl(QString("file:///mnt/music/Compilation Artist %1/Compilation Album %2/%3 - compilation song-n-%3").arg(artist_number).arg(album_number).arg(QString::number(song_number)))); - song.set_artist(QString("Compilation Artist %1").arg(artist_number)); - song.set_composer(QString("Composer %1").arg(artist_number)); - song.set_performer(QString("Performer %1").arg(artist_number)); - song.set_title(QString("Title %1").arg(song_number)); + song.set_url(QUrl(QStringLiteral("file:///mnt/music/Compilation Artist %1/Compilation Album %2/%3 - compilation song-n-%3").arg(artist_number).arg(album_number).arg(QString::number(song_number)))); + song.set_artist(QStringLiteral("Compilation Artist %1").arg(artist_number)); + song.set_composer(QStringLiteral("Composer %1").arg(artist_number)); + song.set_performer(QStringLiteral("Performer %1").arg(artist_number)); + song.set_title(QStringLiteral("Title %1").arg(song_number)); song.set_track(song_number); songs << song; ++artist_number; @@ -448,27 +417,27 @@ TEST_F(CollectionModelTest, TestContainerNodes) { song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); - song.set_url(QUrl(QString("file:///mnt/music/no album song 1/song-only-1"))); - song.set_title("Only Title 1"); + song.set_url(QUrl(QStringLiteral("file:///mnt/music/no album song 1/song-only-1"))); + song.set_title(QStringLiteral("Only Title 1")); songs << song; - song.set_url(QUrl(QString("file:///mnt/music/no album song 2/song-only-2"))); - song.set_title("Only Title 2"); + song.set_url(QUrl(QStringLiteral("file:///mnt/music/no album song 2/song-only-2"))); + song.set_title(QStringLiteral("Only Title 2")); songs << song; } // Song with only artist, album and title. { Song song(Song::Source::Collection); - song.set_url(QUrl(QString("file:///tmp/artist-album-title-song"))); - song.set_artist("Not Only Artist"); - song.set_album("Not Only Album"); - song.set_title("Not Only Title"); + song.set_url(QUrl(QStringLiteral("file:///tmp/artist-album-title-song"))); + song.set_artist(QStringLiteral("Not Only Artist")); + song.set_album(QStringLiteral("Not Only Album")); + song.set_title(QStringLiteral("Not Only Title")); song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); song.set_year(1970); song.set_track(1); @@ -478,14 +447,14 @@ TEST_F(CollectionModelTest, TestContainerNodes) { // Add possible Various artists conflicting songs. { Song song(Song::Source::Collection); - song.set_url(QUrl(QString("file:///tmp/song-va-conflicting-1"))); - song.set_artist("Various artists"); - song.set_album("VA Album"); - song.set_title("VA Title"); + song.set_url(QUrl(QStringLiteral("file:///tmp/song-va-conflicting-1"))); + song.set_artist(QStringLiteral("Various artists")); + song.set_album(QStringLiteral("VA Album")); + song.set_title(QStringLiteral("VA Title")); song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); song.set_year(1970); song.set_track(1); @@ -494,15 +463,15 @@ TEST_F(CollectionModelTest, TestContainerNodes) { { Song song(Song::Source::Collection); - song.set_url(QUrl(QString("file:///tmp/song-va-conflicting-2"))); - song.set_artist("Various artists"); - song.set_albumartist("Various artists"); - song.set_album("VA Album"); - song.set_title("VA Title"); + song.set_url(QUrl(QStringLiteral("file:///tmp/song-va-conflicting-2"))); + song.set_artist(QStringLiteral("Various artists")); + song.set_albumartist(QStringLiteral("Various artists")); + song.set_album(QStringLiteral("VA Album")); + song.set_title(QStringLiteral("VA Title")); song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); song.set_year(1970); song.set_track(1); @@ -511,14 +480,14 @@ TEST_F(CollectionModelTest, TestContainerNodes) { { Song song(Song::Source::Collection); - song.set_url(QUrl(QString("file:///tmp/song-va-conflicting-3"))); - song.set_albumartist("Various artists"); - song.set_album("VA Album"); - song.set_title("VA Title"); + song.set_url(QUrl(QStringLiteral("file:///tmp/song-va-conflicting-3"))); + song.set_albumartist(QStringLiteral("Various artists")); + song.set_album(QStringLiteral("VA Album")); + song.set_title(QStringLiteral("VA Title")); song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); song.set_year(1970); song.set_track(1); @@ -528,35 +497,35 @@ TEST_F(CollectionModelTest, TestContainerNodes) { // Albums with Album ID. for (int album_id = 0; album_id <= 2 ; ++album_id) { Song song(Song::Source::Collection); - song.set_url(QUrl(QString("file:///tmp/song-with-album-id-1"))); - song.set_artist("Artist with Album ID"); - song.set_album(QString("Album %1 with Album ID").arg(album_id)); - song.set_album_id(QString("Album ID %1").arg(album_id)); + song.set_url(QUrl(QStringLiteral("file:///tmp/song-with-album-id-1"))); + song.set_artist(QStringLiteral("Artist with Album ID")); + song.set_album(QStringLiteral("Album %1 with Album ID").arg(album_id)); + song.set_album_id(QStringLiteral("Album ID %1").arg(album_id)); song.set_mtime(1); song.set_ctime(1); song.set_directory_id(1); - song.set_filetype(Song::FileType_FLAC); + song.set_filetype(Song::FileType::FLAC); song.set_filesize(1); song.set_year(1970); for (int i = 0; i <= 3 ; ++i) { - song.set_title(QString("Title %1 %2").arg(album_id).arg(i)); + song.set_title(QStringLiteral("Title %1 %2").arg(album_id).arg(i)); song.set_track(i); songs << song; } } - for (int f = CollectionModel::GroupBy_None + 1 ; f < CollectionModel::GroupByCount ; ++f) { - for (int s = CollectionModel::GroupBy_None ; s < CollectionModel::GroupByCount ; ++s) { - for (int t = CollectionModel::GroupBy_None ; t < CollectionModel::GroupByCount ; ++t) { + for (int f = static_cast(CollectionModel::GroupBy::None) + 1 ; f < static_cast(CollectionModel::GroupBy::GroupByCount) ; ++f) { + for (int s = static_cast(CollectionModel::GroupBy::None) ; s < static_cast(CollectionModel::GroupBy::GroupByCount) ; ++s) { + for (int t = static_cast(CollectionModel::GroupBy::None) ; t < static_cast(CollectionModel::GroupBy::GroupByCount) ; ++t) { qLog(Debug) << "Testing collection model grouping: " << f << s << t; - ScopedPtr database1; - ScopedPtr database2; - ScopedPtr database3; - ScopedPtr backend1; - ScopedPtr backend2; - ScopedPtr backend3; + SharedPtr database1; + SharedPtr database2; + SharedPtr database3; + SharedPtr backend1; + SharedPtr backend2; + SharedPtr backend3; ScopedPtr model1; ScopedPtr model2; ScopedPtr model3; @@ -564,39 +533,54 @@ TEST_F(CollectionModelTest, TestContainerNodes) { database1 = make_unique(nullptr); database2 = make_unique(nullptr); database3 = make_unique(nullptr); - backend1 = make_unique(); - backend2= make_unique(); - backend3 = make_unique(); - backend1->Init(database1.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable); - backend2->Init(database2.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable); - backend3->Init(database3.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable); - model1 = make_unique(backend1.get(), nullptr); - model2 = make_unique(backend2.get(), nullptr); - model3 = make_unique(backend3.get(), nullptr); + backend1 = make_shared(); + backend2= make_shared(); + backend3 = make_shared(); + backend1->Init(database1, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable)); + backend2->Init(database2, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable)); + backend3->Init(database3, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable)); + model1 = make_unique(backend1, nullptr); + model2 = make_unique(backend2, nullptr); + model3 = make_unique(backend3, nullptr); - backend1->AddDirectory("/mnt/music"); - backend2->AddDirectory("/mnt/music"); - backend3->AddDirectory("/mut/music"); + backend1->AddDirectory(QStringLiteral("/mnt/music")); + backend2->AddDirectory(QStringLiteral("/mnt/music")); + backend3->AddDirectory(QStringLiteral("/mut/music")); model1->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy(f), CollectionModel::GroupBy(s), CollectionModel::GroupBy(t))); model2->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy(f), CollectionModel::GroupBy(s), CollectionModel::GroupBy(t))); model3->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy(f), CollectionModel::GroupBy(s), CollectionModel::GroupBy(t))); - model3->set_use_lazy_loading(false); + QSignalSpy model1_update(&*model1, &CollectionModel::SongsAdded); + QSignalSpy model2_update(&*model2, &CollectionModel::SongsAdded); + QSignalSpy model3_update(&*model3, &CollectionModel::SongsAdded); - QSignalSpy model1_update(model1.get(), &CollectionModel::rowsInserted); - QSignalSpy model2_update(model2.get(), &CollectionModel::rowsInserted); - QSignalSpy model3_update(model3.get(), &CollectionModel::rowsInserted); + { + QEventLoop event_loop; + QObject::connect(&*model1, &CollectionModel::rowsInserted, &event_loop, &QEventLoop::quit); + backend1->AddOrUpdateSongs(songs); + event_loop.exec(); + } - backend1->AddOrUpdateSongs(songs); - backend2->AddOrUpdateSongs(songs); - backend3->AddOrUpdateSongs(songs); + { + QEventLoop event_loop; + QObject::connect(&*model2, &CollectionModel::rowsInserted, &event_loop, &QEventLoop::quit); + backend2->AddOrUpdateSongs(songs); + event_loop.exec(); + } - ASSERT_EQ(model1->song_nodes().count(), 0); - ASSERT_EQ(model2->song_nodes().count(), 0); + { + QEventLoop event_loop; + QObject::connect(&*model3, &CollectionModel::rowsInserted, &event_loop, &QEventLoop::quit); + backend3->AddOrUpdateSongs(songs); + event_loop.exec(); + } + + ASSERT_EQ(model1->song_nodes().count(), songs.count()); + ASSERT_EQ(model2->song_nodes().count(), songs.count()); ASSERT_EQ(model3->song_nodes().count(), songs.count()); - model1->Init(false); + model1->Init(); model1->ExpandAll(); model2->ExpandAll(); @@ -654,13 +638,30 @@ TEST_F(CollectionModelTest, TestContainerNodes) { } } - QSignalSpy database_reset_1(backend1.get(), &CollectionBackend::DatabaseReset); - QSignalSpy database_reset_2(backend2.get(), &CollectionBackend::DatabaseReset); - QSignalSpy database_reset_3(backend3.get(), &CollectionBackend::DatabaseReset); + QSignalSpy database_reset_1(&*backend1, &CollectionBackend::DatabaseReset); + QSignalSpy database_reset_2(&*backend2, &CollectionBackend::DatabaseReset); + QSignalSpy database_reset_3(&*backend3, &CollectionBackend::DatabaseReset); - backend1->DeleteAll(); - backend2->DeleteAll(); - backend3->DeleteAll(); + { + QEventLoop event_loop; + QObject::connect(&*model1, &CollectionModel::modelReset, &event_loop, &QEventLoop::quit); + backend1->DeleteAll(); + event_loop.exec(); + } + + { + QEventLoop event_loop; + QObject::connect(&*model2, &CollectionModel::modelReset, &event_loop, &QEventLoop::quit); + backend2->DeleteAll(); + event_loop.exec(); + } + + { + QEventLoop event_loop; + QObject::connect(&*model3, &CollectionModel::modelReset, &event_loop, &QEventLoop::quit); + backend3->DeleteAll(); + event_loop.exec(); + } ASSERT_EQ(database_reset_1.count(), 1); ASSERT_EQ(database_reset_2.count(), 1);