Rewrite collection model and search

Fixes #392
This commit is contained in:
Jonas Kvinge 2021-06-27 22:54:08 +02:00
parent ea1e4541c0
commit e477449cd4
52 changed files with 2321 additions and 2637 deletions

View File

@ -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 <QSqlDatabase>
#include <QSqlQuery>
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()

View File

@ -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/)

View File

@ -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

View File

@ -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);

View File

@ -76,16 +76,13 @@ CollectionBackend::~CollectionBackend() {
}
void CollectionBackend::Init(SharedPtr<Database> db, SharedPtr<TaskManager> 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<Database> db, SharedPtr<TaskManager> 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<int>(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();
}

View File

@ -80,12 +80,13 @@ class CollectionBackendInterface : public QObject {
using AlbumList = QList<Album>;
virtual QString songs_table() const = 0;
virtual QString fts_table() const = 0;
virtual Song::Source source() const = 0;
virtual SharedPtr<Database> 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<Database> db, SharedPtr<TaskManager> 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<Database> db, SharedPtr<TaskManager> 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<Database> 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_;
};

View File

@ -0,0 +1,335 @@
/*
* Strawberry Music Player
* Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QSortFilterProxyModel>
#include <QVariant>
#include <QString>
#include <QStringList>
#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<CollectionModel*>(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<qint64>(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();