parent
ea1e4541c0
commit
e477449cd4
|
@ -467,25 +467,6 @@ if(NOT CMAKE_CROSSCOMPILING)
|
|||
"
|
||||
QT_SQLITE_TEST
|
||||
)
|
||||
if(QT_SQLITE_TEST)
|
||||
# Check that we have sqlite3 with FTS5
|
||||
check_cxx_source_runs("
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
int main() {
|
||||
QSqlDatabase db = QSqlDatabase::addDatabase(\"QSQLITE\");
|
||||
db.setDatabaseName(\":memory:\");
|
||||
if (!db.open()) { return 1; }
|
||||
QSqlQuery q(db);
|
||||
q.prepare(\"CREATE VIRTUAL TABLE test_fts USING fts5(test, tokenize = 'unicode61 remove_diacritics 0');\");
|
||||
if (!q.exec()) return 1;
|
||||
}
|
||||
"
|
||||
SQLITE_FTS5_TEST
|
||||
)
|
||||
endif()
|
||||
unset(CMAKE_REQUIRED_FLAGS)
|
||||
unset(CMAKE_REQUIRED_LIBRARIES)
|
||||
endif()
|
||||
|
||||
# Set up definitions
|
||||
|
@ -546,11 +527,7 @@ if(QT_VERSION_MAJOR EQUAL 5)
|
|||
endif()
|
||||
|
||||
if(NOT CMAKE_CROSSCOMPILING)
|
||||
if(QT_SQLITE_TEST)
|
||||
if(NOT SQLITE_FTS5_TEST)
|
||||
message(WARNING "sqlite must be enabled with FTS5. See: https://www.sqlite.org/fts5.html")
|
||||
endif()
|
||||
else()
|
||||
if(NOT QT_SQLITE_TEST)
|
||||
message(WARNING "The Qt sqlite driver test failed.")
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -76,7 +76,7 @@ To build Strawberry from source you need the following installed on your system
|
|||
* [Boost](https://www.boost.org/)
|
||||
* [GLib](https://developer.gnome.org/glib/)
|
||||
* [Qt 6 or Qt 5.12 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
|
||||
* [SQLite 3.9 or newer with FTS5](https://www.sqlite.org)
|
||||
* [SQLite 3.9 or newer](https://www.sqlite.org)
|
||||
* [Protobuf](https://developers.google.com/protocol-buffers/)
|
||||
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
|
||||
* [D-Bus (Required on Linux)](https://www.freedesktop.org/wiki/Software/dbus/)
|
||||
|
|
|
@ -92,12 +92,14 @@ set(SOURCES
|
|||
collection/collectiondirectorymodel.cpp
|
||||
collection/collectionfilteroptions.cpp
|
||||
collection/collectionfilterwidget.cpp
|
||||
collection/collectionfilter.cpp
|
||||
collection/collectionplaylistitem.cpp
|
||||
collection/collectionquery.cpp
|
||||
collection/collectionqueryoptions.cpp
|
||||
collection/savedgroupingmanager.cpp
|
||||
collection/groupbydialog.cpp
|
||||
collection/collectiontask.cpp
|
||||
collection/collectionmodelupdate.cpp
|
||||
|
||||
playlist/playlist.cpp
|
||||
playlist/playlistbackend.cpp
|
||||
|
@ -349,6 +351,7 @@ set(HEADERS
|
|||
collection/collectionviewcontainer.h
|
||||
collection/collectiondirectorymodel.h
|
||||
collection/collectionfilterwidget.h
|
||||
collection/collectionfilter.h
|
||||
collection/savedgroupingmanager.h
|
||||
collection/groupbydialog.h
|
||||
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
using std::make_shared;
|
||||
|
||||
const char *SCollection::kSongsTable = "songs";
|
||||
const char *SCollection::kFtsTable = "songs_fts";
|
||||
const char *SCollection::kDirsTable = "directories";
|
||||
const char *SCollection::kSubdirsTable = "subdirectories";
|
||||
|
||||
|
@ -70,7 +69,7 @@ SCollection::SCollection(Application *app, QObject *parent)
|
|||
backend()->moveToThread(app->database()->thread());
|
||||
qLog(Debug) << &*backend_ << "moved to thread" << app->database()->thread();
|
||||
|
||||
backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, QLatin1String(kSongsTable), QLatin1String(kFtsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable));
|
||||
backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, QLatin1String(kSongsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable));
|
||||
|
||||
model_ = new CollectionModel(backend_, app_, this);
|
||||
|
||||
|
|
|
@ -76,16 +76,13 @@ CollectionBackend::~CollectionBackend() {
|
|||
|
||||
}
|
||||
|
||||
void CollectionBackend::Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &fts_table, const QString &dirs_table, const QString &subdirs_table) {
|
||||
|
||||
void CollectionBackend::Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table, const QString &subdirs_table) {
|
||||
db_ = db;
|
||||
task_manager_ = task_manager;
|
||||
source_ = source;
|
||||
songs_table_ = songs_table;
|
||||
dirs_table_ = dirs_table;
|
||||
subdirs_table_ = subdirs_table;
|
||||
fts_table_ = fts_table;
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::Close() {
|
||||
|
@ -123,6 +120,35 @@ void CollectionBackend::ReportErrors(const CollectionQuery &query) {
|
|||
|
||||
}
|
||||
|
||||
void CollectionBackend::GetAllSongsAsync(const int id) {
|
||||
metaObject()->invokeMethod(this, "GetAllSongs", Qt::QueuedConnection, Q_ARG(int, id));
|
||||
}
|
||||
|
||||
void CollectionBackend::GetAllSongs(const int id) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
SqlQuery q(db);
|
||||
q.setForwardOnly(true);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
if (!q.exec()) {
|
||||
db_->ReportErrors(q);
|
||||
emit GotSongs(SongList(), id);
|
||||
return;
|
||||
}
|
||||
|
||||
SongList songs;
|
||||
while (q.next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(q, true);
|
||||
songs << song;
|
||||
}
|
||||
|
||||
emit GotSongs(songs, id);
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::LoadDirectoriesAsync() {
|
||||
QMetaObject::invokeMethod(this, &CollectionBackend::LoadDirectories, Qt::QueuedConnection);
|
||||
}
|
||||
|
@ -596,7 +622,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
|||
ScopedTransaction transaction(&db);
|
||||
|
||||
SongList added_songs;
|
||||
SongList deleted_songs;
|
||||
SongList changed_songs;
|
||||
|
||||
for (const Song &song : songs) {
|
||||
|
||||
|
@ -633,19 +659,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec));
|
||||
song.BindToFtsQuery(&q);
|
||||
q.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
deleted_songs << old_song;
|
||||
added_songs << song;
|
||||
changed_songs << song;
|
||||
|
||||
continue;
|
||||
|
||||
|
@ -672,19 +686,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec));
|
||||
new_song.BindToFtsQuery(&q);
|
||||
q.BindValue(QStringLiteral(":id"), new_song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
deleted_songs << old_song;
|
||||
added_songs << new_song;
|
||||
changed_songs << new_song;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -707,17 +709,6 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
|||
|
||||
if (id == -1) return;
|
||||
|
||||
{ // Add to the FTS index
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("INSERT INTO %1 (ROWID, %2) VALUES (:id, %3)").arg(fts_table_, Song::kFtsColumnSpec, Song::kFtsBindSpec));
|
||||
q.BindValue(QStringLiteral(":id"), id);
|
||||
song.BindToFtsQuery(&q);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Song song_copy(song);
|
||||
song_copy.set_id(id);
|
||||
added_songs << song_copy;
|
||||
|
@ -726,8 +717,8 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
|||
|
||||
transaction.Commit();
|
||||
|
||||
if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs);
|
||||
if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs);
|
||||
if (!added_songs.isEmpty()) emit SongsAdded(added_songs);
|
||||
if (!changed_songs.isEmpty()) emit SongsChanged(changed_songs);
|
||||
|
||||
UpdateTotalSongCountAsync();
|
||||
UpdateTotalArtistCountAsync();
|
||||
|
@ -748,11 +739,12 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
|
|||
ScopedTransaction transaction(&db);
|
||||
|
||||
SongList added_songs;
|
||||
SongList changed_songs;
|
||||
SongList deleted_songs;
|
||||
|
||||
SongMap old_songs;
|
||||
{
|
||||
CollectionQuery query(db, songs_table_, fts_table_);
|
||||
CollectionQuery query(db, songs_table_);
|
||||
if (!ExecCollectionQuery(&query, old_songs)) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
|
@ -778,21 +770,10 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec));
|
||||
new_song.BindToFtsQuery(&q);
|
||||
q.BindValue(QStringLiteral(":id"), old_song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
deleted_songs << old_song;
|
||||
Song new_song_copy(new_song);
|
||||
new_song_copy.set_id(old_song.id());
|
||||
added_songs << new_song_copy;
|
||||
changed_songs << new_song_copy;
|
||||
|
||||
}
|
||||
|
||||
|
@ -813,17 +794,6 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
|
|||
|
||||
if (id == -1) return;
|
||||
|
||||
{ // Add to the FTS index
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("INSERT INTO %1 (ROWID, %2) VALUES (:id, %3)").arg(fts_table_, Song::kFtsColumnSpec, Song::kFtsBindSpec));
|
||||
q.BindValue(QStringLiteral(":id"), id);
|
||||
new_song.BindToFtsQuery(&q);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Song new_song_copy(new_song);
|
||||
new_song_copy.set_id(id);
|
||||
added_songs << new_song_copy;
|
||||
|
@ -843,15 +813,6 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_));
|
||||
q.BindValue(QStringLiteral(":id"), old_song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
deleted_songs << old_song;
|
||||
}
|
||||
}
|
||||
|
@ -859,7 +820,8 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
|
|||
transaction.Commit();
|
||||
|
||||
if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs);
|
||||
if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs);
|
||||
if (!added_songs.isEmpty()) emit SongsAdded(added_songs);
|
||||
if (!changed_songs.isEmpty()) emit SongsChanged(changed_songs);
|
||||
|
||||
UpdateTotalSongCountAsync();
|
||||
UpdateTotalArtistCountAsync();
|
||||
|
@ -872,11 +834,10 @@ void CollectionBackend::UpdateMTimesOnly(const SongList &songs) {
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id").arg(songs_table_));
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const Song &song : songs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":mtime"), song.mtime());
|
||||
q.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!q.Exec()) {
|
||||
|
@ -893,25 +854,17 @@ void CollectionBackend::DeleteSongs(const SongList &songs) {
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
SqlQuery remove(db);
|
||||
remove.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
|
||||
SqlQuery remove_fts(db);
|
||||
remove_fts.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_));
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const Song &song : songs) {
|
||||
remove.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!remove.Exec()) {
|
||||
db_->ReportErrors(remove);
|
||||
return;
|
||||
}
|
||||
|
||||
remove_fts.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!remove_fts.Exec()) {
|
||||
db_->ReportErrors(remove_fts);
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
emit SongsDeleted(songs);
|
||||
|
@ -944,7 +897,7 @@ void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool u
|
|||
emit SongsDeleted(songs);
|
||||
}
|
||||
else {
|
||||
emit SongsDiscovered(songs);
|
||||
emit SongsAdded(songs);
|
||||
}
|
||||
|
||||
UpdateTotalSongCountAsync();
|
||||
|
@ -958,7 +911,7 @@ QStringList CollectionBackend::GetAll(const QString &column, const CollectionFil
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_, filter_options);
|
||||
CollectionQuery query(db, songs_table_, filter_options);
|
||||
query.SetColumnSpec(QStringLiteral("DISTINCT ") + column);
|
||||
query.AddCompilationRequirement(false);
|
||||
|
||||
|
@ -986,13 +939,13 @@ QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOpt
|
|||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
// Albums with 'albumartist' field set:
|
||||
CollectionQuery query(db, songs_table_, fts_table_, opt);
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.SetColumnSpec(QStringLiteral("DISTINCT albumartist"));
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!="));
|
||||
|
||||
// Albums with no 'albumartist' (extract 'artist'):
|
||||
CollectionQuery query2(db, songs_table_, fts_table_, opt);
|
||||
CollectionQuery query2(db, songs_table_, opt);
|
||||
query2.SetColumnSpec(QStringLiteral("DISTINCT artist"));
|
||||
query2.AddCompilationRequirement(false);
|
||||
query2.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!="));
|
||||
|
@ -1033,7 +986,7 @@ SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist,
|
|||
QSqlDatabase db(db_->Connect());
|
||||
QMutexLocker l(db_->Mutex());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_, opt);
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
|
||||
|
@ -1051,7 +1004,7 @@ SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist,
|
|||
QSqlDatabase db(db_->Connect());
|
||||
QMutexLocker l(db_->Mutex());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_, opt);
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
@ -1070,7 +1023,7 @@ SongList CollectionBackend::GetSongsByAlbum(const QString &album, const Collecti
|
|||
QSqlDatabase db(db_->Connect());
|
||||
QMutexLocker l(db_->Mutex());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_, opt);
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
|
@ -1207,11 +1160,10 @@ Song CollectionBackend::GetSongByUrl(const QUrl &url, const qint64 beginning) {
|
|||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND beginning = :beginning AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
|
||||
q.BindValue(QStringLiteral(":url1"), url);
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded());
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":beginning"), beginning);
|
||||
|
||||
if (!q.Exec()) {
|
||||
|
@ -1237,11 +1189,10 @@ Song CollectionBackend::GetSongByUrlAndTrack(const QUrl &url, const int track) {
|
|||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND track = :track AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
|
||||
q.BindValue(QStringLiteral(":url1"), url);
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded());
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":track"), track);
|
||||
|
||||
if (!q.Exec()) {
|
||||
|
@ -1267,23 +1218,21 @@ SongList CollectionBackend::GetSongsByUrl(const QUrl &url, const bool unavailabl
|
|||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = :unavailable").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
|
||||
q.BindValue(QStringLiteral(":url1"), url);
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded());
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":unavailable"), (unavailable ? 1 : 0));
|
||||
|
||||
SongList songs;
|
||||
if (q.Exec()) {
|
||||
while (q.next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(q, true);
|
||||
songs << song;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return SongList();
|
||||
}
|
||||
while (q.next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(q, true);
|
||||
songs << song;
|
||||
}
|
||||
|
||||
return songs;
|
||||
|
@ -1377,7 +1326,7 @@ SongList CollectionBackend::GetCompilationSongs(const QString &album, const Coll
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_, opt);
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
|
||||
query.AddCompilationRequirement(true);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
@ -1434,8 +1383,7 @@ void CollectionBackend::CompilationsNeedUpdating() {
|
|||
}
|
||||
|
||||
// Now mark the songs that we think are in compilations
|
||||
SongList deleted_songs;
|
||||
SongList added_songs;
|
||||
SongList changed_songs;
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
|
||||
|
@ -1448,12 +1396,12 @@ void CollectionBackend::CompilationsNeedUpdating() {
|
|||
for (const QUrl &url : info.urls) {
|
||||
if (info.artists.count() > 1) { // This directory+album is a compilation.
|
||||
if (info.has_not_compilation_detected > 0) { // Run updates if any of the songs is not marked as compilations.
|
||||
UpdateCompilations(db, deleted_songs, added_songs, url, true);
|
||||
UpdateCompilations(db, changed_songs, url, true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (info.has_compilation_detected > 0) {
|
||||
UpdateCompilations(db, deleted_songs, added_songs, url, false);
|
||||
UpdateCompilations(db, changed_songs, url, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1461,29 +1409,27 @@ void CollectionBackend::CompilationsNeedUpdating() {
|
|||
|
||||
transaction.Commit();
|
||||
|
||||
if (!deleted_songs.isEmpty()) {
|
||||
emit SongsDeleted(deleted_songs);
|
||||
emit SongsDiscovered(added_songs);
|
||||
if (!changed_songs.isEmpty()) {
|
||||
emit SongsChanged(changed_songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_songs, const QUrl &url, const bool compilation_detected) {
|
||||
bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &changed_songs, const QUrl &url, const bool compilation_detected) {
|
||||
|
||||
{ // Get song, so we can tell the model its updated
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(QStringLiteral(":url1"), url);
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded());
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
if (q.Exec()) {
|
||||
while (q.next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(q, true);
|
||||
deleted_songs << song;
|
||||
song.set_compilation_detected(compilation_detected);
|
||||
added_songs << song;
|
||||
changed_songs << song;
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -1496,10 +1442,10 @@ bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &del
|
|||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET compilation_detected = :compilation_detected, compilation_effective = ((compilation OR :compilation_detected OR compilation_on) AND NOT compilation_off) + 0 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":compilation_detected"), static_cast<int>(compilation_detected));
|
||||
q.BindValue(QStringLiteral(":url1"), url);
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url3"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded());
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return false;
|
||||
|
@ -1514,7 +1460,7 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_, opt);
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.SetColumnSpec(QStringLiteral("url, filetype, cue_path, effective_albumartist, album, compilation_effective, art_embedded, art_automatic, art_manual, art_unset"));
|
||||
query.SetOrderBy(QStringLiteral("effective_albumartist, album, url"));
|
||||
|
||||
|
@ -1605,7 +1551,7 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective
|
|||
ret.album = album;
|
||||
ret.album_artist = effective_albumartist;
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_);
|
||||
CollectionQuery query(db, songs_table_);
|
||||
query.SetColumnSpec(QStringLiteral("url, art_embedded, art_automatic, art_manual, art_unset"));
|
||||
if (!effective_albumartist.isEmpty()) {
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
|
@ -1640,54 +1586,37 @@ void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumart
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
// Get the songs before they're updated
|
||||
CollectionQuery query(db, songs_table_, fts_table_);
|
||||
query.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_embedded = :art_embedded, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":art_embedded"), art_embedded ? 1 : 0);
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SongList deleted_songs;
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
deleted_songs << song;
|
||||
SongList songs;
|
||||
{
|
||||
CollectionQuery q(db, songs_table_);
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
q.AddWhere(QStringLiteral("album"), album);
|
||||
if (!q.Exec()) {
|
||||
ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
while (q.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(q, true);
|
||||
songs << song;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the songs
|
||||
QString sql = QStringLiteral("UPDATE %1 SET art_embedded = :art_embedded, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_);
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(sql);
|
||||
q.BindValue(QStringLiteral(":art_embedded"), art_embedded ? 1 : 0);
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now get the updated songs
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
}
|
||||
|
||||
SongList added_songs;
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
added_songs << song;
|
||||
}
|
||||
|
||||
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
|
||||
emit SongsDeleted(deleted_songs);
|
||||
emit SongsDiscovered(added_songs);
|
||||
if (!songs.isEmpty()) {
|
||||
emit SongsChanged(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1703,49 +1632,37 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_);
|
||||
query.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_manual = :art_manual, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":art_manual"), art_manual.isValid() ? art_manual.toString(QUrl::FullyEncoded) : QLatin1String(""));
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SongList deleted_songs;
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
deleted_songs << song;
|
||||
SongList songs;
|
||||
{
|
||||
CollectionQuery q(db, songs_table_);
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
q.AddWhere(QStringLiteral("album"), album);
|
||||
if (!q.Exec()) {
|
||||
ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
while (q.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(q, true);
|
||||
songs << song;
|
||||
}
|
||||
}
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_manual = :art_manual, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":art_manual"), art_manual.isValid() ? art_manual.toString(QUrl::FullyEncoded) : QLatin1String(""));
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
}
|
||||
|
||||
SongList added_songs;
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
added_songs << song;
|
||||
}
|
||||
|
||||
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
|
||||
emit SongsDeleted(deleted_songs);
|
||||
emit SongsDiscovered(added_songs);
|
||||
if (!songs.isEmpty()) {
|
||||
emit SongsChanged(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1761,48 +1678,36 @@ void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, cons
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_);
|
||||
query.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_unset = 1, art_manual = '', art_automatic = '', art_embedded = '' WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SongList deleted_songs;
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
deleted_songs << song;
|
||||
SongList songs;
|
||||
{
|
||||
CollectionQuery q(db, songs_table_);
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
q.AddWhere(QStringLiteral("album"), album);
|
||||
if (!q.Exec()) {
|
||||
ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
while (q.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(q, true);
|
||||
songs << song;
|
||||
}
|
||||
}
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_unset = 1, art_manual = '', art_automatic = '', art_embedded = '' WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
}
|
||||
|
||||
SongList added_songs;
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
added_songs << song;
|
||||
}
|
||||
|
||||
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
|
||||
emit SongsDeleted(deleted_songs);
|
||||
emit SongsDiscovered(added_songs);
|
||||
if (!songs.isEmpty()) {
|
||||
emit SongsChanged(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1818,49 +1723,37 @@ void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, cons
|
|||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, fts_table_);
|
||||
query.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_embedded = 0, art_automatic = '', art_manual = '', art_unset = :art_unset WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":art_unset"), art_unset ? 1 : 0);
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SongList deleted_songs;
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
deleted_songs << song;
|
||||
SongList songs;
|
||||
{
|
||||
CollectionQuery q(db, songs_table_);
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
q.AddWhere(QStringLiteral("album"), album);
|
||||
if (!q.Exec()) {
|
||||
ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
while (q.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(q, true);
|
||||
songs << song;
|
||||
}
|
||||
}
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_embedded = 0, art_automatic = '', art_manual = '', art_unset = :art_unset WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(QStringLiteral(":art_unset"), art_unset ? 1 : 0);
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
}
|
||||
|
||||
SongList added_songs;
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
added_songs << song;
|
||||
}
|
||||
|
||||
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
|
||||
emit SongsDeleted(deleted_songs);
|
||||
emit SongsDiscovered(added_songs);
|
||||
if (!songs.isEmpty()) {
|
||||
emit SongsChanged(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1869,25 +1762,9 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList
|
|||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
SongList deleted_songs, added_songs;
|
||||
SongList songs;
|
||||
|
||||
for (const QString &artist : artists) {
|
||||
// Get the songs before they're updated
|
||||
CollectionQuery query(db, songs_table_, fts_table_);
|
||||
query.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
if (!artist.isEmpty()) query.AddWhere(QStringLiteral("artist"), artist);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
}
|
||||
|
||||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
deleted_songs << song;
|
||||
}
|
||||
|
||||
// Update the songs
|
||||
QString sql(QStringLiteral("UPDATE %1 SET compilation_on = :compilation_on, compilation_off = :compilation_off, compilation_effective = ((compilation OR compilation_detected OR :compilation_on) AND NOT :compilation_off) + 0 WHERE album = :album AND unavailable = 0").arg(songs_table_));
|
||||
|
@ -1905,7 +1782,13 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList
|
|||
return;
|
||||
}
|
||||
|
||||
// Now get the updated songs
|
||||
// Get the updated songs
|
||||
|
||||
CollectionQuery query(db, songs_table_);
|
||||
query.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
if (!artist.isEmpty()) query.AddWhere(QStringLiteral("artist"), artist);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
return;
|
||||
|
@ -1914,13 +1797,12 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList
|
|||
while (query.Next()) {
|
||||
Song song(source_);
|
||||
song.InitFromQuery(query, true);
|
||||
added_songs << song;
|
||||
songs << song;
|
||||
}
|
||||
}
|
||||
|
||||
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
|
||||
emit SongsDeleted(deleted_songs);
|
||||
emit SongsDiscovered(added_songs);
|
||||
if (!songs.isEmpty()) {
|
||||
emit SongsChanged(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2035,15 +1917,6 @@ void CollectionBackend::DeleteAll() {
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM ") + fts_table_);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
t.Commit();
|
||||
}
|
||||
|
||||
|
|
|
@ -80,12 +80,13 @@ class CollectionBackendInterface : public QObject {
|
|||
using AlbumList = QList<Album>;
|
||||
|
||||
virtual QString songs_table() const = 0;
|
||||
virtual QString fts_table() const = 0;
|
||||
|
||||
virtual Song::Source source() const = 0;
|
||||
|
||||
virtual SharedPtr<Database> db() const = 0;
|
||||
|
||||
virtual void GetAllSongsAsync(const int id = 0) = 0;
|
||||
|
||||
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
|
||||
virtual void LoadDirectoriesAsync() = 0;
|
||||
|
||||
|
@ -145,7 +146,8 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||
|
||||
~CollectionBackend();
|
||||
|
||||
void Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &fts_table, const QString &dirs_table = QString(), const QString &subdirs_table = QString());
|
||||
void Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table = QString(), const QString &subdirs_table = QString());
|
||||
|
||||
void Close();
|
||||
|
||||
void ExitAsync();
|
||||
|
@ -157,10 +159,11 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||
SharedPtr<Database> db() const override { return db_; }
|
||||
|
||||
QString songs_table() const override { return songs_table_; }
|
||||
QString fts_table() const override { return fts_table_; }
|
||||
QString dirs_table() const { return dirs_table_; }
|
||||
QString subdirs_table() const { return subdirs_table_; }
|
||||
|
||||
void GetAllSongsAsync(const int id = 0) override;
|
||||
|
||||
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
|
||||
void LoadDirectoriesAsync() override;
|
||||
|
||||
|
@ -235,6 +238,7 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||
|
||||
public slots:
|
||||
void Exit();
|
||||
void GetAllSongs(const int id);
|
||||
void LoadDirectories();
|
||||
void UpdateTotalSongCount();
|
||||
void UpdateTotalArtistCount();
|
||||
|
@ -275,8 +279,10 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||
void DirectoryAdded(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdir);
|
||||
void DirectoryDeleted(const CollectionDirectory &dir);
|
||||
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
void GotSongs(const SongList &songs, const int id);
|
||||
void SongsAdded(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
void SongsChanged(const SongList &songs);
|
||||
void SongsStatisticsChanged(const SongList &songs, const bool save_tags = false);
|
||||
|
||||
void DatabaseReset();
|
||||
|
@ -301,7 +307,7 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||
int has_not_compilation_detected;
|
||||
};
|
||||
|
||||
bool UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_songs, const QUrl &url, const bool compilation_detected);
|
||||
bool UpdateCompilations(const QSqlDatabase &db, SongList &changed_songs, const QUrl &url, const bool compilation_detected);
|
||||
AlbumList GetAlbums(const QString &artist, const QString &album_artist, const bool compilation_required = false, const CollectionFilterOptions &opt = CollectionFilterOptions());
|
||||
AlbumList GetAlbums(const QString &artist, const bool compilation_required, const CollectionFilterOptions &opt = CollectionFilterOptions());
|
||||
CollectionSubdirectoryList SubdirsInDirectory(const int id, QSqlDatabase &db);
|
||||
|
@ -319,7 +325,6 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||
QString songs_table_;
|
||||
QString dirs_table_;
|
||||
QString subdirs_table_;
|
||||
QString fts_table_;
|
||||
QThread *original_thread_;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "utilities/timeconstants.h"
|
||||
#include "utilities/searchparserutils.h"
|
||||
|
||||
#include "collectionfilter.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionitem.h"
|
||||
|
||||
const QStringList CollectionFilter::Operators = QStringList() << QStringLiteral(":")
|
||||
<< QStringLiteral("=")
|
||||
<< QStringLiteral("==")
|
||||
<< QStringLiteral("<>")
|
||||
<< QStringLiteral("<")
|
||||
<< QStringLiteral("<=")
|
||||
<< QStringLiteral(">")
|
||||
<< QStringLiteral(">=");
|
||||
|
||||
CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent) {}
|
||||
|
||||
bool CollectionFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const {
|
||||
|
||||
CollectionModel *model = qobject_cast<CollectionModel*>(sourceModel());
|
||||
if (!model) return false;
|
||||
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
|
||||
if (!idx.isValid()) return false;
|
||||
CollectionItem *item = model->IndexToItem(idx);
|
||||
if (!item) return false;
|
||||
|
||||
if (item->type == CollectionItem::Type::LoadingIndicator) return true;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QString filter_text = filterRegularExpression().pattern().remove(QLatin1Char('\\'));
|
||||
#else
|
||||
QString filter_text = filterRegExp().pattern();
|
||||
#endif
|
||||
|
||||
if (filter_text.isEmpty()) return true;
|
||||
|
||||
filter_text = filter_text.replace(QRegularExpression(QStringLiteral("\\s*:\\s*")), QStringLiteral(":"))
|
||||
.replace(QRegularExpression(QStringLiteral("\\s*=\\s*")), QStringLiteral("="))
|
||||
.replace(QRegularExpression(QStringLiteral("\\s*==\\s*")), QStringLiteral("=="))
|
||||
.replace(QRegularExpression(QStringLiteral("\\s*<>\\s*")), QStringLiteral("<>"))
|
||||
.replace(QRegularExpression(QStringLiteral("\\s*<\\s*")), QStringLiteral("<"))
|
||||
.replace(QRegularExpression(QStringLiteral("\\s*>\\s*")), QStringLiteral(">"))
|
||||
.replace(QRegularExpression(QStringLiteral("\\s*<=\\s*")), QStringLiteral("<="))
|
||||
.replace(QRegularExpression(QStringLiteral("\\s*>=\\s*")), QStringLiteral(">="));
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
#else
|
||||
const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts);
|
||||
#endif
|
||||
|
||||
filter_text.clear();
|
||||
|
||||
FilterList filters;
|
||||
static QRegularExpression operator_regex(QStringLiteral("(=|<[>=]?|>=?|!=)"));
|
||||
for (int i = 0; i < tokens.count(); ++i) {
|
||||
const QString &token = tokens[i];
|
||||
if (token.contains(QLatin1Char(':'))) {
|
||||
QString field = token.section(QLatin1Char(':'), 0, 0).remove(QLatin1Char(':')).trimmed();
|
||||
QString value = token.section(QLatin1Char(':'), 1, -1).remove(QLatin1Char(':')).trimmed();
|
||||
if (field.isEmpty() || value.isEmpty()) continue;
|
||||
if (Song::kTextSearchColumns.contains(field, Qt::CaseInsensitive) && value.count(QLatin1Char('"')) <= 2) {
|
||||
bool quotation_mark_start = false;
|
||||
bool quotation_mark_end = false;
|
||||
if (value.left(1) == QLatin1Char('"')) {
|
||||
value.remove(0, 1);
|
||||
quotation_mark_start = true;
|
||||
if (value.length() >= 1 && value.count(QLatin1Char('"')) == 1) {
|
||||
value = value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed();
|
||||
quotation_mark_end = true;
|
||||
}
|
||||
}
|
||||
for (int y = i + 1; y < tokens.count() && !quotation_mark_end; ++y) {
|
||||
QString next_value = tokens[y];
|
||||
if (!quotation_mark_start && ContainsOperators(next_value)) {
|
||||
break;
|
||||
}
|
||||
if (quotation_mark_start && next_value.contains(QLatin1Char('"'))) {
|
||||
next_value = next_value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed();
|
||||
quotation_mark_end = true;
|
||||
}
|
||||
value.append(QLatin1Char(' ') + next_value);
|
||||
i = y;
|
||||
}
|
||||
if (!field.isEmpty() && !value.isEmpty()) {
|
||||
filters.insert(field, Filter(field, value));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (token.contains(operator_regex)) {
|
||||
QRegularExpressionMatch re_match = operator_regex.match(token);
|
||||
if (re_match.hasMatch()) {
|
||||
const QString foperator = re_match.captured(0);
|
||||
const QString field = token.section(foperator, 0, 0).remove(foperator).trimmed();
|
||||
const QString value = token.section(foperator, 1, -1).remove(foperator).trimmed();
|
||||
if (value.isEmpty()) continue;
|
||||
if (Song::kNumericalSearchColumns.contains(field, Qt::CaseInsensitive)) {
|
||||
if (Song::kIntSearchColumns.contains(field, Qt::CaseInsensitive)) {
|
||||
bool ok = false;
|
||||
const int value_int = value.toInt(&ok);
|
||||
if (ok) {
|
||||
filters.insert(field, Filter(field, value_int, foperator));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (Song::kUIntSearchColumns.contains(field, Qt::CaseInsensitive)) {
|
||||
bool ok = false;
|
||||
const uint value_uint = value.toUInt(&ok);
|
||||
if (ok) {
|
||||
filters.insert(field, Filter(field, value_uint, foperator));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (field.compare(QStringLiteral("length"), Qt::CaseInsensitive) == 0) {
|
||||
filters.insert(field, Filter(field, static_cast<qint64>(Utilities::ParseSearchTime(value)) * kNsecPerSec, foperator));
|
||||
continue;
|
||||
}
|
||||
else if (field.compare(QStringLiteral("rating"), Qt::CaseInsensitive) == 0) {
|
||||
filters.insert(field, Filter(field, Utilities::ParseSearchRating(value), foperator));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!filter_text.isEmpty()) filter_text.append(QLatin1Char(' '));
|
||||
filter_text += token;
|
||||
}
|
||||
|
||||
if (filter_text.isEmpty() && filters.isEmpty()) return true;
|
||||
|
||||
return ItemMatchesFilters(item, filters, filter_text);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text) {
|
||||
|
||||
if (item->type == CollectionItem::Type::Song &&
|
||||
item->metadata.is_valid() &&
|
||||
ItemMetadataMatchesFilters(item->metadata, filters, filter_text)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (CollectionItem *child : std::as_const(item->children)) {
|
||||
if (ItemMatchesFilters(child, filters, filter_text)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QString &filter_text) {
|
||||
|
||||
for (FilterList::const_iterator it = filters.begin() ; it != filters.end() ; ++it) {
|
||||
const QString &field = it.key();
|
||||
const Filter &filter = it.value();
|
||||
const QVariant &value = filter.value;
|
||||
const QString &foperator = filter.foperator;
|
||||
if (field.isEmpty() || !value.isValid()) {
|
||||
continue;
|
||||
}
|
||||
const QVariant data = DataFromField(field, metadata);
|
||||
if (
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
value.metaType() != data.metaType()
|
||||
#else
|
||||
value.type() != data.type()
|
||||
#endif
|
||||
|| !FieldValueMatchesData(value, data, foperator)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return filter_text.isEmpty() || ItemMetadataMatchesFilterText(metadata, filter_text);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::ItemMetadataMatchesFilterText(const Song &metadata, const QString &filter_text) {
|
||||
|
||||
return metadata.effective_albumartist().contains(filter_text, Qt::CaseInsensitive) ||
|
||||
metadata.artist().contains(filter_text, Qt::CaseInsensitive) ||
|
||||
metadata.album().contains(filter_text, Qt::CaseInsensitive) ||
|
||||
metadata.title().contains(filter_text, Qt::CaseInsensitive) ||
|
||||
metadata.composer().contains(filter_text, Qt::CaseInsensitive) ||
|
||||
metadata.performer().contains(filter_text, Qt::CaseInsensitive) ||
|
||||
metadata.grouping().contains(filter_text, Qt::CaseInsensitive) ||
|
||||
metadata.genre().contains(filter_text, Qt::CaseInsensitive) ||
|
||||
metadata.comment().contains(filter_text, Qt::CaseInsensitive);
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionFilter::DataFromField(const QString &field, const Song &metadata) {
|
||||
|
||||
if (field == QStringLiteral("albumartist")) return metadata.effective_albumartist();
|
||||
if (field == QStringLiteral("artist")) return metadata.artist();
|
||||
if (field == QStringLiteral("album")) return metadata.album();
|
||||
if (field == QStringLiteral("title")) return metadata.title();
|
||||
if (field == QStringLiteral("composer")) return metadata.composer();
|
||||
if (field == QStringLiteral("performer")) return metadata.performer();
|
||||
if (field == QStringLiteral("grouping")) return metadata.grouping();
|
||||
if (field == QStringLiteral("genre")) return metadata.genre();
|
||||
if (field == QStringLiteral("comment")) return metadata.comment();
|
||||
if (field == QStringLiteral("track")) return metadata.track();
|
||||
if (field == QStringLiteral("year")) return metadata.year();
|
||||
if (field == QStringLiteral("length")) return metadata.length_nanosec();
|
||||
if (field == QStringLiteral("samplerate")) return metadata.samplerate();
|
||||
if (field == QStringLiteral("bitdepth")) return metadata.bitdepth();
|
||||
if (field == QStringLiteral("bitrate")) return metadata.bitrate();
|
||||
if (field == QStringLiteral("rating")) return metadata.rating();
|
||||
if (field == QStringLiteral("playcount")) return metadata.playcount();
|
||||
if (field == QStringLiteral("skipcount")) return metadata.skipcount();
|
||||
|
||||
return QVariant();
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator) {
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
switch (value.metaType().id()) {
|
||||
#else
|
||||
switch (value.userType()) {
|
||||
#endif
|
||||
case QMetaType::QString:{
|
||||
const QString str_value = value.toString();
|
||||
const QString str_data = data.toString();
|
||||
return str_data.contains(str_value, Qt::CaseInsensitive);
|
||||
}
|
||||
case QMetaType::Int:{
|
||||
return FieldIntValueMatchesData(value.toInt(), foperator, data.toInt());
|
||||
}
|
||||
case QMetaType::UInt:{
|
||||
return FieldUIntValueMatchesData(value.toUInt(), foperator, data.toUInt());
|
||||
}
|
||||
case QMetaType::LongLong:{
|
||||
return FieldLongLongValueMatchesData(value.toLongLong(), foperator, data.toLongLong());
|
||||
}
|
||||
case QMetaType::Float:{
|
||||
return FieldFloatValueMatchesData(value.toFloat(), foperator, data.toFloat());
|
||||
}
|
||||
default:{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool CollectionFilter::FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data) {
|
||||
|
||||
if (foperator == QStringLiteral("=") || foperator == QStringLiteral("==")) {
|
||||
return data == value;
|
||||
}
|
||||
else if (foperator == QStringLiteral("!=") || foperator == QStringLiteral("<>")) {
|
||||
return data != value;
|
||||
}
|
||||
else if (foperator == QStringLiteral("<")) {
|
||||
return data < value;
|
||||
}
|
||||
else if (foperator == QStringLiteral(">")) {
|
||||
return data > value;
|
||||
}
|
||||
else if (foperator == QStringLiteral(">=")) {
|
||||
return data >= value;
|
||||
}
|
||||
else if (foperator == QStringLiteral("<=")) {
|
||||
return data <= value;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldIntValueMatchesData(const int value, const QString &foperator, const int data) {
|
||||
|
||||
return FieldNumericalValueMatchesData(value, foperator, data);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data) {
|
||||
|
||||
return FieldNumericalValueMatchesData(value, foperator, data);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data) {
|
||||
|
||||
return FieldNumericalValueMatchesData(value, foperator, data);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldFloatValueMatchesData(const float value, const QString &foperator, const float data) {
|
||||
|
||||
return FieldNumericalValueMatchesData(value, foperator, data);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::ContainsOperators(const QString &token) {
|
||||
|
||||
for (const QString &foperator : Operators) {
|
||||
if (token.contains(foperator, Qt::CaseInsensitive)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONFILTER_H
|
||||
#define COLLECTIONFILTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class CollectionItem;
|
||||
|
||||
class CollectionFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CollectionFilter(QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
private:
|
||||
static const QStringList Operators;
|
||||
struct Filter {
|
||||
public:
|
||||
Filter(const QString &_field = QString(), const QVariant &_value = QVariant(), const QString &_foperator = QString()) : field(_field), value(_value), foperator(_foperator) {}
|
||||
QString field;
|
||||
QVariant value;
|
||||
QString foperator;
|
||||
};
|
||||
using FilterList = QMap<QString, Filter>;
|
||||
static bool ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text);
|
||||
static bool ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QString &filter_text);
|
||||
static bool ItemMetadataMatchesFilterText(const Song &metadata, const QString &filter_text);
|
||||
static QVariant DataFromField(const QString &field, const Song &metadata);
|
||||
static bool FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator);
|
||||
template<typename T>
|
||||
static bool FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data);
|
||||
static bool FieldIntValueMatchesData(const int value, const QString &foperator, const int data);
|
||||
static bool FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data);
|
||||
static bool FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data);
|
||||
static bool FieldFloatValueMatchesData(const float value, const QString &foperator, const float data);
|
||||
static bool ContainsOperators(const QString &token);
|
||||
};
|
||||
|
||||
#endif // COLLECTIONFILTER_H
|
|
@ -50,6 +50,8 @@
|
|||
#include "core/settings.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionfilter.h"
|
||||
#include "collectionquery.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "groupbydialog.h"
|
||||
|
@ -62,6 +64,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
|||
: QWidget(parent),
|
||||
ui_(new Ui_CollectionFilterWidget),
|
||||
model_(nullptr),
|
||||
filter_(nullptr),
|
||||
group_by_dialog_(new GroupByDialog(this)),
|
||||
groupings_manager_(nullptr),
|
||||
filter_age_menu_(nullptr),
|
||||
|
@ -74,8 +77,8 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
|||
|
||||
ui_->setupUi(this);
|
||||
|
||||
QString available_fields = Song::kFtsColumns.join(QStringLiteral(", ")).replace(QRegularExpression(QStringLiteral("\\bfts")), QLatin1String(""));
|
||||
available_fields += QStringLiteral(", ") + Song::kNumericalColumns.join(QStringLiteral(", "));
|
||||
QString available_fields = Song::kTextSearchColumns.join(QStringLiteral(", "));
|
||||
available_fields += QStringLiteral(", ") + Song::kNumericalSearchColumns.join(QStringLiteral(", "));
|
||||
|
||||
ui_->search_field->setToolTip(
|
||||
QStringLiteral("<html><head/><body><p>") +
|
||||
|
@ -125,12 +128,12 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
|||
filter_age_menu_ = new QMenu(tr("Show"), this);
|
||||
filter_age_menu_->addActions(filter_age_group->actions());
|
||||
|
||||
filter_ages_[ui_->filter_age_all] = -1;
|
||||
filter_ages_[ui_->filter_age_today] = 60 * 60 * 24;
|
||||
filter_ages_[ui_->filter_age_week] = 60 * 60 * 24 * 7;
|
||||
filter_ages_[ui_->filter_age_month] = 60 * 60 * 24 * 30;
|
||||
filter_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3;
|
||||
filter_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365;
|
||||
filter_max_ages_[ui_->filter_age_all] = -1;
|
||||
filter_max_ages_[ui_->filter_age_today] = 60 * 60 * 24;
|
||||
filter_max_ages_[ui_->filter_age_week] = 60 * 60 * 24 * 7;
|
||||
filter_max_ages_[ui_->filter_age_month] = 60 * 60 * 24 * 30;
|
||||
filter_max_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3;
|
||||
filter_max_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365;
|
||||
|
||||
group_by_menu_ = new QMenu(tr("Group by"), this);
|
||||
|
||||
|
@ -156,29 +159,30 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
|||
|
||||
CollectionFilterWidget::~CollectionFilterWidget() { delete ui_; }
|
||||
|
||||
void CollectionFilterWidget::Init(CollectionModel *model) {
|
||||
void CollectionFilterWidget::Init(CollectionModel *model, CollectionFilter *filter) {
|
||||
|
||||
if (model_) {
|
||||
QObject::disconnect(model_, nullptr, this, nullptr);
|
||||
QObject::disconnect(model_, nullptr, group_by_dialog_, nullptr);
|
||||
QObject::disconnect(group_by_dialog_, nullptr, model_, nullptr);
|
||||
const QList<QAction*> filter_ages = filter_ages_.keys();
|
||||
for (QAction *action : filter_ages) {
|
||||
const QList<QAction*> actions = filter_max_ages_.keys();
|
||||
for (QAction *action : actions) {
|
||||
QObject::disconnect(action, &QAction::triggered, model_, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
model_ = model;
|
||||
filter_ = filter;
|
||||
|
||||
// Connect signals
|
||||
QObject::connect(model_, &CollectionModel::GroupingChanged, group_by_dialog_, &GroupByDialog::CollectionGroupingChanged);
|
||||
QObject::connect(model_, &CollectionModel::GroupingChanged, this, &CollectionFilterWidget::GroupingChanged);
|
||||
QObject::connect(group_by_dialog_, &GroupByDialog::Accepted, model_, &CollectionModel::SetGroupBy);
|
||||
|
||||
const QList<QAction*> filter_ages = filter_ages_.keys();
|
||||
for (QAction *action : filter_ages) {
|
||||
int age = filter_ages_[action];
|
||||
QObject::connect(action, &QAction::triggered, this, [this, age]() { model_->SetFilterAge(age); } );
|
||||
const QList<QAction*> actions = filter_max_ages_.keys();
|
||||
for (QAction *action : actions) {
|
||||
int filter_max_age = filter_max_ages_[action];
|
||||
QObject::connect(action, &QAction::triggered, this, [this, filter_max_age]() { model_->SetFilterMaxAge(filter_max_age); } );
|
||||
}
|
||||
|
||||
// Load settings
|
||||
|
@ -217,6 +221,10 @@ void CollectionFilterWidget::SetSettingsPrefix(const QString &prefix) {
|
|||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::setFilter(CollectionFilter *filter) {
|
||||
filter_ = filter;
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::ReloadSettings() {
|
||||
|
||||
Settings s;
|
||||
|
@ -518,9 +526,6 @@ void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) {
|
|||
|
||||
void CollectionFilterWidget::FilterTextChanged(const QString &text) {
|
||||
|
||||
// Searching with one or two characters can be very expensive on the database even with FTS,
|
||||
// so if there are a large number of songs in the database introduce a small delay before actually filtering the model,
|
||||
// so if the user is typing the first few characters of something it will be quicker.
|
||||
const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
|
||||
|
||||
if (delay) {
|
||||
|
@ -535,9 +540,8 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
|
|||
|
||||
void CollectionFilterWidget::FilterDelayTimeout() {
|
||||
|
||||
emit Filter(ui_->search_field->text());
|
||||
if (filter_applies_to_model_) {
|
||||
model_->SetFilterText(ui_->search_field->text());
|
||||
filter_->setFilterFixedString(ui_->search_field->text());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ class QKeyEvent;
|
|||
|
||||
class GroupByDialog;
|
||||
class SavedGroupingManager;
|
||||
class CollectionFilter;
|
||||
class Ui_CollectionFilterWidget;
|
||||
|
||||
class CollectionFilterWidget : public QWidget {
|
||||
|
@ -58,7 +59,9 @@ class CollectionFilterWidget : public QWidget {
|
|||
AlwaysDelayed,
|
||||
};
|
||||
|
||||
void Init(CollectionModel *model);
|
||||
void Init(CollectionModel *model, CollectionFilter *filter);
|
||||
|
||||
void setFilter(CollectionFilter *filter);
|
||||
|
||||
static QActionGroup *CreateGroupByActions(const QString &saved_groupings_settings_group, QObject *parent);
|
||||
|
||||
|
@ -94,7 +97,6 @@ class CollectionFilterWidget : public QWidget {
|
|||
void UpPressed();
|
||||
void DownPressed();
|
||||
void ReturnPressed();
|
||||
void Filter(const QString &text);
|
||||
|
||||
protected:
|
||||
void keyReleaseEvent(QKeyEvent *e) override;
|
||||
|
@ -115,6 +117,7 @@ class CollectionFilterWidget : public QWidget {
|
|||
private:
|
||||
Ui_CollectionFilterWidget *ui_;
|
||||
CollectionModel *model_;
|
||||
CollectionFilter *filter_;
|
||||
|
||||
GroupByDialog *group_by_dialog_;
|
||||
SavedGroupingManager *groupings_manager_;
|
||||
|
@ -123,7 +126,7 @@ class CollectionFilterWidget : public QWidget {
|
|||
QMenu *group_by_menu_;
|
||||
QMenu *collection_menu_;
|
||||
QActionGroup *group_by_group_;
|
||||
QHash<QAction*, int> filter_ages_;
|
||||
QHash<QAction*, int> filter_max_ages_;
|
||||
|
||||
QTimer *filter_delay_;
|
||||
|
||||
|
|
|
@ -29,24 +29,27 @@
|
|||
|
||||
class CollectionItem : public SimpleTreeItem<CollectionItem> {
|
||||
public:
|
||||
enum Type {
|
||||
Type_Root,
|
||||
Type_Divider,
|
||||
Type_Container,
|
||||
Type_Song,
|
||||
Type_LoadingIndicator,
|
||||
enum class Type {
|
||||
Root,
|
||||
Divider,
|
||||
Container,
|
||||
Song,
|
||||
LoadingIndicator,
|
||||
};
|
||||
|
||||
explicit CollectionItem(SimpleTreeModel<CollectionItem> *_model)
|
||||
: SimpleTreeItem<CollectionItem>(Type_Root, _model),
|
||||
: SimpleTreeItem<CollectionItem>(_model),
|
||||
type(Type::Root),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
explicit CollectionItem(Type _type, CollectionItem *_parent = nullptr)
|
||||
: SimpleTreeItem<CollectionItem>(_type, _parent),
|
||||
explicit CollectionItem(const Type _type, CollectionItem *_parent = nullptr)
|
||||
: SimpleTreeItem<CollectionItem>(_parent),
|
||||
type(_type),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
Type type;
|
||||
int container_level;
|
||||
Song metadata;
|
||||
CollectionItem *compilation_artist_node_;
|
||||
|
@ -55,4 +58,6 @@ class CollectionItem : public SimpleTreeItem<CollectionItem> {
|
|||
Q_DISABLE_COPY(CollectionItem)
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(CollectionItem::Type)
|
||||
|
||||
#endif // COLLECTIONITEM_H
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
|
@ -44,6 +42,7 @@
|
|||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QQueue>
|
||||
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/simpletreemodel.h"
|
||||
|
@ -51,15 +50,17 @@
|
|||
#include "core/sqlrow.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "collectionmodelupdate.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionqueryoptions.h"
|
||||
#include "collectionitem.h"
|
||||
|
||||
class QTimer;
|
||||
class Settings;
|
||||
|
||||
class Application;
|
||||
class CollectionBackend;
|
||||
class CollectionDirectoryModel;
|
||||
class CollectionFilter;
|
||||
|
||||
class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
Q_OBJECT
|
||||
|
@ -69,13 +70,12 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||
~CollectionModel() override;
|
||||
|
||||
static const int kPrettyCoverSize;
|
||||
static const char *kPixmapDiskCacheDir;
|
||||
|
||||
enum Role {
|
||||
Role_Type = Qt::UserRole + 1,
|
||||
Role_ContainerType,
|
||||
Role_SortText,
|
||||
Role_Key,
|
||||
Role_ContainerKey,
|
||||
Role_Artist,
|
||||
Role_IsDivider,
|
||||
Role_Editable,
|
||||
|
@ -125,147 +125,123 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||
bool operator!=(const Grouping other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
struct QueryResult {
|
||||
QueryResult() : create_va(false) {}
|
||||
struct Options {
|
||||
Options() : group_by(GroupBy::AlbumArtist, GroupBy::AlbumDisc, GroupBy::None),
|
||||
show_dividers(true),
|
||||
show_pretty_covers(true),
|
||||
show_various_artists(true),
|
||||
sort_skips_articles(true),
|
||||
separate_albums_by_grouping(false) {}
|
||||
|
||||
SqlRowList rows;
|
||||
bool create_va;
|
||||
Grouping group_by;
|
||||
bool show_dividers;
|
||||
bool show_pretty_covers;
|
||||
bool show_various_artists;
|
||||
bool sort_skips_articles;
|
||||
bool separate_albums_by_grouping;
|
||||
CollectionFilterOptions filter_options;
|
||||
};
|
||||
|
||||
SharedPtr<CollectionBackend> backend() const { return backend_; }
|
||||
CollectionFilter *filter() const { return filter_; }
|
||||
|
||||
void Init();
|
||||
void Reset();
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
CollectionDirectoryModel *directory_model() const { return dir_model_; }
|
||||
|
||||
// Call before Init()
|
||||
void set_show_various_artists(const bool show_various_artists) { show_various_artists_ = show_various_artists; }
|
||||
|
||||
// Get information about the collection
|
||||
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
|
||||
SongList GetChildSongs(const QModelIndex &idx) const;
|
||||
SongList GetChildSongs(const QModelIndexList &indexes) const;
|
||||
|
||||
// Might be accurate
|
||||
int total_song_count() const { return total_song_count_; }
|
||||
int total_artist_count() const { return total_artist_count_; }
|
||||
int total_album_count() const { return total_album_count_; }
|
||||
|
||||
// QAbstractItemModel
|
||||
QVariant data(const QModelIndex &idx, const int role = Qt::DisplayRole) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &idx) const override;
|
||||
QStringList mimeTypes() const override;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
|
||||
// Whether or not to use album cover art, if it exists, in the collection view
|
||||
void set_pretty_covers(const bool use_pretty_covers);
|
||||
bool use_pretty_covers() const { return use_pretty_covers_; }
|
||||
|
||||
// Whether or not to show letters heading in the collection view
|
||||
void set_show_dividers(const bool show_dividers);
|
||||
|
||||
// Whether to skip articles such as “The” when sorting artist names
|
||||
void set_sort_skips_articles(const bool sort_skips_articles);
|
||||
|
||||
// Reload settings.
|
||||
void ReloadSettings();
|
||||
|
||||
// Utility functions for manipulating text
|
||||
static QString TextOrUnknown(const QString &text);
|
||||
static QString PrettyYearAlbum(const int year, const QString &album);
|
||||
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||
static QString PrettyDisc(const int disc);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForNumber(const int number);
|
||||
static QString SortTextForArtist(QString artist, const bool skip_articles);
|
||||
static QString SortTextForSong(const Song &song);
|
||||
static QString SortTextForYear(const int year);
|
||||
static QString SortTextForBitrate(const int bitrate);
|
||||
|
||||
quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); }
|
||||
|
||||
const CollectionModel::Grouping GetGroupBy() const { return options_current_.group_by; }
|
||||
void SetGroupBy(const CollectionModel::Grouping g, const std::optional<bool> separate_albums_by_grouping = std::optional<bool>());
|
||||
|
||||
static bool IsArtistGroupBy(const GroupBy group_by) {
|
||||
return group_by == CollectionModel::GroupBy::Artist || group_by == CollectionModel::GroupBy::AlbumArtist;
|
||||
}
|
||||
static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy::Album || group_by == GroupBy::YearAlbum || group_by == GroupBy::AlbumDisc || group_by == GroupBy::YearAlbumDisc || group_by == GroupBy::OriginalYearAlbum || group_by == GroupBy::OriginalYearAlbumDisc; }
|
||||
|
||||
void set_use_lazy_loading(const bool value) { use_lazy_loading_ = value; }
|
||||
|
||||
QMap<QString, CollectionItem*> container_nodes(const int i) { return container_nodes_[i]; }
|
||||
QList<CollectionItem*> song_nodes() const { return song_nodes_.values(); }
|
||||
int divider_nodes_count() const { return divider_nodes_.count(); }
|
||||
|
||||
// QAbstractItemModel
|
||||
QVariant data(const QModelIndex &idx, const int role = Qt::DisplayRole) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &idx) const override;
|
||||
QStringList mimeTypes() const override;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
|
||||
// Utility functions for manipulating text
|
||||
static QString DisplayText(const GroupBy group_by, const Song &song);
|
||||
static QString TextOrUnknown(const QString &text);
|
||||
static QString PrettyYearAlbum(const int year, const QString &album);
|
||||
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||
static QString PrettyDisc(const int disc);
|
||||
static QString PrettyFormat(const Song &song);
|
||||
QString SortText(const GroupBy group_by, const int container_level, const Song &song, const bool sort_skips_articles);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForNumber(const int number);
|
||||
static QString SortTextForArtist(QString artist, const bool skip_articles);
|
||||
static QString SortTextForSong(const Song &song);
|
||||
static QString SortTextForYear(const int year);
|
||||
static QString SortTextForBitrate(const int bitrate);
|
||||
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
|
||||
QString ContainerKey(const GroupBy group_by, const Song &song) const;
|
||||
|
||||
// Get information about the collection
|
||||
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
|
||||
SongList GetChildSongs(const QModelIndex &idx) const;
|
||||
SongList GetChildSongs(const QModelIndexList &indexes) const;
|
||||
|
||||
void ExpandAll(CollectionItem *item = nullptr) const;
|
||||
|
||||
const CollectionModel::Grouping GetGroupBy() const { return group_by_; }
|
||||
void SetGroupBy(const CollectionModel::Grouping g, const std::optional<bool> separate_albums_by_grouping = std::optional<bool>());
|
||||
|
||||
static QString ContainerKey(const GroupBy group_by, const bool separate_albums_by_grouping, const Song &song);
|
||||
|
||||
signals:
|
||||
void TotalSongCountUpdated(const int count);
|
||||
void TotalArtistCountUpdated(const int count);
|
||||
void TotalAlbumCountUpdated(const int count);
|
||||
void GroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping);
|
||||
void SongsAdded(const SongList &songs);
|
||||
void SongsRemoved(const SongList &songs);
|
||||
|
||||
public slots:
|
||||
void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode);
|
||||
void SetFilterAge(const int filter_age);
|
||||
void SetFilterText(const QString &filter_text);
|
||||
void SetFilterMode(const CollectionFilterOptions::FilterMode filter_mode);
|
||||
void SetFilterMaxAge(const int filter_max_age);
|
||||
|
||||
void Init(const bool async = true);
|
||||
void Reset();
|
||||
void ResetAsync();
|
||||
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
|
||||
protected:
|
||||
void LazyPopulate(CollectionItem *item) override { LazyPopulate(item, true); }
|
||||
void LazyPopulate(CollectionItem *parent, const bool signal);
|
||||
|
||||
private slots:
|
||||
// From CollectionBackend
|
||||
void SongsDeleted(const SongList &songs);
|
||||
void SongsSlightlyChanged(const SongList &songs);
|
||||
void TotalSongCountUpdatedSlot(const int count);
|
||||
void TotalArtistCountUpdatedSlot(const int count);
|
||||
void TotalAlbumCountUpdatedSlot(const int count);
|
||||
static void ClearDiskCache();
|
||||
|
||||
// Called after ResetAsync
|
||||
void ResetAsyncQueryFinished();
|
||||
|
||||
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
|
||||
void AddReAddOrUpdate(const SongList &songs);
|
||||
void RemoveSongs(const SongList &songs);
|
||||
|
||||
private:
|
||||
// Provides some optimizations for loading the list of items in the root.
|
||||
// This gets called a lot when filtering the playlist, so it's nice to be able to do it in a background thread.
|
||||
CollectionQueryOptions PrepareQuery(CollectionItem *parent);
|
||||
QueryResult RunQuery(const CollectionFilterOptions &filter_options = CollectionFilterOptions(), const CollectionQueryOptions &query_options = CollectionQueryOptions());
|
||||
void PostQuery(CollectionItem *parent, const QueryResult &result, const bool signal);
|
||||
|
||||
bool HasCompilations(const QSqlDatabase &db, const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options);
|
||||
|
||||
void Clear();
|
||||
void BeginReset();
|
||||
void EndReset();
|
||||
|
||||
// Functions for working with queries and creating items.
|
||||
// When the model is reset or when a node is lazy-loaded the Collection constructs a database query to populate the items.
|
||||
// Filters are added for each parent item, restricting the songs returned to a particular album or artist for example.
|
||||
static void SetQueryColumnSpec(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQueryOptions *query_options);
|
||||
static void AddQueryWhere(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQueryOptions *query_options);
|
||||
QVariant data(const CollectionItem *item, const int role) const;
|
||||
|
||||
// Items can be created either from a query that's been run to populate a node, or by a spontaneous SongsDiscovered emission from the backend.
|
||||
CollectionItem *ItemFromQuery(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level);
|
||||
CollectionItem *ItemFromSong(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const Song &s, const int container_level);
|
||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
|
||||
void ScheduleAddSongs(const SongList &songs);
|
||||
void ScheduleUpdateSongs(const SongList &songs);
|
||||
void ScheduleRemoveSongs(const SongList &songs);
|
||||
|
||||
// The "Various Artists" node is an annoying special case.
|
||||
CollectionItem *CreateCompilationArtistNode(const bool signal, CollectionItem *parent);
|
||||
void AddReAddOrUpdateSongsInternal(const SongList &songs);
|
||||
void AddSongsInternal(const SongList &songs);
|
||||
void UpdateSongsInternal(const SongList &songs);
|
||||
void RemoveSongsInternal(const SongList &songs);
|
||||
|
||||
// Helpers for ItemFromQuery and ItemFromSong
|
||||
CollectionItem *InitItem(const GroupBy group_by, const bool signal, CollectionItem *parent, const int container_level);
|
||||
void FinishItem(const GroupBy group_by, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item);
|
||||
void CreateDividerItem(const QString ÷r_key, const QString &display_text, CollectionItem *parent);
|
||||
CollectionItem *CreateContainerItem(const GroupBy group_by, const int container_level, const QString &container_key, const Song &song, CollectionItem *parent);
|
||||
void CreateSongItem(const Song &song, CollectionItem *parent);
|
||||
void SetSongItemData(CollectionItem *item, const Song &song);
|
||||
CollectionItem *CreateCompilationArtistNode(CollectionItem *parent);
|
||||
|
||||
static QString DividerKey(const GroupBy group_by, CollectionItem *item);
|
||||
void LoadSongsFromSqlAsync();
|
||||
SongList LoadSongsFromSql(const CollectionFilterOptions &filter_options = CollectionFilterOptions());
|
||||
|
||||
static QString DividerKey(const GroupBy group_by, const Song &song, const QString &sort_text);
|
||||
static QString DividerDisplayText(const GroupBy group_by, const QString &key);
|
||||
|
||||
// Helpers
|
||||
|
@ -273,24 +249,50 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||
QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const;
|
||||
QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key) const;
|
||||
QVariant AlbumIcon(const QModelIndex &idx);
|
||||
QVariant data(const CollectionItem *item, const int role) const;
|
||||
void ClearItemPixmapCache(CollectionItem *item);
|
||||
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
|
||||
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
|
||||
|
||||
private slots:
|
||||
void Reload();
|
||||
void ScheduleReset();
|
||||
void ProcessUpdate();
|
||||
void LoadSongsFromSqlAsyncFinished();
|
||||
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
|
||||
|
||||
// From CollectionBackend
|
||||
void TotalSongCountUpdatedSlot(const int count);
|
||||
void TotalArtistCountUpdatedSlot(const int count);
|
||||
void TotalAlbumCountUpdatedSlot(const int count);
|
||||
|
||||
static void ClearDiskCache();
|
||||
|
||||
void RowsInserted(const QModelIndex &parent, const int first, const int last);
|
||||
void RowsRemoved(const QModelIndex &parent, const int first, const int last);
|
||||
|
||||
private:
|
||||
static QNetworkDiskCache *sIconCache;
|
||||
SharedPtr<CollectionBackend> backend_;
|
||||
Application *app_;
|
||||
CollectionDirectoryModel *dir_model_;
|
||||
bool show_various_artists_;
|
||||
bool sort_skips_articles_;
|
||||
CollectionFilter *filter_;
|
||||
QTimer *timer_reload_;
|
||||
QTimer *timer_update_;
|
||||
|
||||
QPixmap pixmap_no_cover_;
|
||||
QIcon icon_artist_;
|
||||
|
||||
Options options_current_;
|
||||
Options options_active_;
|
||||
|
||||
bool use_disk_cache_;
|
||||
AlbumCoverLoaderOptions::Types cover_types_;
|
||||
|
||||
int total_song_count_;
|
||||
int total_artist_count_;
|
||||
int total_album_count_;
|
||||
|
||||
CollectionFilterOptions filter_options_;
|
||||
Grouping group_by_;
|
||||
bool separate_albums_by_grouping_;
|
||||
QQueue<CollectionModelUpdate> updates_;
|
||||
|
||||
// Keyed on database ID
|
||||
QMap<int, CollectionItem*> song_nodes_;
|
||||
|
@ -301,22 +303,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||
// Keyed on a letter, a year, a century, etc.
|
||||
QMap<QString, CollectionItem*> divider_nodes_;
|
||||
|
||||
QIcon artist_icon_;
|
||||
QIcon album_icon_;
|
||||
// Used as a generic icon to show when no cover art is found, fixed to the same size as the artwork (32x32)
|
||||
QPixmap no_cover_icon_;
|
||||
|
||||
static QNetworkDiskCache *sIconCache;
|
||||
|
||||
int init_task_id_;
|
||||
|
||||
bool use_pretty_covers_;
|
||||
bool show_dividers_;
|
||||
bool use_disk_cache_;
|
||||
bool use_lazy_loading_;
|
||||
|
||||
AlbumCoverLoaderOptions::Types cover_types_;
|
||||
|
||||
using ItemAndCacheKey = QPair<CollectionItem*, QString>;
|
||||
QMap<quint64, ItemAndCacheKey> pending_art_;
|
||||
QSet<QString> pending_cache_keys_;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 "collectionmodelupdate.h"
|
||||
|
||||
CollectionModelUpdate::CollectionModelUpdate(const Type &_type, const SongList &_songs)
|
||||
: type(_type), songs(_songs) {}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 COLLECTIONMODELUPDATE_H
|
||||
#define COLLECTIONMODELUPDATE_H
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class CollectionModelUpdate {
|
||||
public:
|
||||
enum class Type {
|
||||
AddReAddOrUpdate,
|
||||
Add,
|
||||
Update,
|
||||
Remove,
|
||||
};
|
||||
explicit CollectionModelUpdate(const Type &_type, const SongList &_songs);
|
||||
Type type;
|
||||
SongList songs;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONMODELUPDATE_H
|
|
@ -38,78 +38,13 @@
|
|||
#include "collectionfilteroptions.h"
|
||||
#include "utilities/searchparserutils.h"
|
||||
|
||||
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options)
|
||||
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options)
|
||||
: SqlQuery(db),
|
||||
songs_table_(songs_table),
|
||||
fts_table_(fts_table),
|
||||
include_unavailable_(false),
|
||||
join_with_fts_(false),
|
||||
duplicates_only_(false),
|
||||
limit_(-1) {
|
||||
|
||||
if (!filter_options.filter_text().isEmpty()) {
|
||||
// We need to munge the filter text a little bit to get it to work as expected with sqlite's FTS5:
|
||||
// 1) Append * to all tokens.
|
||||
// 2) Prefix "fts" to column names.
|
||||
// 3) Remove colons which don't correspond to column names.
|
||||
|
||||
// Split on whitespace
|
||||
QString filter_text = filter_options.filter_text().replace(QRegularExpression(QStringLiteral(":\\s+")), QStringLiteral(":"));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList tokens(filter_text.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts));
|
||||
#else
|
||||
QStringList tokens(filter_text.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts));
|
||||
#endif
|
||||
QString query;
|
||||
for (QString token : tokens) {
|
||||
token.remove(QLatin1Char('('))
|
||||
.remove(QLatin1Char(')'))
|
||||
.remove(QLatin1Char('"'))
|
||||
.replace(QLatin1Char('-'), QLatin1Char(' '));
|
||||
|
||||
if (token.contains(QLatin1Char(':'))) {
|
||||
const QString columntoken = token.section(QLatin1Char(':'), 0, 0);
|
||||
QString subtoken = token.section(QLatin1Char(':'), 1, -1).replace(QLatin1String(":"), QLatin1String(" ")).trimmed();
|
||||
if (subtoken.isEmpty()) continue;
|
||||
if (Song::kFtsColumns.contains(QLatin1String("fts") + columntoken, Qt::CaseInsensitive)) {
|
||||
if (!query.isEmpty()) query.append(QLatin1String(" "));
|
||||
query += QStringLiteral("fts") + columntoken + QStringLiteral(":\"") + subtoken + QStringLiteral("\"*");
|
||||
}
|
||||
else if (Song::kNumericalColumns.contains(columntoken, Qt::CaseInsensitive)) {
|
||||
QString comparator = RemoveSqlOperator(subtoken);
|
||||
if (columntoken.compare(QLatin1String("rating"), Qt::CaseInsensitive) == 0) {
|
||||
AddWhereRating(subtoken, comparator);
|
||||
}
|
||||
else if (columntoken.compare(QLatin1String("length"), Qt::CaseInsensitive) == 0) {
|
||||
// Time is saved in nanoseconds, so add 9 0's
|
||||
QString parsedTime = QString::number(Utilities::ParseSearchTime(subtoken)) + QStringLiteral("000000000");
|
||||
AddWhere(columntoken, parsedTime, comparator);
|
||||
}
|
||||
else {
|
||||
AddWhere(columntoken, subtoken, comparator);
|
||||
}
|
||||
}
|
||||
// Not a valid filter, remove
|
||||
else {
|
||||
token = token.replace(QLatin1String(":"), QLatin1String(" ")).trimmed();
|
||||
if (!token.isEmpty()) {
|
||||
if (!query.isEmpty()) query.append(QLatin1Char(' '));
|
||||
query += QLatin1Char('\"') + token + QStringLiteral("\"*");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!query.isEmpty()) query.append(QLatin1Char(' '));
|
||||
query += QLatin1Char('\"') + token + QStringLiteral("\"*");
|
||||
}
|
||||
}
|
||||
if (!query.isEmpty()) {
|
||||
where_clauses_ << QStringLiteral("fts.%fts_table_noprefix MATCH ?");
|
||||
bound_values_ << query;
|
||||
join_with_fts_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter_options.max_age() != -1) {
|
||||
qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - filter_options.max_age();
|
||||
|
||||
|
@ -117,12 +52,6 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
|||
bound_values_ << cutoff;
|
||||
}
|
||||
|
||||
// TODO: Currently you cannot use any FilterMode other than All and FTS at the same time.
|
||||
// Joining songs, duplicated_songs and songs_fts all together takes a huge amount of time.
|
||||
// The query takes about 20 seconds on my machine then. Why?
|
||||
// Untagged mode could work with additional filtering but I'm disabling it just to be consistent
|
||||
// this way filtering is available only in the All mode.
|
||||
// Remember though that when you fix the Duplicates + FTS cooperation, enable the filtering in both Duplicates and Untagged modes.
|
||||
duplicates_only_ = filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Duplicates;
|
||||
|
||||
if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Untagged) {
|
||||
|
@ -131,28 +60,10 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
|||
|
||||
}
|
||||
|
||||
QString CollectionQuery::RemoveSqlOperator(QString &token) {
|
||||
|
||||
QString op = QStringLiteral("=");
|
||||
static QRegularExpression rxOp(QStringLiteral("^(=|<[>=]?|>=?|!=)"));
|
||||
QRegularExpressionMatch match = rxOp.match(token);
|
||||
if (match.hasMatch()) {
|
||||
op = match.captured(0);
|
||||
}
|
||||
token.remove(rxOp);
|
||||
|
||||
if (op == QStringLiteral("!=")) {
|
||||
op = QStringLiteral("<>");
|
||||
}
|
||||
|
||||
return op;
|
||||
|
||||
}
|
||||
|
||||
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
|
||||
|
||||
// Ignore 'literal' for IN
|
||||
if (op.compare(QLatin1String("IN"), Qt::CaseInsensitive) == 0) {
|
||||
if (op.compare(QStringLiteral("IN"), Qt::CaseInsensitive) == 0) {
|
||||
QStringList values = value.toStringList();
|
||||
QStringList final_values;
|
||||
final_values.reserve(values.count());
|
||||
|
@ -161,7 +72,7 @@ void CollectionQuery::AddWhere(const QString &column, const QVariant &value, con
|
|||
bound_values_ << single_value;
|
||||
}
|
||||
|
||||
where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(QStringLiteral(",")));
|
||||
where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(QLatin1Char(',')));
|
||||
}
|
||||
else {
|
||||
// Do integers inline - sqlite seems to get confused when you pass integers to bound parameters
|
||||
|
@ -190,49 +101,8 @@ void CollectionQuery::AddWhere(const QString &column, const QVariant &value, con
|
|||
|
||||
}
|
||||
|
||||
void CollectionQuery::AddWhereArtist(const QVariant &value) {
|
||||
|
||||
where_clauses_ << QStringLiteral("((artist = ? AND albumartist = '') OR albumartist = ?)");
|
||||
bound_values_ << value;
|
||||
bound_values_ << value;
|
||||
|
||||
}
|
||||
|
||||
void CollectionQuery::AddWhereRating(const QVariant &value, const QString &op) {
|
||||
|
||||
float parsed_rating = Utilities::ParseSearchRating(value.toString());
|
||||
|
||||
// You can't query the database for a float, due to float precision errors,
|
||||
// So we have to use a certain tolerance, so that the searched value is definetly included.
|
||||
const float tolerance = 0.001F;
|
||||
if (op == QStringLiteral("<")) {
|
||||
AddWhere(QStringLiteral("rating"), parsed_rating-tolerance, QStringLiteral("<"));
|
||||
}
|
||||
else if (op == QStringLiteral(">")) {
|
||||
AddWhere(QStringLiteral("rating"), parsed_rating+tolerance, QStringLiteral(">"));
|
||||
}
|
||||
else if (op == QStringLiteral("<=")) {
|
||||
AddWhere(QStringLiteral("rating"), parsed_rating+tolerance, QStringLiteral("<="));
|
||||
}
|
||||
else if (op == QStringLiteral(">=")) {
|
||||
AddWhere(QStringLiteral("rating"), parsed_rating-tolerance, QStringLiteral(">="));
|
||||
}
|
||||
else if (op == QStringLiteral("<>")) {
|
||||
where_clauses_ << QStringLiteral("(rating<? OR rating>?)");
|
||||
bound_values_ << parsed_rating - tolerance;
|
||||
bound_values_ << parsed_rating + tolerance;
|
||||
}
|
||||
else /* (op == "=") */ {
|
||||
AddWhere(QStringLiteral("rating"), parsed_rating+tolerance, QStringLiteral("<"));
|
||||
AddWhere(QStringLiteral("rating"), parsed_rating-tolerance, QStringLiteral(">"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionQuery::AddCompilationRequirement(const bool compilation) {
|
||||
// The unary + is added to prevent sqlite from using the index idx_comp_artist.
|
||||
// When joining with fts, sqlite 3.8 has a tendency to use this index and thereby nesting the tables in an order which gives very poor performance
|
||||
|
||||
where_clauses_ << QStringLiteral("+compilation_effective = %1").arg(compilation ? 1 : 0);
|
||||
|
||||
}
|
||||
|
@ -248,14 +118,7 @@ QString CollectionQuery::GetInnerQuery() const {
|
|||
|
||||
bool CollectionQuery::Exec() {
|
||||
|
||||
QString sql;
|
||||
|
||||
if (join_with_fts_) {
|
||||
sql = QStringLiteral("SELECT %1 FROM %2 INNER JOIN %3 AS fts ON %2.ROWID = fts.ROWID").arg(column_spec_, songs_table_, fts_table_);
|
||||
}
|
||||
else {
|
||||
sql = QStringLiteral("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery());
|
||||
}
|
||||
QString sql = QStringLiteral("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery());
|
||||
|
||||
QStringList where_clauses(where_clauses_);
|
||||
if (!include_unavailable_) {
|
||||
|
@ -268,9 +131,7 @@ bool CollectionQuery::Exec() {
|
|||
|
||||
if (limit_ != -1) sql += QStringLiteral(" LIMIT ") + QString::number(limit_);
|
||||
|
||||
sql.replace(QLatin1String("%songs_table"), songs_table_);
|
||||
sql.replace(QLatin1String("%fts_table_noprefix"), fts_table_.section(QLatin1Char('.'), -1, -1));
|
||||
sql.replace(QLatin1String("%fts_table"), fts_table_);
|
||||
sql.replace(QStringLiteral("%songs_table"), songs_table_);
|
||||
|
||||
if (!QSqlQuery::prepare(sql)) return false;
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
class CollectionQuery : public SqlQuery {
|
||||
public:
|
||||
explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
|
||||
explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
|
||||
|
||||
QVariant Value(const int column) const;
|
||||
QVariant value(const int column) const { return Value(column); }
|
||||
|
@ -51,7 +51,6 @@ class CollectionQuery : public SqlQuery {
|
|||
QStringList where_clauses() const { return where_clauses_; }
|
||||
QVariantList bound_values() const { return bound_values_; }
|
||||
bool include_unavailable() const { return include_unavailable_; }
|
||||
bool join_with_fts() const { return join_with_fts_; }
|
||||
bool duplicates_only() const { return duplicates_only_; }
|
||||
int limit() const { return limit_; }
|
||||
|
||||
|
@ -63,14 +62,9 @@ class CollectionQuery : public SqlQuery {
|
|||
|
||||
void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; }
|
||||
|
||||
// Removes = < > <= >= <> from the beginning of the input string and returns the operator
|
||||
// If the input String has no operator, returns "="
|
||||
QString RemoveSqlOperator(QString &token);
|
||||
// Adds a fragment of WHERE clause. When executed, this Query will connect all the fragments with AND operator.
|
||||
// Please note that IN operator expects a QStringList as value.
|
||||
void AddWhere(const QString &column, const QVariant &value, const QString &op = QStringLiteral("="));
|
||||
void AddWhereArtist(const QVariant &value);
|
||||
void AddWhereRating(const QVariant &value, const QString &op = QStringLiteral("="));
|
||||
|
||||
void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; }
|
||||
void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; }
|
||||
|
@ -83,7 +77,6 @@ class CollectionQuery : public SqlQuery {
|
|||
|
||||
QSqlDatabase db_;
|
||||
QString songs_table_;
|
||||
QString fts_table_;
|
||||
|
||||
QString column_spec_;
|
||||
QString order_by_;
|
||||
|
@ -91,7 +84,6 @@ class CollectionQuery : public SqlQuery {
|
|||
QVariantList bound_values_;
|
||||
|
||||
bool include_unavailable_;
|
||||
bool join_with_fts_;
|
||||
bool duplicates_only_;
|
||||
int limit_;
|
||||
};
|
||||
|
|
|
@ -120,9 +120,14 @@ CollectionView::~CollectionView() = default;
|
|||
|
||||
void CollectionView::SaveFocus() {
|
||||
|
||||
QModelIndex current = currentIndex();
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || (type.toInt() != CollectionItem::Type_Song && type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) {
|
||||
const QModelIndex current = currentIndex();
|
||||
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!role_type.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -130,8 +135,8 @@ void CollectionView::SaveFocus() {
|
|||
last_selected_song_ = Song();
|
||||
last_selected_container_ = QString();
|
||||
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song:{
|
||||
switch (item_type) {
|
||||
case CollectionItem::Type::Song:{
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
if (!songs.isEmpty()) {
|
||||
|
@ -140,8 +145,8 @@ void CollectionView::SaveFocus() {
|
|||
break;
|
||||
}
|
||||
|
||||
case CollectionItem::Type_Container:
|
||||
case CollectionItem::Type_Divider:{
|
||||
case CollectionItem::Type::Container:
|
||||
case CollectionItem::Type::Divider:{
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
last_selected_container_ = text;
|
||||
break;
|
||||
|
@ -157,9 +162,14 @@ void CollectionView::SaveFocus() {
|
|||
|
||||
void CollectionView::SaveContainerPath(const QModelIndex &child) {
|
||||
|
||||
QModelIndex current = model()->parent(child);
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || (type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) {
|
||||
const QModelIndex current = model()->parent(child);
|
||||
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!role_type.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
if (item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -183,12 +193,17 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
|||
if (model()->canFetchMore(parent)) {
|
||||
model()->fetchMore(parent);
|
||||
}
|
||||
int rows = model()->rowCount(parent);
|
||||
const int rows = model()->rowCount(parent);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
QModelIndex current = model()->index(i, 0, parent);
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song:
|
||||
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!role_type.isValid()) return false;
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
switch (item_type) {
|
||||
case CollectionItem::Type::Root:
|
||||
case CollectionItem::Type::LoadingIndicator:
|
||||
break;
|
||||
case CollectionItem::Type::Song:
|
||||
if (!last_selected_song_.url().isEmpty()) {
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
const SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
|
@ -199,8 +214,8 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
|||
}
|
||||
break;
|
||||
|
||||
case CollectionItem::Type_Container:
|
||||
case CollectionItem::Type_Divider:{
|
||||
case CollectionItem::Type::Container:
|
||||
case CollectionItem::Type::Divider:{
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
if (!last_selected_container_.isEmpty() && last_selected_container_ == text) {
|
||||
expand(current);
|
||||
|
@ -228,18 +243,9 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
|||
void CollectionView::ReloadSettings() {
|
||||
|
||||
Settings settings;
|
||||
|
||||
settings.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
SetAutoOpen(settings.value("auto_open", false).toBool());
|
||||
|
||||
if (app_) {
|
||||
app_->collection_model()->set_pretty_covers(settings.value("pretty_covers", true).toBool());
|
||||
app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool());
|
||||
app_->collection_model()->set_sort_skips_articles(settings.value("sort_skips_articles", true).toBool());
|
||||
}
|
||||
|
||||
delete_files_ = settings.value("delete_files", false).toBool();
|
||||
|
||||
settings.endGroup();
|
||||
|
||||
}
|
||||
|
@ -573,15 +579,20 @@ void CollectionView::OpenInNewPlaylist() {
|
|||
void CollectionView::SearchForThis() {
|
||||
|
||||
QModelIndex current = currentIndex();
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || (type.toInt() != CollectionItem::Type_Song && type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) {
|
||||
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!role_type.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
|
||||
return;
|
||||
}
|
||||
QString search;
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song:{
|
||||
switch (item_type) {
|
||||
case CollectionItem::Type::Song:{
|
||||
SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
if (!songs.isEmpty()) {
|
||||
last_selected_song_ = songs.last();
|
||||
|
@ -590,11 +601,11 @@ void CollectionView::SearchForThis() {
|
|||
break;
|
||||
}
|
||||
|
||||
case CollectionItem::Type_Divider:{
|
||||
case CollectionItem::Type::Divider:{
|
||||
break;
|
||||
}
|
||||
|
||||
case CollectionItem::Type_Container:{
|
||||
case CollectionItem::Type::Container:{
|
||||
CollectionItem *item = app_->collection_model()->IndexToItem(index);
|
||||
|
||||
int container_level = item->container_level;
|
||||
|
@ -750,8 +761,11 @@ void CollectionView::FilterReturnPressed() {
|
|||
if (!currentIndex().isValid()) {
|
||||
// Pick the first thing that isn't a divider
|
||||
for (int row = 0; row < model()->rowCount(); ++row) {
|
||||
QModelIndex idx(model()->index(row, 0));
|
||||
if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) {
|
||||
const QModelIndex idx = model()->index(row, 0);
|
||||
const QVariant role_type = idx.data(CollectionModel::Role_Type);
|
||||
if (!role_type.isValid()) continue;
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
if (item_type != CollectionItem::Type::Divider) {
|
||||
setCurrentIndex(idx);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -131,10 +131,11 @@
|
|||
#include "collection/collection.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectiondirectorymodel.h"
|
||||
#include "collection/collectionviewcontainer.h"
|
||||
#include "collection/collectionfilterwidget.h"
|
||||
#include "collection/collectionfilter.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionview.h"
|
||||
#include "collection/collectionviewcontainer.h"
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistbackend.h"
|
||||
#include "playlist/playlistcontainer.h"
|
||||
|
@ -335,7 +336,6 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
|||
playlist_add_to_another_(nullptr),
|
||||
playlistitem_actions_separator_(nullptr),
|
||||
playlist_rescan_songs_(nullptr),
|
||||
collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
track_position_timer_(new QTimer(this)),
|
||||
track_slider_timer_(new QTimer(this)),
|
||||
keep_running_(false),
|
||||
|
@ -416,23 +416,13 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
|||
ui_->volume->SetValue(volume);
|
||||
VolumeChanged(volume);
|
||||
|
||||
// Models
|
||||
qLog(Debug) << "Creating models";
|
||||
collection_sort_model_->setSourceModel(app_->collection()->model());
|
||||
collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
collection_sort_model_->setDynamicSortFilter(true);
|
||||
collection_sort_model_->setSortLocaleAware(true);
|
||||
collection_sort_model_->sort(0);
|
||||
|
||||
qLog(Debug) << "Creating models finished";
|
||||
|
||||
QObject::connect(ui_->playlist, &PlaylistContainer::ViewSelectionModelChanged, this, &MainWindow::PlaylistViewSelectionModelChanged);
|
||||
|
||||
ui_->playlist->SetManager(app_->playlist_manager());
|
||||
|
||||
ui_->playlist->view()->Init(app_);
|
||||
|
||||
collection_view_->view()->setModel(collection_sort_model_);
|
||||
collection_view_->view()->setModel(app_->collection()->model()->filter());
|
||||
collection_view_->view()->SetApplication(app_);
|
||||
#ifndef Q_OS_WIN
|
||||
device_view_->view()->SetApplication(app_);
|
||||
|
@ -692,7 +682,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
|||
QAction *collection_config_action = new QAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure collection..."), this);
|
||||
QObject::connect(collection_config_action, &QAction::triggered, this, &MainWindow::ShowCollectionConfig);
|
||||
collection_view_->filter_widget()->SetSettingsGroup(QLatin1String(CollectionSettingsPage::kSettingsGroup));
|
||||
collection_view_->filter_widget()->Init(app_->collection()->model());
|
||||
collection_view_->filter_widget()->Init(app_->collection()->model(), app_->collection()->model()->filter());
|
||||
|
||||
QAction *separator = new QAction(this);
|
||||
separator->setSeparator(true);
|
||||
|
|
|
@ -68,6 +68,7 @@ class AlbumCoverManager;
|
|||
class Application;
|
||||
class ContextView;
|
||||
class CollectionViewContainer;
|
||||
class CollectionFilter;
|
||||
class AlbumCoverChoiceController;
|
||||
class CommandlineOptions;
|
||||
#ifndef Q_OS_WIN
|
||||
|
@ -376,8 +377,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||
|
||||
QModelIndex playlist_menu_index_;
|
||||
|
||||
QSortFilterProxyModel *collection_sort_model_;
|
||||
|
||||
QTimer *track_position_timer_;
|
||||
QTimer *track_slider_timer_;
|
||||
Settings settings_;
|
||||
|
|
|
@ -33,9 +33,9 @@
|
|||
template<typename T>
|
||||
class SimpleTreeItem {
|
||||
public:
|
||||
explicit SimpleTreeItem(int _type, SimpleTreeModel<T> *_model); // For the root item
|
||||
explicit SimpleTreeItem(int _type, const QString &_key, T *_parent = nullptr);
|
||||
explicit SimpleTreeItem(int _type, T *_parent = nullptr);
|
||||
explicit SimpleTreeItem(SimpleTreeModel<T> *_model); // For the root item
|
||||
explicit SimpleTreeItem(const QString &_key, T *_parent = nullptr);
|
||||
explicit SimpleTreeItem(T *_parent = nullptr);
|
||||
virtual ~SimpleTreeItem();
|
||||
|
||||
void InsertNotify(T *_parent);
|
||||
|
@ -49,13 +49,11 @@ class SimpleTreeItem {
|
|||
QString DisplayText() const { return display_text; }
|
||||
QString SortText() const { return sort_text; }
|
||||
|
||||
int type;
|
||||
QString key;
|
||||
QString container_key;
|
||||
QString sort_text;
|
||||
QString display_text;
|
||||
|
||||
int row;
|
||||
bool lazy_loaded;
|
||||
|
||||
T *parent;
|
||||
QList<T*> children;
|
||||
|
@ -65,19 +63,15 @@ class SimpleTreeItem {
|
|||
};
|
||||
|
||||
template<typename T>
|
||||
SimpleTreeItem<T>::SimpleTreeItem(int _type, SimpleTreeModel<T> *_model)
|
||||
: type(_type),
|
||||
row(0),
|
||||
lazy_loaded(true),
|
||||
SimpleTreeItem<T>::SimpleTreeItem(SimpleTreeModel<T> *_model)
|
||||
: row(0),
|
||||
parent(nullptr),
|
||||
child_model(nullptr),
|
||||
model(_model) {}
|
||||
|
||||
template<typename T>
|
||||
SimpleTreeItem<T>::SimpleTreeItem(int _type, const QString &_key, T *_parent)
|
||||
: type(_type),
|
||||
key(_key),
|
||||
lazy_loaded(false),
|
||||
SimpleTreeItem<T>::SimpleTreeItem(const QString &_container_key, T *_parent)
|
||||
: container_key(_container_key),
|
||||
parent(_parent),
|
||||
child_model(nullptr),
|
||||
model(_parent ? _parent->model : nullptr) {
|
||||
|
@ -88,10 +82,8 @@ SimpleTreeItem<T>::SimpleTreeItem(int _type, const QString &_key, T *_parent)
|
|||
}
|
||||
|
||||
template<typename T>
|
||||
SimpleTreeItem<T>::SimpleTreeItem(int _type, T *_parent)
|
||||
: type(_type),
|
||||
lazy_loaded(false),
|
||||
parent(_parent),
|
||||
SimpleTreeItem<T>::SimpleTreeItem(T *_parent)
|
||||
: parent(_parent),
|
||||
child_model(nullptr),
|
||||
model(_parent ? _parent->model : nullptr) {
|
||||
if (parent) {
|
||||
|
@ -112,7 +104,7 @@ void SimpleTreeItem<T>::InsertNotify(T *_parent) {
|
|||
}
|
||||
|
||||
template<typename T>
|
||||
void SimpleTreeItem<T>::DeleteNotify(int child_row) {
|
||||
void SimpleTreeItem<T>::DeleteNotify(const int child_row) {
|
||||
model->BeginDelete(static_cast<T*>(this), child_row);
|
||||
delete children.takeAt(child_row);
|
||||
|
||||
|
|
|
@ -35,12 +35,10 @@ class SimpleTreeModel : public QAbstractItemModel {
|
|||
|
||||
// QAbstractItemModel
|
||||
int columnCount(const QModelIndex &parent) const override;
|
||||
QModelIndex index(int row, int, const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex index(const int row, const int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex &idx) const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
bool hasChildren(const QModelIndex &parent) const override;
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
|
||||
T *IndexToItem(const QModelIndex &idx) const;
|
||||
QModelIndex ItemToIndex(T *item) const;
|
||||
|
@ -52,9 +50,6 @@ class SimpleTreeModel : public QAbstractItemModel {
|
|||
void EndDelete();
|
||||
void EmitDataChanged(T *item);
|
||||
|
||||
protected:
|
||||
virtual void LazyPopulate(T *item) { item->lazy_loaded = true; }
|
||||
|
||||
protected:
|
||||
T *root_;
|
||||
};
|
||||
|
@ -81,7 +76,9 @@ int SimpleTreeModel<T>::columnCount(const QModelIndex&) const {
|
|||
}
|
||||
|
||||
template<typename T>
|
||||
QModelIndex SimpleTreeModel<T>::index(int row, int, const QModelIndex &parent) const {
|
||||
QModelIndex SimpleTreeModel<T>::index(const int row, const int column, const QModelIndex &parent) const {
|
||||
|
||||
Q_UNUSED(column);
|
||||
|
||||
T *parent_item = IndexToItem(parent);
|
||||
if (!parent_item || row < 0 || parent_item->children.count() <= row)
|
||||
|
@ -106,25 +103,8 @@ int SimpleTreeModel<T>::rowCount(const QModelIndex &parent) const {
|
|||
template<typename T>
|
||||
bool SimpleTreeModel<T>::hasChildren(const QModelIndex &parent) const {
|
||||
T *item = IndexToItem(parent);
|
||||
if (!item) return false;
|
||||
if (item->lazy_loaded)
|
||||
return !item->children.isEmpty();
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool SimpleTreeModel<T>::canFetchMore(const QModelIndex &parent) const {
|
||||
T *item = IndexToItem(parent);
|
||||
return item && !item->lazy_loaded;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void SimpleTreeModel<T>::fetchMore(const QModelIndex &parent) {
|
||||
T *item = IndexToItem(parent);
|
||||
if (item && !item->lazy_loaded) {
|
||||
LazyPopulate(item);
|
||||
}
|
||||
if (!item) return 0;
|
||||
return !item->children.isEmpty();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
|
|
|
@ -144,35 +144,41 @@ const QStringList Song::kColumns = QStringList() << QStringLiteral("title")
|
|||
|
||||
const QStringList Song::kRowIdColumns = QStringList() << QStringLiteral("ROWID") << kColumns;
|
||||
|
||||
const QString Song::kColumnSpec = Song::kColumns.join(QStringLiteral(", "));
|
||||
const QString Song::kRowIdColumnSpec = Song::kRowIdColumns.join(QStringLiteral(", "));
|
||||
const QString Song::kBindSpec = Utilities::Prepend(QStringLiteral(":"), Song::kColumns).join(QStringLiteral(", "));
|
||||
const QString Song::kUpdateSpec = Utilities::Updateify(Song::kColumns).join(QStringLiteral(", "));
|
||||
const QString Song::kColumnSpec = kColumns.join(QStringLiteral(", "));
|
||||
const QString Song::kRowIdColumnSpec = kRowIdColumns.join(QStringLiteral(", "));
|
||||
const QString Song::kBindSpec = Utilities::Prepend(QStringLiteral(":"), kColumns).join(QStringLiteral(", "));
|
||||
const QString Song::kUpdateSpec = Utilities::Updateify(kColumns).join(QStringLiteral(", "));
|
||||
|
||||
// used to indicate, what columns can be filtered numerically. Used by the CollectionQuery.
|
||||
const QStringList Song::kNumericalColumns = QStringList() << QStringLiteral("year")
|
||||
<< QStringLiteral("length")
|
||||
<< QStringLiteral("samplerate")
|
||||
<< QStringLiteral("bitdepth")
|
||||
<< QStringLiteral("bitrate")
|
||||
<< QStringLiteral("rating")
|
||||
<< QStringLiteral("playcount")
|
||||
<< QStringLiteral("skipcount");
|
||||
const QStringList Song::kTextSearchColumns = QStringList() << QStringLiteral("title")
|
||||
<< QStringLiteral("album")
|
||||
<< QStringLiteral("artist")
|
||||
<< QStringLiteral("albumartist")
|
||||
<< QStringLiteral("composer")
|
||||
<< QStringLiteral("performer")
|
||||
<< QStringLiteral("grouping")
|
||||
<< QStringLiteral("genre")
|
||||
<< QStringLiteral("comment");
|
||||
|
||||
const QStringList Song::kIntSearchColumns = QStringList() << QStringLiteral("track")
|
||||
<< QStringLiteral("year")
|
||||
<< QStringLiteral("samplerate")
|
||||
<< QStringLiteral("bitdepth")
|
||||
<< QStringLiteral("bitrate");
|
||||
|
||||
const QStringList Song::kFtsColumns = QStringList() << QStringLiteral("ftstitle")
|
||||
<< QStringLiteral("ftsalbum")
|
||||
<< QStringLiteral("ftsartist")
|
||||
<< QStringLiteral("ftsalbumartist")
|
||||
<< QStringLiteral("ftscomposer")
|
||||
<< QStringLiteral("ftsperformer")
|
||||
<< QStringLiteral("ftsgrouping")
|
||||
<< QStringLiteral("ftsgenre")
|
||||
<< QStringLiteral("ftscomment");
|
||||
const QStringList Song::kUIntSearchColumns = QStringList() << QStringLiteral("playcount")
|
||||
<< QStringLiteral("skipcount");
|
||||
|
||||
const QString Song::kFtsColumnSpec = Song::kFtsColumns.join(QStringLiteral(", "));
|
||||
const QString Song::kFtsBindSpec = Utilities::Prepend(QStringLiteral(":"), Song::kFtsColumns).join(QStringLiteral(", "));
|
||||
const QString Song::kFtsUpdateSpec = Utilities::Updateify(Song::kFtsColumns).join(QStringLiteral(", "));
|
||||
const QStringList Song::kInt64SearchColumns = QStringList() << QStringLiteral("length");
|
||||
|
||||
const QStringList Song::kFloatSearchColumns = QStringList() << QStringLiteral("rating");
|
||||
|
||||
const QStringList Song::kNumericalSearchColumns = QStringList() << kIntSearchColumns
|
||||
<< kUIntSearchColumns
|
||||
<< kInt64SearchColumns
|
||||
<< kFloatSearchColumns;
|
||||
|
||||
const QStringList Song::kSearchColumns = QStringList() << kTextSearchColumns
|
||||
<< kNumericalSearchColumns;
|
||||
|
||||
const Song::RegularExpressionList Song::kAlbumDisc = Song::RegularExpressionList()
|
||||
<< QRegularExpression(QStringLiteral("\\s+-*\\s*(Disc|CD)\\s*([0-9]{1,2})$"), QRegularExpression::CaseInsensitiveOption)
|
||||
|
@ -199,10 +205,33 @@ const Song::RegularExpressionList Song::kTitleMisc = Song::RegularExpressionList
|
|||
|
||||
const QStringList Song::kArticles = QStringList() << QStringLiteral("the ") << QStringLiteral("a ") << QStringLiteral("an ");
|
||||
|
||||
const QStringList Song::kAcceptedExtensions = QStringList() << QStringLiteral("wav") << QStringLiteral("flac") << QStringLiteral("wv") << QStringLiteral("ogg") << QStringLiteral("oga") << QStringLiteral("opus") << QStringLiteral("spx") << QStringLiteral("ape") << QStringLiteral("mpc")
|
||||
<< QStringLiteral("mp2") << QStringLiteral("mp3") << QStringLiteral("m4a") << QStringLiteral("mp4") << QStringLiteral("aac") << QStringLiteral("asf") << QStringLiteral("asx") << QStringLiteral("wma")
|
||||
<< QStringLiteral("aif << aiff") << QStringLiteral("mka") << QStringLiteral("tta") << QStringLiteral("dsf") << QStringLiteral("dsd")
|
||||
<< QStringLiteral("ac3") << QStringLiteral("dts") << QStringLiteral("spc") << QStringLiteral("vgm");
|
||||
const QStringList Song::kAcceptedExtensions = QStringList() << QStringLiteral("wav")
|
||||
<< QStringLiteral("flac")
|
||||
<< QStringLiteral("wv")
|
||||
<< QStringLiteral("ogg")
|
||||
<< QStringLiteral("oga")
|
||||
<< QStringLiteral("opus")
|
||||
<< QStringLiteral("spx")
|
||||
<< QStringLiteral("ape")
|
||||
<< QStringLiteral("mpc")
|
||||
<< QStringLiteral("mp2")
|
||||
<< QStringLiteral("mp3")
|
||||
<< QStringLiteral("m4a")
|
||||
<< QStringLiteral("mp4")
|
||||
<< QStringLiteral("aac")
|
||||
<< QStringLiteral("asf")
|
||||
<< QStringLiteral("asx")
|
||||
<< QStringLiteral("wma")
|
||||
<< QStringLiteral("aif")
|
||||
<< QStringLiteral("aiff")
|
||||
<< QStringLiteral("mka")
|
||||
<< QStringLiteral("tta")
|
||||
<< QStringLiteral("dsf")
|
||||
<< QStringLiteral("dsd")
|
||||
<< QStringLiteral("ac3")
|
||||
<< QStringLiteral("dts")
|
||||
<< QStringLiteral("spc")
|
||||
<< QStringLiteral("vgm");
|
||||
|
||||
struct Song::Private : public QSharedData {
|
||||
|
||||
|
@ -1781,20 +1810,6 @@ void Song::BindToQuery(SqlQuery *query) const {
|
|||
|
||||
}
|
||||
|
||||
void Song::BindToFtsQuery(SqlQuery *query) const {
|
||||
|
||||
query->BindValue(QStringLiteral(":ftstitle"), d->title_);
|
||||
query->BindValue(QStringLiteral(":ftsalbum"), d->album_);
|
||||
query->BindValue(QStringLiteral(":ftsartist"), d->artist_);
|
||||
query->BindValue(QStringLiteral(":ftsalbumartist"), d->albumartist_);
|
||||
query->BindValue(QStringLiteral(":ftscomposer"), d->composer_);
|
||||
query->BindValue(QStringLiteral(":ftsperformer"), d->performer_);
|
||||
query->BindValue(QStringLiteral(":ftsgrouping"), d->grouping_);
|
||||
query->BindValue(QStringLiteral(":ftsgenre"), d->genre_);
|
||||
query->BindValue(QStringLiteral(":ftscomment"), d->comment_);
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
void Song::ToXesam(QVariantMap *map) const {
|
||||
|
||||
|
|
|
@ -119,12 +119,13 @@ class Song {
|
|||
static const QString kBindSpec;
|
||||
static const QString kUpdateSpec;
|
||||
|
||||
static const QStringList kNumericalColumns;
|
||||
|
||||
static const QStringList kFtsColumns;
|
||||
static const QString kFtsColumnSpec;
|
||||
static const QString kFtsBindSpec;
|
||||
static const QString kFtsUpdateSpec;
|
||||
static const QStringList kTextSearchColumns;
|
||||
static const QStringList kIntSearchColumns;
|
||||
static const QStringList kUIntSearchColumns;
|
||||
static const QStringList kInt64SearchColumns;
|
||||
static const QStringList kFloatSearchColumns;
|
||||
static const QStringList kNumericalSearchColumns;
|
||||
static const QStringList kSearchColumns;
|
||||
|
||||
using RegularExpressionList = QList<QRegularExpression>;
|
||||
static const RegularExpressionList kAlbumDisc;
|
||||
|
@ -439,7 +440,6 @@ class Song {
|
|||
|
||||
// Save
|
||||
void BindToQuery(SqlQuery *query) const;
|
||||
void BindToFtsQuery(SqlQuery *query) const;
|
||||
#ifdef HAVE_DBUS
|
||||
void ToXesam(QVariantMap *map) const;
|
||||
#endif
|
||||
|
|
|
@ -236,7 +236,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
|
|||
QMutexLocker l(collection_backend_->db()->Mutex());
|
||||
QSqlDatabase db(collection_backend_->db()->Connect());
|
||||
|
||||
CollectionQuery query(db, collection_backend_->songs_table(), collection_backend_->fts_table());
|
||||
CollectionQuery query(db, collection_backend_->songs_table());
|
||||
query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
|
||||
query.AddWhere(QStringLiteral("url"), url.toEncoded());
|
||||
|
||||
|
|
|
@ -876,7 +876,7 @@ SongList AlbumCoverManager::GetSongsInAlbum(const QModelIndex &idx) const {
|
|||
QMutexLocker l(collection_backend_->db()->Mutex());
|
||||
QSqlDatabase db(collection_backend_->db()->Connect());
|
||||
|
||||
CollectionQuery q(db, collection_backend_->songs_table(), collection_backend_->fts_table());
|
||||
CollectionQuery q(db, collection_backend_->songs_table());
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(QStringLiteral("album"), idx.data(Role_Album).toString());
|
||||
q.SetOrderBy(QStringLiteral("disc, track, title"));
|
||||
|
|
|
@ -41,7 +41,7 @@ CddaDevice::CddaDevice(const QUrl &url, DeviceLister *lister, const QString &uni
|
|||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsDurationLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsMetadataLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(this, &CddaDevice::SongsDiscovered, model_, &CollectionModel::SongsDiscovered);
|
||||
QObject::connect(this, &CddaDevice::SongsDiscovered, model_, &CollectionModel::AddReAddOrUpdate);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,6 @@ ConnectedDevice::ConnectedDevice(const QUrl &url, DeviceLister *lister, const QS
|
|||
app_->task_manager(),
|
||||
Song::Source::Device,
|
||||
QStringLiteral("device_%1_songs").arg(database_id),
|
||||
QStringLiteral("device_%1_fts").arg(database_id),
|
||||
QStringLiteral("device_%1_directories").arg(database_id),
|
||||
QStringLiteral("device_%1_subdirectories").arg(database_id));
|
||||
|
||||
|
|
|
@ -186,7 +186,7 @@ void DeviceDatabaseBackend::RemoveDevice(const int id) {
|
|||
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DROP TABLE device_%1_fts").arg(id));
|
||||
q.prepare(QStringLiteral("DROP TABLE IF EXISTS device_%1_fts").arg(id));
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
|
|
|
@ -55,13 +55,14 @@ class ConnectedDevice;
|
|||
class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
|
||||
|
||||
public:
|
||||
enum Type {
|
||||
Type_Root,
|
||||
Type_Device,
|
||||
enum class Type {
|
||||
Root,
|
||||
Device,
|
||||
};
|
||||
|
||||
explicit DeviceInfo(SimpleTreeModel<DeviceInfo> *_model)
|
||||
: SimpleTreeItem<DeviceInfo>(Type_Root, _model),
|
||||
: SimpleTreeItem<DeviceInfo>(_model),
|
||||
type_(Type::Root),
|
||||
database_id_(-1),
|
||||
size_(0),
|
||||
transcode_mode_(MusicStorage::TranscodeMode::Transcode_Unsupported),
|
||||
|
@ -71,7 +72,8 @@ class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
|
|||
forget_(false) {}
|
||||
|
||||
explicit DeviceInfo(const Type _type, DeviceInfo *_parent = nullptr)
|
||||
: SimpleTreeItem<DeviceInfo>(_type, _parent),
|
||||
: SimpleTreeItem<DeviceInfo>(_parent),
|
||||
type_(_type),
|
||||
database_id_(-1),
|
||||
size_(0),
|
||||
transcode_mode_(MusicStorage::TranscodeMode::Transcode_Unsupported),
|
||||
|
@ -101,6 +103,7 @@ class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
|
|||
// Gets the best backend available (the one with the highest priority)
|
||||
const Backend *BestBackend() const;
|
||||
|
||||
Type type_;
|
||||
int database_id_; // -1 if not remembered in the database
|
||||
SharedPtr<ConnectedDevice> device_; // nullptr if not connected
|
||||
QList<Backend> backends_;
|
||||
|
@ -122,4 +125,6 @@ class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
|
|||
Q_DISABLE_COPY(DeviceInfo)
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(DeviceInfo::Type)
|
||||
|
||||
#endif // DEVICEINFO_H
|
||||
|
|
|
@ -233,7 +233,7 @@ void DeviceManager::LoadAllDevices() {
|
|||
|
||||
DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
|
||||
for (const DeviceDatabaseBackend::Device &device : devices) {
|
||||
DeviceInfo *info = new DeviceInfo(DeviceInfo::Type_Device, root_);
|
||||
DeviceInfo *info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
info->InitFromDb(device);
|
||||
emit DeviceCreatedFromDB(info);
|
||||
}
|
||||
|
@ -479,7 +479,7 @@ void DeviceManager::PhysicalDeviceAdded(const QString &id) {
|
|||
}
|
||||
else {
|
||||
// It's a completely new device
|
||||
info = new DeviceInfo(DeviceInfo::Type_Device, root_);
|
||||
info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
info->size_ = lister->DeviceCapacity(id);
|
||||
|
|
|
@ -89,10 +89,6 @@ void InternetCollectionView::Init(Application *app, SharedPtr<CollectionBackend>
|
|||
collection_model_ = collection_model;
|
||||
favorite_ = favorite;
|
||||
|
||||
collection_model_->set_pretty_covers(true);
|
||||
collection_model_->set_show_dividers(true);
|
||||
collection_model_->set_sort_skips_articles(true);
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
@ -105,15 +101,20 @@ void InternetCollectionView::SetFilter(CollectionFilterWidget *filter) {
|
|||
|
||||
void InternetCollectionView::ReloadSettings() {
|
||||
|
||||
if (collection_model_) collection_model_->ReloadSettings();
|
||||
if (filter_) filter_->ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void InternetCollectionView::SaveFocus() {
|
||||
|
||||
QModelIndex current = currentIndex();
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || (type.toInt() != CollectionItem::Type_Song && type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) {
|
||||
const QModelIndex current = currentIndex();
|
||||
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!role_type.isValid()) {
|
||||
return;
|
||||
}
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -121,8 +122,8 @@ void InternetCollectionView::SaveFocus() {
|
|||
last_selected_song_ = Song();
|
||||
last_selected_container_ = QString();
|
||||
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song:{
|
||||
switch (item_type) {
|
||||
case CollectionItem::Type::Song:{
|
||||
QModelIndex idx = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
SongList songs = collection_model_->GetChildSongs(idx);
|
||||
if (!songs.isEmpty()) {
|
||||
|
@ -131,8 +132,8 @@ void InternetCollectionView::SaveFocus() {
|
|||
break;
|
||||
}
|
||||
|
||||
case CollectionItem::Type_Container:
|
||||
case CollectionItem::Type_Divider:{
|
||||
case CollectionItem::Type::Container:
|
||||
case CollectionItem::Type::Divider:{
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
last_selected_container_ = text;
|
||||
break;
|
||||
|
@ -148,9 +149,13 @@ void InternetCollectionView::SaveFocus() {
|
|||
|
||||
void InternetCollectionView::SaveContainerPath(const QModelIndex &child) {
|
||||
|
||||
QModelIndex current = model()->parent(child);
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || (type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) {
|
||||
const QModelIndex current = model()->parent(child);
|
||||
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!role_type.isValid()) {
|
||||
return;
|
||||
}
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
if (item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -174,12 +179,18 @@ bool InternetCollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
|||
if (model()->canFetchMore(parent)) {
|
||||
model()->fetchMore(parent);
|
||||
}
|
||||
int rows = model()->rowCount(parent);
|
||||
const int rows = model()->rowCount(parent);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
QModelIndex current = model()->index(i, 0, parent);
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song:
|
||||
const QModelIndex current = model()->index(i, 0, parent);
|
||||
if (!current.isValid()) continue;
|
||||
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!role_type.isValid()) continue;
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
switch (item_type) {
|
||||
case CollectionItem::Type::Root:
|
||||
case CollectionItem::Type::LoadingIndicator:
|
||||
break;
|
||||
case CollectionItem::Type::Song:
|
||||
if (!last_selected_song_.url().isEmpty()) {
|
||||
QModelIndex idx = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
const SongList songs = collection_model_->GetChildSongs(idx);
|
||||
|
@ -192,8 +203,8 @@ bool InternetCollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
|||
}
|
||||
break;
|
||||
|
||||
case CollectionItem::Type_Container:
|
||||
case CollectionItem::Type_Divider:{
|
||||
case CollectionItem::Type::Container:
|
||||
case CollectionItem::Type::Divider:{
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
if (!last_selected_container_.isEmpty() && last_selected_container_ == text) {
|
||||
expand(current);
|
||||
|
@ -435,8 +446,11 @@ void InternetCollectionView::FilterReturnPressed() {
|
|||
if (!currentIndex().isValid()) {
|
||||
// Pick the first thing that isn't a divider
|
||||
for (int row = 0; row < model()->rowCount(); ++row) {
|
||||
QModelIndex idx(model()->index(row, 0));
|
||||
if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) {
|
||||
QModelIndex idx = model()->index(row, 0);
|
||||
const QVariant role_type = idx.data(CollectionModel::Role::Role_Type);
|
||||
if (!role_type.isValid()) continue;
|
||||
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
|
||||
if (item_type != CollectionItem::Type::Divider) {
|
||||
setCurrentIndex(idx);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -33,10 +33,10 @@
|
|||
#include "settings/settingsdialog.h"
|
||||
#include "internetsearchview.h"
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
class Application;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class CollectionFilter;
|
||||
|
||||
class InternetService : public QObject {
|
||||
Q_OBJECT
|
||||
|
@ -69,9 +69,9 @@ class InternetService : public QObject {
|
|||
virtual CollectionModel *albums_collection_model() { return nullptr; }
|
||||
virtual CollectionModel *songs_collection_model() { return nullptr; }
|
||||
|
||||
virtual QSortFilterProxyModel *artists_collection_sort_model() { return nullptr; }
|
||||
virtual QSortFilterProxyModel *albums_collection_sort_model() { return nullptr; }
|
||||
virtual QSortFilterProxyModel *songs_collection_sort_model() { return nullptr; }
|
||||
virtual CollectionFilter *artists_collection_filter_model() { return nullptr; }
|
||||
virtual CollectionFilter *albums_collection_filter_model() { return nullptr; }
|
||||
virtual CollectionFilter *songs_collection_filter_model() { return nullptr; }
|
||||
|
||||
public slots:
|
||||
virtual void ShowConfig() {}
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
#include "core/iconloader.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionfilter.h"
|
||||
#include "internetservice.h"
|
||||
#include "internetsongsview.h"
|
||||
#include "internetcollectionview.h"
|
||||
|
@ -51,10 +52,10 @@ InternetSongsView::InternetSongsView(Application *app, InternetServicePtr servic
|
|||
|
||||
ui_->stacked->setCurrentWidget(ui_->internetcollection_page);
|
||||
ui_->view->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model(), false);
|
||||
ui_->view->setModel(service_->songs_collection_sort_model());
|
||||
ui_->view->setModel(service_->songs_collection_filter_model());
|
||||
ui_->view->SetFilter(ui_->filter_widget);
|
||||
ui_->filter_widget->SetSettingsGroup(settings_group);
|
||||
ui_->filter_widget->Init(service_->songs_collection_model());
|
||||
ui_->filter_widget->Init(service_->songs_collection_model(), service_->songs_collection_filter_model());
|
||||
|
||||
QAction *action_configure = new QAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure %1...").arg(Song::DescriptionForSource(service_->source())), this);
|
||||
QObject::connect(action_configure, &QAction::triggered, this, &InternetSongsView::OpenSettingsDialog);
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
#include "core/settings.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionfilter.h"
|
||||
#include "collection/collectionfilterwidget.h"
|
||||
#include "internetservice.h"
|
||||
#include "internettabsview.h"
|
||||
|
@ -66,11 +67,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service,
|
|||
if (service_->artists_collection_model()) {
|
||||
ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->internetcollection_page());
|
||||
ui_->artists_collection->view()->Init(app_, service_->artists_collection_backend(), service_->artists_collection_model(), true);
|
||||
ui_->artists_collection->view()->setModel(service_->artists_collection_sort_model());
|
||||
ui_->artists_collection->view()->setModel(service_->artists_collection_filter_model());
|
||||
ui_->artists_collection->view()->SetFilter(ui_->artists_collection->filter_widget());
|
||||
ui_->artists_collection->filter_widget()->SetSettingsGroup(settings_group);
|
||||
ui_->artists_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("artists"));
|
||||
ui_->artists_collection->filter_widget()->Init(service_->artists_collection_model());
|
||||
ui_->artists_collection->filter_widget()->Init(service_->artists_collection_model(), service_->artists_collection_filter_model());
|
||||
ui_->artists_collection->filter_widget()->AddMenuAction(action_configure);
|
||||
|
||||
QObject::connect(ui_->artists_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetArtists);
|
||||
|
@ -98,11 +99,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service,
|
|||
if (service_->albums_collection_model()) {
|
||||
ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->internetcollection_page());
|
||||
ui_->albums_collection->view()->Init(app_, service_->albums_collection_backend(), service_->albums_collection_model(), true);
|
||||
ui_->albums_collection->view()->setModel(service_->albums_collection_sort_model());
|
||||
ui_->albums_collection->view()->setModel(service_->albums_collection_filter_model());
|
||||
ui_->albums_collection->view()->SetFilter(ui_->albums_collection->filter_widget());
|
||||
ui_->albums_collection->filter_widget()->SetSettingsGroup(settings_group);
|
||||
ui_->albums_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("albums"));
|
||||
ui_->albums_collection->filter_widget()->Init(service_->albums_collection_model());
|
||||
ui_->albums_collection->filter_widget()->Init(service_->albums_collection_model(), service_->albums_collection_filter_model());
|
||||
ui_->albums_collection->filter_widget()->AddMenuAction(action_configure);
|
||||
|
||||
QObject::connect(ui_->albums_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetAlbums);
|
||||
|
@ -130,11 +131,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service,
|
|||
if (service_->songs_collection_model()) {
|
||||
ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->internetcollection_page());
|
||||
ui_->songs_collection->view()->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model(), true);
|
||||
ui_->songs_collection->view()->setModel(service_->songs_collection_sort_model());
|
||||
ui_->songs_collection->view()->setModel(service_->songs_collection_filter_model());
|
||||
ui_->songs_collection->view()->SetFilter(ui_->songs_collection->filter_widget());
|
||||
ui_->songs_collection->filter_widget()->SetSettingsGroup(settings_group);
|
||||
ui_->songs_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("songs"));
|
||||
ui_->songs_collection->filter_widget()->Init(service_->songs_collection_model());
|
||||
ui_->songs_collection->filter_widget()->Init(service_->songs_collection_model(), service_->songs_collection_filter_model());
|
||||
ui_->songs_collection->filter_widget()->AddMenuAction(action_configure);
|
||||
|
||||
QObject::connect(ui_->songs_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetSongs);
|
||||
|
|
|
@ -96,9 +96,9 @@ void PlaylistManager::Init(SharedPtr<CollectionBackend> collection_backend, Shar
|
|||
parser_ = new PlaylistParser(collection_backend, this);
|
||||
playlist_container_ = playlist_container;
|
||||
|
||||
QObject::connect(&*collection_backend_, &CollectionBackend::SongsDiscovered, this, &PlaylistManager::SongsDiscovered);
|
||||
QObject::connect(&*collection_backend_, &CollectionBackend::SongsStatisticsChanged, this, &PlaylistManager::SongsDiscovered);
|
||||
QObject::connect(&*collection_backend_, &CollectionBackend::SongsRatingChanged, this, &PlaylistManager::SongsDiscovered);
|
||||
QObject::connect(&*collection_backend_, &CollectionBackend::SongsChanged, this, &PlaylistManager::UpdateSongs);
|
||||
QObject::connect(&*collection_backend_, &CollectionBackend::SongsStatisticsChanged, this, &PlaylistManager::UpdateSongs);
|
||||
QObject::connect(&*collection_backend_, &CollectionBackend::SongsRatingChanged, this, &PlaylistManager::UpdateSongs);
|
||||
|
||||
for (const PlaylistBackend::Playlist &p : playlist_backend->GetAllOpenPlaylists()) {
|
||||
++playlists_loading_;
|
||||
|
@ -463,7 +463,7 @@ void PlaylistManager::SelectionChanged(const QItemSelection &selection) {
|
|||
UpdateSummaryText();
|
||||
}
|
||||
|
||||
void PlaylistManager::SongsDiscovered(const SongList &songs) {
|
||||
void PlaylistManager::UpdateSongs(const SongList &songs) {
|
||||
|
||||
// Some songs might've changed in the collection, let's update any playlist items we have that match those songs
|
||||
|
||||
|
|
|
@ -233,7 +233,7 @@ class PlaylistManager : public PlaylistManagerInterface {
|
|||
|
||||
void OneOfPlaylistsChanged();
|
||||
void UpdateSummaryText();
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
void UpdateSongs(const SongList &songs);
|
||||
void ItemsLoadedForSavePlaylist(const SongList &songs, const QString &filename, const PlaylistSettingsPage::PathType path_type);
|
||||
void PlaylistLoaded();
|
||||
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QSslError>
|
||||
|
||||
#include "core/shared_ptr.h"
|
||||
|
@ -50,6 +49,7 @@
|
|||
#include "internet/internetsearchview.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionfilter.h"
|
||||
#include "qobuzservice.h"
|
||||
#include "qobuzurlhandler.h"
|
||||
#include "qobuzbaserequest.h"
|
||||
|
@ -75,10 +75,6 @@ constexpr char kArtistsSongsTable[] = "qobuz_artists_songs";
|
|||
constexpr char kAlbumsSongsTable[] = "qobuz_albums_songs";
|
||||
constexpr char kSongsTable[] = "qobuz_songs";
|
||||
|
||||
constexpr char kArtistsSongsFtsTable[] = "qobuz_artists_songs_fts";
|
||||
constexpr char kAlbumsSongsFtsTable[] = "qobuz_albums_songs_fts";
|
||||
constexpr char kSongsFtsTable[] = "qobuz_songs_fts";
|
||||
|
||||
} // namespace
|
||||
|
||||
QobuzService::QobuzService(Application *app, QObject *parent)
|
||||
|
@ -92,9 +88,6 @@ QobuzService::QobuzService(Application *app, QObject *parent)
|
|||
artists_collection_model_(nullptr),
|
||||
albums_collection_model_(nullptr),
|
||||
songs_collection_model_(nullptr),
|
||||
artists_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
albums_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
songs_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
timer_search_delay_(new QTimer(this)),
|
||||
timer_login_attempt_(new QTimer(this)),
|
||||
favorite_request_(new QobuzFavoriteRequest(this, network_, this)),
|
||||
|
@ -120,38 +113,21 @@ QobuzService::QobuzService(Application *app, QObject *parent)
|
|||
|
||||
artists_collection_backend_ = make_shared<CollectionBackend>();
|
||||
artists_collection_backend_->moveToThread(app_->database()->thread());
|
||||
artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kArtistsSongsTable), QLatin1String(kArtistsSongsFtsTable));
|
||||
artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kArtistsSongsTable));
|
||||
|
||||
albums_collection_backend_ = make_shared<CollectionBackend>();
|
||||
albums_collection_backend_->moveToThread(app_->database()->thread());
|
||||
albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kAlbumsSongsTable), QLatin1String(kAlbumsSongsFtsTable));
|
||||
albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kAlbumsSongsTable));
|
||||
|
||||
songs_collection_backend_ = make_shared<CollectionBackend>();
|
||||
songs_collection_backend_->moveToThread(app_->database()->thread());
|
||||
songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable));
|
||||
songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kSongsTable));
|
||||
|
||||
// Models
|
||||
artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this);
|
||||
albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this);
|
||||
songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this);
|
||||
|
||||
artists_collection_sort_model_->setSourceModel(artists_collection_model_);
|
||||
artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
artists_collection_sort_model_->setDynamicSortFilter(true);
|
||||
artists_collection_sort_model_->setSortLocaleAware(true);
|
||||
artists_collection_sort_model_->sort(0);
|
||||
|
||||
albums_collection_sort_model_->setSourceModel(albums_collection_model_);
|
||||
albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
albums_collection_sort_model_->setDynamicSortFilter(true);
|
||||
albums_collection_sort_model_->setSortLocaleAware(true);
|
||||
albums_collection_sort_model_->sort(0);
|
||||
|
||||
songs_collection_sort_model_->setSourceModel(songs_collection_model_);
|
||||
songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
songs_collection_sort_model_->setDynamicSortFilter(true);
|
||||
songs_collection_sort_model_->setSortLocaleAware(true);
|
||||
songs_collection_sort_model_->sort(0);
|
||||
|
||||
// Search
|
||||
|
||||
timer_search_delay_->setSingleShot(true);
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
|
||||
class QTimer;
|
||||
class QNetworkReply;
|
||||
class QSortFilterProxyModel;
|
||||
class Application;
|
||||
class NetworkAccessManager;
|
||||
class QobuzUrlHandler;
|
||||
|
@ -53,6 +52,7 @@ class QobuzFavoriteRequest;
|
|||
class QobuzStreamURLRequest;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class CollectionFilter;
|
||||
|
||||
class QobuzService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
@ -105,9 +105,9 @@ class QobuzService : public InternetService {
|
|||
CollectionModel *albums_collection_model() override { return albums_collection_model_; }
|
||||
CollectionModel *songs_collection_model() override { return songs_collection_model_; }
|
||||
|
||||
QSortFilterProxyModel *artists_collection_sort_model() override { return artists_collection_sort_model_; }
|
||||
QSortFilterProxyModel *albums_collection_sort_model() override { return albums_collection_sort_model_; }
|
||||
QSortFilterProxyModel *songs_collection_sort_model() override { return songs_collection_sort_model_; }
|
||||
CollectionFilter *artists_collection_filter_model() override { return artists_collection_model_->filter(); }
|
||||
CollectionFilter *albums_collection_filter_model() override { return albums_collection_model_->filter(); }
|
||||
CollectionFilter *songs_collection_filter_model() override { return songs_collection_model_->filter(); }
|
||||
|
||||
public slots:
|
||||
void ShowConfig() override;
|
||||
|
@ -160,10 +160,6 @@ class QobuzService : public InternetService {
|
|||
CollectionModel *albums_collection_model_;
|
||||
CollectionModel *songs_collection_model_;
|
||||
|
||||
QSortFilterProxyModel *artists_collection_sort_model_;
|
||||
QSortFilterProxyModel *albums_collection_sort_model_;
|
||||
QSortFilterProxyModel *songs_collection_sort_model_;
|
||||
|
||||
QTimer *timer_search_delay_;
|
||||
QTimer *timer_login_attempt_;
|
||||
|
||||
|
|
|
@ -29,16 +29,17 @@
|
|||
class RadioItem : public SimpleTreeItem<RadioItem> {
|
||||
public:
|
||||
|
||||
enum Type {
|
||||
Type_LoadingIndicator,
|
||||
Type_Root,
|
||||
Type_Service,
|
||||
Type_Channel
|
||||
enum class Type {
|
||||
LoadingIndicator,
|
||||
Root,
|
||||
Service,
|
||||
Channel
|
||||
};
|
||||
|
||||
explicit RadioItem(SimpleTreeModel<RadioItem> *_model) : SimpleTreeItem<RadioItem>(Type_Root, _model) {}
|
||||
explicit RadioItem(Type _type, RadioItem *_parent = nullptr) : SimpleTreeItem<RadioItem>(_type, _parent) {}
|
||||
explicit RadioItem(SimpleTreeModel<RadioItem> *_model) : SimpleTreeItem<RadioItem>(_model), type(Type::Root) {}
|
||||
explicit RadioItem(const Type _type, RadioItem *_parent = nullptr) : SimpleTreeItem<RadioItem>(_parent), type(_type) {}
|
||||
|
||||
Type type;
|
||||
Song::Source source;
|
||||
RadioChannel channel;
|
||||
|
||||
|
@ -46,4 +47,6 @@ class RadioItem : public SimpleTreeItem<RadioItem> {
|
|||
Q_DISABLE_COPY(RadioItem)
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(RadioItem::Type)
|
||||
|
||||
#endif // RADIOITEM_H
|
||||
|
|
|
@ -45,8 +45,6 @@ RadioModel::RadioModel(Application *app, QObject *parent)
|
|||
: SimpleTreeModel<RadioItem>(new RadioItem(this), parent),
|
||||
app_(app) {
|
||||
|
||||
root_->lazy_loaded = true;
|
||||
|
||||
if (app_) {
|
||||
QObject::connect(&*app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &RadioModel::AlbumCoverLoaded);
|
||||
}
|
||||
|
@ -60,11 +58,11 @@ RadioModel::~RadioModel() {
|
|||
Qt::ItemFlags RadioModel::flags(const QModelIndex &idx) const {
|
||||
|
||||
switch (IndexToItem(idx)->type) {
|
||||
case RadioItem::Type_Service:
|
||||
case RadioItem::Type_Channel:
|
||||
case RadioItem::Type::Service:
|
||||
case RadioItem::Type::Channel:
|
||||
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
|
||||
case RadioItem::Type_Root:
|
||||
case RadioItem::Type_LoadingIndicator:
|
||||
case RadioItem::Type::Root:
|
||||
case RadioItem::Type::LoadingIndicator:
|
||||
default:
|
||||
return Qt::ItemIsEnabled;
|
||||
}
|
||||
|
@ -78,7 +76,7 @@ QVariant RadioModel::data(const QModelIndex &idx, int role) const {
|
|||
const RadioItem *item = IndexToItem(idx);
|
||||
if (!item) return QVariant();
|
||||
|
||||
if (role == Qt::DecorationRole && item->type == RadioItem::Type_Channel) {
|
||||
if (role == Qt::DecorationRole && item->type == RadioItem::Type::Channel) {
|
||||
return const_cast<RadioModel*>(this)->ChannelIcon(idx);
|
||||
}
|
||||
|
||||
|
@ -90,7 +88,7 @@ QVariant RadioModel::data(const RadioItem *item, int role) const {
|
|||
|
||||
switch (role) {
|
||||
case Qt::DecorationRole:
|
||||
if (item->type == RadioItem::Type_Service) {
|
||||
if (item->type == RadioItem::Type::Service) {
|
||||
return Song::IconForSource(item->source);
|
||||
}
|
||||
break;
|
||||
|
@ -98,7 +96,7 @@ QVariant RadioModel::data(const RadioItem *item, int role) const {
|
|||
return item->DisplayText();
|
||||
break;
|
||||
case Role_Type:
|
||||
return item->type;
|
||||
return QVariant::fromValue(item->type);
|
||||
break;
|
||||
case Role_SortText:
|
||||
return item->SortText();
|
||||
|
@ -154,7 +152,6 @@ void RadioModel::Reset() {
|
|||
pending_cache_keys_.clear();
|
||||
delete root_;
|
||||
root_ = new RadioItem(this);
|
||||
root_->lazy_loaded = true;
|
||||
endResetModel();
|
||||
|
||||
}
|
||||
|
@ -168,22 +165,20 @@ void RadioModel::AddChannels(const RadioChannelList &channels) {
|
|||
}
|
||||
else {
|
||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(root_->children.count()), static_cast<int>(root_->children.count()));
|
||||
RadioItem *item = new RadioItem(RadioItem::Type_Service, root_);
|
||||
RadioItem *item = new RadioItem(RadioItem::Type::Service, root_);
|
||||
item->source = channel.source;
|
||||
item->display_text = Song::DescriptionForSource(channel.source);
|
||||
item->sort_text = SortText(Song::TextForSource(channel.source));
|
||||
item->lazy_loaded = true;
|
||||
container_nodes_.insert(channel.source, item);
|
||||
endInsertRows();
|
||||
container = item;
|
||||
}
|
||||
beginInsertRows(ItemToIndex(container), static_cast<int>(container->children.count()), static_cast<int>(container->children.count()));
|
||||
RadioItem *item = new RadioItem(RadioItem::Type_Channel, container);
|
||||
RadioItem *item = new RadioItem(RadioItem::Type::Channel, container);
|
||||
item->source = channel.source;
|
||||
item->display_text = channel.name;
|
||||
item->sort_text = SortText(Song::TextForSource(channel.source) + QStringLiteral(" - ") + channel.name);
|
||||
item->channel = channel;
|
||||
item->lazy_loaded = true;
|
||||
items_ << item;
|
||||
endInsertRows();
|
||||
}
|
||||
|
@ -192,7 +187,7 @@ void RadioModel::AddChannels(const RadioChannelList &channels) {
|
|||
|
||||
bool RadioModel::IsPlayable(const QModelIndex &idx) const {
|
||||
|
||||
return idx.data(Role_Type) == RadioItem::Type_Channel;
|
||||
return idx.data(Role_Type).value<RadioItem::Type>() == RadioItem::Type::Channel;
|
||||
|
||||
}
|
||||
|
||||
|
@ -214,7 +209,7 @@ bool RadioModel::CompareItems(const RadioItem *a, const RadioItem *b) const {
|
|||
void RadioModel::GetChildSongs(RadioItem *item, QList<QUrl> *urls, SongList *songs) const {
|
||||
|
||||
switch (item->type) {
|
||||
case RadioItem::Type_Service:{
|
||||
case RadioItem::Type::Service:{
|
||||
QList<RadioItem*> children = item->children;
|
||||
std::sort(children.begin(), children.end(), std::bind(&RadioModel::CompareItems, this, std::placeholders::_1, std::placeholders::_2));
|
||||
for (RadioItem *child : children) {
|
||||
|
@ -222,7 +217,7 @@ void RadioModel::GetChildSongs(RadioItem *item, QList<QUrl> *urls, SongList *son
|
|||
}
|
||||
break;
|
||||
}
|
||||
case RadioItem::Type_Channel:
|
||||
case RadioItem::Type::Channel:
|
||||
if (!urls->contains(item->channel.url)) {
|
||||
urls->append(item->channel.url);
|
||||
songs->append(item->channel.ToSong());
|
||||
|
|
|
@ -144,8 +144,9 @@ void CollectionSettingsPage::Load() {
|
|||
|
||||
s.beginGroup(kSettingsGroup);
|
||||
ui_->auto_open->setChecked(s.value("auto_open", true).toBool());
|
||||
ui_->pretty_covers->setChecked(s.value("pretty_covers", true).toBool());
|
||||
ui_->show_dividers->setChecked(s.value("show_dividers", true).toBool());
|
||||
ui_->pretty_covers->setChecked(s.value("pretty_covers", true).toBool());
|
||||
ui_->various_artists->setChecked(s.value("various_artists", true).toBool());
|
||||
ui_->sort_skips_articles->setChecked(s.value("sort_skips_articles", true).toBool());
|
||||
ui_->startup_scan->setChecked(s.value("startup_scan", true).toBool());
|
||||
ui_->monitor->setChecked(s.value("monitor", true).toBool());
|
||||
|
@ -192,8 +193,9 @@ void CollectionSettingsPage::Save() {
|
|||
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("auto_open", ui_->auto_open->isChecked());
|
||||
s.setValue("pretty_covers", ui_->pretty_covers->isChecked());
|
||||
s.setValue("show_dividers", ui_->show_dividers->isChecked());
|
||||
s.setValue("pretty_covers", ui_->pretty_covers->isChecked());
|
||||
s.setValue("various_artists", ui_->various_artists->isChecked());
|
||||
s.setValue("sort_skips_articles", ui_->sort_skips_articles->isChecked());
|
||||
s.setValue("startup_scan", ui_->startup_scan->isChecked());
|
||||
s.setValue("monitor", ui_->monitor->isChecked());
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>565</width>
|
||||
<height>973</height>
|
||||
<height>1003</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -211,6 +211,13 @@ If there are no matches then it will use the largest image in the directory.</st
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="show_dividers">
|
||||
<property name="text">
|
||||
<string>Show dividers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="pretty_covers">
|
||||
<property name="text">
|
||||
|
@ -219,9 +226,9 @@ If there are no matches then it will use the largest image in the directory.</st
|
|||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="show_dividers">
|
||||
<widget class="QCheckBox" name="various_artists">
|
||||
<property name="text">
|
||||
<string>Show dividers</string>
|
||||
<string>Use various artists for compilation albums</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -508,11 +515,13 @@ If there are no matches then it will use the largest image in the directory.</st
|
|||
<tabstop>monitor</tabstop>
|
||||
<tabstop>song_tracking</tabstop>
|
||||
<tabstop>mark_songs_unavailable</tabstop>
|
||||
<tabstop>song_ebur128_loudness_analysis</tabstop>
|
||||
<tabstop>expire_unavailable_songs_days</tabstop>
|
||||
<tabstop>cover_art_patterns</tabstop>
|
||||
<tabstop>auto_open</tabstop>
|
||||
<tabstop>pretty_covers</tabstop>
|
||||
<tabstop>show_dividers</tabstop>
|
||||
<tabstop>pretty_covers</tabstop>
|
||||
<tabstop>various_artists</tabstop>
|
||||
<tabstop>sort_skips_articles</tabstop>
|
||||
<tabstop>spinbox_cache_size</tabstop>
|
||||
<tabstop>combobox_cache_size</tabstop>
|
||||
|
@ -520,6 +529,11 @@ If there are no matches then it will use the largest image in the directory.</st
|
|||
<tabstop>spinbox_disk_cache_size</tabstop>
|
||||
<tabstop>combobox_disk_cache_size</tabstop>
|
||||
<tabstop>button_clear_disk_cache</tabstop>
|
||||
<tabstop>checkbox_save_playcounts</tabstop>
|
||||
<tabstop>checkbox_save_ratings</tabstop>
|
||||
<tabstop>checkbox_overwrite_playcount</tabstop>
|
||||
<tabstop>checkbox_overwrite_rating</tabstop>
|
||||
<tabstop>button_save_stats</tabstop>
|
||||
<tabstop>checkbox_delete_files</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
|
|
|
@ -31,14 +31,15 @@
|
|||
|
||||
class SmartPlaylistsItem : public SimpleTreeItem<SmartPlaylistsItem> {
|
||||
public:
|
||||
enum Type {
|
||||
Type_Root,
|
||||
Type_SmartPlaylist
|
||||
enum class Type {
|
||||
Root,
|
||||
SmartPlaylist
|
||||
};
|
||||
|
||||
SmartPlaylistsItem(SimpleTreeModel<SmartPlaylistsItem> *_model) : SimpleTreeItem<SmartPlaylistsItem>(Type_Root, _model) {}
|
||||
SmartPlaylistsItem(const Type _type, SmartPlaylistsItem *_parent = nullptr) : SimpleTreeItem<SmartPlaylistsItem>(_type, _parent) {}
|
||||
SmartPlaylistsItem(SimpleTreeModel<SmartPlaylistsItem> *_model) : SimpleTreeItem<SmartPlaylistsItem>(_model), type(Type::Root) {}
|
||||
SmartPlaylistsItem(const Type _type, SmartPlaylistsItem *_parent = nullptr) : SimpleTreeItem<SmartPlaylistsItem>(_parent), type(_type) {}
|
||||
|
||||
Type type;
|
||||
PlaylistGenerator::Type smart_playlist_type;
|
||||
QByteArray smart_playlist_data;
|
||||
|
||||
|
|
|
@ -51,11 +51,7 @@ const int SmartPlaylistsModel::kSmartPlaylistsVersion = 1;
|
|||
SmartPlaylistsModel::SmartPlaylistsModel(SharedPtr<CollectionBackend> collection_backend, QObject *parent)
|
||||
: SimpleTreeModel<SmartPlaylistsItem>(new SmartPlaylistsItem(this), parent),
|
||||
collection_backend_(collection_backend),
|
||||
icon_(IconLoader::Load(QStringLiteral("view-media-playlist"))) {
|
||||
|
||||
root_->lazy_loaded = true;
|
||||
|
||||
}
|
||||
icon_(IconLoader::Load(QStringLiteral("view-media-playlist"))) {}
|
||||
|
||||
SmartPlaylistsModel::~SmartPlaylistsModel() { delete root_; }
|
||||
|
||||
|
@ -159,12 +155,11 @@ void SmartPlaylistsModel::Init() {
|
|||
|
||||
void SmartPlaylistsModel::ItemFromSmartPlaylist(const Settings &s, const bool notify) {
|
||||
|
||||
SmartPlaylistsItem *item = new SmartPlaylistsItem(SmartPlaylistsItem::Type_SmartPlaylist, notify ? nullptr : root_);
|
||||
SmartPlaylistsItem *item = new SmartPlaylistsItem(SmartPlaylistsItem::Type::SmartPlaylist, notify ? nullptr : root_);
|
||||
item->display_text = tr(qUtf8Printable(s.value("name").toString()));
|
||||
item->sort_text = item->display_text;
|
||||
item->smart_playlist_type = PlaylistGenerator::Type(s.value("type").toInt());
|
||||
item->smart_playlist_data = s.value("data").toByteArray();
|
||||
item->lazy_loaded = true;
|
||||
|
||||
if (notify) item->InsertNotify(root_);
|
||||
|
||||
|
@ -259,7 +254,7 @@ PlaylistGeneratorPtr SmartPlaylistsModel::CreateGenerator(const QModelIndex &idx
|
|||
PlaylistGeneratorPtr ret;
|
||||
|
||||
const SmartPlaylistsItem *item = IndexToItem(idx);
|
||||
if (!item || item->type != SmartPlaylistsItem::Type_SmartPlaylist) return ret;
|
||||
if (!item || item->type != SmartPlaylistsItem::Type::SmartPlaylist) return ret;
|
||||
|
||||
ret = PlaylistGenerator::Create(item->smart_playlist_type);
|
||||
if (!ret) return ret;
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/shared_ptr.h"
|
||||
|
@ -52,6 +51,7 @@
|
|||
#include "utilities/randutils.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionfilter.h"
|
||||
#include "subsonicservice.h"
|
||||
#include "subsonicurlhandler.h"
|
||||
#include "subsonicrequest.h"
|
||||
|
@ -68,7 +68,6 @@ const char *SubsonicService::kApiVersion = "1.11.0";
|
|||
|
||||
namespace {
|
||||
constexpr char kSongsTable[] = "subsonic_songs";
|
||||
constexpr char kSongsFtsTable[] = "subsonic_songs_fts";
|
||||
constexpr int kMaxRedirects = 3;
|
||||
} // namespace
|
||||
|
||||
|
@ -78,7 +77,6 @@ SubsonicService::SubsonicService(Application *app, QObject *parent)
|
|||
url_handler_(new SubsonicUrlHandler(app, this)),
|
||||
collection_backend_(nullptr),
|
||||
collection_model_(nullptr),
|
||||
collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
http2_(false),
|
||||
verify_certificate_(false),
|
||||
download_album_covers_(true),
|
||||
|
@ -87,21 +85,10 @@ SubsonicService::SubsonicService(Application *app, QObject *parent)
|
|||
|
||||
app->player()->RegisterUrlHandler(url_handler_);
|
||||
|
||||
// Backend
|
||||
|
||||
|
||||
collection_backend_ = make_shared<CollectionBackend>();
|
||||
collection_backend_->moveToThread(app_->database()->thread());
|
||||
collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Subsonic, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable));
|
||||
|
||||
// Model
|
||||
|
||||
collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Subsonic, QLatin1String(kSongsTable));
|
||||
collection_model_ = new CollectionModel(collection_backend_, app_, this);
|
||||
collection_sort_model_->setSourceModel(collection_model_);
|
||||
collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
collection_sort_model_->setDynamicSortFilter(true);
|
||||
collection_sort_model_->setSortLocaleAware(true);
|
||||
collection_sort_model_->sort(0);
|
||||
|
||||
SubsonicService::ReloadSettings();
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
#include "internet/internetservice.h"
|
||||
#include "settings/subsonicsettingspage.h"
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
class QNetworkReply;
|
||||
|
||||
class Application;
|
||||
|
@ -51,6 +50,7 @@ class SubsonicRequest;
|
|||
class SubsonicScrobbleRequest;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class CollectionFilter;
|
||||
|
||||
class SubsonicService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
@ -78,11 +78,11 @@ class SubsonicService : public InternetService {
|
|||
|
||||
SharedPtr<CollectionBackend> collection_backend() const { return collection_backend_; }
|
||||
CollectionModel *collection_model() const { return collection_model_; }
|
||||
QSortFilterProxyModel *collection_sort_model() const { return collection_sort_model_; }
|
||||
CollectionFilter *collection_filter_model() const { return collection_model_->filter(); }
|
||||
|
||||
SharedPtr<CollectionBackend> songs_collection_backend() override { return collection_backend_; }
|
||||
CollectionModel *songs_collection_model() override { return collection_model_; }
|
||||
QSortFilterProxyModel *songs_collection_sort_model() override { return collection_sort_model_; }
|
||||
CollectionFilter *songs_collection_filter_model() override { return collection_model_->filter(); }
|
||||
|
||||
void CheckConfiguration();
|
||||
void Scrobble(const QString &song_id, const bool submission, const QDateTime &time);
|
||||
|
@ -109,7 +109,6 @@ class SubsonicService : public InternetService {
|
|||
|
||||
SharedPtr<CollectionBackend> collection_backend_;
|
||||
CollectionModel *collection_model_;
|
||||
QSortFilterProxyModel *collection_sort_model_;
|
||||
|
||||
SharedPtr<SubsonicRequest> songs_request_;
|
||||
SharedPtr<SubsonicScrobbleRequest> scrobble_request_;
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
#include "internet/internetsearchview.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionfilter.h"
|
||||
#include "tidalservice.h"
|
||||
#include "tidalurlhandler.h"
|
||||
#include "tidalbaserequest.h"
|
||||
|
@ -85,10 +86,6 @@ constexpr char kArtistsSongsTable[] = "tidal_artists_songs";
|
|||
constexpr char kAlbumsSongsTable[] = "tidal_albums_songs";
|
||||
constexpr char kSongsTable[] = "tidal_songs";
|
||||
|
||||
constexpr char kArtistsSongsFtsTable[] = "tidal_artists_songs_fts";
|
||||
constexpr char kAlbumsSongsFtsTable[] = "tidal_albums_songs_fts";
|
||||
constexpr char kSongsFtsTable[] = "tidal_songs_fts";
|
||||
|
||||
} // namespace
|
||||
|
||||
TidalService::TidalService(Application *app, QObject *parent)
|
||||
|
@ -102,9 +99,6 @@ TidalService::TidalService(Application *app, QObject *parent)
|
|||
artists_collection_model_(nullptr),
|
||||
albums_collection_model_(nullptr),
|
||||
songs_collection_model_(nullptr),
|
||||
artists_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
albums_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
songs_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
timer_search_delay_(new QTimer(this)),
|
||||
timer_login_attempt_(new QTimer(this)),
|
||||
timer_refresh_login_(new QTimer(this)),
|
||||
|
@ -135,38 +129,21 @@ TidalService::TidalService(Application *app, QObject *parent)
|
|||
|
||||
artists_collection_backend_ = make_shared<CollectionBackend>();
|
||||
artists_collection_backend_->moveToThread(app_->database()->thread());
|
||||
artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kArtistsSongsTable), QLatin1String(kArtistsSongsFtsTable));
|
||||
artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kArtistsSongsTable));
|
||||
|
||||
albums_collection_backend_ = make_shared<CollectionBackend>();
|
||||
albums_collection_backend_->moveToThread(app_->database()->thread());
|
||||
albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kAlbumsSongsTable), QLatin1String(kAlbumsSongsFtsTable));
|
||||
albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kAlbumsSongsTable));
|
||||
|
||||
songs_collection_backend_ = make_shared<CollectionBackend>();
|
||||
songs_collection_backend_->moveToThread(app_->database()->thread());
|
||||
songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable));
|
||||
songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kSongsTable));
|
||||
|
||||
// Models
|
||||
artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this);
|
||||
albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this);
|
||||
songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this);
|
||||
|
||||
artists_collection_sort_model_->setSourceModel(artists_collection_model_);
|
||||
artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
artists_collection_sort_model_->setDynamicSortFilter(true);
|
||||
artists_collection_sort_model_->setSortLocaleAware(true);
|
||||
artists_collection_sort_model_->sort(0);
|
||||
|
||||
albums_collection_sort_model_->setSourceModel(albums_collection_model_);
|
||||
albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
albums_collection_sort_model_->setDynamicSortFilter(true);
|
||||
albums_collection_sort_model_->setSortLocaleAware(true);
|
||||
albums_collection_sort_model_->sort(0);
|
||||
|
||||
songs_collection_sort_model_->setSourceModel(songs_collection_model_);
|
||||
songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
songs_collection_sort_model_->setDynamicSortFilter(true);
|
||||
songs_collection_sort_model_->setSortLocaleAware(true);
|
||||
songs_collection_sort_model_->sort(0);
|
||||
|
||||
// Search
|
||||
|
||||
timer_search_delay_->setSingleShot(true);
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
#include "internet/internetsearchview.h"
|
||||
#include "settings/tidalsettingspage.h"
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
class QNetworkReply;
|
||||
class QTimer;
|
||||
|
||||
|
@ -54,6 +53,7 @@ class TidalFavoriteRequest;
|
|||
class TidalStreamURLRequest;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class CollectionFilter;
|
||||
|
||||
class TidalService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
@ -112,9 +112,9 @@ class TidalService : public InternetService {
|
|||
CollectionModel *albums_collection_model() override { return albums_collection_model_; }
|
||||
CollectionModel *songs_collection_model() override { return songs_collection_model_; }
|
||||
|
||||
QSortFilterProxyModel *artists_collection_sort_model() override { return artists_collection_sort_model_; }
|
||||
QSortFilterProxyModel *albums_collection_sort_model() override { return albums_collection_sort_model_; }
|
||||
QSortFilterProxyModel *songs_collection_sort_model() override { return songs_collection_sort_model_; }
|
||||
CollectionFilter *artists_collection_filter_model() override { return artists_collection_model_->filter(); }
|
||||
CollectionFilter *albums_collection_filter_model() override { return albums_collection_model_->filter(); }
|
||||
CollectionFilter *songs_collection_filter_model() override { return songs_collection_model_->filter(); }
|
||||
|
||||
public slots:
|
||||
void ShowConfig() override;
|
||||
|
@ -172,10 +172,6 @@ class TidalService : public InternetService {
|
|||
CollectionModel *albums_collection_model_;
|
||||
CollectionModel *songs_collection_model_;
|
||||
|
||||
QSortFilterProxyModel *artists_collection_sort_model_;
|
||||
QSortFilterProxyModel *albums_collection_sort_model_;
|
||||
QSortFilterProxyModel *songs_collection_sort_model_;
|
||||
|
||||
QTimer *timer_search_delay_;
|
||||
QTimer *timer_login_attempt_;
|
||||
QTimer *timer_refresh_login_;
|
||||
|
|
|
@ -24,22 +24,19 @@
|
|||
|
||||
namespace Utilities {
|
||||
|
||||
/**
|
||||
* @brief Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
|
||||
* and return the number of seconds if it parses correctly.
|
||||
* If not, the original string is returned.
|
||||
* The 'h', 'm' and 's' components can have any length (including 0).
|
||||
* A few examples:
|
||||
* "::" is parsed to "0"
|
||||
* "1::" is parsed to "3600"
|
||||
* "3:45" is parsed to "225"
|
||||
* "1:165" is parsed to "225"
|
||||
* "225" is parsed to "225" (srsly! ^.^)
|
||||
* "2:3:4:5" is parsed to "2:3:4:5"
|
||||
* "25m" is parsed to "25m"
|
||||
* @param time_str
|
||||
* @return
|
||||
*/
|
||||
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
|
||||
// and return the number of seconds if it parses correctly.
|
||||
// If not, the original string is returned.
|
||||
// The 'h', 'm' and 's' components can have any length (including 0).
|
||||
// A few examples:
|
||||
// "::" is parsed to "0"
|
||||
// "1::" is parsed to "3600"
|
||||
// "3:45" is parsed to "225"
|
||||
// "1:165" is parsed to "225"
|
||||
// "225" is parsed to "225" (srsly! ^.^)
|
||||
// "2:3:4:5" is parsed to "2:3:4:5"
|
||||
// "25m" is parsed to "25m"
|
||||
|
||||
int ParseSearchTime(const QString &time_str) {
|
||||
|
||||
int seconds = 0;
|
||||
|
@ -67,38 +64,48 @@ int ParseSearchTime(const QString &time_str) {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parses a rating search term to float.
|
||||
* If the rating is a number from 0-5, map it to 0-1
|
||||
* To use float values directly, the search term can be prefixed with "f" (rating:>f0.2)
|
||||
* If search str is 0, or by default, uses -1
|
||||
* @param rating_str: Rating search 0-5, or "f0.2"
|
||||
* @return float: rating from 0-1 or -1 if not rated.
|
||||
*/
|
||||
// Parses a rating search term to float.
|
||||
// If the rating is a number from 0-5, map it to 0-1
|
||||
// To use float values directly, the search term can be prefixed with "f" (rating:>f0.2)
|
||||
// If search string is 0, or by default, uses -1
|
||||
// @param rating_str: Rating search 0-5, or "f0.2"
|
||||
// @return float: rating from 0-1 or -1 if not rated.
|
||||
|
||||
float ParseSearchRating(const QString &rating_str) {
|
||||
|
||||
if (rating_str.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
float rating = -1.0F;
|
||||
bool ok = false;
|
||||
float rating_input = rating_str.toFloat(&ok);
|
||||
// is valid int from 0-5: convert to float
|
||||
if (ok && rating_input >= 0 && rating_input <= 5) {
|
||||
rating = rating_input / 5.0F;
|
||||
}
|
||||
|
||||
// check if the search is a float
|
||||
else if (rating_str.at(0) == QLatin1Char('f')) {
|
||||
QString rating_float = rating_str;
|
||||
rating_float = rating_float.remove(0, 1);
|
||||
|
||||
ok = false;
|
||||
rating_float.toFloat(&ok);
|
||||
// Check if the search is a float
|
||||
if (rating_str.contains(QLatin1Char('f'), Qt::CaseInsensitive)) {
|
||||
if (rating_str.count(QLatin1Char('f'), Qt::CaseInsensitive) > 1) {
|
||||
return rating;
|
||||
}
|
||||
QString rating_float_str = rating_str;
|
||||
if (rating_str.at(0) == QLatin1Char('f') || rating_str.at(0) == QLatin1Char('F')) {
|
||||
rating_float_str = rating_float_str.remove(0, 1);
|
||||
}
|
||||
if (rating_str.right(1) == QLatin1Char('f') || rating_str.right(1) == QLatin1Char('F')) {
|
||||
rating_float_str.chop(1);
|
||||
}
|
||||
bool ok = false;
|
||||
const float rating_input = rating_float_str.toFloat(&ok);
|
||||
if (ok) {
|
||||
rating = rating_float.toFloat(&ok);
|
||||
rating = rating_input;
|
||||
}
|
||||
}
|
||||
else {
|
||||
bool ok = false;
|
||||
const int rating_input = rating_str.toInt(&ok);
|
||||
// Is valid int from 0-5: convert to float
|
||||
if (ok && rating_input >= 0 && rating_input <= 5) {
|
||||
rating = static_cast<float>(rating_input) / 5.0F;
|
||||
}
|
||||
}
|
||||
|
||||
// Songs with zero rating have -1 in the DB
|
||||
if (rating == 0) {
|
||||
rating = -1;
|
||||
|
|
|
@ -47,7 +47,7 @@ class CollectionBackendTest : public ::testing::Test {
|
|||
void SetUp() override {
|
||||
database_.reset(new MemoryDatabase(nullptr));
|
||||
backend_ = make_unique<CollectionBackend>();
|
||||
backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
|
||||
backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
|
||||
}
|
||||
|
||||
static Song MakeDummySong(int directory_id) {
|
||||
|
@ -132,7 +132,7 @@ class SingleSong : public CollectionBackendTest {
|
|||
}
|
||||
|
||||
void AddDummySong() {
|
||||
QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsDiscovered);
|
||||
QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsAdded);
|
||||
QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted);
|
||||
|
||||
// Add the song
|
||||
|
@ -265,22 +265,20 @@ TEST_F(SingleSong, UpdateSong) {
|
|||
new_song.set_id(1);
|
||||
new_song.set_title(QStringLiteral("A different title"));
|
||||
|
||||
QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsAdded);
|
||||
QSignalSpy changed_spy(&*backend_, &CollectionBackend::SongsChanged);
|
||||
QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted);
|
||||
QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsDiscovered);
|
||||
|
||||
backend_->AddOrUpdateSongs(SongList() << new_song);
|
||||
|
||||
ASSERT_EQ(1, added_spy.size());
|
||||
ASSERT_EQ(1, deleted_spy.size());
|
||||
ASSERT_EQ(0, added_spy.size());
|
||||
ASSERT_EQ(1, changed_spy.size());
|
||||
ASSERT_EQ(0, deleted_spy.size());
|
||||
|
||||
SongList songs_added = *(reinterpret_cast<SongList*>(added_spy[0][0].data()));
|
||||
SongList songs_deleted = *(reinterpret_cast<SongList*>(deleted_spy[0][0].data()));
|
||||
ASSERT_EQ(1, songs_added.size());
|
||||
ASSERT_EQ(1, songs_deleted.size());
|
||||
EXPECT_EQ(QStringLiteral("Title"), songs_deleted[0].title());
|
||||
EXPECT_EQ(QStringLiteral("A different title"), songs_added[0].title());
|
||||
EXPECT_EQ(1, songs_deleted[0].id());
|
||||
EXPECT_EQ(1, songs_added[0].id());
|
||||
SongList songs_changed = *(reinterpret_cast<SongList*>(changed_spy[0][0].data()));
|
||||
ASSERT_EQ(1, songs_changed.size());
|
||||
EXPECT_EQ(QStringLiteral("A different title"), songs_changed[0].title());
|
||||
EXPECT_EQ(1, songs_changed[0].id());
|
||||
|
||||
}
|
||||
|
||||
|
@ -389,7 +387,7 @@ TEST_F(TestUrls, TestUrls) {
|
|||
|
||||
}
|
||||
|
||||
QSignalSpy spy(&*backend_, &CollectionBackend::SongsDiscovered);
|
||||
QSignalSpy spy(&*backend_, &CollectionBackend::SongsAdded);
|
||||
|
||||
backend_->AddOrUpdateSongs(songs);
|
||||
if (HasFatalFailure()) return;
|
||||
|
@ -474,7 +472,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
|
|||
|
||||
}
|
||||
|
||||
QSignalSpy spy(&*backend_, &CollectionBackend::SongsDiscovered);
|
||||
QSignalSpy spy(&*backend_, &CollectionBackend::SongsAdded);
|
||||
|
||||
backend_->UpdateSongsBySongID(songs);
|
||||
|
||||
|
@ -495,7 +493,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
|
|||
SongMap songs;
|
||||
{
|
||||
QSqlDatabase db(database_->Connect());
|
||||
CollectionQuery query(db, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable));
|
||||
CollectionQuery query(db, QLatin1String(SCollection::kSongsTable));
|
||||
EXPECT_TRUE(backend_->ExecCollectionQuery(&query, songs));
|
||||
}
|
||||
|
||||
|
@ -512,7 +510,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
|
|||
}
|
||||
|
||||
{ // Remove some songs
|
||||
QSignalSpy spy1(&*backend_, &CollectionBackend::SongsDiscovered);
|
||||
QSignalSpy spy1(&*backend_, &CollectionBackend::SongsAdded);
|
||||
QSignalSpy spy2(&*backend_, &CollectionBackend::SongsDeleted);
|
||||
|
||||
SongMap songs;
|
||||
|
@ -558,7 +556,8 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
|
|||
|
||||
{ // Update some songs
|
||||
QSignalSpy spy1(&*backend_, &CollectionBackend::SongsDeleted);
|
||||
QSignalSpy spy2(&*backend_, &CollectionBackend::SongsDiscovered);
|
||||
QSignalSpy spy2(&*backend_, &CollectionBackend::SongsAdded);
|
||||
QSignalSpy spy3(&*backend_, &CollectionBackend::SongsChanged);
|
||||
|
||||
SongMap songs;
|
||||
|
||||
|
@ -595,16 +594,14 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
|
|||
|
||||
backend_->UpdateSongsBySongID(songs);
|
||||
|
||||
ASSERT_EQ(1, spy1.count());
|
||||
ASSERT_EQ(1, spy2.count());
|
||||
SongList deleted_songs = spy1[0][0].value<SongList>();
|
||||
SongList added_songs = spy2[0][0].value<SongList>();
|
||||
EXPECT_EQ(deleted_songs.count(), 2);
|
||||
EXPECT_EQ(added_songs.count(), 2);
|
||||
EXPECT_EQ(deleted_songs[0].song_id(), QStringLiteral("song1"));
|
||||
EXPECT_EQ(deleted_songs[1].song_id(), QStringLiteral("song6"));
|
||||
EXPECT_EQ(added_songs[0].song_id(), QStringLiteral("song1"));
|
||||
EXPECT_EQ(added_songs[1].song_id(), QStringLiteral("song6"));
|
||||
ASSERT_EQ(0, spy1.count());
|
||||
ASSERT_EQ(0, spy2.count());
|
||||
ASSERT_EQ(1, spy3.count());
|
||||
|
||||
SongList changed_songs = spy3[0][0].value<SongList>();
|
||||
EXPECT_EQ(changed_songs.count(), 2);
|
||||
EXPECT_EQ(changed_songs[0].song_id(), QStringLiteral("song1"));
|
||||
EXPECT_EQ(changed_songs[1].song_id(), QStringLiteral("song6"));
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -28,16 +28,16 @@
|
|||
#include <QUrl>
|
||||
#include <QThread>
|
||||
#include <QSignalSpy>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/scoped_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/database.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collection.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionfilter.h"
|
||||
|
||||
using std::make_unique;
|
||||
using std::make_shared;
|
||||
|
@ -48,23 +48,18 @@ namespace {
|
|||
|
||||
class CollectionModelTest : public ::testing::Test {
|
||||
public:
|
||||
CollectionModelTest() : added_dir_(false) {}
|
||||
CollectionModelTest() : collection_filter_(nullptr), added_dir_(false) {}
|
||||
|
||||
protected:
|
||||
void SetUp() override {
|
||||
database_ = make_shared<MemoryDatabase>(nullptr);
|
||||
backend_ = make_shared<CollectionBackend>();
|
||||
backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
|
||||
backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
|
||||
model_ = make_unique<CollectionModel>(backend_, nullptr);
|
||||
collection_filter_ = model_->filter();
|
||||
|
||||
added_dir_ = false;
|
||||
|
||||
model_sorted_ = make_unique<QSortFilterProxyModel>();
|
||||
model_sorted_->setSourceModel(&*model_);
|
||||
model_sorted_->setSortRole(CollectionModel::Role_SortText);
|
||||
model_sorted_->setDynamicSortFilter(true);
|
||||
model_sorted_->sort(0);
|
||||
|
||||
}
|
||||
|
||||
Song AddSong(Song &song) {
|
||||
|
@ -79,7 +74,11 @@ class CollectionModelTest : public ::testing::Test {
|
|||
added_dir_ = true;
|
||||
}
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(&*model_, &CollectionModel::rowsInserted, &loop, &QEventLoop::quit);
|
||||
backend_->AddOrUpdateSongs(SongList() << song);
|
||||
loop.exec();
|
||||
|
||||
return song;
|
||||
}
|
||||
|
||||
|
@ -94,7 +93,7 @@ class CollectionModelTest : public ::testing::Test {
|
|||
SharedPtr<Database> database_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
SharedPtr<CollectionBackend> backend_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
ScopedPtr<CollectionModel> model_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
ScopedPtr<QSortFilterProxyModel> model_sorted_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
CollectionFilter *collection_filter_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
|
||||
bool added_dir_; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
|
||||
};
|
||||
|
@ -108,14 +107,13 @@ TEST_F(CollectionModelTest, WithInitialArtists) {
|
|||
AddSong(QStringLiteral("Title"), QStringLiteral("Artist 1"), QStringLiteral("Album"), 123);
|
||||
AddSong(QStringLiteral("Title"), QStringLiteral("Artist 2"), QStringLiteral("Album"), 123);
|
||||
AddSong(QStringLiteral("Title"), QStringLiteral("Foo"), QStringLiteral("Album"), 123);
|
||||
model_->Init(false);
|
||||
|
||||
ASSERT_EQ(5, model_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ(QStringLiteral("A"), model_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Artist 1"), model_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Artist 2"), model_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("F"), model_sorted_->index(3, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Foo"), model_sorted_->index(4, 0, QModelIndex()).data().toString());
|
||||
ASSERT_EQ(5, collection_filter_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ(QStringLiteral("A"), collection_filter_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Artist 1"), collection_filter_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Artist 2"), collection_filter_->index(2, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("F"), collection_filter_->index(3, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Foo"), collection_filter_->index(4, 0, QModelIndex()).data().toString());
|
||||
|
||||
}
|
||||
|
||||
|
@ -128,8 +126,6 @@ TEST_F(CollectionModelTest, CompilationAlbums) {
|
|||
song.set_ctime(0);
|
||||
|
||||
AddSong(song);
|
||||
model_->Init(false);
|
||||
model_->fetchMore(model_->index(0, 0));
|
||||
|
||||
ASSERT_EQ(1, model_->rowCount(QModelIndex()));
|
||||
|
||||
|
@ -150,15 +146,14 @@ TEST_F(CollectionModelTest, NumericHeaders) {
|
|||
AddSong(QStringLiteral("Title"), QStringLiteral("2artist"), QStringLiteral("Album"), 123);
|
||||
AddSong(QStringLiteral("Title"), QStringLiteral("0artist"), QStringLiteral("Album"), 123);
|
||||
AddSong(QStringLiteral("Title"), QStringLiteral("zartist"), QStringLiteral("Album"), 123);
|
||||
model_->Init(false);
|
||||
|
||||
ASSERT_EQ(6, model_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ(QStringLiteral("0-9"), model_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("0artist"), model_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("1artist"), model_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("2artist"), model_sorted_->index(3, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Z"), model_sorted_->index(4, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("zartist"), model_sorted_->index(5, 0, QModelIndex()).data().toString());
|
||||
ASSERT_EQ(6, collection_filter_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ(QStringLiteral("0-9"), collection_filter_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("0artist"), collection_filter_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("1artist"), collection_filter_->index(2, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("2artist"), collection_filter_->index(3, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Z"), collection_filter_->index(4, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("zartist"), collection_filter_->index(5, 0, QModelIndex()).data().toString());
|
||||
|
||||
}
|
||||
|
||||
|
@ -166,20 +161,17 @@ TEST_F(CollectionModelTest, MixedCaseHeaders) {
|
|||
|
||||
AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123);
|
||||
AddSong(QStringLiteral("Title"), QStringLiteral("artist"), QStringLiteral("Album"), 123);
|
||||
model_->Init(false);
|
||||
|
||||
ASSERT_EQ(3, model_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ(QStringLiteral("A"), model_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Artist"), model_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("artist"), model_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
ASSERT_EQ(3, collection_filter_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ(QStringLiteral("A"), collection_filter_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("Artist"), collection_filter_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ(QStringLiteral("artist"), collection_filter_->index(2, 0, QModelIndex()).data().toString());
|
||||
|
||||
}
|
||||
|
||||
TEST_F(CollectionModelTest, UnknownArtists) {
|
||||
|
||||
AddSong(QStringLiteral("Title"), QLatin1String(""), QStringLiteral("Album"), 123);
|
||||
model_->Init(false);
|
||||
model_->fetchMore(model_->index(0, 0));
|
||||
|
||||
ASSERT_EQ(1, model_->rowCount(QModelIndex()));
|
||||
QModelIndex unknown_index = model_->index(0, 0, QModelIndex());
|
||||
|
@ -194,10 +186,9 @@ TEST_F(CollectionModelTest, UnknownAlbums) {
|
|||
|
||||
AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QLatin1String(""), 123);
|
||||
AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123);
|
||||
model_->Init(false);
|
||||
model_->fetchMore(model_->index(0, 0));
|
||||
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
QModelIndex artist_index = model_->index(1, 0, QModelIndex());
|
||||
EXPECT_EQ(artist_index.isValid(), true);
|
||||
ASSERT_EQ(2, model_->rowCount(artist_index));
|
||||
|
||||
QModelIndex unknown_album_index = model_->index(0, 0, artist_index);
|
||||
|
@ -228,14 +219,11 @@ TEST_F(CollectionModelTest, VariousArtistSongs) {
|
|||
|
||||
for (int i=0 ; i < 4 ; ++i)
|
||||
AddSong(songs[i]);
|
||||
model_->Init(false);
|
||||
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
model_->fetchMore(artist_index);
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
|
||||
QModelIndex album_index = model_->index(0, 0, artist_index);
|
||||
model_->fetchMore(album_index);
|
||||
ASSERT_EQ(4, model_->rowCount(album_index));
|
||||
|
||||
EXPECT_EQ(QStringLiteral("Artist 1 - Title 1"), model_->index(0, 0, album_index).data().toString());
|
||||
|
@ -245,19 +233,16 @@ TEST_F(CollectionModelTest, VariousArtistSongs) {
|
|||
|
||||
}
|
||||
|
||||
TEST_F(CollectionModelTest, RemoveSongsLazyLoaded) {
|
||||
TEST_F(CollectionModelTest, RemoveSongs) {
|
||||
|
||||
Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1);
|
||||
Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); two.set_id(2);
|
||||
AddSong(QStringLiteral("Title 3"), QStringLiteral("Artist"), QStringLiteral("Album"), 123);
|
||||
model_->Init(false);
|
||||
|
||||
// Lazy load the items
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
model_->fetchMore(artist_index);
|
||||
QModelIndex artist_index = model_->index(1, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
|
||||
QModelIndex album_index = model_->index(0, 0, artist_index);
|
||||
model_->fetchMore(album_index);
|
||||
ASSERT_EQ(3, model_->rowCount(album_index));
|
||||
|
||||
// Remove the first two songs
|
||||
|
@ -265,13 +250,16 @@ TEST_F(CollectionModelTest, RemoveSongsLazyLoaded) {
|
|||
QSignalSpy spy_remove(&*model_, &CollectionModel::rowsRemoved);
|
||||
QSignalSpy spy_reset(&*model_, &CollectionModel::modelReset);
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(&*model_, &CollectionModel::rowsRemoved, &loop, &QEventLoop::quit);
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
loop.exec();
|
||||
|
||||
ASSERT_EQ(2, spy_preremove.count());
|
||||
ASSERT_EQ(2, spy_remove.count());
|
||||
ASSERT_EQ(0, spy_reset.count());
|
||||
|
||||
artist_index = model_->index(0, 0, QModelIndex());
|
||||
artist_index = model_->index(1, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
album_index = model_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(1, model_->rowCount(album_index));
|
||||
|
@ -279,45 +267,26 @@ TEST_F(CollectionModelTest, RemoveSongsLazyLoaded) {
|
|||
|
||||
}
|
||||
|
||||
TEST_F(CollectionModelTest, RemoveSongsNotLazyLoaded) {
|
||||
|
||||
Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1);
|
||||
Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); two.set_id(2);
|
||||
model_->Init(false);
|
||||
|
||||
// Remove the first two songs
|
||||
QSignalSpy spy_preremove(&*model_, &CollectionModel::rowsAboutToBeRemoved);
|
||||
QSignalSpy spy_remove(&*model_, &CollectionModel::rowsRemoved);
|
||||
QSignalSpy spy_reset(&*model_, &CollectionModel::modelReset);
|
||||
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
|
||||
ASSERT_EQ(0, spy_preremove.count());
|
||||
ASSERT_EQ(0, spy_remove.count());
|
||||
ASSERT_EQ(1, spy_reset.count());
|
||||
|
||||
}
|
||||
|
||||
TEST_F(CollectionModelTest, RemoveEmptyAlbums) {
|
||||
|
||||
Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album 1"), 123); one.set_id(1);
|
||||
Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album 2"), 123); two.set_id(2);
|
||||
Song three = AddSong(QStringLiteral("Title 3"), QStringLiteral("Artist"), QStringLiteral("Album 2"), 123); three.set_id(3);
|
||||
model_->Init(false);
|
||||
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
model_->fetchMore(artist_index);
|
||||
QModelIndex artist_index = model_->index(1, 0, QModelIndex());
|
||||
ASSERT_EQ(2, model_->rowCount(artist_index));
|
||||
|
||||
// Remove one song from each album
|
||||
QEventLoop loop;
|
||||
QObject::connect(&*model_, &CollectionModel::rowsRemoved, &loop, &QEventLoop::quit);
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
loop.exec();
|
||||
|
||||
// Check the model
|
||||
artist_index = model_->index(0, 0, QModelIndex());
|
||||
model_->fetchMore(artist_index);
|
||||
artist_index = model_->index(1, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
|
||||
QModelIndex album_index = model_->index(0, 0, artist_index);
|
||||
model_->fetchMore(album_index);
|
||||
EXPECT_EQ(QStringLiteral("Album 2"), album_index.data().toString());
|
||||
|
||||
ASSERT_EQ(1, model_->rowCount(album_index));
|
||||
|
@ -328,21 +297,21 @@ TEST_F(CollectionModelTest, RemoveEmptyAlbums) {
|
|||
TEST_F(CollectionModelTest, RemoveEmptyArtists) {
|
||||
|
||||
Song one = AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1);
|
||||
model_->Init(false);
|
||||
|
||||
// Lazy load the items
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
model_->fetchMore(artist_index);
|
||||
QModelIndex artist_index = model_->index(1, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
|
||||
QModelIndex album_index = model_->index(0, 0, artist_index);
|
||||
model_->fetchMore(album_index);
|
||||
ASSERT_EQ(1, model_->rowCount(album_index));
|
||||
|
||||
// The artist header is there too right?
|
||||
ASSERT_EQ(2, model_->rowCount(QModelIndex()));
|
||||
|
||||
// Remove the song
|
||||
QEventLoop loop;
|
||||
QObject::connect(&*model_, &CollectionModel::rowsRemoved, &loop, &QEventLoop::quit);
|
||||
backend_->DeleteSongs(SongList() << one);
|
||||
loop.exec();
|
||||
|
||||
// Everything should be gone - even the artist header
|
||||
ASSERT_EQ(0, model_->rowCount(QModelIndex()));
|
||||
|
@ -351,8 +320,8 @@ TEST_F(CollectionModelTest, RemoveEmptyArtists) {
|
|||
|
||||
// Test to check that the container nodes are created identical and unique all through the model with all possible collection groupings.
|
||||
// model1 - Nodes are created from a complete reset done through lazy-loading.
|
||||
// model2 - Initial container nodes are created in SongsDiscovered.
|
||||
// model3 - All container nodes are created in SongsDiscovered.
|
||||
// model2 - Initial container nodes are created in SongsAdded.
|
||||
// model3 - All container nodes are created in SongsAdded.
|
||||
|
||||
// WARNING: This test can take up to 30 minutes to complete.
|
||||
#if 0
|
||||
|
@ -363,23 +332,23 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
// Add some normal albums.
|
||||
for (int artist_number = 1; artist_number <= 3 ; ++artist_number) {
|
||||
Song song(Song::Source::Collection);
|
||||
song.set_artist(QString("Artist %1").arg(artist_number));
|
||||
song.set_composer(QString("Composer %1").arg(artist_number));
|
||||
song.set_performer(QString("Performer %1").arg(artist_number));
|
||||
song.set_artist(QStringLiteral("Artist %1").arg(artist_number));
|
||||
song.set_composer(QStringLiteral("Composer %1").arg(artist_number));
|
||||
song.set_performer(QStringLiteral("Performer %1").arg(artist_number));
|
||||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
for (int album_number = 1; album_number <= 3 ; ++album_number) {
|
||||
if (year > 2020) year = 1960;
|
||||
song.set_album(QString("Artist %1 - Album %2").arg(artist_number).arg(album_number));
|
||||
song.set_album(QStringLiteral("Artist %1 - Album %2").arg(artist_number).arg(album_number));
|
||||
song.set_album_id(QString::number(album_number));
|
||||
song.set_year(year++);
|
||||
song.set_genre("Rock");
|
||||
song.set_genre(QStringLiteral("Rock"));
|
||||
for (int song_number = 1; song_number <= 5 ; ++song_number) {
|
||||
song.set_url(QUrl(QString("file:///mnt/music/Artist %1/Album %2/%3 - artist song-n-%3").arg(artist_number).arg(album_number).arg(song_number)));
|
||||
song.set_title(QString("Title %1").arg(song_number));
|
||||
song.set_url(QUrl(QStringLiteral("file:///mnt/music/Artist %1/Album %2/%3 - artist song-n-%3").arg(artist_number).arg(album_number).arg(song_number)));
|
||||
song.set_title(QStringLiteral("Title %1").arg(song_number));
|
||||
song.set_track(song_number);
|
||||
songs << song;
|
||||
}
|
||||
|
@ -389,26 +358,26 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
// Add some albums with 'album artist'.
|
||||
for (int album_artist_number = 1; album_artist_number <= 3 ; ++album_artist_number) {
|
||||
Song song(Song::Source::Collection);
|
||||
song.set_albumartist(QString("Album Artist %1").arg(album_artist_number));
|
||||
song.set_composer(QString("Composer %1").arg(album_artist_number));
|
||||
song.set_performer(QString("Performer %1").arg(album_artist_number));
|
||||
song.set_albumartist(QStringLiteral("Album Artist %1").arg(album_artist_number));
|
||||
song.set_composer(QStringLiteral("Composer %1").arg(album_artist_number));
|
||||
song.set_performer(QStringLiteral("Performer %1").arg(album_artist_number));
|
||||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
for (int album_number = 1; album_number <= 3 ; ++album_number) {
|
||||
if (year > 2020) year = 1960;
|
||||
song.set_album(QString("Album Artist %1 - Album %2").arg(album_artist_number).arg(album_number));
|
||||
song.set_album(QStringLiteral("Album Artist %1 - Album %2").arg(album_artist_number).arg(album_number));
|
||||
song.set_album_id(QString::number(album_number));
|
||||
song.set_year(year++);
|
||||
song.set_genre("Rock");
|
||||
song.set_genre(QStringLiteral("Rock"));
|
||||
int artist_number = 1;
|
||||
for (int song_number = 1; song_number <= 5 ; ++song_number) {
|
||||
song.set_url(QUrl(QString("file:///mnt/music/Album Artist %1/Album %2/%3 - album artist song-n-%3").arg(album_artist_number).arg(album_number).arg(QString::number(song_number))));
|
||||
song.set_title("Title " + QString::number(song_number));
|
||||
song.set_url(QUrl(QStringLiteral("file:///mnt/music/Album Artist %1/Album %2/%3 - album artist song-n-%3").arg(album_artist_number).arg(album_number).arg(QString::number(song_number))));
|
||||
song.set_title(QStringLiteral("Title ") + QString::number(song_number));
|
||||
song.set_track(song_number);
|
||||
song.set_artist("Artist " + QString::number(artist_number));
|
||||
song.set_artist(QStringLiteral("Artist ") + QString::number(artist_number));
|
||||
songs << song;
|
||||
++artist_number;
|
||||
}
|
||||
|
@ -422,20 +391,20 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
song.set_album(QString("Compilation Album %1").arg(album_number));
|
||||
song.set_album(QStringLiteral("Compilation Album %1").arg(album_number));
|
||||
song.set_album_id(QString::number(album_number));
|
||||
song.set_year(year++);
|
||||
song.set_genre("Pop");
|
||||
song.set_genre(QStringLiteral("Pop"));
|
||||
song.set_compilation(true);
|
||||
int artist_number = 1;
|
||||
for (int song_number = 1; song_number <= 4 ; ++song_number) {
|
||||
song.set_url(QUrl(QString("file:///mnt/music/Compilation Artist %1/Compilation Album %2/%3 - compilation song-n-%3").arg(artist_number).arg(album_number).arg(QString::number(song_number))));
|
||||
song.set_artist(QString("Compilation Artist %1").arg(artist_number));
|
||||
song.set_composer(QString("Composer %1").arg(artist_number));
|
||||
song.set_performer(QString("Performer %1").arg(artist_number));
|
||||
song.set_title(QString("Title %1").arg(song_number));
|
||||
song.set_url(QUrl(QStringLiteral("file:///mnt/music/Compilation Artist %1/Compilation Album %2/%3 - compilation song-n-%3").arg(artist_number).arg(album_number).arg(QString::number(song_number))));
|
||||
song.set_artist(QStringLiteral("Compilation Artist %1").arg(artist_number));
|
||||
song.set_composer(QStringLiteral("Composer %1").arg(artist_number));
|
||||
song.set_performer(QStringLiteral("Performer %1").arg(artist_number));
|
||||
song.set_title(QStringLiteral("Title %1").arg(song_number));
|
||||
song.set_track(song_number);
|
||||
songs << song;
|
||||
++artist_number;
|
||||
|
@ -448,27 +417,27 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
song.set_url(QUrl(QString("file:///mnt/music/no album song 1/song-only-1")));
|
||||
song.set_title("Only Title 1");
|
||||
song.set_url(QUrl(QStringLiteral("file:///mnt/music/no album song 1/song-only-1")));
|
||||
song.set_title(QStringLiteral("Only Title 1"));
|
||||
songs << song;
|
||||
song.set_url(QUrl(QString("file:///mnt/music/no album song 2/song-only-2")));
|
||||
song.set_title("Only Title 2");
|
||||
song.set_url(QUrl(QStringLiteral("file:///mnt/music/no album song 2/song-only-2")));
|
||||
song.set_title(QStringLiteral("Only Title 2"));
|
||||
songs << song;
|
||||
}
|
||||
|
||||
// Song with only artist, album and title.
|
||||
{
|
||||
Song song(Song::Source::Collection);
|
||||
song.set_url(QUrl(QString("file:///tmp/artist-album-title-song")));
|
||||
song.set_artist("Not Only Artist");
|
||||
song.set_album("Not Only Album");
|
||||
song.set_title("Not Only Title");
|
||||
song.set_url(QUrl(QStringLiteral("file:///tmp/artist-album-title-song")));
|
||||
song.set_artist(QStringLiteral("Not Only Artist"));
|
||||
song.set_album(QStringLiteral("Not Only Album"));
|
||||
song.set_title(QStringLiteral("Not Only Title"));
|
||||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
song.set_year(1970);
|
||||
song.set_track(1);
|
||||
|
@ -478,14 +447,14 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
// Add possible Various artists conflicting songs.
|
||||
{
|
||||
Song song(Song::Source::Collection);
|
||||
song.set_url(QUrl(QString("file:///tmp/song-va-conflicting-1")));
|
||||
song.set_artist("Various artists");
|
||||
song.set_album("VA Album");
|
||||
song.set_title("VA Title");
|
||||
song.set_url(QUrl(QStringLiteral("file:///tmp/song-va-conflicting-1")));
|
||||
song.set_artist(QStringLiteral("Various artists"));
|
||||
song.set_album(QStringLiteral("VA Album"));
|
||||
song.set_title(QStringLiteral("VA Title"));
|
||||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
song.set_year(1970);
|
||||
song.set_track(1);
|
||||
|
@ -494,15 +463,15 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
|
||||
{
|
||||
Song song(Song::Source::Collection);
|
||||
song.set_url(QUrl(QString("file:///tmp/song-va-conflicting-2")));
|
||||
song.set_artist("Various artists");
|
||||
song.set_albumartist("Various artists");
|
||||
song.set_album("VA Album");
|
||||
song.set_title("VA Title");
|
||||
song.set_url(QUrl(QStringLiteral("file:///tmp/song-va-conflicting-2")));
|
||||
song.set_artist(QStringLiteral("Various artists"));
|
||||
song.set_albumartist(QStringLiteral("Various artists"));
|
||||
song.set_album(QStringLiteral("VA Album"));
|
||||
song.set_title(QStringLiteral("VA Title"));
|
||||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
song.set_year(1970);
|
||||
song.set_track(1);
|
||||
|
@ -511,14 +480,14 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
|
||||
{
|
||||
Song song(Song::Source::Collection);
|
||||
song.set_url(QUrl(QString("file:///tmp/song-va-conflicting-3")));
|
||||
song.set_albumartist("Various artists");
|
||||
song.set_album("VA Album");
|
||||
song.set_title("VA Title");
|
||||
song.set_url(QUrl(QStringLiteral("file:///tmp/song-va-conflicting-3")));
|
||||
song.set_albumartist(QStringLiteral("Various artists"));
|
||||
song.set_album(QStringLiteral("VA Album"));
|
||||
song.set_title(QStringLiteral("VA Title"));
|
||||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
song.set_year(1970);
|
||||
song.set_track(1);
|
||||
|
@ -528,35 +497,35 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
// Albums with Album ID.
|
||||
for (int album_id = 0; album_id <= 2 ; ++album_id) {
|
||||
Song song(Song::Source::Collection);
|
||||
song.set_url(QUrl(QString("file:///tmp/song-with-album-id-1")));
|
||||
song.set_artist("Artist with Album ID");
|
||||
song.set_album(QString("Album %1 with Album ID").arg(album_id));
|
||||
song.set_album_id(QString("Album ID %1").arg(album_id));
|
||||
song.set_url(QUrl(QStringLiteral("file:///tmp/song-with-album-id-1")));
|
||||
song.set_artist(QStringLiteral("Artist with Album ID"));
|
||||
song.set_album(QStringLiteral("Album %1 with Album ID").arg(album_id));
|
||||
song.set_album_id(QStringLiteral("Album ID %1").arg(album_id));
|
||||
song.set_mtime(1);
|
||||
song.set_ctime(1);
|
||||
song.set_directory_id(1);
|
||||
song.set_filetype(Song::FileType_FLAC);
|
||||
song.set_filetype(Song::FileType::FLAC);
|
||||
song.set_filesize(1);
|
||||
song.set_year(1970);
|
||||
for (int i = 0; i <= 3 ; ++i) {
|
||||
song.set_title(QString("Title %1 %2").arg(album_id).arg(i));
|
||||
song.set_title(QStringLiteral("Title %1 %2").arg(album_id).arg(i));
|
||||
song.set_track(i);
|
||||
songs << song;
|
||||
}
|
||||
}
|
||||
|
||||
for (int f = CollectionModel::GroupBy_None + 1 ; f < CollectionModel::GroupByCount ; ++f) {
|
||||
for (int s = CollectionModel::GroupBy_None ; s < CollectionModel::GroupByCount ; ++s) {
|
||||
for (int t = CollectionModel::GroupBy_None ; t < CollectionModel::GroupByCount ; ++t) {
|
||||
for (int f = static_cast<int>(CollectionModel::GroupBy::None) + 1 ; f < static_cast<int>(CollectionModel::GroupBy::GroupByCount) ; ++f) {
|
||||
for (int s = static_cast<int>(CollectionModel::GroupBy::None) ; s < static_cast<int>(CollectionModel::GroupBy::GroupByCount) ; ++s) {
|
||||
for (int t = static_cast<int>(CollectionModel::GroupBy::None) ; t < static_cast<int>(CollectionModel::GroupBy::GroupByCount) ; ++t) {
|
||||
|
||||
qLog(Debug) << "Testing collection model grouping: " << f << s << t;
|
||||
|
||||
ScopedPtr<Database> database1;
|
||||
ScopedPtr<Database> database2;
|
||||
ScopedPtr<Database> database3;
|
||||
ScopedPtr<CollectionBackend> backend1;
|
||||
ScopedPtr<CollectionBackend> backend2;
|
||||
ScopedPtr<CollectionBackend> backend3;
|
||||
SharedPtr<Database> database1;
|
||||
SharedPtr<Database> database2;
|
||||
SharedPtr<Database> database3;
|
||||
SharedPtr<CollectionBackend> backend1;
|
||||
SharedPtr<CollectionBackend> backend2;
|
||||
SharedPtr<CollectionBackend> backend3;
|
||||
ScopedPtr<CollectionModel> model1;
|
||||
ScopedPtr<CollectionModel> model2;
|
||||
ScopedPtr<CollectionModel> model3;
|
||||
|
@ -564,39 +533,54 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
database1 = make_unique<MemoryDatabase>(nullptr);
|
||||
database2 = make_unique<MemoryDatabase>(nullptr);
|
||||
database3 = make_unique<MemoryDatabase>(nullptr);
|
||||
backend1 = make_unique<CollectionBackend>();
|
||||
backend2= make_unique<CollectionBackend>();
|
||||
backend3 = make_unique<CollectionBackend>();
|
||||
backend1->Init(database1.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
|
||||
backend2->Init(database2.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
|
||||
backend3->Init(database3.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
|
||||
model1 = make_unique<CollectionModel>(backend1.get(), nullptr);
|
||||
model2 = make_unique<CollectionModel>(backend2.get(), nullptr);
|
||||
model3 = make_unique<CollectionModel>(backend3.get(), nullptr);
|
||||
backend1 = make_shared<CollectionBackend>();
|
||||
backend2= make_shared<CollectionBackend>();
|
||||
backend3 = make_shared<CollectionBackend>();
|
||||
backend1->Init(database1, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
|
||||
backend2->Init(database2, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
|
||||
backend3->Init(database3, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
|
||||
model1 = make_unique<CollectionModel>(backend1, nullptr);
|
||||
model2 = make_unique<CollectionModel>(backend2, nullptr);
|
||||
model3 = make_unique<CollectionModel>(backend3, nullptr);
|
||||
|
||||
backend1->AddDirectory("/mnt/music");
|
||||
backend2->AddDirectory("/mnt/music");
|
||||
backend3->AddDirectory("/mut/music");
|
||||
backend1->AddDirectory(QStringLiteral("/mnt/music"));
|
||||
backend2->AddDirectory(QStringLiteral("/mnt/music"));
|
||||
backend3->AddDirectory(QStringLiteral("/mut/music"));
|
||||
|
||||
model1->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy(f), CollectionModel::GroupBy(s), CollectionModel::GroupBy(t)));
|
||||
model2->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy(f), CollectionModel::GroupBy(s), CollectionModel::GroupBy(t)));
|
||||
model3->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy(f), CollectionModel::GroupBy(s), CollectionModel::GroupBy(t)));
|
||||
|
||||
model3->set_use_lazy_loading(false);
|
||||
QSignalSpy model1_update(&*model1, &CollectionModel::SongsAdded);
|
||||
QSignalSpy model2_update(&*model2, &CollectionModel::SongsAdded);
|
||||
QSignalSpy model3_update(&*model3, &CollectionModel::SongsAdded);
|
||||
|
||||
QSignalSpy model1_update(model1.get(), &CollectionModel::rowsInserted);
|
||||
QSignalSpy model2_update(model2.get(), &CollectionModel::rowsInserted);
|
||||
QSignalSpy model3_update(model3.get(), &CollectionModel::rowsInserted);
|
||||
{
|
||||
QEventLoop event_loop;
|
||||
QObject::connect(&*model1, &CollectionModel::rowsInserted, &event_loop, &QEventLoop::quit);
|
||||
backend1->AddOrUpdateSongs(songs);
|
||||
event_loop.exec();
|
||||
}
|
||||
|
||||
backend1->AddOrUpdateSongs(songs);
|
||||
backend2->AddOrUpdateSongs(songs);
|
||||
backend3->AddOrUpdateSongs(songs);
|
||||
{
|
||||
QEventLoop event_loop;
|
||||
QObject::connect(&*model2, &CollectionModel::rowsInserted, &event_loop, &QEventLoop::quit);
|
||||
backend2->AddOrUpdateSongs(songs);
|
||||
event_loop.exec();
|
||||
}
|
||||
|
||||
ASSERT_EQ(model1->song_nodes().count(), 0);
|
||||
ASSERT_EQ(model2->song_nodes().count(), 0);
|
||||
{
|
||||
QEventLoop event_loop;
|
||||
QObject::connect(&*model3, &CollectionModel::rowsInserted, &event_loop, &QEventLoop::quit);
|
||||
backend3->AddOrUpdateSongs(songs);
|
||||
event_loop.exec();
|
||||
}
|
||||
|
||||
ASSERT_EQ(model1->song_nodes().count(), songs.count());
|
||||
ASSERT_EQ(model2->song_nodes().count(), songs.count());
|
||||
ASSERT_EQ(model3->song_nodes().count(), songs.count());
|
||||
|
||||
model1->Init(false);
|
||||
model1->Init();
|
||||
|
||||
model1->ExpandAll();
|
||||
model2->ExpandAll();
|
||||
|
@ -654,13 +638,30 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
|
|||
}
|
||||
}
|
||||
|
||||
QSignalSpy database_reset_1(backend1.get(), &CollectionBackend::DatabaseReset);
|
||||
QSignalSpy database_reset_2(backend2.get(), &CollectionBackend::DatabaseReset);
|
||||
QSignalSpy database_reset_3(backend3.get(), &CollectionBackend::DatabaseReset);
|
||||
QSignalSpy database_reset_1(&*backend1, &CollectionBackend::DatabaseReset);
|
||||
QSignalSpy database_reset_2(&*backend2, &CollectionBackend::DatabaseReset);
|
||||
QSignalSpy database_reset_3(&*backend3, &CollectionBackend::DatabaseReset);
|
||||
|
||||
backend1->DeleteAll();
|
||||
backend2->DeleteAll();
|
||||
backend3->DeleteAll();
|
||||
{
|
||||
QEventLoop event_loop;
|
||||
QObject::connect(&*model1, &CollectionModel::modelReset, &event_loop, &QEventLoop::quit);
|
||||
backend1->DeleteAll();
|
||||
event_loop.exec();
|
||||
}
|
||||
|
||||
{
|
||||
QEventLoop event_loop;
|
||||
QObject::connect(&*model2, &CollectionModel::modelReset, &event_loop, &QEventLoop::quit);
|
||||
backend2->DeleteAll();
|
||||
event_loop.exec();
|
||||
}
|
||||
|
||||
{
|
||||
QEventLoop event_loop;
|
||||
QObject::connect(&*model3, &CollectionModel::modelReset, &event_loop, &QEventLoop::quit);
|
||||
backend3->DeleteAll();
|
||||
event_loop.exec();
|
||||
}
|
||||
|
||||
ASSERT_EQ(database_reset_1.count(), 1);
|
||||
ASSERT_EQ(database_reset_2.count(), 1);
|
||||
|
|
Loading…
Reference in New Issue