From b5fa401db96c1b2b450995782ff50a7ef514397d Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 8 Jan 2023 15:40:54 +0100 Subject: [PATCH] Collection: Make sure `RunQuery` does not access collection items - Rename `QueryOptions` to `CollectionFilterOptions`. - Create new class `CollectionQueryOptions` for passing options from model to `CollectionQuery`. - Rename `Directory` to `CollectionDirectory`. Fixes #1095 --- src/CMakeLists.txt | 2 + src/collection/collectionbackend.cpp | 59 ++-- src/collection/collectionbackend.h | 65 ++--- src/collection/collectiondirectory.h | 57 ++++ src/collection/collectiondirectorymodel.cpp | 8 +- src/collection/collectiondirectorymodel.h | 6 +- src/collection/collectionfilteroptions.cpp | 42 +++ src/collection/collectionfilteroptions.h | 65 +++++ src/collection/collectionfilterwidget.cpp | 7 +- src/collection/collectionfilterwidget.h | 3 +- src/collection/collectionmodel.cpp | 271 +++++++++--------- src/collection/collectionmodel.h | 21 +- src/collection/collectionquery.cpp | 75 ++--- src/collection/collectionquery.h | 84 ++---- src/collection/collectionqueryoptions.cpp | 34 +++ src/collection/collectionqueryoptions.h | 57 ++++ src/collection/collectionwatcher.cpp | 76 ++--- src/collection/collectionwatcher.h | 36 +-- src/collection/directory.h | 60 ---- src/core/mainwindow.cpp | 10 +- src/core/mainwindow.h | 2 +- src/core/metatypes.cpp | 10 +- .../albumcoverchoicecontroller.cpp | 9 +- src/device/connecteddevice.cpp | 6 +- tests/src/collectionbackend_test.cpp | 4 +- tests/src/metatypes_env.h | 10 +- 26 files changed, 620 insertions(+), 459 deletions(-) create mode 100644 src/collection/collectiondirectory.h create mode 100644 src/collection/collectionfilteroptions.cpp create mode 100644 src/collection/collectionfilteroptions.h create mode 100644 src/collection/collectionqueryoptions.cpp create mode 100644 src/collection/collectionqueryoptions.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5a3bed2d..366985ba 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,9 +82,11 @@ set(SOURCES collection/collectionitemdelegate.cpp collection/collectionviewcontainer.cpp collection/collectiondirectorymodel.cpp + collection/collectionfilteroptions.cpp collection/collectionfilterwidget.cpp collection/collectionplaylistitem.cpp collection/collectionquery.cpp + collection/collectionqueryoptions.cpp collection/savedgroupingmanager.cpp collection/groupbydialog.cpp collection/collectiontask.cpp diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index 45c9e32b..0580c118 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -50,8 +50,9 @@ #include "core/sqlrow.h" #include "smartplaylists/smartplaylistsearch.h" -#include "directory.h" +#include "collectiondirectory.h" #include "collectionbackend.h" +#include "collectionfilteroptions.h" #include "collectionquery.h" #include "collectiontask.h" @@ -145,12 +146,12 @@ void CollectionBackend::ResetStatisticsAsync(const int id) { void CollectionBackend::LoadDirectories() { - DirectoryList dirs = GetAllDirectories(); + CollectionDirectoryList dirs = GetAllDirectories(); QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - for (const Directory &dir : dirs) { + for (const CollectionDirectory &dir : dirs) { emit DirectoryDiscovered(dir, SubdirsInDirectory(dir.id, db)); } @@ -207,12 +208,12 @@ void CollectionBackend::ChangeDirPath(const int id, const QString &old_path, con } -DirectoryList CollectionBackend::GetAllDirectories() { +CollectionDirectoryList CollectionBackend::GetAllDirectories() { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - DirectoryList ret; + CollectionDirectoryList ret; SqlQuery q(db); q.prepare(QString("SELECT ROWID, path FROM %1").arg(dirs_table_)); @@ -222,7 +223,7 @@ DirectoryList CollectionBackend::GetAllDirectories() { } while (q.next()) { - Directory dir; + CollectionDirectory dir; dir.id = q.value(0).toInt(); dir.path = q.value(1).toString(); @@ -232,7 +233,7 @@ DirectoryList CollectionBackend::GetAllDirectories() { } -SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) { +CollectionSubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) { QMutexLocker l(db_->Mutex()); QSqlDatabase db = db_->Connect(); @@ -240,19 +241,19 @@ SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) { } -SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id, QSqlDatabase &db) { +CollectionSubdirectoryList CollectionBackend::SubdirsInDirectory(const int id, QSqlDatabase &db) { SqlQuery q(db); q.prepare(QString("SELECT path, mtime FROM %1 WHERE directory_id = :dir").arg(subdirs_table_)); q.BindValue(":dir", id); if (!q.Exec()) { db_->ReportErrors(q); - return SubdirectoryList(); + return CollectionSubdirectoryList(); } - SubdirectoryList subdirs; + CollectionSubdirectoryList subdirs; while (q.next()) { - Subdirectory subdir; + CollectionSubdirectory subdir; subdir.directory_id = id; subdir.path = q.value(0).toString(); subdir.mtime = q.value(1).toLongLong(); @@ -339,15 +340,15 @@ void CollectionBackend::AddDirectory(const QString &path) { return; } - Directory dir; + CollectionDirectory dir; dir.path = canonical_path; dir.id = q.lastInsertId().toInt(); - emit DirectoryDiscovered(dir, SubdirectoryList()); + emit DirectoryDiscovered(dir, CollectionSubdirectoryList()); } -void CollectionBackend::RemoveDirectory(const Directory &dir) { +void CollectionBackend::RemoveDirectory(const CollectionDirectory &dir) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); @@ -447,13 +448,13 @@ void CollectionBackend::SongPathChanged(const Song &song, const QFileInfo &new_f } -void CollectionBackend::AddOrUpdateSubdirs(const SubdirectoryList &subdirs) { +void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); ScopedTransaction transaction(&db); - for (const Subdirectory &subdir : subdirs) { + for (const CollectionSubdirectory &subdir : subdirs) { if (subdir.mtime == 0) { // Delete the subdirectory SqlQuery q(db); @@ -900,12 +901,12 @@ void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool u } -QStringList CollectionBackend::GetAll(const QString &column, const QueryOptions &opt) { +QStringList CollectionBackend::GetAll(const QString &column, const CollectionFilterOptions &filter_options) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - CollectionQuery query(db, songs_table_, fts_table_, opt); + CollectionQuery query(db, songs_table_, fts_table_, filter_options); query.SetColumnSpec("DISTINCT " + column); query.AddCompilationRequirement(false); @@ -922,12 +923,12 @@ QStringList CollectionBackend::GetAll(const QString &column, const QueryOptions } -QStringList CollectionBackend::GetAllArtists(const QueryOptions &opt) { +QStringList CollectionBackend::GetAllArtists(const CollectionFilterOptions &opt) { return GetAll("artist", opt); } -QStringList CollectionBackend::GetAllArtistsWithAlbums(const QueryOptions &opt) { +QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOptions &opt) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); @@ -967,15 +968,15 @@ QStringList CollectionBackend::GetAllArtistsWithAlbums(const QueryOptions &opt) } -CollectionBackend::AlbumList CollectionBackend::GetAllAlbums(const QueryOptions &opt) { +CollectionBackend::AlbumList CollectionBackend::GetAllAlbums(const CollectionFilterOptions &opt) { return GetAlbums(QString(), false, opt); } -CollectionBackend::AlbumList CollectionBackend::GetAlbumsByArtist(const QString &artist, const QueryOptions &opt) { +CollectionBackend::AlbumList CollectionBackend::GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt) { return GetAlbums(artist, false, opt); } -SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt) { +SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt) { QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); @@ -993,7 +994,7 @@ SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, } -SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt) { +SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt) { QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); @@ -1012,7 +1013,7 @@ SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, } -SongList CollectionBackend::GetSongsByAlbum(const QString &album, const QueryOptions &opt) { +SongList CollectionBackend::GetSongsByAlbum(const QString &album, const CollectionFilterOptions &opt) { QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); @@ -1285,11 +1286,11 @@ SongList CollectionBackend::GetSongsByFingerprint(const QString &fingerprint) { } -CollectionBackend::AlbumList CollectionBackend::GetCompilationAlbums(const QueryOptions &opt) { +CollectionBackend::AlbumList CollectionBackend::GetCompilationAlbums(const CollectionFilterOptions &opt) { return GetAlbums(QString(), true, opt); } -SongList CollectionBackend::GetCompilationSongs(const QString &album, const QueryOptions &opt) { +SongList CollectionBackend::GetCompilationSongs(const QString &album, const CollectionFilterOptions &opt) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); @@ -1426,7 +1427,7 @@ bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &del } -CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, const bool compilation_required, const QueryOptions &opt) { +CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, const bool compilation_required, const CollectionFilterOptions &opt) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); @@ -1516,7 +1517,7 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective ret.album = album; ret.album_artist = effective_albumartist; - CollectionQuery query(db, songs_table_, fts_table_, QueryOptions()); + CollectionQuery query(db, songs_table_, fts_table_); query.SetColumnSpec("art_automatic, art_manual, url"); if (!effective_albumartist.isEmpty()) { query.AddWhere("effective_albumartist", effective_albumartist); diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index e20eeab4..10c2a96d 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -38,8 +38,9 @@ #include "core/song.h" #include "core/sqlquery.h" +#include "collectionfilteroptions.h" #include "collectionquery.h" -#include "directory.h" +#include "collectiondirectory.h" class QThread; class TaskManager; @@ -90,23 +91,23 @@ class CollectionBackendInterface : public QObject { virtual SongList FindSongsInDirectory(const int id) = 0; virtual SongList SongsWithMissingFingerprint(const int id) = 0; - virtual SubdirectoryList SubdirsInDirectory(const int id) = 0; - virtual DirectoryList GetAllDirectories() = 0; + virtual CollectionSubdirectoryList SubdirsInDirectory(const int id) = 0; + virtual CollectionDirectoryList GetAllDirectories() = 0; virtual void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) = 0; virtual SongList GetAllSongs() = 0; - virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0; - virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0; - virtual SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) = 0; - virtual SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt = QueryOptions()) = 0; - virtual SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) = 0; + virtual QStringList GetAllArtists(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; + virtual QStringList GetAllArtistsWithAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; + virtual SongList GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; + virtual SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; + virtual SongList GetSongsByAlbum(const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; - virtual SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions()) = 0; + virtual SongList GetCompilationSongs(const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; - virtual AlbumList GetAllAlbums(const QueryOptions &opt = QueryOptions()) = 0; - virtual AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) = 0; - virtual AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) = 0; + virtual AlbumList GetAllAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; + virtual AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; + virtual AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; virtual void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false) = 0; virtual void UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false) = 0; @@ -124,7 +125,7 @@ class CollectionBackendInterface : public QObject { virtual Song GetSongByUrl(const QUrl &url, const qint64 beginning = 0) = 0; virtual void AddDirectory(const QString &path) = 0; - virtual void RemoveDirectory(const Directory &dir) = 0; + virtual void RemoveDirectory(const CollectionDirectory &dir) = 0; }; class CollectionBackend : public CollectionBackendInterface { @@ -159,24 +160,24 @@ class CollectionBackend : public CollectionBackendInterface { SongList FindSongsInDirectory(const int id) override; SongList SongsWithMissingFingerprint(const int id) override; - SubdirectoryList SubdirsInDirectory(const int id) override; - DirectoryList GetAllDirectories() override; + CollectionSubdirectoryList SubdirsInDirectory(const int id) override; + CollectionDirectoryList GetAllDirectories() override; void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) override; SongList GetAllSongs() override; - QStringList GetAll(const QString &column, const QueryOptions &opt = QueryOptions()); - QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) override; - QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) override; - SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) override; - SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt = QueryOptions()) override; - SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) override; + QStringList GetAll(const QString &column, const CollectionFilterOptions &filter_options = CollectionFilterOptions()); + QStringList GetAllArtists(const CollectionFilterOptions &opt = CollectionFilterOptions()) override; + QStringList GetAllArtistsWithAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override; + SongList GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt = CollectionFilterOptions()) override; + SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) override; + SongList GetSongsByAlbum(const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) override; - SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions()) override; + SongList GetCompilationSongs(const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) override; - AlbumList GetAllAlbums(const QueryOptions &opt = QueryOptions()) override; - AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) override; - AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) override; + AlbumList GetAllAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override; + AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override; + AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) override; void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false) override; void UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false) override; @@ -192,7 +193,7 @@ class CollectionBackend : public CollectionBackendInterface { Song GetSongByUrl(const QUrl &url, qint64 beginning = 0) override; void AddDirectory(const QString &path) override; - void RemoveDirectory(const Directory &dir) override; + void RemoveDirectory(const CollectionDirectory &dir) override; bool ExecCollectionQuery(CollectionQuery *query, SongList &songs); bool ExecCollectionQuery(CollectionQuery *query, SongMap &songs); @@ -228,7 +229,7 @@ class CollectionBackend : public CollectionBackendInterface { void UpdateMTimesOnly(const SongList &songs); void DeleteSongs(const SongList &songs); void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true); - void AddOrUpdateSubdirs(const SubdirectoryList &subdirs); + void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs); void CompilationsNeedUpdating(); void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false); void UpdateAutomaticAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false); @@ -250,8 +251,8 @@ class CollectionBackend : public CollectionBackendInterface { void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days); signals: - void DirectoryDiscovered(Directory, SubdirectoryList); - void DirectoryDeleted(Directory); + void DirectoryDiscovered(CollectionDirectory, CollectionSubdirectoryList); + void DirectoryDeleted(CollectionDirectory); void SongsDiscovered(SongList); void SongsDeleted(SongList); @@ -280,9 +281,9 @@ class CollectionBackend : public CollectionBackendInterface { }; bool UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_songs, const QUrl &url, const bool compilation_detected); - AlbumList GetAlbums(const QString &artist, const QString &album_artist, const bool compilation_required = false, const QueryOptions &opt = QueryOptions()); - AlbumList GetAlbums(const QString &artist, const bool compilation_required, const QueryOptions &opt = QueryOptions()); - SubdirectoryList SubdirsInDirectory(const int id, QSqlDatabase &db); + 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); Song GetSongById(const int id, QSqlDatabase &db); SongList GetSongsById(const QStringList &ids, QSqlDatabase &db); diff --git a/src/collection/collectiondirectory.h b/src/collection/collectiondirectory.h new file mode 100644 index 00000000..f74673a1 --- /dev/null +++ b/src/collection/collectiondirectory.h @@ -0,0 +1,57 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * 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 COLLECTIONDIRECTORY_H +#define COLLECTIONDIRECTORY_H + +#include "config.h" + +#include +#include +#include + +struct CollectionDirectory { + CollectionDirectory() : id(-1) {} + + bool operator==(const CollectionDirectory &other) const { + return path == other.path && id == other.id; + } + + QString path; + int id; +}; +Q_DECLARE_METATYPE(CollectionDirectory) + +using CollectionDirectoryList = QList; +Q_DECLARE_METATYPE(CollectionDirectoryList) + +struct CollectionSubdirectory { + CollectionSubdirectory() : directory_id(-1), mtime(0) {} + + int directory_id; + QString path; + qint64 mtime; +}; +Q_DECLARE_METATYPE(CollectionSubdirectory) + +using CollectionSubdirectoryList = QList; +Q_DECLARE_METATYPE(CollectionSubdirectoryList) + +#endif // COLLECTIONDIRECTORY_H diff --git a/src/collection/collectiondirectorymodel.cpp b/src/collection/collectiondirectorymodel.cpp index 13d5f6b5..704ba046 100644 --- a/src/collection/collectiondirectorymodel.cpp +++ b/src/collection/collectiondirectorymodel.cpp @@ -30,7 +30,7 @@ #include "core/iconloader.h" #include "core/musicstorage.h" #include "utilities/diskutils.h" -#include "directory.h" +#include "collectiondirectory.h" #include "collectionbackend.h" #include "collectiondirectorymodel.h" @@ -46,7 +46,7 @@ CollectionDirectoryModel::CollectionDirectoryModel(CollectionBackend *backend, Q CollectionDirectoryModel::~CollectionDirectoryModel() = default; -void CollectionDirectoryModel::DirectoryDiscovered(const Directory &dir) { +void CollectionDirectoryModel::DirectoryDiscovered(const CollectionDirectory &dir) { QStandardItem *item = new QStandardItem(dir.path); item->setData(dir.id, kIdRole); @@ -56,7 +56,7 @@ void CollectionDirectoryModel::DirectoryDiscovered(const Directory &dir) { } -void CollectionDirectoryModel::DirectoryDeleted(const Directory &dir) { +void CollectionDirectoryModel::DirectoryDeleted(const CollectionDirectory &dir) { for (int i = 0; i < rowCount(); ++i) { if (item(i, 0)->data(kIdRole).toInt() == dir.id) { @@ -80,7 +80,7 @@ void CollectionDirectoryModel::RemoveDirectory(const QModelIndex &idx) { if (!backend_ || !idx.isValid()) return; - Directory dir; + CollectionDirectory dir; dir.path = idx.data().toString(); dir.id = idx.data(kIdRole).toInt(); diff --git a/src/collection/collectiondirectorymodel.h b/src/collection/collectiondirectorymodel.h index 158a15e6..130a594e 100644 --- a/src/collection/collectiondirectorymodel.h +++ b/src/collection/collectiondirectorymodel.h @@ -34,7 +34,7 @@ class QModelIndex; -struct Directory; +struct CollectionDirectory; class CollectionBackend; class MusicStorage; @@ -53,8 +53,8 @@ class CollectionDirectoryModel : public QStandardItemModel { private slots: // To be called by the backend - void DirectoryDiscovered(const Directory &directories); - void DirectoryDeleted(const Directory &directories); + void DirectoryDiscovered(const CollectionDirectory &directories); + void DirectoryDeleted(const CollectionDirectory &directories); private: static const int kIdRole = Qt::UserRole + 1; diff --git a/src/collection/collectionfilteroptions.cpp b/src/collection/collectionfilteroptions.cpp new file mode 100644 index 00000000..2a19478e --- /dev/null +++ b/src/collection/collectionfilteroptions.cpp @@ -0,0 +1,42 @@ +/* + * 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 +#include + +#include "core/song.h" + +#include "collectionfilteroptions.h" + +CollectionFilterOptions::CollectionFilterOptions() : filter_mode_(FilterMode_All), max_age_(-1) {} + +bool CollectionFilterOptions::Matches(const Song &song) const { + + if (max_age_ != -1) { + const qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - max_age_; + if (song.ctime() <= cutoff) return false; + } + + if (!filter_text_.isNull()) { + return song.artist().contains(filter_text_, Qt::CaseInsensitive) || song.album().contains(filter_text_, Qt::CaseInsensitive) || song.title().contains(filter_text_, Qt::CaseInsensitive); + } + + return true; + +} diff --git a/src/collection/collectionfilteroptions.h b/src/collection/collectionfilteroptions.h new file mode 100644 index 00000000..97353532 --- /dev/null +++ b/src/collection/collectionfilteroptions.h @@ -0,0 +1,65 @@ +/* + * 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 COLLECTIONFILTEROPTIONS_H +#define COLLECTIONFILTEROPTIONS_H + +#include + +#include "core/song.h" + +class CollectionFilterOptions { + public: + + explicit CollectionFilterOptions(); + + // Filter mode: + // - use the all songs table + // - use the duplicated songs view; by duplicated we mean those songs for which the (artist, album, title) tuple is found more than once in the songs table + // - use the untagged songs view; by untagged we mean those for which at least one of the (artist, album, title) tags is empty + // Please note that additional filtering based on FTS table (the filter attribute) won't work in Duplicates and Untagged modes. + enum FilterMode { + FilterMode_All, + FilterMode_Duplicates, + FilterMode_Untagged + }; + + FilterMode filter_mode() const { return filter_mode_; } + int max_age() const { return max_age_; } + QString filter_text() const { return filter_text_; } + + void set_filter_mode(const FilterMode filter_mode) { + filter_mode_ = filter_mode; + filter_text_.clear(); + } + void set_max_age(const int max_age) { max_age_ = max_age; } + void set_filter_text(const QString &filter_text) { + filter_mode_ = FilterMode_All; + filter_text_ = filter_text; + } + + bool Matches(const Song &song) const; + + private: + FilterMode filter_mode_; + int max_age_; + QString filter_text_; +}; + +#endif // COLLECTIONFILTEROPTIONS_H diff --git a/src/collection/collectionfilterwidget.cpp b/src/collection/collectionfilterwidget.cpp index 3731572e..475267a1 100644 --- a/src/collection/collectionfilterwidget.cpp +++ b/src/collection/collectionfilterwidget.cpp @@ -46,6 +46,7 @@ #include "core/iconloader.h" #include "core/song.h" #include "core/logging.h" +#include "collectionfilteroptions.h" #include "collectionmodel.h" #include "collectionquery.h" #include "savedgroupingmanager.h" @@ -455,12 +456,12 @@ void CollectionFilterWidget::SetFilterHint(const QString &hint) { ui_->search_field->setPlaceholderText(hint); } -void CollectionFilterWidget::SetQueryMode(QueryOptions::QueryMode query_mode) { +void CollectionFilterWidget::SetFilterMode(CollectionFilterOptions::FilterMode filter_mode) { ui_->search_field->clear(); - ui_->search_field->setEnabled(query_mode == QueryOptions::QueryMode_All); + ui_->search_field->setEnabled(filter_mode == CollectionFilterOptions::FilterMode_All); - model_->SetFilterQueryMode(query_mode); + model_->SetFilterMode(filter_mode); } diff --git a/src/collection/collectionfilterwidget.h b/src/collection/collectionfilterwidget.h index 422b20ab..5cb78610 100644 --- a/src/collection/collectionfilterwidget.h +++ b/src/collection/collectionfilterwidget.h @@ -32,6 +32,7 @@ #include #include "collectionquery.h" +#include "collectionqueryoptions.h" #include "collectionmodel.h" class QTimer; @@ -88,7 +89,7 @@ class CollectionFilterWidget : public QWidget { public slots: void UpdateGroupByActions(); - void SetQueryMode(QueryOptions::QueryMode query_mode); + void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode); void FocusOnFilter(QKeyEvent *e); signals: diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp index 6b853065..2bd6a584 100644 --- a/src/collection/collectionmodel.cpp +++ b/src/collection/collectionmodel.cpp @@ -60,7 +60,9 @@ #include "core/logging.h" #include "core/taskmanager.h" #include "core/sqlrow.h" +#include "collectionfilteroptions.h" #include "collectionquery.h" +#include "collectionqueryoptions.h" #include "collectionbackend.h" #include "collectiondirectorymodel.h" #include "collectionitem.h" @@ -210,7 +212,7 @@ void CollectionModel::SongsDiscovered(const SongList &songs) { for (const Song &song : songs) { // Sanity check to make sure we don't add songs that are outside the user's filter - if (!query_options_.Matches(song)) continue; + if (!filter_options_.Matches(song)) continue; // Hey, we've already got that one! if (song_nodes_.contains(song.id())) continue; @@ -805,16 +807,13 @@ QVariant CollectionModel::data(const CollectionItem *item, const int role) const } -bool CollectionModel::HasCompilations(const QSqlDatabase &db, const CollectionQuery &query) { +bool CollectionModel::HasCompilations(const QSqlDatabase &db, const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options) { - CollectionQuery q(db, backend_->songs_table(), backend_->fts_table(), query_options_); - - q.SetColumnSpec(query.column_spec()); - q.SetOrderBy(query.order_by()); - q.SetWhereClauses(query.where_clauses()); - q.SetBoundValues(query.bound_values()); - q.SetIncludeUnavailable(query.include_unavailable()); - q.SetDuplicatesOnly(query.duplicates_only()); + 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); @@ -827,57 +826,66 @@ bool CollectionModel::HasCompilations(const QSqlDatabase &db, const CollectionQu } -CollectionModel::QueryResult CollectionModel::RunQuery(CollectionItem *parent) { - - QueryResult result; +CollectionQueryOptions CollectionModel::PrepareQuery(CollectionItem *parent) { // 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]; + 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; { - QMutexLocker l(backend_->db()->Mutex()); - { - QSqlDatabase db(backend_->db()->Connect()); - CollectionQuery q(db, backend_->songs_table(), backend_->fts_table(), query_options_); - InitQuery(child_group_by, separate_albums_by_grouping_, &q); - - // Walk up through the item's parents adding filters as necessary - for (CollectionItem *p = parent; p && p->type == CollectionItem::Type_Container; p = p->parent) { - FilterQuery(group_by_[p->container_level], separate_albums_by_grouping_, p, &q); - } - - // Artists GroupBy is special - we don't want compilation albums appearing - if (IsArtistGroupBy(child_group_by)) { - // Add the special Various artists node - if (show_various_artists_ && HasCompilations(db, q)) { - result.create_va = true; - } - - // Don't show compilations again outside the Various artists node - q.AddCompilationRequirement(false); - } - - // Execute the query - if (q.Exec()) { - while (q.Next()) { - result.rows << SqlRow(q); - } - } - else { - backend_->ReportErrors(q); - } + 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; } - if (QThread::currentThread() != thread() && QThread::currentThread() != backend_->thread()) { - backend_->db()->Close(); + 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(query_options.compilation_requirement()); + + 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; } @@ -913,17 +921,20 @@ void CollectionModel::LazyPopulate(CollectionItem *parent, const bool signal) { if (parent->lazy_loaded) return; parent->lazy_loaded = true; - QueryResult result = RunQuery(parent); + CollectionQueryOptions query_options = PrepareQuery(parent); + QueryResult result = RunQuery(filter_options_, query_options); PostQuery(parent, result, signal); } void CollectionModel::ResetAsync() { + CollectionQueryOptions query_options = PrepareQuery(root_); + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QFuture future = QtConcurrent::run(&CollectionModel::RunQuery, this, root_); + QFuture future = QtConcurrent::run(&CollectionModel::RunQuery, this, filter_options_, query_options); #else - QFuture future = QtConcurrent::run(this, &CollectionModel::RunQuery, root_); + QFuture future = QtConcurrent::run(this, &CollectionModel::RunQuery, filter_options_, query_options); #endif QFutureWatcher *watcher = new QFutureWatcher(); QObject::connect(watcher, &QFutureWatcher::finished, this, &CollectionModel::ResetAsyncQueryFinished); @@ -937,10 +948,6 @@ void CollectionModel::ResetAsyncQueryFinished() { const struct QueryResult result = watcher->result(); watcher->deleteLater(); - if (QThread::currentThread() != thread() && QThread::currentThread() != backend_->thread()) { - backend_->Close(); - } - BeginReset(); root_->lazy_loaded = true; @@ -986,197 +993,197 @@ void CollectionModel::Reset() { } -void CollectionModel::InitQuery(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQuery *q) { +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: - q->SetColumnSpec("DISTINCT effective_albumartist"); + query_options->set_column_spec("DISTINCT effective_albumartist"); break; case GroupBy_Artist: - q->SetColumnSpec("DISTINCT artist"); + query_options->set_column_spec("DISTINCT artist"); break; case GroupBy_Album:{ QString query("DISTINCT album, album_id"); if (separate_albums_by_grouping) query.append(", grouping"); - q->SetColumnSpec(query); + query_options->set_column_spec(query); break; } case GroupBy_AlbumDisc:{ QString query("DISTINCT album, album_id, disc"); if (separate_albums_by_grouping) query.append(", grouping"); - q->SetColumnSpec(query); + query_options->set_column_spec(query); break; } case GroupBy_YearAlbum:{ QString query("DISTINCT year, album, album_id"); if (separate_albums_by_grouping) query.append(", grouping"); - q->SetColumnSpec(query); + query_options->set_column_spec(query); break; } case GroupBy_YearAlbumDisc:{ QString query("DISTINCT year, album, album_id, disc"); if (separate_albums_by_grouping) query.append(", grouping"); - q->SetColumnSpec(query); + query_options->set_column_spec(query); break; } case GroupBy_OriginalYearAlbum:{ QString query("DISTINCT year, originalyear, album, album_id"); if (separate_albums_by_grouping) query.append(", grouping"); - q->SetColumnSpec(query); + query_options->set_column_spec(query); break; } case GroupBy_OriginalYearAlbumDisc:{ QString query("DISTINCT year, originalyear, album, album_id, disc"); if (separate_albums_by_grouping) query.append(", grouping"); - q->SetColumnSpec(query); + query_options->set_column_spec(query); break; } case GroupBy_Disc: - q->SetColumnSpec("DISTINCT disc"); + query_options->set_column_spec("DISTINCT disc"); break; case GroupBy_Year: - q->SetColumnSpec("DISTINCT year"); + query_options->set_column_spec("DISTINCT year"); break; case GroupBy_OriginalYear: - q->SetColumnSpec("DISTINCT effective_originalyear"); + query_options->set_column_spec("DISTINCT effective_originalyear"); break; case GroupBy_Genre: - q->SetColumnSpec("DISTINCT genre"); + query_options->set_column_spec("DISTINCT genre"); break; case GroupBy_Composer: - q->SetColumnSpec("DISTINCT composer"); + query_options->set_column_spec("DISTINCT composer"); break; case GroupBy_Performer: - q->SetColumnSpec("DISTINCT performer"); + query_options->set_column_spec("DISTINCT performer"); break; case GroupBy_Grouping: - q->SetColumnSpec("DISTINCT grouping"); + query_options->set_column_spec("DISTINCT grouping"); break; case GroupBy_FileType: - q->SetColumnSpec("DISTINCT filetype"); + query_options->set_column_spec("DISTINCT filetype"); break; case GroupBy_Format: - q->SetColumnSpec("DISTINCT filetype, samplerate, bitdepth"); + query_options->set_column_spec("DISTINCT filetype, samplerate, bitdepth"); break; case GroupBy_Samplerate: - q->SetColumnSpec("DISTINCT samplerate"); + query_options->set_column_spec("DISTINCT samplerate"); break; case GroupBy_Bitdepth: - q->SetColumnSpec("DISTINCT bitdepth"); + query_options->set_column_spec("DISTINCT bitdepth"); break; case GroupBy_Bitrate: - q->SetColumnSpec("DISTINCT bitrate"); + query_options->set_column_spec("DISTINCT bitrate"); break; case GroupBy_None: case GroupByCount: - q->SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec); + query_options->set_column_spec("%songs_table.ROWID, " + Song::kColumnSpec); break; } } -void CollectionModel::FilterQuery(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQuery *q) { +void CollectionModel::AddQueryWhere(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQueryOptions *query_options) { // Say how we want the query to be filtered. This is done once for each parent going up the tree. switch (group_by) { case GroupBy_AlbumArtist: if (IsCompilationArtistNode(item)) { - q->AddCompilationRequirement(true); + query_options->set_compilation_requirement(true); } else { // Don't duplicate compilations outside the Various artists node - q->AddCompilationRequirement(false); - q->AddWhere("effective_albumartist", item->metadata.effective_albumartist()); + query_options->set_compilation_requirement(false); + query_options->AddWhere("effective_albumartist", item->metadata.effective_albumartist()); } break; case GroupBy_Artist: if (IsCompilationArtistNode(item)) { - q->AddCompilationRequirement(true); + query_options->set_compilation_requirement(true); } else { // Don't duplicate compilations outside the Various artists node - q->AddCompilationRequirement(false); - q->AddWhere("artist", item->metadata.artist()); + query_options->set_compilation_requirement(false); + query_options->AddWhere("artist", item->metadata.artist()); } break; case GroupBy_Album: - q->AddWhere("album", item->metadata.album()); - q->AddWhere("album_id", item->metadata.album_id()); - if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); + query_options->AddWhere("album", item->metadata.album()); + query_options->AddWhere("album_id", item->metadata.album_id()); + if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy_AlbumDisc: - q->AddWhere("album", item->metadata.album()); - q->AddWhere("album_id", item->metadata.album_id()); - q->AddWhere("disc", item->metadata.disc()); - if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); + query_options->AddWhere("album", item->metadata.album()); + query_options->AddWhere("album_id", item->metadata.album_id()); + query_options->AddWhere("disc", item->metadata.disc()); + if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy_YearAlbum: - q->AddWhere("year", item->metadata.year()); - q->AddWhere("album", item->metadata.album()); - q->AddWhere("album_id", item->metadata.album_id()); - if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); + query_options->AddWhere("year", item->metadata.year()); + query_options->AddWhere("album", item->metadata.album()); + query_options->AddWhere("album_id", item->metadata.album_id()); + if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy_YearAlbumDisc: - q->AddWhere("year", item->metadata.year()); - q->AddWhere("album", item->metadata.album()); - q->AddWhere("album_id", item->metadata.album_id()); - q->AddWhere("disc", item->metadata.disc()); - if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); + query_options->AddWhere("year", item->metadata.year()); + query_options->AddWhere("album", item->metadata.album()); + query_options->AddWhere("album_id", item->metadata.album_id()); + query_options->AddWhere("disc", item->metadata.disc()); + if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy_OriginalYearAlbum: - q->AddWhere("year", item->metadata.year()); - q->AddWhere("originalyear", item->metadata.originalyear()); - q->AddWhere("album", item->metadata.album()); - q->AddWhere("album_id", item->metadata.album_id()); - if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); + query_options->AddWhere("year", item->metadata.year()); + query_options->AddWhere("originalyear", item->metadata.originalyear()); + query_options->AddWhere("album", item->metadata.album()); + query_options->AddWhere("album_id", item->metadata.album_id()); + if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy_OriginalYearAlbumDisc: - q->AddWhere("year", item->metadata.year()); - q->AddWhere("originalyear", item->metadata.originalyear()); - q->AddWhere("album", item->metadata.album()); - q->AddWhere("album_id", item->metadata.album_id()); - q->AddWhere("disc", item->metadata.disc()); - if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); + query_options->AddWhere("year", item->metadata.year()); + query_options->AddWhere("originalyear", item->metadata.originalyear()); + query_options->AddWhere("album", item->metadata.album()); + query_options->AddWhere("album_id", item->metadata.album_id()); + query_options->AddWhere("disc", item->metadata.disc()); + if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy_Disc: - q->AddWhere("disc", item->metadata.disc()); + query_options->AddWhere("disc", item->metadata.disc()); break; case GroupBy_Year: - q->AddWhere("year", item->metadata.year()); + query_options->AddWhere("year", item->metadata.year()); break; case GroupBy_OriginalYear: - q->AddWhere("effective_originalyear", item->metadata.effective_originalyear()); + query_options->AddWhere("effective_originalyear", item->metadata.effective_originalyear()); break; case GroupBy_Genre: - q->AddWhere("genre", item->metadata.genre()); + query_options->AddWhere("genre", item->metadata.genre()); break; case GroupBy_Composer: - q->AddWhere("composer", item->metadata.composer()); + query_options->AddWhere("composer", item->metadata.composer()); break; case GroupBy_Performer: - q->AddWhere("performer", item->metadata.performer()); + query_options->AddWhere("performer", item->metadata.performer()); break; case GroupBy_Grouping: - q->AddWhere("grouping", item->metadata.grouping()); + query_options->AddWhere("grouping", item->metadata.grouping()); break; case GroupBy_FileType: - q->AddWhere("filetype", item->metadata.filetype()); + query_options->AddWhere("filetype", item->metadata.filetype()); break; case GroupBy_Format: - q->AddWhere("filetype", item->metadata.filetype()); - q->AddWhere("samplerate", item->metadata.samplerate()); - q->AddWhere("bitdepth", item->metadata.bitdepth()); + query_options->AddWhere("filetype", item->metadata.filetype()); + query_options->AddWhere("samplerate", item->metadata.samplerate()); + query_options->AddWhere("bitdepth", item->metadata.bitdepth()); break; case GroupBy_Samplerate: - q->AddWhere("samplerate", item->metadata.samplerate()); + query_options->AddWhere("samplerate", item->metadata.samplerate()); break; case GroupBy_Bitdepth: - q->AddWhere("bitdepth", item->metadata.bitdepth()); + query_options->AddWhere("bitdepth", item->metadata.bitdepth()); break; case GroupBy_Bitrate: - q->AddWhere("bitrate", item->metadata.bitrate()); + query_options->AddWhere("bitrate", item->metadata.bitrate()); break; case GroupBy_None: case GroupByCount: @@ -1851,21 +1858,19 @@ SongList CollectionModel::GetChildSongs(const QModelIndex &idx) const { return GetChildSongs(QModelIndexList() << idx); } -void CollectionModel::SetFilterAge(const int age) { - query_options_.set_max_age(age); +void CollectionModel::SetFilterMode(CollectionFilterOptions::FilterMode filter_mode) { + filter_options_.set_filter_mode(filter_mode); ResetAsync(); } -void CollectionModel::SetFilterText(const QString &text) { - query_options_.set_filter(text); +void CollectionModel::SetFilterAge(const int filter_age) { + filter_options_.set_max_age(filter_age); ResetAsync(); - } -void CollectionModel::SetFilterQueryMode(QueryOptions::QueryMode query_mode) { - query_options_.set_query_mode(query_mode); +void CollectionModel::SetFilterText(const QString &filter_text) { + filter_options_.set_filter_text(filter_text); ResetAsync(); - } bool CollectionModel::canFetchMore(const QModelIndex &parent) const { diff --git a/src/collection/collectionmodel.h b/src/collection/collectionmodel.h index 2d38a579..9f8097fd 100644 --- a/src/collection/collectionmodel.h +++ b/src/collection/collectionmodel.h @@ -49,7 +49,9 @@ #include "core/song.h" #include "core/sqlrow.h" #include "covermanager/albumcoverloader.h" +#include "collectionfilteroptions.h" #include "collectionquery.h" +#include "collectionqueryoptions.h" #include "collectionitem.h" #include "covermanager/albumcoverloaderoptions.h" @@ -203,9 +205,9 @@ class CollectionModel : public SimpleTreeModel { void GroupingChanged(CollectionModel::Grouping g, bool separate_albums_by_grouping); public slots: - void SetFilterAge(const int age); - void SetFilterText(const QString &text); - void SetFilterQueryMode(QueryOptions::QueryMode query_mode); + void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode); + void SetFilterAge(const int filter_age); + void SetFilterText(const QString &filter_text); void Init(const bool async = true); void Reset(); @@ -232,20 +234,21 @@ class CollectionModel : public SimpleTreeModel { void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result); private: - // Provides some optimisations for loading the list of items in the root. + // 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. - QueryResult RunQuery(CollectionItem *parent); + 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 CollectionQuery &query); + bool HasCompilations(const QSqlDatabase &db, const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options); void BeginReset(); // 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 InitQuery(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQuery *q); - static void FilterQuery(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQuery *q); + 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); // 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); @@ -279,7 +282,7 @@ class CollectionModel : public SimpleTreeModel { int total_artist_count_; int total_album_count_; - QueryOptions query_options_; + CollectionFilterOptions filter_options_; Grouping group_by_; bool separate_albums_by_grouping_; diff --git a/src/collection/collectionquery.cpp b/src/collection/collectionquery.cpp index 15d58907..5af8f15f 100644 --- a/src/collection/collectionquery.cpp +++ b/src/collection/collectionquery.cpp @@ -31,16 +31,16 @@ #include #include #include -#include #include "core/logging.h" +#include "core/sqlquery.h" #include "core/song.h" #include "collectionquery.h" +#include "collectionfilteroptions.h" +#include "collectionqueryoptions.h" -QueryOptions::QueryOptions() : max_age_(-1), query_mode_(QueryMode_All) {} - -CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const QueryOptions &options) +CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options) : QSqlQuery(db), songs_table_(songs_table), fts_table_(fts_table), @@ -49,7 +49,7 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta duplicates_only_(false), limit_(-1) { - if (!options.filter().isEmpty()) { + 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. @@ -57,9 +57,9 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta // Split on whitespace #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QStringList tokens(options.filter().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts)); + QStringList tokens(filter_options.filter_text().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts)); #else - QStringList tokens(options.filter().split(QRegularExpression("\\s+"), QString::SkipEmptyParts)); + QStringList tokens(filter_options.filter_text().split(QRegularExpression("\\s+"), QString::SkipEmptyParts)); #endif QString query; for (QString token : tokens) { @@ -100,49 +100,40 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta } } - if (options.max_age() != -1) { - qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - options.max_age(); + if (filter_options.max_age() != -1) { + qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - filter_options.max_age(); where_clauses_ << "ctime > ?"; bound_values_ << cutoff; } - // TODO: Currently you cannot use any QueryMode other than All and FTS at the same time. + // 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_ = options.query_mode() == QueryOptions::QueryMode_Duplicates; + duplicates_only_ = filter_options.filter_mode() == CollectionFilterOptions::FilterMode_Duplicates; - if (options.query_mode() == QueryOptions::QueryMode_Untagged) { + if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode_Untagged) { where_clauses_ << "(artist = '' OR album = '' OR title ='')"; } } -QString CollectionQuery::GetInnerQuery() const { - return duplicates_only_ - ? QString(" INNER JOIN (select * from duplicated_songs) dsongs " - "ON (%songs_table.artist = dsongs.dup_artist " - "AND %songs_table.album = dsongs.dup_album " - "AND %songs_table.title = dsongs.dup_title) ") - : QString(); -} - void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) { // Ignore 'literal' for IN if (op.compare("IN", Qt::CaseInsensitive) == 0) { QStringList values = value.toStringList(); - QStringList final; - final.reserve(values.count()); + QStringList final_values; + final_values.reserve(values.count()); for (const QString &single_value : values) { - final.append("?"); + final_values.append("?"); bound_values_ << single_value; } - where_clauses_ << QString("%1 IN (" + final.join(",") + ")").arg(column); + where_clauses_ << QString("%1 IN (" + final_values.join(",") + ")").arg(column); } else { // Do integers inline - sqlite seems to get confused when you pass integers to bound parameters @@ -187,6 +178,15 @@ void CollectionQuery::AddCompilationRequirement(const bool compilation) { } +QString CollectionQuery::GetInnerQuery() const { + return duplicates_only_ + ? QString(" INNER JOIN (select * from duplicated_songs) dsongs " + "ON (%songs_table.artist = dsongs.dup_artist " + "AND %songs_table.album = dsongs.dup_album " + "AND %songs_table.title = dsongs.dup_title) ") + : QString(); +} + bool CollectionQuery::Exec() { QString sql; @@ -213,32 +213,17 @@ bool CollectionQuery::Exec() { sql.replace("%fts_table_noprefix", fts_table_.section('.', -1, -1)); sql.replace("%fts_table", fts_table_); - prepare(sql); + QSqlQuery::prepare(sql); // Bind values for (const QVariant &value : bound_values_) { - addBindValue(value); + QSqlQuery::addBindValue(value); } - return exec(); + return QSqlQuery::exec(); } -bool CollectionQuery::Next() { return next(); } +bool CollectionQuery::Next() { return QSqlQuery::next(); } -QVariant CollectionQuery::Value(const int column) const { return value(column); } - -bool QueryOptions::Matches(const Song &song) const { - - if (max_age_ != -1) { - const qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - max_age_; - if (song.ctime() <= cutoff) return false; - } - - if (!filter_.isNull()) { - return song.artist().contains(filter_, Qt::CaseInsensitive) || song.album().contains(filter_, Qt::CaseInsensitive) || song.title().contains(filter_, Qt::CaseInsensitive); - } - - return true; - -} +QVariant CollectionQuery::Value(const int column) const { return QSqlQuery::value(column); } diff --git a/src/collection/collectionquery.h b/src/collection/collectionquery.h index 20c4c198..7a731915 100644 --- a/src/collection/collectionquery.h +++ b/src/collection/collectionquery.h @@ -28,75 +28,23 @@ #include #include #include -#include #include #include -class Song; - -// This structure let's you customize behaviour of any CollectionQuery. -struct QueryOptions { - // Modes of CollectionQuery: - // - use the all songs table - // - use the duplicated songs view; by duplicated we mean those songs for which the (artist, album, title) tuple is found more than once in the songs table - // - use the untagged songs view; by untagged we mean those for which at least one of the (artist, album, title) tags is empty - // Please note that additional filtering based on FTS table (the filter attribute) won't work in Duplicates and Untagged modes. - enum QueryMode { - QueryMode_All, - QueryMode_Duplicates, - QueryMode_Untagged - }; - - QueryOptions(); - - bool Matches(const Song &song) const; - - QString filter() const { return filter_; } - void set_filter(const QString &filter) { - filter_ = filter; - query_mode_ = QueryMode_All; - } - - int max_age() const { return max_age_; } - void set_max_age(int max_age) { max_age_ = max_age; } - - QueryMode query_mode() const { return query_mode_; } - void set_query_mode(QueryMode query_mode) { - query_mode_ = query_mode; - filter_ = QString(); - } - - private: - QString filter_; - int max_age_; - QueryMode query_mode_; -}; +#include "collectionfilteroptions.h" +#include "collectionqueryoptions.h" class CollectionQuery : public QSqlQuery { public: - explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const QueryOptions &options = QueryOptions()); + explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions()); - // Sets contents of SELECT clause on the query (list of columns to get). - void SetColumnSpec(const QString &spec) { column_spec_ = spec; } - - // Sets an ORDER BY clause on the query. - void SetOrderBy(const QString &order_by) { order_by_ = order_by; } - - // 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 = "="); - void AddWhereArtist(const QVariant &value); - - void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; } - void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; } - void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; } - void SetIncludeUnavailable(const bool include_unavailable) { include_unavailable_ = include_unavailable; } - void SetLimit(const int limit) { limit_ = limit; } - void AddCompilationRequirement(const bool compilation); + QVariant Value(const int column) const; + QVariant value(const int column) const { return Value(column); } bool Exec(); + bool exec() { return QSqlQuery::exec(); } + bool Next(); - QVariant Value(const int column) const; QString column_spec() const { return column_spec_; } QString order_by() const { return order_by_; } @@ -107,6 +55,24 @@ class CollectionQuery : public QSqlQuery { bool duplicates_only() const { return duplicates_only_; } int limit() const { return limit_; } + // Sets contents of SELECT clause on the query (list of columns to get). + void SetColumnSpec(const QString &column_spec) { column_spec_ = column_spec; } + + // Sets an ORDER BY clause on the query. + void SetOrderBy(const QString &order_by) { order_by_ = order_by; } + + void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; } + // 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 = "="); + void AddWhereArtist(const QVariant &value); + + void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; } + void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; } + void SetIncludeUnavailable(const bool include_unavailable) { include_unavailable_ = include_unavailable; } + void SetLimit(const int limit) { limit_ = limit; } + void AddCompilationRequirement(const bool compilation); + private: QString GetInnerQuery() const; diff --git a/src/collection/collectionqueryoptions.cpp b/src/collection/collectionqueryoptions.cpp new file mode 100644 index 00000000..ea34b501 --- /dev/null +++ b/src/collection/collectionqueryoptions.cpp @@ -0,0 +1,34 @@ +/* + * 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 +#include + +#include "collectionqueryoptions.h" +#include "collectionfilteroptions.h" + +CollectionQueryOptions::CollectionQueryOptions() + : compilation_requirement_(false), + query_have_compilations_(false) {} + +void CollectionQueryOptions::AddWhere(const QString &column, const QVariant &value, const QString &op) { + + where_clauses_ << Where(column, value, op); + +} diff --git a/src/collection/collectionqueryoptions.h b/src/collection/collectionqueryoptions.h new file mode 100644 index 00000000..9e6c0be4 --- /dev/null +++ b/src/collection/collectionqueryoptions.h @@ -0,0 +1,57 @@ +/* + * 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 COLLECTIONQUERYOPTIONS_H +#define COLLECTIONQUERYOPTIONS_H + +#include +#include +#include + +class CollectionQueryOptions { + public: + + explicit CollectionQueryOptions(); + + struct Where { + explicit Where(const QString _column = QString(), const QVariant _value = QString(), const QString _op = QString()) : column(_column), value(_value), op(_op) {} + QString column; + QVariant value; + QString op; + }; + + QString column_spec() const { return column_spec_; } + bool compilation_requirement() const { return compilation_requirement_; } + bool query_have_compilations() const { return query_have_compilations_; } + + void set_column_spec(const QString &column_spec) { column_spec_ = column_spec; } + void set_compilation_requirement(const bool compilation_requirement) { compilation_requirement_ = compilation_requirement; } + void set_query_have_compilations(const bool query_have_compilations) { query_have_compilations_ = query_have_compilations; } + + QList where_clauses() const { return where_clauses_; } + void AddWhere(const QString &column, const QVariant &value, const QString &op = "="); + + private: + QString column_spec_; + bool compilation_requirement_; + bool query_have_compilations_; + QList where_clauses_; +}; + +#endif // COLLECTIONQUERYOPTIONS_H diff --git a/src/collection/collectionwatcher.cpp b/src/collection/collectionwatcher.cpp index bbf76e38..ae55cf26 100644 --- a/src/collection/collectionwatcher.cpp +++ b/src/collection/collectionwatcher.cpp @@ -50,7 +50,7 @@ #include "core/taskmanager.h" #include "utilities/imageutils.h" #include "utilities/timeconstants.h" -#include "directory.h" +#include "collectiondirectory.h" #include "collectionbackend.h" #include "collectionwatcher.h" #include "playlistparsers/cueparser.h" @@ -167,9 +167,9 @@ void CollectionWatcher::ReloadSettings() { } else if (monitor_ && !was_monitoring_before) { // Add all directories to all QFileSystemWatchers again - for (const Directory &dir : std::as_const(watched_dirs_)) { - SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id); - for (const Subdirectory &subdir : subdirs) { + for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) { + CollectionSubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id); + for (const CollectionSubdirectory &subdir : subdirs) { AddWatch(dir, subdir.path); } } @@ -272,7 +272,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() { touched_subdirs.clear(); } - for (const Subdirectory &subdir : deleted_subdirs) { + for (const CollectionSubdirectory &subdir : deleted_subdirs) { if (watcher_->watched_dirs_.contains(dir_)) { watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir); } @@ -281,7 +281,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() { if (watcher_->monitor_) { // Watch the new subdirectories - for (const Subdirectory &subdir : new_subdirs) { + for (const CollectionSubdirectory &subdir : new_subdirs) { if (watcher_->watched_dirs_.contains(dir_)) { watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path); } @@ -329,7 +329,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS } -void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const SubdirectoryList &subdirs) { +void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdirectoryList &subdirs) { known_subdirs_ = subdirs; known_subdirs_dirty_ = false; @@ -342,18 +342,18 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) { SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_)); } - return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const Subdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; }); + return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; }); } -SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) { +CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) { if (known_subdirs_dirty_) { SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_)); } - SubdirectoryList ret; - for (const Subdirectory &subdir : known_subdirs_) { + CollectionSubdirectoryList ret; + for (const CollectionSubdirectory &subdir : known_subdirs_) { if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path && subdir.mtime != 0) { ret << subdir; } @@ -363,7 +363,7 @@ SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const Q } -SubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() { +CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() { if (known_subdirs_dirty_) { SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_)); @@ -373,7 +373,7 @@ SubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() { } -void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryList &subdirs) { +void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdirs) { stop_requested_ = false; @@ -385,7 +385,7 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis const quint64 files_count = FilesCountForPath(&transaction, dir.path); transaction.SetKnownSubdirs(subdirs); transaction.AddToProgressMax(files_count); - ScanSubdirectory(dir.path, Subdirectory(), files_count, &transaction); + ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction); last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch(); } else { @@ -395,7 +395,7 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis const quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count); transaction.SetKnownSubdirs(subdirs); transaction.AddToProgressMax(files_count); - for (const Subdirectory &subdir : subdirs) { + for (const CollectionSubdirectory &subdir : subdirs) { if (stop_requested_ || abort_requested_) break; if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction); @@ -411,14 +411,14 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis } -void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) { +void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) { QFileInfo path_info(path); // Do not scan symlinked dirs that are already in collection if (path_info.isSymLink()) { QString real_path = path_info.symLinkTarget(); - for (const Directory &dir : std::as_const(watched_dirs_)) { + for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) { if (real_path.startsWith(dir.path)) { return; } @@ -440,12 +440,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory QMap album_art; QStringList files_on_disk; - SubdirectoryList my_new_subdirs; + CollectionSubdirectoryList my_new_subdirs; // If a directory is moved then only its parent gets a changed notification, so we need to look and see if any of our children don't exist anymore. // If one has been removed, "rescan" it to get the deleted songs - SubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path); - for (const Subdirectory &prev_subdir : previous_subdirs) { + CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path); + for (const CollectionSubdirectory &prev_subdir : previous_subdirs) { if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) { ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true); } @@ -463,7 +463,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory if (child_info.isDir()) { if (!t->HasSeenSubdir(child)) { // We haven't seen this subdirectory before - add it to a list, and later we'll tell the backend about it and scan it. - Subdirectory new_subdir; + CollectionSubdirectory new_subdir; new_subdir.directory_id = -1; new_subdir.path = child; new_subdir.mtime = child_info.lastModified().toSecsSinceEpoch(); @@ -676,7 +676,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory } // Add this subdir to the new or touched list - Subdirectory updated_subdir; + CollectionSubdirectory updated_subdir; updated_subdir.directory_id = t->dir(); updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0; updated_subdir.path = path; @@ -688,12 +688,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory t->touched_subdirs << updated_subdir; } - if (updated_subdir.mtime == 0) { // Subdirectory deleted, mark it for removal from the watcher. + if (updated_subdir.mtime == 0) { // CollectionSubdirectory deleted, mark it for removal from the watcher. t->deleted_subdirs << updated_subdir; } // Recurse into the new subdirs that we found - for (const Subdirectory &my_new_subdir : my_new_subdirs) { + for (const CollectionSubdirectory &my_new_subdir : my_new_subdirs) { if (stop_requested_ || abort_requested_) return; ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true); } @@ -897,7 +897,7 @@ quint64 CollectionWatcher::GetMtimeForCue(const QString &cue_path) { return cue_last_modified.isValid() ? cue_last_modified.toSecsSinceEpoch() : 0; } -void CollectionWatcher::AddWatch(const Directory &dir, const QString &path) { +void CollectionWatcher::AddWatch(const CollectionDirectory &dir, const QString &path) { if (!QFile::exists(path)) return; @@ -907,7 +907,7 @@ void CollectionWatcher::AddWatch(const Directory &dir, const QString &path) { } -void CollectionWatcher::RemoveWatch(const Directory &dir, const Subdirectory &subdir) { +void CollectionWatcher::RemoveWatch(const CollectionDirectory &dir, const CollectionSubdirectory &subdir) { QStringList subdir_paths = subdir_mapping_.keys(dir); for (const QString &subdir_path : subdir_paths) { @@ -919,7 +919,7 @@ void CollectionWatcher::RemoveWatch(const Directory &dir, const Subdirectory &su } -void CollectionWatcher::RemoveDirectory(const Directory &dir) { +void CollectionWatcher::RemoveDirectory(const CollectionDirectory &dir) { rescan_queue_.remove(dir.id); watched_dirs_.remove(dir.id); @@ -979,11 +979,11 @@ bool CollectionWatcher::FindSongsByFingerprint(const QString &file, const SongLi void CollectionWatcher::DirectoryChanged(const QString &subdir) { // Find what dir it was in - QHash::const_iterator it = subdir_mapping_.constFind(subdir); + QHash::const_iterator it = subdir_mapping_.constFind(subdir); if (it == subdir_mapping_.constEnd()) { return; } - Directory dir = *it; + CollectionDirectory dir = *it; qLog(Debug) << "Subdir" << subdir << "changed under directory" << dir.path << "id" << dir.id; @@ -1010,7 +1010,7 @@ void CollectionWatcher::RescanPathsNow() { for (const QString &path : rescan_queue_[dir]) { if (stop_requested_ || abort_requested_) break; - Subdirectory subdir; + CollectionSubdirectory subdir; subdir.directory_id = dir; subdir.mtime = 0; subdir.path = path; @@ -1154,7 +1154,7 @@ void CollectionWatcher::RescanTracksNow() { qLog(Debug) << "Song" << song.title() << "dir id" << song.directory_id() << "dir" << songdir; ScanTransaction transaction(this, song.directory_id(), false, false, mark_songs_unavailable_); quint64 files_count = FilesCountForPath(&transaction, songdir); - ScanSubdirectory(songdir, Subdirectory(), files_count, &transaction); + ScanSubdirectory(songdir, CollectionSubdirectory(), files_count, &transaction); scanned_dirs << songdir; emit CompilationsNeedUpdating(); } @@ -1171,16 +1171,16 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt stop_requested_ = false; - for (const Directory &dir : std::as_const(watched_dirs_)) { + for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) { if (stop_requested_ || abort_requested_) break; ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_); - SubdirectoryList subdirs(transaction.GetAllSubdirs()); + CollectionSubdirectoryList subdirs(transaction.GetAllSubdirs()); if (subdirs.isEmpty()) { qLog(Debug) << "Collection directory wasn't in subdir list."; - Subdirectory subdir; + CollectionSubdirectory subdir; subdir.path = dir.path; subdir.directory_id = dir.id; subdirs << subdir; @@ -1190,7 +1190,7 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count); transaction.AddToProgressMax(files_count); - for (const Subdirectory &subdir : subdirs) { + for (const CollectionSubdirectory &subdir : subdirs) { if (stop_requested_ || abort_requested_) break; ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction); } @@ -1217,7 +1217,7 @@ quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString & if (path_info.isDir()) { if (path_info.isSymLink()) { QString real_path = path_info.symLinkTarget(); - for (const Directory &dir : std::as_const(watched_dirs_)) { + for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) { if (real_path.startsWith(dir.path)) { continue; } @@ -1239,10 +1239,10 @@ quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString & } -quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const SubdirectoryList &subdirs, QMap &subdir_files_count) { +quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const CollectionSubdirectoryList &subdirs, QMap &subdir_files_count) { quint64 i = 0; - for (const Subdirectory &subdir : subdirs) { + for (const CollectionSubdirectory &subdir : subdirs) { if (stop_requested_ || abort_requested_) break; const quint64 files_count = FilesCountForPath(t, subdir.path); subdir_files_count[subdir.path] = files_count; diff --git a/src/collection/collectionwatcher.h b/src/collection/collectionwatcher.h index 8caae490..008ee76e 100644 --- a/src/collection/collectionwatcher.h +++ b/src/collection/collectionwatcher.h @@ -34,7 +34,7 @@ #include #include -#include "directory.h" +#include "collectiondirectory.h" #include "core/song.h" class QThread; @@ -74,8 +74,8 @@ class CollectionWatcher : public QObject { void SongsDeleted(SongList); void SongsUnavailable(SongList songs, bool unavailable = true); void SongsReadded(SongList songs, bool unavailable = false); - void SubdirsDiscovered(SubdirectoryList subdirs); - void SubdirsMTimeUpdated(SubdirectoryList subdirs); + void SubdirsDiscovered(CollectionSubdirectoryList subdirs); + void SubdirsMTimeUpdated(CollectionSubdirectoryList subdirs); void CompilationsNeedUpdating(); void UpdateLastSeen(int directory_id, int expire_unavailable_songs_days); void ExitFinished(); @@ -83,8 +83,8 @@ class CollectionWatcher : public QObject { void ScanStarted(int task_id); public slots: - void AddDirectory(const Directory &dir, const SubdirectoryList &subdirs); - void RemoveDirectory(const Directory &dir); + void AddDirectory(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdirs); + void RemoveDirectory(const CollectionDirectory &dir); void SetRescanPaused(bool pause); private: @@ -102,9 +102,9 @@ class CollectionWatcher : public QObject { SongList FindSongsInSubdirectory(const QString &path); bool HasSongsWithMissingFingerprint(const QString &path); bool HasSeenSubdir(const QString &path); - void SetKnownSubdirs(const SubdirectoryList &subdirs); - SubdirectoryList GetImmediateSubdirs(const QString &path); - SubdirectoryList GetAllSubdirs(); + void SetKnownSubdirs(const CollectionSubdirectoryList &subdirs); + CollectionSubdirectoryList GetImmediateSubdirs(const QString &path); + CollectionSubdirectoryList GetAllSubdirs(); void AddToProgress(const quint64 n = 1); void AddToProgressMax(const quint64 n); @@ -120,9 +120,9 @@ class CollectionWatcher : public QObject { SongList readded_songs; SongList new_songs; SongList touched_songs; - SubdirectoryList new_subdirs; - SubdirectoryList touched_subdirs; - SubdirectoryList deleted_subdirs; + CollectionSubdirectoryList new_subdirs; + CollectionSubdirectoryList touched_subdirs; + CollectionSubdirectoryList deleted_subdirs; QStringList files_changed_path_; @@ -155,7 +155,7 @@ class CollectionWatcher : public QObject { QMultiMap cached_songs_missing_fingerprint_; bool cached_songs_missing_fingerprint_dirty_; - SubdirectoryList known_subdirs_; + CollectionSubdirectoryList known_subdirs_; bool known_subdirs_dirty_; }; @@ -168,7 +168,7 @@ class CollectionWatcher : public QObject { void FullScanNow(); void RescanTracksNow(); void RescanPathsNow(); - void ScanSubdirectory(const QString &path, const Subdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false); + void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false); private: static bool FindSongsByPath(const SongList &songs, const QString &path, SongList *out); @@ -179,8 +179,8 @@ class CollectionWatcher : public QObject { inline static QString DirectoryPart(const QString &fileName); QString PickBestImage(const QStringList &images); QUrl ImageForSong(const QString &path, QMap &album_art); - void AddWatch(const Directory &dir, const QString &path); - void RemoveWatch(const Directory &dir, const Subdirectory &subdir); + void AddWatch(const CollectionDirectory &dir, const QString &path); + void RemoveWatch(const CollectionDirectory &dir, const CollectionSubdirectory &subdir); static quint64 GetMtimeForCue(const QString &cue_path); void PerformScan(const bool incremental, const bool ignore_mtimes); @@ -195,7 +195,7 @@ class CollectionWatcher : public QObject { static void AddChangedSong(const QString &file, const Song &matching_song, const Song &new_song, ScanTransaction *t); quint64 FilesCountForPath(ScanTransaction *t, const QString &path); - quint64 FilesCountForSubdirs(ScanTransaction *t, const SubdirectoryList &subdirs, QMap &subdir_files_count); + quint64 FilesCountForSubdirs(ScanTransaction *t, const CollectionSubdirectoryList &subdirs, QMap &subdir_files_count); QString FindCueFilename(const QString &filename); @@ -207,7 +207,7 @@ class CollectionWatcher : public QObject { FileSystemWatcherInterface *fs_watcher_; QThread *original_thread_; - QHash subdir_mapping_; + QHash subdir_mapping_; // A list of words use to try to identify the (likely) best image found in an directory to use as cover artwork. // e.g. using ["front", "cover"] would identify front.jpg and exclude back.jpg. @@ -225,7 +225,7 @@ class CollectionWatcher : public QObject { bool abort_requested_; bool rescan_in_progress_; // True if RescanTracksNow() has been called and is working. - QMap watched_dirs_; + QMap watched_dirs_; QTimer *rescan_timer_; QTimer *periodic_scan_timer_; QMap rescan_queue_; // dir id -> list of subdirs to be scanned diff --git a/src/collection/directory.h b/src/collection/directory.h index 7f3f8498..e69de29b 100644 --- a/src/collection/directory.h +++ b/src/collection/directory.h @@ -1,60 +0,0 @@ -/* - * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2010, David Sansome - * - * 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 DIRECTORY_H -#define DIRECTORY_H - -#include "config.h" - -#include -#include -#include -#include - -struct Directory { - Directory() : id(-1) {} - - bool operator==(const Directory &other) const { - return path == other.path && id == other.id; - } - - QString path; - int id; -}; -Q_DECLARE_METATYPE(Directory) - -using DirectoryList = QList; -Q_DECLARE_METATYPE(DirectoryList) - - -struct Subdirectory { - Subdirectory() : directory_id(-1), mtime(0) {} - - int directory_id; - QString path; - qint64 mtime; -}; -Q_DECLARE_METATYPE(Subdirectory) - -using SubdirectoryList = QList; -Q_DECLARE_METATYPE(SubdirectoryList) - -#endif // DIRECTORY_H - diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index a38b76b6..2443bde0 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -675,7 +675,7 @@ MainWindow::MainWindow(Application *app, std::shared_ptr tray_ic collection_show_untagged_->setCheckable(true); collection_show_all_->setChecked(true); - QObject::connect(collection_view_group, &QActionGroup::triggered, this, &MainWindow::ChangeCollectionQueryMode); + QObject::connect(collection_view_group, &QActionGroup::triggered, this, &MainWindow::ChangeCollectionFilterMode); QAction *collection_config_action = new QAction(IconLoader::Load("configure"), tr("Configure collection..."), this); QObject::connect(collection_config_action, &QAction::triggered, this, &MainWindow::ShowCollectionConfig); @@ -2752,16 +2752,16 @@ void MainWindow::PlaylistCopyToDevice() { } -void MainWindow::ChangeCollectionQueryMode(QAction *action) { +void MainWindow::ChangeCollectionFilterMode(QAction *action) { if (action == collection_show_duplicates_) { - collection_view_->filter_widget()->SetQueryMode(QueryOptions::QueryMode_Duplicates); + collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode_Duplicates); } else if (action == collection_show_untagged_) { - collection_view_->filter_widget()->SetQueryMode(QueryOptions::QueryMode_Untagged); + collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode_Untagged); } else { - collection_view_->filter_widget()->SetQueryMode(QueryOptions::QueryMode_All); + collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode_All); } } diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index ba7e9264..7451cff9 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -180,7 +180,7 @@ class MainWindow : public QMainWindow, public PlatformInterface { void PlaylistCopyUrl(); void ShowInCollection(); - void ChangeCollectionQueryMode(QAction *action); + void ChangeCollectionFilterMode(QAction *action); void PlayIndex(const QModelIndex &idx, Playlist::AutoScroll autoscroll); void PlaylistDoubleClick(const QModelIndex &idx); diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp index 02d1aa66..a7043086 100644 --- a/src/core/metatypes.cpp +++ b/src/core/metatypes.cpp @@ -54,7 +54,7 @@ #ifdef HAVE_GSTREAMER # include "engine/gstenginepipeline.h" #endif -#include "collection/directory.h" +#include "collection/collectiondirectory.h" #include "playlist/playlistitem.h" #include "playlist/playlistsequence.h" #include "covermanager/albumcoverloaderresult.h" @@ -98,10 +98,10 @@ void RegisterMetaTypes() { qRegisterMetaTypeStreamOperators>("ColumnAlignmentMap"); qRegisterMetaTypeStreamOperators>("ColumnAlignmentIntMap"); #endif - qRegisterMetaType("Directory"); - qRegisterMetaType("DirectoryList"); - qRegisterMetaType("Subdirectory"); - qRegisterMetaType("SubdirectoryList"); + qRegisterMetaType("Directory"); + qRegisterMetaType("DirectoryList"); + qRegisterMetaType("Subdirectory"); + qRegisterMetaType("SubdirectoryList"); qRegisterMetaType("Song"); qRegisterMetaType("SongList"); qRegisterMetaType("SongMap"); diff --git a/src/covermanager/albumcoverchoicecontroller.cpp b/src/covermanager/albumcoverchoicecontroller.cpp index 8c17bde6..5ad00ab1 100644 --- a/src/covermanager/albumcoverchoicecontroller.cpp +++ b/src/covermanager/albumcoverchoicecontroller.cpp @@ -60,6 +60,7 @@ #include "core/song.h" #include "core/iconloader.h" +#include "collection/collectionfilteroptions.h" #include "collection/collectionbackend.h" #include "settings/collectionsettingspage.h" #include "organize/organizeformat.h" @@ -654,9 +655,9 @@ void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, co if (song.source() == Song::Source_Collection) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QFuture future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions()); + QFuture future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), CollectionFilterOptions()); #else - QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), QueryOptions()); + QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), CollectionFilterOptions()); #endif QFutureWatcher *watcher = new QFutureWatcher(); QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, song, result]() { @@ -699,9 +700,9 @@ void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, co if (song.source() == Song::Source_Collection) { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QFuture future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions()); + QFuture future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), CollectionFilterOptions()); #else - QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), QueryOptions()); + QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), CollectionFilterOptions()); #endif QFutureWatcher *watcher = new QFutureWatcher(); QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, song, cover_filename]() { diff --git a/src/device/connecteddevice.cpp b/src/device/connecteddevice.cpp index 63029a1e..9e4eb971 100644 --- a/src/device/connecteddevice.cpp +++ b/src/device/connecteddevice.cpp @@ -29,7 +29,7 @@ #include "core/database.h" #include "collection/collectionbackend.h" #include "collection/collectionmodel.h" -#include "collection/directory.h" +#include "collection/collectiondirectory.h" #include "connecteddevice.h" #include "devicelister.h" #include "devicemanager.h" @@ -78,7 +78,7 @@ ConnectedDevice::~ConnectedDevice() { void ConnectedDevice::InitBackendDirectory(const QString &mount_point, const bool first_time, const bool rewrite_path) { - QList directories = backend_->GetAllDirectories(); + QList directories = backend_->GetAllDirectories(); if (first_time || directories.isEmpty()) { backend_->AddDirectory(mount_point); } @@ -90,7 +90,7 @@ void ConnectedDevice::InitBackendDirectory(const QString &mount_point, const boo // This can be done entirely in sqlite so it's relatively fast... // Get the directory it was mounted at last time. Devices only have one directory (the root). - Directory dir = directories[0]; + CollectionDirectory dir = directories[0]; if (dir.path != mount_point) { // The directory is different, commence the munging. qLog(Info) << "Changing path from" << dir.path << "to" << mount_point; diff --git a/tests/src/collectionbackend_test.cpp b/tests/src/collectionbackend_test.cpp index 1e5a5381..36bcbad8 100644 --- a/tests/src/collectionbackend_test.cpp +++ b/tests/src/collectionbackend_test.cpp @@ -83,7 +83,7 @@ TEST_F(CollectionBackendTest, AddDirectory) { // Check the signal was emitted correctly ASSERT_EQ(1, spy.count()); - Directory dir = spy[0][0].value(); + CollectionDirectory dir = spy[0][0].value(); EXPECT_EQ(QFileInfo("/tmp").canonicalFilePath(), dir.path); EXPECT_EQ(1, dir.id); EXPECT_EQ(0, spy[0][1].value().size()); @@ -93,7 +93,7 @@ TEST_F(CollectionBackendTest, AddDirectory) { TEST_F(CollectionBackendTest, RemoveDirectory) { // Add a directory - Directory dir; + CollectionDirectory dir; dir.id = 1; dir.path = "/tmp"; backend_->AddDirectory(dir.path); diff --git a/tests/src/metatypes_env.h b/tests/src/metatypes_env.h index ef6d6115..fd210e61 100644 --- a/tests/src/metatypes_env.h +++ b/tests/src/metatypes_env.h @@ -28,16 +28,16 @@ #include "core/song.h" #include "core/songloader.h" -#include "collection/directory.h" +#include "collection/collectiondirectory.h" class MetatypesEnvironment : public ::testing::Environment { public: MetatypesEnvironment() = default; void SetUp() override { - qRegisterMetaType("Directory"); - qRegisterMetaType("DirectoryList"); - qRegisterMetaType("Subdirectory"); - qRegisterMetaType("SubdirectoryList"); + qRegisterMetaType("Directory"); + qRegisterMetaType("DirectoryList"); + qRegisterMetaType("Subdirectory"); + qRegisterMetaType("SubdirectoryList"); qRegisterMetaType("SongList"); qRegisterMetaType("QModelIndex"); qRegisterMetaType("SongLoader::Result");