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
This commit is contained in:
Jonas Kvinge 2023-01-08 15:40:54 +01:00
parent 41f2710dea
commit b5fa401db9
26 changed files with 620 additions and 459 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,57 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONDIRECTORY_H
#define COLLECTIONDIRECTORY_H
#include "config.h"
#include <QMetaType>
#include <QList>
#include <QString>
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<CollectionDirectory>;
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<CollectionSubdirectory>;
Q_DECLARE_METATYPE(CollectionSubdirectoryList)
#endif // COLLECTIONDIRECTORY_H

View File

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

View File

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

View File

@ -0,0 +1,42 @@
/*
* Strawberry Music Player
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QtGlobal>
#include <QDateTime>
#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;
}

View File

@ -0,0 +1,65 @@
/*
* Strawberry Music Player
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONFILTEROPTIONS_H
#define COLLECTIONFILTEROPTIONS_H
#include <QString>
#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

View File

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

View File

@ -32,6 +32,7 @@
#include <QString>
#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:

View File

@ -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<CollectionModel::QueryResult> future = QtConcurrent::run(&CollectionModel::RunQuery, this, root_);
QFuture<CollectionModel::QueryResult> future = QtConcurrent::run(&CollectionModel::RunQuery, this, filter_options_, query_options);
#else
QFuture<CollectionModel::QueryResult> future = QtConcurrent::run(this, &CollectionModel::RunQuery, root_);
QFuture<CollectionModel::QueryResult> future = QtConcurrent::run(this, &CollectionModel::RunQuery, filter_options_, query_options);
#endif
QFutureWatcher<CollectionModel::QueryResult> *watcher = new QFutureWatcher<CollectionModel::QueryResult>();
QObject::connect(watcher, &QFutureWatcher<CollectionModel::QueryResult>::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 {

View File

@ -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<CollectionItem> {
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<CollectionItem> {
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<CollectionItem> {
int total_artist_count_;
int total_album_count_;
QueryOptions query_options_;
CollectionFilterOptions filter_options_;
Grouping group_by_;
bool separate_albums_by_grouping_;

View File

@ -31,16 +31,16 @@
#include <QRegularExpression>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#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); }

View File

@ -28,75 +28,23 @@
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QMap>
#include <QSqlDatabase>
#include <QSqlQuery>
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;

View File

@ -0,0 +1,34 @@
/*
* Strawberry Music Player
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QVariant>
#include <QString>
#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);
}

View File

@ -0,0 +1,57 @@
/*
* Strawberry Music Player
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONQUERYOPTIONS_H
#define COLLECTIONQUERYOPTIONS_H
#include <QList>
#include <QVariant>
#include <QString>
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> 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> where_clauses_;
};
#endif // COLLECTIONQUERYOPTIONS_H

View File

@ -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<QString, QStringList> 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<QString, Directory>::const_iterator it = subdir_mapping_.constFind(subdir);
QHash<QString, CollectionDirectory>::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<QString, quint64> &subdir_files_count) {
quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const CollectionSubdirectoryList &subdirs, QMap<QString, quint64> &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;

View File

@ -34,7 +34,7 @@
#include <QStringList>
#include <QUrl>
#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<QString, Song> 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<QString, QStringList> &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<QString, quint64> &subdir_files_count);
quint64 FilesCountForSubdirs(ScanTransaction *t, const CollectionSubdirectoryList &subdirs, QMap<QString, quint64> &subdir_files_count);
QString FindCueFilename(const QString &filename);
@ -207,7 +207,7 @@ class CollectionWatcher : public QObject {
FileSystemWatcherInterface *fs_watcher_;
QThread *original_thread_;
QHash<QString, Directory> subdir_mapping_;
QHash<QString, CollectionDirectory> 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<int, Directory> watched_dirs_;
QMap<int, CollectionDirectory> watched_dirs_;
QTimer *rescan_timer_;
QTimer *periodic_scan_timer_;
QMap<int, QStringList> rescan_queue_; // dir id -> list of subdirs to be scanned

View File

@ -1,60 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef DIRECTORY_H
#define DIRECTORY_H
#include "config.h"
#include <QMetaType>
#include <QList>
#include <QString>
#include <QSqlQuery>
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<Directory>;
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<Subdirectory>;
Q_DECLARE_METATYPE(SubdirectoryList)
#endif // DIRECTORY_H

View File

@ -675,7 +675,7 @@ MainWindow::MainWindow(Application *app, std::shared_ptr<SystemTrayIcon> 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);
}
}

View File

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

View File

@ -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<QMap<int, Qt::Alignment>>("ColumnAlignmentMap");
qRegisterMetaTypeStreamOperators<QMap<int, int>>("ColumnAlignmentIntMap");
#endif
qRegisterMetaType<Directory>("Directory");
qRegisterMetaType<DirectoryList>("DirectoryList");
qRegisterMetaType<Subdirectory>("Subdirectory");
qRegisterMetaType<SubdirectoryList>("SubdirectoryList");
qRegisterMetaType<CollectionDirectory>("Directory");
qRegisterMetaType<CollectionDirectoryList>("DirectoryList");
qRegisterMetaType<CollectionSubdirectory>("Subdirectory");
qRegisterMetaType<CollectionSubdirectoryList>("SubdirectoryList");
qRegisterMetaType<Song>("Song");
qRegisterMetaType<SongList>("SongList");
qRegisterMetaType<SongMap>("SongMap");

View File

@ -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<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions());
QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), CollectionFilterOptions());
#else
QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), QueryOptions());
QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), CollectionFilterOptions());
#endif
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
QObject::connect(watcher, &QFutureWatcher<SongList>::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<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions());
QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), CollectionFilterOptions());
#else
QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), QueryOptions());
QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), CollectionFilterOptions());
#endif
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [this, watcher, song, cover_filename]() {

View File

@ -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<Directory> directories = backend_->GetAllDirectories();
QList<CollectionDirectory> 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;

View File

@ -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<Directory>();
CollectionDirectory dir = spy[0][0].value<Directory>();
EXPECT_EQ(QFileInfo("/tmp").canonicalFilePath(), dir.path);
EXPECT_EQ(1, dir.id);
EXPECT_EQ(0, spy[0][1].value<SubdirectoryList>().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);

View File

@ -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>("Directory");
qRegisterMetaType<DirectoryList>("DirectoryList");
qRegisterMetaType<Subdirectory>("Subdirectory");
qRegisterMetaType<SubdirectoryList>("SubdirectoryList");
qRegisterMetaType<CollectionDirectory>("Directory");
qRegisterMetaType<CollectionDirectoryList>("DirectoryList");
qRegisterMetaType<CollectionSubdirectory>("Subdirectory");
qRegisterMetaType<CollectionSubdirectoryList>("SubdirectoryList");
qRegisterMetaType<SongList>("SongList");
qRegisterMetaType<QModelIndex>("QModelIndex");
qRegisterMetaType<SongLoader::Result>("SongLoader::Result");