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/collectionitemdelegate.cpp
collection/collectionviewcontainer.cpp collection/collectionviewcontainer.cpp
collection/collectiondirectorymodel.cpp collection/collectiondirectorymodel.cpp
collection/collectionfilteroptions.cpp
collection/collectionfilterwidget.cpp collection/collectionfilterwidget.cpp
collection/collectionplaylistitem.cpp collection/collectionplaylistitem.cpp
collection/collectionquery.cpp collection/collectionquery.cpp
collection/collectionqueryoptions.cpp
collection/savedgroupingmanager.cpp collection/savedgroupingmanager.cpp
collection/groupbydialog.cpp collection/groupbydialog.cpp
collection/collectiontask.cpp collection/collectiontask.cpp

View File

@ -50,8 +50,9 @@
#include "core/sqlrow.h" #include "core/sqlrow.h"
#include "smartplaylists/smartplaylistsearch.h" #include "smartplaylists/smartplaylistsearch.h"
#include "directory.h" #include "collectiondirectory.h"
#include "collectionbackend.h" #include "collectionbackend.h"
#include "collectionfilteroptions.h"
#include "collectionquery.h" #include "collectionquery.h"
#include "collectiontask.h" #include "collectiontask.h"
@ -145,12 +146,12 @@ void CollectionBackend::ResetStatisticsAsync(const int id) {
void CollectionBackend::LoadDirectories() { void CollectionBackend::LoadDirectories() {
DirectoryList dirs = GetAllDirectories(); CollectionDirectoryList dirs = GetAllDirectories();
QMutexLocker l(db_->Mutex()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); QSqlDatabase db(db_->Connect());
for (const Directory &dir : dirs) { for (const CollectionDirectory &dir : dirs) {
emit DirectoryDiscovered(dir, SubdirsInDirectory(dir.id, db)); 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()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); QSqlDatabase db(db_->Connect());
DirectoryList ret; CollectionDirectoryList ret;
SqlQuery q(db); SqlQuery q(db);
q.prepare(QString("SELECT ROWID, path FROM %1").arg(dirs_table_)); q.prepare(QString("SELECT ROWID, path FROM %1").arg(dirs_table_));
@ -222,7 +223,7 @@ DirectoryList CollectionBackend::GetAllDirectories() {
} }
while (q.next()) { while (q.next()) {
Directory dir; CollectionDirectory dir;
dir.id = q.value(0).toInt(); dir.id = q.value(0).toInt();
dir.path = q.value(1).toString(); 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()); QMutexLocker l(db_->Mutex());
QSqlDatabase db = db_->Connect(); 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); SqlQuery q(db);
q.prepare(QString("SELECT path, mtime FROM %1 WHERE directory_id = :dir").arg(subdirs_table_)); q.prepare(QString("SELECT path, mtime FROM %1 WHERE directory_id = :dir").arg(subdirs_table_));
q.BindValue(":dir", id); q.BindValue(":dir", id);
if (!q.Exec()) { if (!q.Exec()) {
db_->ReportErrors(q); db_->ReportErrors(q);
return SubdirectoryList(); return CollectionSubdirectoryList();
} }
SubdirectoryList subdirs; CollectionSubdirectoryList subdirs;
while (q.next()) { while (q.next()) {
Subdirectory subdir; CollectionSubdirectory subdir;
subdir.directory_id = id; subdir.directory_id = id;
subdir.path = q.value(0).toString(); subdir.path = q.value(0).toString();
subdir.mtime = q.value(1).toLongLong(); subdir.mtime = q.value(1).toLongLong();
@ -339,15 +340,15 @@ void CollectionBackend::AddDirectory(const QString &path) {
return; return;
} }
Directory dir; CollectionDirectory dir;
dir.path = canonical_path; dir.path = canonical_path;
dir.id = q.lastInsertId().toInt(); 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()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); 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()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); QSqlDatabase db(db_->Connect());
ScopedTransaction transaction(&db); ScopedTransaction transaction(&db);
for (const Subdirectory &subdir : subdirs) { for (const CollectionSubdirectory &subdir : subdirs) {
if (subdir.mtime == 0) { if (subdir.mtime == 0) {
// Delete the subdirectory // Delete the subdirectory
SqlQuery q(db); 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()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); 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.SetColumnSpec("DISTINCT " + column);
query.AddCompilationRequirement(false); 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); return GetAll("artist", opt);
} }
QStringList CollectionBackend::GetAllArtistsWithAlbums(const QueryOptions &opt) { QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOptions &opt) {
QMutexLocker l(db_->Mutex()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); 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); 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); 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()); QSqlDatabase db(db_->Connect());
QMutexLocker l(db_->Mutex()); 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()); QSqlDatabase db(db_->Connect());
QMutexLocker l(db_->Mutex()); 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()); QSqlDatabase db(db_->Connect());
QMutexLocker l(db_->Mutex()); 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); 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()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); 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()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); QSqlDatabase db(db_->Connect());
@ -1516,7 +1517,7 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective
ret.album = album; ret.album = album;
ret.album_artist = effective_albumartist; 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"); query.SetColumnSpec("art_automatic, art_manual, url");
if (!effective_albumartist.isEmpty()) { if (!effective_albumartist.isEmpty()) {
query.AddWhere("effective_albumartist", effective_albumartist); query.AddWhere("effective_albumartist", effective_albumartist);

View File

@ -38,8 +38,9 @@
#include "core/song.h" #include "core/song.h"
#include "core/sqlquery.h" #include "core/sqlquery.h"
#include "collectionfilteroptions.h"
#include "collectionquery.h" #include "collectionquery.h"
#include "directory.h" #include "collectiondirectory.h"
class QThread; class QThread;
class TaskManager; class TaskManager;
@ -90,23 +91,23 @@ class CollectionBackendInterface : public QObject {
virtual SongList FindSongsInDirectory(const int id) = 0; virtual SongList FindSongsInDirectory(const int id) = 0;
virtual SongList SongsWithMissingFingerprint(const int id) = 0; virtual SongList SongsWithMissingFingerprint(const int id) = 0;
virtual SubdirectoryList SubdirsInDirectory(const int id) = 0; virtual CollectionSubdirectoryList SubdirsInDirectory(const int id) = 0;
virtual DirectoryList GetAllDirectories() = 0; virtual CollectionDirectoryList GetAllDirectories() = 0;
virtual void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) = 0; virtual void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) = 0;
virtual SongList GetAllSongs() = 0; virtual SongList GetAllSongs() = 0;
virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0; virtual QStringList GetAllArtists(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0; virtual QStringList GetAllArtistsWithAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
virtual SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) = 0; virtual SongList GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
virtual SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt = QueryOptions()) = 0; virtual SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
virtual SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) = 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 GetAllAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
virtual AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) = 0; virtual AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
virtual AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) = 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 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; 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 Song GetSongByUrl(const QUrl &url, const qint64 beginning = 0) = 0;
virtual void AddDirectory(const QString &path) = 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 { class CollectionBackend : public CollectionBackendInterface {
@ -159,24 +160,24 @@ class CollectionBackend : public CollectionBackendInterface {
SongList FindSongsInDirectory(const int id) override; SongList FindSongsInDirectory(const int id) override;
SongList SongsWithMissingFingerprint(const int id) override; SongList SongsWithMissingFingerprint(const int id) override;
SubdirectoryList SubdirsInDirectory(const int id) override; CollectionSubdirectoryList SubdirsInDirectory(const int id) override;
DirectoryList GetAllDirectories() override; CollectionDirectoryList GetAllDirectories() override;
void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) override; void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) override;
SongList GetAllSongs() override; SongList GetAllSongs() override;
QStringList GetAll(const QString &column, const QueryOptions &opt = QueryOptions()); QStringList GetAll(const QString &column, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) override; QStringList GetAllArtists(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) override; QStringList GetAllArtistsWithAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) override; SongList GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt = QueryOptions()) override; SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) 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 GetAllAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) override; AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) 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 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; 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; Song GetSongByUrl(const QUrl &url, qint64 beginning = 0) override;
void AddDirectory(const QString &path) 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, SongList &songs);
bool ExecCollectionQuery(CollectionQuery *query, SongMap &songs); bool ExecCollectionQuery(CollectionQuery *query, SongMap &songs);
@ -228,7 +229,7 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateMTimesOnly(const SongList &songs); void UpdateMTimesOnly(const SongList &songs);
void DeleteSongs(const SongList &songs); void DeleteSongs(const SongList &songs);
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true); void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
void AddOrUpdateSubdirs(const SubdirectoryList &subdirs); void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating(); void CompilationsNeedUpdating();
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false); 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); 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); void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days);
signals: signals:
void DirectoryDiscovered(Directory, SubdirectoryList); void DirectoryDiscovered(CollectionDirectory, CollectionSubdirectoryList);
void DirectoryDeleted(Directory); void DirectoryDeleted(CollectionDirectory);
void SongsDiscovered(SongList); void SongsDiscovered(SongList);
void SongsDeleted(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); 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 QString &album_artist, const bool compilation_required = false, const CollectionFilterOptions &opt = CollectionFilterOptions());
AlbumList GetAlbums(const QString &artist, const bool compilation_required, const QueryOptions &opt = QueryOptions()); AlbumList GetAlbums(const QString &artist, const bool compilation_required, const CollectionFilterOptions &opt = CollectionFilterOptions());
SubdirectoryList SubdirsInDirectory(const int id, QSqlDatabase &db); CollectionSubdirectoryList SubdirsInDirectory(const int id, QSqlDatabase &db);
Song GetSongById(const int id, QSqlDatabase &db); Song GetSongById(const int id, QSqlDatabase &db);
SongList GetSongsById(const QStringList &ids, 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/iconloader.h"
#include "core/musicstorage.h" #include "core/musicstorage.h"
#include "utilities/diskutils.h" #include "utilities/diskutils.h"
#include "directory.h" #include "collectiondirectory.h"
#include "collectionbackend.h" #include "collectionbackend.h"
#include "collectiondirectorymodel.h" #include "collectiondirectorymodel.h"
@ -46,7 +46,7 @@ CollectionDirectoryModel::CollectionDirectoryModel(CollectionBackend *backend, Q
CollectionDirectoryModel::~CollectionDirectoryModel() = default; CollectionDirectoryModel::~CollectionDirectoryModel() = default;
void CollectionDirectoryModel::DirectoryDiscovered(const Directory &dir) { void CollectionDirectoryModel::DirectoryDiscovered(const CollectionDirectory &dir) {
QStandardItem *item = new QStandardItem(dir.path); QStandardItem *item = new QStandardItem(dir.path);
item->setData(dir.id, kIdRole); 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) { for (int i = 0; i < rowCount(); ++i) {
if (item(i, 0)->data(kIdRole).toInt() == dir.id) { if (item(i, 0)->data(kIdRole).toInt() == dir.id) {
@ -80,7 +80,7 @@ void CollectionDirectoryModel::RemoveDirectory(const QModelIndex &idx) {
if (!backend_ || !idx.isValid()) return; if (!backend_ || !idx.isValid()) return;
Directory dir; CollectionDirectory dir;
dir.path = idx.data().toString(); dir.path = idx.data().toString();
dir.id = idx.data(kIdRole).toInt(); dir.id = idx.data(kIdRole).toInt();

View File

@ -34,7 +34,7 @@
class QModelIndex; class QModelIndex;
struct Directory; struct CollectionDirectory;
class CollectionBackend; class CollectionBackend;
class MusicStorage; class MusicStorage;
@ -53,8 +53,8 @@ class CollectionDirectoryModel : public QStandardItemModel {
private slots: private slots:
// To be called by the backend // To be called by the backend
void DirectoryDiscovered(const Directory &directories); void DirectoryDiscovered(const CollectionDirectory &directories);
void DirectoryDeleted(const Directory &directories); void DirectoryDeleted(const CollectionDirectory &directories);
private: private:
static const int kIdRole = Qt::UserRole + 1; 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/iconloader.h"
#include "core/song.h" #include "core/song.h"
#include "core/logging.h" #include "core/logging.h"
#include "collectionfilteroptions.h"
#include "collectionmodel.h" #include "collectionmodel.h"
#include "collectionquery.h" #include "collectionquery.h"
#include "savedgroupingmanager.h" #include "savedgroupingmanager.h"
@ -455,12 +456,12 @@ void CollectionFilterWidget::SetFilterHint(const QString &hint) {
ui_->search_field->setPlaceholderText(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->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 <QString>
#include "collectionquery.h" #include "collectionquery.h"
#include "collectionqueryoptions.h"
#include "collectionmodel.h" #include "collectionmodel.h"
class QTimer; class QTimer;
@ -88,7 +89,7 @@ class CollectionFilterWidget : public QWidget {
public slots: public slots:
void UpdateGroupByActions(); void UpdateGroupByActions();
void SetQueryMode(QueryOptions::QueryMode query_mode); void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode);
void FocusOnFilter(QKeyEvent *e); void FocusOnFilter(QKeyEvent *e);
signals: signals:

View File

@ -60,7 +60,9 @@
#include "core/logging.h" #include "core/logging.h"
#include "core/taskmanager.h" #include "core/taskmanager.h"
#include "core/sqlrow.h" #include "core/sqlrow.h"
#include "collectionfilteroptions.h"
#include "collectionquery.h" #include "collectionquery.h"
#include "collectionqueryoptions.h"
#include "collectionbackend.h" #include "collectionbackend.h"
#include "collectiondirectorymodel.h" #include "collectiondirectorymodel.h"
#include "collectionitem.h" #include "collectionitem.h"
@ -210,7 +212,7 @@ void CollectionModel::SongsDiscovered(const SongList &songs) {
for (const Song &song : songs) { for (const Song &song : songs) {
// Sanity check to make sure we don't add songs that are outside the user's filter // 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! // Hey, we've already got that one!
if (song_nodes_.contains(song.id())) continue; 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_); CollectionQuery q(db, backend_->songs_table(), backend_->fts_table(), filter_options);
q.SetColumnSpec(query_options.column_spec());
q.SetColumnSpec(query.column_spec()); for (const CollectionQueryOptions::Where &where_clauses : query_options.where_clauses()) {
q.SetOrderBy(query.order_by()); q.AddWhere(where_clauses.column, where_clauses.value, where_clauses.op);
q.SetWhereClauses(query.where_clauses()); }
q.SetBoundValues(query.bound_values());
q.SetIncludeUnavailable(query.include_unavailable());
q.SetDuplicatesOnly(query.duplicates_only());
q.AddCompilationRequirement(true); q.AddCompilationRequirement(true);
q.SetLimit(1); q.SetLimit(1);
@ -827,57 +826,66 @@ bool CollectionModel::HasCompilations(const QSqlDatabase &db, const CollectionQu
} }
CollectionModel::QueryResult CollectionModel::RunQuery(CollectionItem *parent) { CollectionQueryOptions CollectionModel::PrepareQuery(CollectionItem *parent) {
QueryResult result;
// Information about what we want the children to be // Information about what we want the children to be
int child_level = parent == root_ ? 0 : parent->container_level + 1; const int child_level = parent == root_ ? 0 : parent->container_level + 1;
GroupBy child_group_by = child_level >= 3 ? GroupBy_None : group_by_[child_level]; 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.) // 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()) { CollectionQuery q(db, backend_->songs_table(), backend_->fts_table(), filter_options);
backend_->db()->Close(); 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; return result;
} }
@ -913,17 +921,20 @@ void CollectionModel::LazyPopulate(CollectionItem *parent, const bool signal) {
if (parent->lazy_loaded) return; if (parent->lazy_loaded) return;
parent->lazy_loaded = true; parent->lazy_loaded = true;
QueryResult result = RunQuery(parent); CollectionQueryOptions query_options = PrepareQuery(parent);
QueryResult result = RunQuery(filter_options_, query_options);
PostQuery(parent, result, signal); PostQuery(parent, result, signal);
} }
void CollectionModel::ResetAsync() { void CollectionModel::ResetAsync() {
CollectionQueryOptions query_options = PrepareQuery(root_);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #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 #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 #endif
QFutureWatcher<CollectionModel::QueryResult> *watcher = new QFutureWatcher<CollectionModel::QueryResult>(); QFutureWatcher<CollectionModel::QueryResult> *watcher = new QFutureWatcher<CollectionModel::QueryResult>();
QObject::connect(watcher, &QFutureWatcher<CollectionModel::QueryResult>::finished, this, &CollectionModel::ResetAsyncQueryFinished); QObject::connect(watcher, &QFutureWatcher<CollectionModel::QueryResult>::finished, this, &CollectionModel::ResetAsyncQueryFinished);
@ -937,10 +948,6 @@ void CollectionModel::ResetAsyncQueryFinished() {
const struct QueryResult result = watcher->result(); const struct QueryResult result = watcher->result();
watcher->deleteLater(); watcher->deleteLater();
if (QThread::currentThread() != thread() && QThread::currentThread() != backend_->thread()) {
backend_->Close();
}
BeginReset(); BeginReset();
root_->lazy_loaded = true; 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. // Say what group_by of thing we want to get back from the database.
switch (group_by) { switch (group_by) {
case GroupBy_AlbumArtist: case GroupBy_AlbumArtist:
q->SetColumnSpec("DISTINCT effective_albumartist"); query_options->set_column_spec("DISTINCT effective_albumartist");
break; break;
case GroupBy_Artist: case GroupBy_Artist:
q->SetColumnSpec("DISTINCT artist"); query_options->set_column_spec("DISTINCT artist");
break; break;
case GroupBy_Album:{ case GroupBy_Album:{
QString query("DISTINCT album, album_id"); QString query("DISTINCT album, album_id");
if (separate_albums_by_grouping) query.append(", grouping"); if (separate_albums_by_grouping) query.append(", grouping");
q->SetColumnSpec(query); query_options->set_column_spec(query);
break; break;
} }
case GroupBy_AlbumDisc:{ case GroupBy_AlbumDisc:{
QString query("DISTINCT album, album_id, disc"); QString query("DISTINCT album, album_id, disc");
if (separate_albums_by_grouping) query.append(", grouping"); if (separate_albums_by_grouping) query.append(", grouping");
q->SetColumnSpec(query); query_options->set_column_spec(query);
break; break;
} }
case GroupBy_YearAlbum:{ case GroupBy_YearAlbum:{
QString query("DISTINCT year, album, album_id"); QString query("DISTINCT year, album, album_id");
if (separate_albums_by_grouping) query.append(", grouping"); if (separate_albums_by_grouping) query.append(", grouping");
q->SetColumnSpec(query); query_options->set_column_spec(query);
break; break;
} }
case GroupBy_YearAlbumDisc:{ case GroupBy_YearAlbumDisc:{
QString query("DISTINCT year, album, album_id, disc"); QString query("DISTINCT year, album, album_id, disc");
if (separate_albums_by_grouping) query.append(", grouping"); if (separate_albums_by_grouping) query.append(", grouping");
q->SetColumnSpec(query); query_options->set_column_spec(query);
break; break;
} }
case GroupBy_OriginalYearAlbum:{ case GroupBy_OriginalYearAlbum:{
QString query("DISTINCT year, originalyear, album, album_id"); QString query("DISTINCT year, originalyear, album, album_id");
if (separate_albums_by_grouping) query.append(", grouping"); if (separate_albums_by_grouping) query.append(", grouping");
q->SetColumnSpec(query); query_options->set_column_spec(query);
break; break;
} }
case GroupBy_OriginalYearAlbumDisc:{ case GroupBy_OriginalYearAlbumDisc:{
QString query("DISTINCT year, originalyear, album, album_id, disc"); QString query("DISTINCT year, originalyear, album, album_id, disc");
if (separate_albums_by_grouping) query.append(", grouping"); if (separate_albums_by_grouping) query.append(", grouping");
q->SetColumnSpec(query); query_options->set_column_spec(query);
break; break;
} }
case GroupBy_Disc: case GroupBy_Disc:
q->SetColumnSpec("DISTINCT disc"); query_options->set_column_spec("DISTINCT disc");
break; break;
case GroupBy_Year: case GroupBy_Year:
q->SetColumnSpec("DISTINCT year"); query_options->set_column_spec("DISTINCT year");
break; break;
case GroupBy_OriginalYear: case GroupBy_OriginalYear:
q->SetColumnSpec("DISTINCT effective_originalyear"); query_options->set_column_spec("DISTINCT effective_originalyear");
break; break;
case GroupBy_Genre: case GroupBy_Genre:
q->SetColumnSpec("DISTINCT genre"); query_options->set_column_spec("DISTINCT genre");
break; break;
case GroupBy_Composer: case GroupBy_Composer:
q->SetColumnSpec("DISTINCT composer"); query_options->set_column_spec("DISTINCT composer");
break; break;
case GroupBy_Performer: case GroupBy_Performer:
q->SetColumnSpec("DISTINCT performer"); query_options->set_column_spec("DISTINCT performer");
break; break;
case GroupBy_Grouping: case GroupBy_Grouping:
q->SetColumnSpec("DISTINCT grouping"); query_options->set_column_spec("DISTINCT grouping");
break; break;
case GroupBy_FileType: case GroupBy_FileType:
q->SetColumnSpec("DISTINCT filetype"); query_options->set_column_spec("DISTINCT filetype");
break; break;
case GroupBy_Format: case GroupBy_Format:
q->SetColumnSpec("DISTINCT filetype, samplerate, bitdepth"); query_options->set_column_spec("DISTINCT filetype, samplerate, bitdepth");
break; break;
case GroupBy_Samplerate: case GroupBy_Samplerate:
q->SetColumnSpec("DISTINCT samplerate"); query_options->set_column_spec("DISTINCT samplerate");
break; break;
case GroupBy_Bitdepth: case GroupBy_Bitdepth:
q->SetColumnSpec("DISTINCT bitdepth"); query_options->set_column_spec("DISTINCT bitdepth");
break; break;
case GroupBy_Bitrate: case GroupBy_Bitrate:
q->SetColumnSpec("DISTINCT bitrate"); query_options->set_column_spec("DISTINCT bitrate");
break; break;
case GroupBy_None: case GroupBy_None:
case GroupByCount: case GroupByCount:
q->SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec); query_options->set_column_spec("%songs_table.ROWID, " + Song::kColumnSpec);
break; 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. // Say how we want the query to be filtered. This is done once for each parent going up the tree.
switch (group_by) { switch (group_by) {
case GroupBy_AlbumArtist: case GroupBy_AlbumArtist:
if (IsCompilationArtistNode(item)) { if (IsCompilationArtistNode(item)) {
q->AddCompilationRequirement(true); query_options->set_compilation_requirement(true);
} }
else { else {
// Don't duplicate compilations outside the Various artists node // Don't duplicate compilations outside the Various artists node
q->AddCompilationRequirement(false); query_options->set_compilation_requirement(false);
q->AddWhere("effective_albumartist", item->metadata.effective_albumartist()); query_options->AddWhere("effective_albumartist", item->metadata.effective_albumartist());
} }
break; break;
case GroupBy_Artist: case GroupBy_Artist:
if (IsCompilationArtistNode(item)) { if (IsCompilationArtistNode(item)) {
q->AddCompilationRequirement(true); query_options->set_compilation_requirement(true);
} }
else { else {
// Don't duplicate compilations outside the Various artists node // Don't duplicate compilations outside the Various artists node
q->AddCompilationRequirement(false); query_options->set_compilation_requirement(false);
q->AddWhere("artist", item->metadata.artist()); query_options->AddWhere("artist", item->metadata.artist());
} }
break; break;
case GroupBy_Album: case GroupBy_Album:
q->AddWhere("album", item->metadata.album()); query_options->AddWhere("album", item->metadata.album());
q->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("album_id", item->metadata.album_id());
if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping());
break; break;
case GroupBy_AlbumDisc: case GroupBy_AlbumDisc:
q->AddWhere("album", item->metadata.album()); query_options->AddWhere("album", item->metadata.album());
q->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("album_id", item->metadata.album_id());
q->AddWhere("disc", item->metadata.disc()); query_options->AddWhere("disc", item->metadata.disc());
if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping());
break; break;
case GroupBy_YearAlbum: case GroupBy_YearAlbum:
q->AddWhere("year", item->metadata.year()); query_options->AddWhere("year", item->metadata.year());
q->AddWhere("album", item->metadata.album()); query_options->AddWhere("album", item->metadata.album());
q->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("album_id", item->metadata.album_id());
if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping());
break; break;
case GroupBy_YearAlbumDisc: case GroupBy_YearAlbumDisc:
q->AddWhere("year", item->metadata.year()); query_options->AddWhere("year", item->metadata.year());
q->AddWhere("album", item->metadata.album()); query_options->AddWhere("album", item->metadata.album());
q->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("album_id", item->metadata.album_id());
q->AddWhere("disc", item->metadata.disc()); query_options->AddWhere("disc", item->metadata.disc());
if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping());
break; break;
case GroupBy_OriginalYearAlbum: case GroupBy_OriginalYearAlbum:
q->AddWhere("year", item->metadata.year()); query_options->AddWhere("year", item->metadata.year());
q->AddWhere("originalyear", item->metadata.originalyear()); query_options->AddWhere("originalyear", item->metadata.originalyear());
q->AddWhere("album", item->metadata.album()); query_options->AddWhere("album", item->metadata.album());
q->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("album_id", item->metadata.album_id());
if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping());
break; break;
case GroupBy_OriginalYearAlbumDisc: case GroupBy_OriginalYearAlbumDisc:
q->AddWhere("year", item->metadata.year()); query_options->AddWhere("year", item->metadata.year());
q->AddWhere("originalyear", item->metadata.originalyear()); query_options->AddWhere("originalyear", item->metadata.originalyear());
q->AddWhere("album", item->metadata.album()); query_options->AddWhere("album", item->metadata.album());
q->AddWhere("album_id", item->metadata.album_id()); query_options->AddWhere("album_id", item->metadata.album_id());
q->AddWhere("disc", item->metadata.disc()); query_options->AddWhere("disc", item->metadata.disc());
if (separate_albums_by_grouping) q->AddWhere("grouping", item->metadata.grouping()); if (separate_albums_by_grouping) query_options->AddWhere("grouping", item->metadata.grouping());
break; break;
case GroupBy_Disc: case GroupBy_Disc:
q->AddWhere("disc", item->metadata.disc()); query_options->AddWhere("disc", item->metadata.disc());
break; break;
case GroupBy_Year: case GroupBy_Year:
q->AddWhere("year", item->metadata.year()); query_options->AddWhere("year", item->metadata.year());
break; break;
case GroupBy_OriginalYear: case GroupBy_OriginalYear:
q->AddWhere("effective_originalyear", item->metadata.effective_originalyear()); query_options->AddWhere("effective_originalyear", item->metadata.effective_originalyear());
break; break;
case GroupBy_Genre: case GroupBy_Genre:
q->AddWhere("genre", item->metadata.genre()); query_options->AddWhere("genre", item->metadata.genre());
break; break;
case GroupBy_Composer: case GroupBy_Composer:
q->AddWhere("composer", item->metadata.composer()); query_options->AddWhere("composer", item->metadata.composer());
break; break;
case GroupBy_Performer: case GroupBy_Performer:
q->AddWhere("performer", item->metadata.performer()); query_options->AddWhere("performer", item->metadata.performer());
break; break;
case GroupBy_Grouping: case GroupBy_Grouping:
q->AddWhere("grouping", item->metadata.grouping()); query_options->AddWhere("grouping", item->metadata.grouping());
break; break;
case GroupBy_FileType: case GroupBy_FileType:
q->AddWhere("filetype", item->metadata.filetype()); query_options->AddWhere("filetype", item->metadata.filetype());
break; break;
case GroupBy_Format: case GroupBy_Format:
q->AddWhere("filetype", item->metadata.filetype()); query_options->AddWhere("filetype", item->metadata.filetype());
q->AddWhere("samplerate", item->metadata.samplerate()); query_options->AddWhere("samplerate", item->metadata.samplerate());
q->AddWhere("bitdepth", item->metadata.bitdepth()); query_options->AddWhere("bitdepth", item->metadata.bitdepth());
break; break;
case GroupBy_Samplerate: case GroupBy_Samplerate:
q->AddWhere("samplerate", item->metadata.samplerate()); query_options->AddWhere("samplerate", item->metadata.samplerate());
break; break;
case GroupBy_Bitdepth: case GroupBy_Bitdepth:
q->AddWhere("bitdepth", item->metadata.bitdepth()); query_options->AddWhere("bitdepth", item->metadata.bitdepth());
break; break;
case GroupBy_Bitrate: case GroupBy_Bitrate:
q->AddWhere("bitrate", item->metadata.bitrate()); query_options->AddWhere("bitrate", item->metadata.bitrate());
break; break;
case GroupBy_None: case GroupBy_None:
case GroupByCount: case GroupByCount:
@ -1851,21 +1858,19 @@ SongList CollectionModel::GetChildSongs(const QModelIndex &idx) const {
return GetChildSongs(QModelIndexList() << idx); return GetChildSongs(QModelIndexList() << idx);
} }
void CollectionModel::SetFilterAge(const int age) { void CollectionModel::SetFilterMode(CollectionFilterOptions::FilterMode filter_mode) {
query_options_.set_max_age(age); filter_options_.set_filter_mode(filter_mode);
ResetAsync(); ResetAsync();
} }
void CollectionModel::SetFilterText(const QString &text) { void CollectionModel::SetFilterAge(const int filter_age) {
query_options_.set_filter(text); filter_options_.set_max_age(filter_age);
ResetAsync(); ResetAsync();
} }
void CollectionModel::SetFilterQueryMode(QueryOptions::QueryMode query_mode) { void CollectionModel::SetFilterText(const QString &filter_text) {
query_options_.set_query_mode(query_mode); filter_options_.set_filter_text(filter_text);
ResetAsync(); ResetAsync();
} }
bool CollectionModel::canFetchMore(const QModelIndex &parent) const { bool CollectionModel::canFetchMore(const QModelIndex &parent) const {

View File

@ -49,7 +49,9 @@
#include "core/song.h" #include "core/song.h"
#include "core/sqlrow.h" #include "core/sqlrow.h"
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "collectionfilteroptions.h"
#include "collectionquery.h" #include "collectionquery.h"
#include "collectionqueryoptions.h"
#include "collectionitem.h" #include "collectionitem.h"
#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderoptions.h"
@ -203,9 +205,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
void GroupingChanged(CollectionModel::Grouping g, bool separate_albums_by_grouping); void GroupingChanged(CollectionModel::Grouping g, bool separate_albums_by_grouping);
public slots: public slots:
void SetFilterAge(const int age); void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode);
void SetFilterText(const QString &text); void SetFilterAge(const int filter_age);
void SetFilterQueryMode(QueryOptions::QueryMode query_mode); void SetFilterText(const QString &filter_text);
void Init(const bool async = true); void Init(const bool async = true);
void Reset(); void Reset();
@ -232,20 +234,21 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
private: 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. // 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); 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(); void BeginReset();
// Functions for working with queries and creating items. // 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. // 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. // 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 SetQueryColumnSpec(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQueryOptions *query_options);
static void FilterQuery(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQuery *q); 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. // Items can be created either from a query that's been run to populate a node, or by a spontaneous SongsDiscovered emission from the backend.
CollectionItem *ItemFromQuery(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level); CollectionItem *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_artist_count_;
int total_album_count_; int total_album_count_;
QueryOptions query_options_; CollectionFilterOptions filter_options_;
Grouping group_by_; Grouping group_by_;
bool separate_albums_by_grouping_; bool separate_albums_by_grouping_;

View File

@ -31,16 +31,16 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QSqlDatabase> #include <QSqlDatabase>
#include <QSqlQuery> #include <QSqlQuery>
#include <QSqlError>
#include "core/logging.h" #include "core/logging.h"
#include "core/sqlquery.h"
#include "core/song.h" #include "core/song.h"
#include "collectionquery.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 CollectionFilterOptions &filter_options)
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const QueryOptions &options)
: QSqlQuery(db), : QSqlQuery(db),
songs_table_(songs_table), songs_table_(songs_table),
fts_table_(fts_table), fts_table_(fts_table),
@ -49,7 +49,7 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
duplicates_only_(false), duplicates_only_(false),
limit_(-1) { 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: // 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. // 1) Append * to all tokens.
// 2) Prefix "fts" to column names. // 2) Prefix "fts" to column names.
@ -57,9 +57,9 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
// Split on whitespace // Split on whitespace
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) #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 #else
QStringList tokens(options.filter().split(QRegularExpression("\\s+"), QString::SkipEmptyParts)); QStringList tokens(filter_options.filter_text().split(QRegularExpression("\\s+"), QString::SkipEmptyParts));
#endif #endif
QString query; QString query;
for (QString token : tokens) { for (QString token : tokens) {
@ -100,49 +100,40 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
} }
} }
if (options.max_age() != -1) { if (filter_options.max_age() != -1) {
qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - options.max_age(); qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - filter_options.max_age();
where_clauses_ << "ctime > ?"; where_clauses_ << "ctime > ?";
bound_values_ << cutoff; 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. // 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? // 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 // 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. // 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. // 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 ='')"; 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) { void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
// Ignore 'literal' for IN // Ignore 'literal' for IN
if (op.compare("IN", Qt::CaseInsensitive) == 0) { if (op.compare("IN", Qt::CaseInsensitive) == 0) {
QStringList values = value.toStringList(); QStringList values = value.toStringList();
QStringList final; QStringList final_values;
final.reserve(values.count()); final_values.reserve(values.count());
for (const QString &single_value : values) { for (const QString &single_value : values) {
final.append("?"); final_values.append("?");
bound_values_ << single_value; bound_values_ << single_value;
} }
where_clauses_ << QString("%1 IN (" + final.join(",") + ")").arg(column); where_clauses_ << QString("%1 IN (" + final_values.join(",") + ")").arg(column);
} }
else { else {
// Do integers inline - sqlite seems to get confused when you pass integers to bound parameters // 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() { bool CollectionQuery::Exec() {
QString sql; QString sql;
@ -213,32 +213,17 @@ bool CollectionQuery::Exec() {
sql.replace("%fts_table_noprefix", fts_table_.section('.', -1, -1)); sql.replace("%fts_table_noprefix", fts_table_.section('.', -1, -1));
sql.replace("%fts_table", fts_table_); sql.replace("%fts_table", fts_table_);
prepare(sql); QSqlQuery::prepare(sql);
// Bind values // Bind values
for (const QVariant &value : bound_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); } QVariant CollectionQuery::Value(const int column) const { return QSqlQuery::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;
}

View File

@ -28,75 +28,23 @@
#include <QVariant> #include <QVariant>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <QMap>
#include <QSqlDatabase> #include <QSqlDatabase>
#include <QSqlQuery> #include <QSqlQuery>
class Song; #include "collectionfilteroptions.h"
#include "collectionqueryoptions.h"
// 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_;
};
class CollectionQuery : public QSqlQuery { class CollectionQuery : public QSqlQuery {
public: 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). QVariant Value(const int column) const;
void SetColumnSpec(const QString &spec) { column_spec_ = spec; } QVariant value(const int column) const { return Value(column); }
// 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);
bool Exec(); bool Exec();
bool exec() { return QSqlQuery::exec(); }
bool Next(); bool Next();
QVariant Value(const int column) const;
QString column_spec() const { return column_spec_; } QString column_spec() const { return column_spec_; }
QString order_by() const { return order_by_; } QString order_by() const { return order_by_; }
@ -107,6 +55,24 @@ class CollectionQuery : public QSqlQuery {
bool duplicates_only() const { return duplicates_only_; } bool duplicates_only() const { return duplicates_only_; }
int limit() const { return limit_; } 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: private:
QString GetInnerQuery() const; 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 "core/taskmanager.h"
#include "utilities/imageutils.h" #include "utilities/imageutils.h"
#include "utilities/timeconstants.h" #include "utilities/timeconstants.h"
#include "directory.h" #include "collectiondirectory.h"
#include "collectionbackend.h" #include "collectionbackend.h"
#include "collectionwatcher.h" #include "collectionwatcher.h"
#include "playlistparsers/cueparser.h" #include "playlistparsers/cueparser.h"
@ -167,9 +167,9 @@ void CollectionWatcher::ReloadSettings() {
} }
else if (monitor_ && !was_monitoring_before) { else if (monitor_ && !was_monitoring_before) {
// Add all directories to all QFileSystemWatchers again // Add all directories to all QFileSystemWatchers again
for (const Directory &dir : std::as_const(watched_dirs_)) { for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id); CollectionSubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
for (const Subdirectory &subdir : subdirs) { for (const CollectionSubdirectory &subdir : subdirs) {
AddWatch(dir, subdir.path); AddWatch(dir, subdir.path);
} }
} }
@ -272,7 +272,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
touched_subdirs.clear(); touched_subdirs.clear();
} }
for (const Subdirectory &subdir : deleted_subdirs) { for (const CollectionSubdirectory &subdir : deleted_subdirs) {
if (watcher_->watched_dirs_.contains(dir_)) { if (watcher_->watched_dirs_.contains(dir_)) {
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir); watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
} }
@ -281,7 +281,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
if (watcher_->monitor_) { if (watcher_->monitor_) {
// Watch the new subdirectories // Watch the new subdirectories
for (const Subdirectory &subdir : new_subdirs) { for (const CollectionSubdirectory &subdir : new_subdirs) {
if (watcher_->watched_dirs_.contains(dir_)) { if (watcher_->watched_dirs_.contains(dir_)) {
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path); 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_ = subdirs;
known_subdirs_dirty_ = false; known_subdirs_dirty_ = false;
@ -342,18 +342,18 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_)); 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_) { if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_)); SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
} }
SubdirectoryList ret; CollectionSubdirectoryList ret;
for (const Subdirectory &subdir : known_subdirs_) { for (const CollectionSubdirectory &subdir : known_subdirs_) {
if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path && subdir.mtime != 0) { if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path && subdir.mtime != 0) {
ret << subdir; ret << subdir;
} }
@ -363,7 +363,7 @@ SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const Q
} }
SubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() { CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
if (known_subdirs_dirty_) { if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_)); 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; stop_requested_ = false;
@ -385,7 +385,7 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis
const quint64 files_count = FilesCountForPath(&transaction, dir.path); const quint64 files_count = FilesCountForPath(&transaction, dir.path);
transaction.SetKnownSubdirs(subdirs); transaction.SetKnownSubdirs(subdirs);
transaction.AddToProgressMax(files_count); transaction.AddToProgressMax(files_count);
ScanSubdirectory(dir.path, Subdirectory(), files_count, &transaction); ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch(); last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
} }
else { else {
@ -395,7 +395,7 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis
const quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count); const quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count);
transaction.SetKnownSubdirs(subdirs); transaction.SetKnownSubdirs(subdirs);
transaction.AddToProgressMax(files_count); transaction.AddToProgressMax(files_count);
for (const Subdirectory &subdir : subdirs) { for (const CollectionSubdirectory &subdir : subdirs) {
if (stop_requested_ || abort_requested_) break; if (stop_requested_ || abort_requested_) break;
if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction); 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); QFileInfo path_info(path);
// Do not scan symlinked dirs that are already in collection // Do not scan symlinked dirs that are already in collection
if (path_info.isSymLink()) { if (path_info.isSymLink()) {
QString real_path = path_info.symLinkTarget(); 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)) { if (real_path.startsWith(dir.path)) {
return; return;
} }
@ -440,12 +440,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
QMap<QString, QStringList> album_art; QMap<QString, QStringList> album_art;
QStringList files_on_disk; 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 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 // If one has been removed, "rescan" it to get the deleted songs
SubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path); CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
for (const Subdirectory &prev_subdir : previous_subdirs) { for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) { if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true); 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 (child_info.isDir()) {
if (!t->HasSeenSubdir(child)) { 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. // 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.directory_id = -1;
new_subdir.path = child; new_subdir.path = child;
new_subdir.mtime = child_info.lastModified().toSecsSinceEpoch(); 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 // Add this subdir to the new or touched list
Subdirectory updated_subdir; CollectionSubdirectory updated_subdir;
updated_subdir.directory_id = t->dir(); updated_subdir.directory_id = t->dir();
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0; updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
updated_subdir.path = path; updated_subdir.path = path;
@ -688,12 +688,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
t->touched_subdirs << updated_subdir; 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; t->deleted_subdirs << updated_subdir;
} }
// Recurse into the new subdirs that we found // 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; if (stop_requested_ || abort_requested_) return;
ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true); 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; 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; 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); QStringList subdir_paths = subdir_mapping_.keys(dir);
for (const QString &subdir_path : subdir_paths) { 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); rescan_queue_.remove(dir.id);
watched_dirs_.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) { void CollectionWatcher::DirectoryChanged(const QString &subdir) {
// Find what dir it was in // 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()) { if (it == subdir_mapping_.constEnd()) {
return; return;
} }
Directory dir = *it; CollectionDirectory dir = *it;
qLog(Debug) << "Subdir" << subdir << "changed under directory" << dir.path << "id" << dir.id; 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]) { for (const QString &path : rescan_queue_[dir]) {
if (stop_requested_ || abort_requested_) break; if (stop_requested_ || abort_requested_) break;
Subdirectory subdir; CollectionSubdirectory subdir;
subdir.directory_id = dir; subdir.directory_id = dir;
subdir.mtime = 0; subdir.mtime = 0;
subdir.path = path; subdir.path = path;
@ -1154,7 +1154,7 @@ void CollectionWatcher::RescanTracksNow() {
qLog(Debug) << "Song" << song.title() << "dir id" << song.directory_id() << "dir" << songdir; qLog(Debug) << "Song" << song.title() << "dir id" << song.directory_id() << "dir" << songdir;
ScanTransaction transaction(this, song.directory_id(), false, false, mark_songs_unavailable_); ScanTransaction transaction(this, song.directory_id(), false, false, mark_songs_unavailable_);
quint64 files_count = FilesCountForPath(&transaction, songdir); quint64 files_count = FilesCountForPath(&transaction, songdir);
ScanSubdirectory(songdir, Subdirectory(), files_count, &transaction); ScanSubdirectory(songdir, CollectionSubdirectory(), files_count, &transaction);
scanned_dirs << songdir; scanned_dirs << songdir;
emit CompilationsNeedUpdating(); emit CompilationsNeedUpdating();
} }
@ -1171,16 +1171,16 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
stop_requested_ = false; 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; if (stop_requested_ || abort_requested_) break;
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_); ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
SubdirectoryList subdirs(transaction.GetAllSubdirs()); CollectionSubdirectoryList subdirs(transaction.GetAllSubdirs());
if (subdirs.isEmpty()) { if (subdirs.isEmpty()) {
qLog(Debug) << "Collection directory wasn't in subdir list."; qLog(Debug) << "Collection directory wasn't in subdir list.";
Subdirectory subdir; CollectionSubdirectory subdir;
subdir.path = dir.path; subdir.path = dir.path;
subdir.directory_id = dir.id; subdir.directory_id = dir.id;
subdirs << subdir; 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); quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count);
transaction.AddToProgressMax(files_count); transaction.AddToProgressMax(files_count);
for (const Subdirectory &subdir : subdirs) { for (const CollectionSubdirectory &subdir : subdirs) {
if (stop_requested_ || abort_requested_) break; if (stop_requested_ || abort_requested_) break;
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction); 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.isDir()) {
if (path_info.isSymLink()) { if (path_info.isSymLink()) {
QString real_path = path_info.symLinkTarget(); 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)) { if (real_path.startsWith(dir.path)) {
continue; 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; quint64 i = 0;
for (const Subdirectory &subdir : subdirs) { for (const CollectionSubdirectory &subdir : subdirs) {
if (stop_requested_ || abort_requested_) break; if (stop_requested_ || abort_requested_) break;
const quint64 files_count = FilesCountForPath(t, subdir.path); const quint64 files_count = FilesCountForPath(t, subdir.path);
subdir_files_count[subdir.path] = files_count; subdir_files_count[subdir.path] = files_count;

View File

@ -34,7 +34,7 @@
#include <QStringList> #include <QStringList>
#include <QUrl> #include <QUrl>
#include "directory.h" #include "collectiondirectory.h"
#include "core/song.h" #include "core/song.h"
class QThread; class QThread;
@ -74,8 +74,8 @@ class CollectionWatcher : public QObject {
void SongsDeleted(SongList); void SongsDeleted(SongList);
void SongsUnavailable(SongList songs, bool unavailable = true); void SongsUnavailable(SongList songs, bool unavailable = true);
void SongsReadded(SongList songs, bool unavailable = false); void SongsReadded(SongList songs, bool unavailable = false);
void SubdirsDiscovered(SubdirectoryList subdirs); void SubdirsDiscovered(CollectionSubdirectoryList subdirs);
void SubdirsMTimeUpdated(SubdirectoryList subdirs); void SubdirsMTimeUpdated(CollectionSubdirectoryList subdirs);
void CompilationsNeedUpdating(); void CompilationsNeedUpdating();
void UpdateLastSeen(int directory_id, int expire_unavailable_songs_days); void UpdateLastSeen(int directory_id, int expire_unavailable_songs_days);
void ExitFinished(); void ExitFinished();
@ -83,8 +83,8 @@ class CollectionWatcher : public QObject {
void ScanStarted(int task_id); void ScanStarted(int task_id);
public slots: public slots:
void AddDirectory(const Directory &dir, const SubdirectoryList &subdirs); void AddDirectory(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdirs);
void RemoveDirectory(const Directory &dir); void RemoveDirectory(const CollectionDirectory &dir);
void SetRescanPaused(bool pause); void SetRescanPaused(bool pause);
private: private:
@ -102,9 +102,9 @@ class CollectionWatcher : public QObject {
SongList FindSongsInSubdirectory(const QString &path); SongList FindSongsInSubdirectory(const QString &path);
bool HasSongsWithMissingFingerprint(const QString &path); bool HasSongsWithMissingFingerprint(const QString &path);
bool HasSeenSubdir(const QString &path); bool HasSeenSubdir(const QString &path);
void SetKnownSubdirs(const SubdirectoryList &subdirs); void SetKnownSubdirs(const CollectionSubdirectoryList &subdirs);
SubdirectoryList GetImmediateSubdirs(const QString &path); CollectionSubdirectoryList GetImmediateSubdirs(const QString &path);
SubdirectoryList GetAllSubdirs(); CollectionSubdirectoryList GetAllSubdirs();
void AddToProgress(const quint64 n = 1); void AddToProgress(const quint64 n = 1);
void AddToProgressMax(const quint64 n); void AddToProgressMax(const quint64 n);
@ -120,9 +120,9 @@ class CollectionWatcher : public QObject {
SongList readded_songs; SongList readded_songs;
SongList new_songs; SongList new_songs;
SongList touched_songs; SongList touched_songs;
SubdirectoryList new_subdirs; CollectionSubdirectoryList new_subdirs;
SubdirectoryList touched_subdirs; CollectionSubdirectoryList touched_subdirs;
SubdirectoryList deleted_subdirs; CollectionSubdirectoryList deleted_subdirs;
QStringList files_changed_path_; QStringList files_changed_path_;
@ -155,7 +155,7 @@ class CollectionWatcher : public QObject {
QMultiMap<QString, Song> cached_songs_missing_fingerprint_; QMultiMap<QString, Song> cached_songs_missing_fingerprint_;
bool cached_songs_missing_fingerprint_dirty_; bool cached_songs_missing_fingerprint_dirty_;
SubdirectoryList known_subdirs_; CollectionSubdirectoryList known_subdirs_;
bool known_subdirs_dirty_; bool known_subdirs_dirty_;
}; };
@ -168,7 +168,7 @@ class CollectionWatcher : public QObject {
void FullScanNow(); void FullScanNow();
void RescanTracksNow(); void RescanTracksNow();
void RescanPathsNow(); 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: private:
static bool FindSongsByPath(const SongList &songs, const QString &path, SongList *out); 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); inline static QString DirectoryPart(const QString &fileName);
QString PickBestImage(const QStringList &images); QString PickBestImage(const QStringList &images);
QUrl ImageForSong(const QString &path, QMap<QString, QStringList> &album_art); QUrl ImageForSong(const QString &path, QMap<QString, QStringList> &album_art);
void AddWatch(const Directory &dir, const QString &path); void AddWatch(const CollectionDirectory &dir, const QString &path);
void RemoveWatch(const Directory &dir, const Subdirectory &subdir); void RemoveWatch(const CollectionDirectory &dir, const CollectionSubdirectory &subdir);
static quint64 GetMtimeForCue(const QString &cue_path); static quint64 GetMtimeForCue(const QString &cue_path);
void PerformScan(const bool incremental, const bool ignore_mtimes); 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); static void AddChangedSong(const QString &file, const Song &matching_song, const Song &new_song, ScanTransaction *t);
quint64 FilesCountForPath(ScanTransaction *t, const QString &path); 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); QString FindCueFilename(const QString &filename);
@ -207,7 +207,7 @@ class CollectionWatcher : public QObject {
FileSystemWatcherInterface *fs_watcher_; FileSystemWatcherInterface *fs_watcher_;
QThread *original_thread_; 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. // 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. // 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 abort_requested_;
bool rescan_in_progress_; // True if RescanTracksNow() has been called and is working. 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 *rescan_timer_;
QTimer *periodic_scan_timer_; QTimer *periodic_scan_timer_;
QMap<int, QStringList> rescan_queue_; // dir id -> list of subdirs to be scanned 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_untagged_->setCheckable(true);
collection_show_all_->setChecked(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); QAction *collection_config_action = new QAction(IconLoader::Load("configure"), tr("Configure collection..."), this);
QObject::connect(collection_config_action, &QAction::triggered, this, &MainWindow::ShowCollectionConfig); 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_) { 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_) { else if (action == collection_show_untagged_) {
collection_view_->filter_widget()->SetQueryMode(QueryOptions::QueryMode_Untagged); collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode_Untagged);
} }
else { 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 PlaylistCopyUrl();
void ShowInCollection(); void ShowInCollection();
void ChangeCollectionQueryMode(QAction *action); void ChangeCollectionFilterMode(QAction *action);
void PlayIndex(const QModelIndex &idx, Playlist::AutoScroll autoscroll); void PlayIndex(const QModelIndex &idx, Playlist::AutoScroll autoscroll);
void PlaylistDoubleClick(const QModelIndex &idx); void PlaylistDoubleClick(const QModelIndex &idx);

View File

@ -54,7 +54,7 @@
#ifdef HAVE_GSTREAMER #ifdef HAVE_GSTREAMER
# include "engine/gstenginepipeline.h" # include "engine/gstenginepipeline.h"
#endif #endif
#include "collection/directory.h" #include "collection/collectiondirectory.h"
#include "playlist/playlistitem.h" #include "playlist/playlistitem.h"
#include "playlist/playlistsequence.h" #include "playlist/playlistsequence.h"
#include "covermanager/albumcoverloaderresult.h" #include "covermanager/albumcoverloaderresult.h"
@ -98,10 +98,10 @@ void RegisterMetaTypes() {
qRegisterMetaTypeStreamOperators<QMap<int, Qt::Alignment>>("ColumnAlignmentMap"); qRegisterMetaTypeStreamOperators<QMap<int, Qt::Alignment>>("ColumnAlignmentMap");
qRegisterMetaTypeStreamOperators<QMap<int, int>>("ColumnAlignmentIntMap"); qRegisterMetaTypeStreamOperators<QMap<int, int>>("ColumnAlignmentIntMap");
#endif #endif
qRegisterMetaType<Directory>("Directory"); qRegisterMetaType<CollectionDirectory>("Directory");
qRegisterMetaType<DirectoryList>("DirectoryList"); qRegisterMetaType<CollectionDirectoryList>("DirectoryList");
qRegisterMetaType<Subdirectory>("Subdirectory"); qRegisterMetaType<CollectionSubdirectory>("Subdirectory");
qRegisterMetaType<SubdirectoryList>("SubdirectoryList"); qRegisterMetaType<CollectionSubdirectoryList>("SubdirectoryList");
qRegisterMetaType<Song>("Song"); qRegisterMetaType<Song>("Song");
qRegisterMetaType<SongList>("SongList"); qRegisterMetaType<SongList>("SongList");
qRegisterMetaType<SongMap>("SongMap"); qRegisterMetaType<SongMap>("SongMap");

View File

@ -60,6 +60,7 @@
#include "core/song.h" #include "core/song.h"
#include "core/iconloader.h" #include "core/iconloader.h"
#include "collection/collectionfilteroptions.h"
#include "collection/collectionbackend.h" #include "collection/collectionbackend.h"
#include "settings/collectionsettingspage.h" #include "settings/collectionsettingspage.h"
#include "organize/organizeformat.h" #include "organize/organizeformat.h"
@ -654,9 +655,9 @@ void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, co
if (song.source() == Song::Source_Collection) { if (song.source() == Song::Source_Collection) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #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 #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 #endif
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>(); QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [this, watcher, song, result]() { 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 (song.source() == Song::Source_Collection) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #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 #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 #endif
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>(); QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [this, watcher, song, cover_filename]() { QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [this, watcher, song, cover_filename]() {

View File

@ -29,7 +29,7 @@
#include "core/database.h" #include "core/database.h"
#include "collection/collectionbackend.h" #include "collection/collectionbackend.h"
#include "collection/collectionmodel.h" #include "collection/collectionmodel.h"
#include "collection/directory.h" #include "collection/collectiondirectory.h"
#include "connecteddevice.h" #include "connecteddevice.h"
#include "devicelister.h" #include "devicelister.h"
#include "devicemanager.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) { 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()) { if (first_time || directories.isEmpty()) {
backend_->AddDirectory(mount_point); 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... // 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). // 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) { if (dir.path != mount_point) {
// The directory is different, commence the munging. // The directory is different, commence the munging.
qLog(Info) << "Changing path from" << dir.path << "to" << mount_point; 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 // Check the signal was emitted correctly
ASSERT_EQ(1, spy.count()); 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(QFileInfo("/tmp").canonicalFilePath(), dir.path);
EXPECT_EQ(1, dir.id); EXPECT_EQ(1, dir.id);
EXPECT_EQ(0, spy[0][1].value<SubdirectoryList>().size()); EXPECT_EQ(0, spy[0][1].value<SubdirectoryList>().size());
@ -93,7 +93,7 @@ TEST_F(CollectionBackendTest, AddDirectory) {
TEST_F(CollectionBackendTest, RemoveDirectory) { TEST_F(CollectionBackendTest, RemoveDirectory) {
// Add a directory // Add a directory
Directory dir; CollectionDirectory dir;
dir.id = 1; dir.id = 1;
dir.path = "/tmp"; dir.path = "/tmp";
backend_->AddDirectory(dir.path); backend_->AddDirectory(dir.path);

View File

@ -28,16 +28,16 @@
#include "core/song.h" #include "core/song.h"
#include "core/songloader.h" #include "core/songloader.h"
#include "collection/directory.h" #include "collection/collectiondirectory.h"
class MetatypesEnvironment : public ::testing::Environment { class MetatypesEnvironment : public ::testing::Environment {
public: public:
MetatypesEnvironment() = default; MetatypesEnvironment() = default;
void SetUp() override { void SetUp() override {
qRegisterMetaType<Directory>("Directory"); qRegisterMetaType<CollectionDirectory>("Directory");
qRegisterMetaType<DirectoryList>("DirectoryList"); qRegisterMetaType<CollectionDirectoryList>("DirectoryList");
qRegisterMetaType<Subdirectory>("Subdirectory"); qRegisterMetaType<CollectionSubdirectory>("Subdirectory");
qRegisterMetaType<SubdirectoryList>("SubdirectoryList"); qRegisterMetaType<CollectionSubdirectoryList>("SubdirectoryList");
qRegisterMetaType<SongList>("SongList"); qRegisterMetaType<SongList>("SongList");
qRegisterMetaType<QModelIndex>("QModelIndex"); qRegisterMetaType<QModelIndex>("QModelIndex");
qRegisterMetaType<SongLoader::Result>("SongLoader::Result"); qRegisterMetaType<SongLoader::Result>("SongLoader::Result");