/* * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome * Copyright 2018-2023, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Strawberry is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Strawberry. If not, see . * */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/shared_ptr.h" #include "core/logging.h" #include "core/database.h" #include "core/scopedtransaction.h" #include "core/song.h" #include "core/sqlrow.h" #include "smartplaylists/smartplaylistsearch.h" #include "collectiondirectory.h" #include "collectionbackend.h" #include "collectionfilteroptions.h" #include "collectionquery.h" #include "collectiontask.h" CollectionBackend::CollectionBackend(QObject *parent) : CollectionBackendInterface(parent), db_(nullptr), task_manager_(nullptr), source_(Song::Source::Unknown), original_thread_(nullptr) { original_thread_ = thread(); } CollectionBackend::~CollectionBackend() { qLog(Debug) << "Collection backend" << this << "for" << Song::TextForSource(source_) << "deleted"; } void CollectionBackend::Init(SharedPtr db, SharedPtr task_manager, const Song::Source source, const QString &songs_table, const QString &fts_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() { if (db_) { QMutexLocker l(db_->Mutex()); db_->Close(); } } void CollectionBackend::ExitAsync() { QMetaObject::invokeMethod(this, &CollectionBackend::Exit, Qt::QueuedConnection); } void CollectionBackend::Exit() { Q_ASSERT(QThread::currentThread() == thread()); moveToThread(original_thread_); emit ExitFinished(); } void CollectionBackend::ReportErrors(const CollectionQuery &query) { const QSqlError sql_error = query.lastError(); if (sql_error.isValid()) { qLog(Error) << "Unable to execute collection SQL query:" << sql_error; qLog(Error) << "Failed SQL query:" << query.lastQuery(); qLog(Error) << "Bound SQL values:" << query.boundValues(); emit Error(tr("Unable to execute collection SQL query: %1").arg(sql_error.text())); emit Error(tr("Failed SQL query: %1").arg(query.lastQuery())); } } void CollectionBackend::LoadDirectoriesAsync() { QMetaObject::invokeMethod(this, &CollectionBackend::LoadDirectories, Qt::QueuedConnection); } void CollectionBackend::UpdateTotalSongCountAsync() { QMetaObject::invokeMethod(this, &CollectionBackend::UpdateTotalSongCount, Qt::QueuedConnection); } void CollectionBackend::UpdateTotalArtistCountAsync() { QMetaObject::invokeMethod(this, &CollectionBackend::UpdateTotalArtistCount, Qt::QueuedConnection); } void CollectionBackend::UpdateTotalAlbumCountAsync() { QMetaObject::invokeMethod(this, &CollectionBackend::UpdateTotalAlbumCount, Qt::QueuedConnection); } void CollectionBackend::IncrementPlayCountAsync(const int id) { QMetaObject::invokeMethod(this, "IncrementPlayCount", Qt::QueuedConnection, Q_ARG(int, id)); } void CollectionBackend::IncrementSkipCountAsync(const int id, const float progress) { QMetaObject::invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, progress)); } void CollectionBackend::ResetPlayStatisticsAsync(const int id, const bool save_tags) { QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(bool, save_tags)); } void CollectionBackend::ResetPlayStatisticsAsync(const QList &id_list, const bool save_tags) { QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(QList, id_list), Q_ARG(bool, save_tags)); } void CollectionBackend::LoadDirectories() { CollectionDirectoryList dirs = GetAllDirectories(); QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); for (const CollectionDirectory &dir : dirs) { emit DirectoryDiscovered(dir, SubdirsInDirectory(dir.id, db)); } } void CollectionBackend::ChangeDirPath(const int id, const QString &old_path, const QString &new_path) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); ScopedTransaction t(&db); // Do the dirs table { SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET path=:path WHERE ROWID=:id").arg(dirs_table_)); q.BindValue(QStringLiteral(":path"), new_path); q.BindValue(QStringLiteral(":id"), id); if (!q.Exec()) { db_->ReportErrors(q); return; } } const QByteArray old_url = QUrl::fromLocalFile(old_path).toEncoded(); const QByteArray new_url = QUrl::fromLocalFile(new_path).toEncoded(); const qint64 path_len = old_url.length(); // Do the subdirs table { SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET path=:path || substr(path, %2) WHERE directory=:id").arg(subdirs_table_).arg(path_len)); q.BindValue(QStringLiteral(":path"), new_url); q.BindValue(QStringLiteral(":id"), id); if (!q.Exec()) { db_->ReportErrors(q); return; } } // Do the songs table { SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET url=:path || substr(url, %2) WHERE directory=:id").arg(songs_table_).arg(path_len)); q.BindValue(QStringLiteral(":path"), new_url); q.BindValue(QStringLiteral(":id"), id); if (!q.Exec()) { db_->ReportErrors(q); return; } } t.Commit(); } CollectionDirectoryList CollectionBackend::GetAllDirectories() { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); CollectionDirectoryList ret; SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, path FROM %1").arg(dirs_table_)); if (!q.Exec()) { db_->ReportErrors(q); return ret; } while (q.next()) { CollectionDirectory dir; dir.id = q.value(0).toInt(); dir.path = q.value(1).toString(); ret << dir; } return ret; } CollectionSubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) { QMutexLocker l(db_->Mutex()); QSqlDatabase db = db_->Connect(); return SubdirsInDirectory(id, db); } CollectionSubdirectoryList CollectionBackend::SubdirsInDirectory(const int id, QSqlDatabase &db) { SqlQuery q(db); q.prepare(QStringLiteral("SELECT path, mtime FROM %1 WHERE directory_id = :dir").arg(subdirs_table_)); q.BindValue(QStringLiteral(":dir"), id); if (!q.Exec()) { db_->ReportErrors(q); return CollectionSubdirectoryList(); } CollectionSubdirectoryList subdirs; while (q.next()) { CollectionSubdirectory subdir; subdir.directory_id = id; subdir.path = q.value(0).toString(); subdir.mtime = q.value(1).toLongLong(); subdirs << subdir; } return subdirs; } void CollectionBackend::UpdateTotalSongCount() { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT COUNT(*) FROM %1 WHERE unavailable = 0").arg(songs_table_)); if (!q.Exec()) { db_->ReportErrors(q); return; } if (!q.next()) { db_->ReportErrors(q); return; } emit TotalSongCountUpdated(q.value(0).toInt()); } void CollectionBackend::UpdateTotalArtistCount() { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT COUNT(DISTINCT artist) FROM %1 WHERE unavailable = 0").arg(songs_table_)); if (!q.Exec()) { db_->ReportErrors(q); return; } if (!q.next()) { db_->ReportErrors(q); return; } emit TotalArtistCountUpdated(q.value(0).toInt()); } void CollectionBackend::UpdateTotalAlbumCount() { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT COUNT(*) FROM (SELECT DISTINCT effective_albumartist, album FROM %1 WHERE unavailable = 0)").arg(songs_table_)); if (!q.Exec()) { db_->ReportErrors(q); return; } if (!q.next()) { db_->ReportErrors(q); return; } emit TotalAlbumCountUpdated(q.value(0).toInt()); } void CollectionBackend::AddDirectory(const QString &path) { QString canonical_path = QFileInfo(path).canonicalFilePath(); QString db_path = canonical_path; QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("INSERT INTO %1 (path, subdirs) VALUES (:path, 1)").arg(dirs_table_)); q.BindValue(QStringLiteral(":path"), db_path); if (!q.Exec()) { db_->ReportErrors(q); return; } CollectionDirectory dir; dir.path = canonical_path; dir.id = q.lastInsertId().toInt(); emit DirectoryDiscovered(dir, CollectionSubdirectoryList()); } void CollectionBackend::RemoveDirectory(const CollectionDirectory &dir) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); // Remove songs first DeleteSongs(FindSongsInDirectory(dir.id)); ScopedTransaction transaction(&db); // Delete the subdirs that were in this directory { SqlQuery q(db); q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id").arg(subdirs_table_)); q.BindValue(QStringLiteral(":id"), dir.id); if (!q.Exec()) { db_->ReportErrors(q); return; } } // Now remove the directory itself { SqlQuery q(db); q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(dirs_table_)); q.BindValue(QStringLiteral(":id"), dir.id); if (!q.Exec()) { db_->ReportErrors(q); return; } } emit DirectoryDeleted(dir); transaction.Commit(); } SongList CollectionBackend::FindSongsInDirectory(const int id) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE directory_id = :directory_id").arg(Song::kColumnSpec, songs_table_)); q.BindValue(QStringLiteral(":directory_id"), id); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } SongList ret; while (q.next()) { Song song(source_); song.InitFromQuery(q, true); ret << song; } return ret; } SongList CollectionBackend::SongsWithMissingFingerprint(const int id) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE directory_id = :directory_id AND unavailable = 0 AND (fingerprint IS NULL OR fingerprint = '')").arg(Song::kColumnSpec, songs_table_)); q.BindValue(QStringLiteral(":directory_id"), id); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } SongList ret; while (q.next()) { Song song(source_); song.InitFromQuery(q, true); ret << song; } return ret; } SongList CollectionBackend::SongsWithMissingLoudnessCharacteristics(const int id) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE directory_id = :directory_id AND unavailable = 0 AND (ebur128_integrated_loudness_lufs IS NULL OR ebur128_loudness_range_lu IS NULL)").arg(Song::kColumnSpec, songs_table_)); q.BindValue(QStringLiteral(":directory_id"), id); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } SongList ret; while (q.next()) { Song song(source_); song.InitFromQuery(q, true); ret << song; } return ret; } void CollectionBackend::SongPathChanged(const Song &song, const QFileInfo &new_file, const std::optional new_collection_directory_id) { // Take a song and update its path Song updated_song = song; updated_song.set_source(source_); updated_song.set_url(QUrl::fromLocalFile(new_file.absoluteFilePath())); updated_song.set_basefilename(new_file.fileName()); updated_song.InitArtManual(); if (updated_song.is_collection_song() && new_collection_directory_id) { updated_song.set_directory_id(new_collection_directory_id.value()); } AddOrUpdateSongs(SongList() << updated_song); } void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); ScopedTransaction transaction(&db); for (const CollectionSubdirectory &subdir : subdirs) { if (subdir.mtime == 0) { // Delete the subdirectory SqlQuery q(db); q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_)); q.BindValue(QStringLiteral(":id"), subdir.directory_id); q.BindValue(QStringLiteral(":path"), subdir.path); if (!q.Exec()) { db_->ReportErrors(q); return; } } else { // See if this subdirectory already exists in the database bool exists = false; { SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_)); q.BindValue(QStringLiteral(":id"), subdir.directory_id); q.BindValue(QStringLiteral(":path"), subdir.path); if (!q.Exec()) { db_->ReportErrors(q); return; } exists = q.next(); } if (exists) { SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_)); q.BindValue(QStringLiteral(":mtime"), subdir.mtime); q.BindValue(QStringLiteral(":id"), subdir.directory_id); q.BindValue(QStringLiteral(":path"), subdir.path); if (!q.Exec()) { db_->ReportErrors(q); return; } } else { SqlQuery q(db); q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_)); q.BindValue(QStringLiteral(":id"), subdir.directory_id); q.BindValue(QStringLiteral(":path"), subdir.path); q.BindValue(QStringLiteral(":mtime"), subdir.mtime); if (!q.Exec()) { db_->ReportErrors(q); return; } } } } transaction.Commit(); } SongList CollectionBackend::GetAllSongs() { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2").arg(Song::kColumnSpec, songs_table_)); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } SongList songs; while (q.next()) { Song song; song.InitFromQuery(q, true); songs << song; } return songs; } void CollectionBackend::AddOrUpdateSongsAsync(const SongList &songs) { QMetaObject::invokeMethod(this, "AddOrUpdateSongs", Qt::QueuedConnection, Q_ARG(SongList, songs)); } void CollectionBackend::AddOrUpdateSongs(const SongList &songs) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); ScopedTransaction transaction(&db); SongList added_songs; SongList deleted_songs; for (const Song &song : songs) { // Do a sanity check first - make sure the song's directory still exists // This is to fix a possible race condition when a directory is removed while CollectionWatcher is scanning it. if (!dirs_table_.isEmpty()) { SqlQuery check_dir(db); check_dir.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE ROWID = :id").arg(dirs_table_)); check_dir.BindValue(QStringLiteral(":id"), song.directory_id()); if (!check_dir.Exec()) { db_->ReportErrors(check_dir); return; } if (!check_dir.next()) continue; } if (song.id() != -1) { // This song exists in the DB. // Get the previous song data first Song old_song(GetSongById(song.id())); if (!old_song.is_valid()) continue; // Update { SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(songs_table_, Song::kUpdateSpec)); song.BindToQuery(&q); q.BindValue(QStringLiteral(":id"), song.id()); if (!q.Exec()) { db_->ReportErrors(q); return; } } { 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; continue; } else if (!song.song_id().isEmpty()) { // Song has a unique id, check if the song exists. // Get the previous song data first Song old_song(GetSongBySongId(song.song_id())); if (old_song.is_valid() && old_song.id() != -1) { Song new_song = song; new_song.set_id(old_song.id()); // Update { SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(songs_table_, Song::kUpdateSpec)); new_song.BindToQuery(&q); q.BindValue(QStringLiteral(":id"), new_song.id()); if (!q.Exec()) { db_->ReportErrors(q); 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"), new_song.id()); if (!q.Exec()) { db_->ReportErrors(q); return; } } deleted_songs << old_song; added_songs << new_song; continue; } } // Create new song int id = -1; { // Insert the row and create a new ID SqlQuery q(db); q.prepare(QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)").arg(songs_table_, Song::kColumnSpec, Song::kBindSpec)); song.BindToQuery(&q); if (!q.Exec()) { db_->ReportErrors(q); return; } // Get the new ID id = q.lastInsertId().toInt(); } 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; } transaction.Commit(); if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs); if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs); UpdateTotalSongCountAsync(); UpdateTotalArtistCountAsync(); UpdateTotalAlbumCountAsync(); } void CollectionBackend::UpdateSongsBySongIDAsync(const SongMap &new_songs) { QMetaObject::invokeMethod(this, "UpdateSongsBySongID", Qt::QueuedConnection, Q_ARG(SongMap, new_songs)); } void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); CollectionTask task(task_manager_, tr("Updating %1 database.").arg(Song::TextForSource(source_))); ScopedTransaction transaction(&db); SongList added_songs; SongList deleted_songs; SongMap old_songs; { CollectionQuery query(db, songs_table_, fts_table_); if (!ExecCollectionQuery(&query, old_songs)) { ReportErrors(query); return; } } // Add or update songs. QList new_songs_list = new_songs.values(); for (const Song &new_song : new_songs_list) { if (old_songs.contains(new_song.song_id())) { Song old_song = old_songs[new_song.song_id()]; if (!new_song.IsAllMetadataEqual(old_song) || !new_song.IsFingerprintEqual(old_song)) { // Update existing song. { SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(songs_table_, Song::kUpdateSpec)); new_song.BindToQuery(&q); q.BindValue(QStringLiteral(":id"), old_song.id()); if (!q.Exec()) { db_->ReportErrors(q); 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; } } else { // Add new song int id = -1; { SqlQuery q(db); q.prepare(QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)").arg(songs_table_, Song::kColumnSpec, Song::kBindSpec)); new_song.BindToQuery(&q); if (!q.Exec()) { db_->ReportErrors(q); return; } // Get the new ID id = q.lastInsertId().toInt(); } 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; } } // Delete songs QList old_songs_list = old_songs.values(); for (const Song &old_song : old_songs_list) { if (!new_songs.contains(old_song.song_id())) { { SqlQuery q(db); q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_)); q.BindValue(QStringLiteral(":id"), old_song.id()); if (!q.Exec()) { db_->ReportErrors(q); 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; } } transaction.Commit(); if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs); if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs); UpdateTotalSongCountAsync(); UpdateTotalArtistCountAsync(); UpdateTotalAlbumCountAsync(); } 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) { q.BindValue(QStringLiteral(":mtime"), song.mtime()); q.BindValue(QStringLiteral(":id"), song.id()); if (!q.Exec()) { db_->ReportErrors(q); return; } } transaction.Commit(); } 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); return; } } transaction.Commit(); emit SongsDeleted(songs); UpdateTotalSongCountAsync(); UpdateTotalArtistCountAsync(); UpdateTotalAlbumCountAsync(); } void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool unavailable) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery query(db); query.prepare(QStringLiteral("UPDATE %1 SET unavailable = %2 WHERE ROWID = :id").arg(songs_table_).arg(static_cast(unavailable))); ScopedTransaction transaction(&db); for (const Song &song : songs) { query.BindValue(QStringLiteral(":id"), song.id()); if (!query.Exec()) { db_->ReportErrors(query); return; } } transaction.Commit(); if (unavailable) { emit SongsDeleted(songs); } else { emit SongsDiscovered(songs); } UpdateTotalSongCountAsync(); UpdateTotalArtistCountAsync(); UpdateTotalAlbumCountAsync(); } QStringList CollectionBackend::GetAll(const QString &column, const CollectionFilterOptions &filter_options) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); CollectionQuery query(db, songs_table_, fts_table_, filter_options); query.SetColumnSpec(QStringLiteral("DISTINCT ") + column); query.AddCompilationRequirement(false); if (!query.Exec()) { ReportErrors(query); return QStringList(); } QStringList ret; while (query.Next()) { ret << query.Value(0).toString(); } return ret; } QStringList CollectionBackend::GetAllArtists(const CollectionFilterOptions &opt) { return GetAll(QStringLiteral("artist"), opt); } QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOptions &opt) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); // Albums with 'albumartist' field set: CollectionQuery query(db, songs_table_, fts_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); query2.SetColumnSpec(QStringLiteral("DISTINCT artist")); query2.AddCompilationRequirement(false); query2.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!=")); query2.AddWhere(QStringLiteral("albumartist"), QLatin1String(""), QStringLiteral("=")); if (!query.Exec()) { ReportErrors(query); return QStringList(); } if (!query2.Exec()) { ReportErrors(query2); return QStringList(); } QSet artists; while (query.Next()) { artists << query.Value(0).toString(); } while (query2.Next()) { artists << query2.Value(0).toString(); } return QStringList(artists.values()); } CollectionBackend::AlbumList CollectionBackend::GetAllAlbums(const CollectionFilterOptions &opt) { return GetAlbums(QString(), false, opt); } CollectionBackend::AlbumList CollectionBackend::GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt) { return GetAlbums(artist, false, opt); } SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, const CollectionFilterOptions &opt) { QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); CollectionQuery query(db, songs_table_, fts_table_, opt); query.AddCompilationRequirement(false); query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); SongList songs; if (!ExecCollectionQuery(&query, songs)) { ReportErrors(query); } return songs; } SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, const QString &album, const CollectionFilterOptions &opt) { QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); CollectionQuery query(db, songs_table_, fts_table_, opt); query.AddCompilationRequirement(false); query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); query.AddWhere(QStringLiteral("album"), album); SongList songs; if (!ExecCollectionQuery(&query, songs)) { ReportErrors(query); } return songs; } SongList CollectionBackend::GetSongsByAlbum(const QString &album, const CollectionFilterOptions &opt) { QSqlDatabase db(db_->Connect()); QMutexLocker l(db_->Mutex()); CollectionQuery query(db, songs_table_, fts_table_, opt); query.AddCompilationRequirement(false); query.AddWhere(QStringLiteral("album"), album); SongList songs; if (!ExecCollectionQuery(&query, songs)) { ReportErrors(query); } return songs; } bool CollectionBackend::ExecCollectionQuery(CollectionQuery *query, SongList &songs) { query->SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec); if (!query->Exec()) return false; while (query->Next()) { Song song(source_); song.InitFromQuery(*query, true); songs << song; } return true; } bool CollectionBackend::ExecCollectionQuery(CollectionQuery *query, SongMap &songs) { query->SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec); if (!query->Exec()) return false; while (query->Next()) { Song song(source_); song.InitFromQuery(*query, true); songs.insert(song.song_id(), song); } return true; } Song CollectionBackend::GetSongById(const int id) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); return GetSongById(id, db); } SongList CollectionBackend::GetSongsById(const QList &ids) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); QStringList str_ids; str_ids.reserve(ids.count()); for (const int id : ids) { str_ids << QString::number(id); } return GetSongsById(str_ids, db); } SongList CollectionBackend::GetSongsById(const QStringList &ids) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); return GetSongsById(ids, db); } SongList CollectionBackend::GetSongsByForeignId(const QStringList &ids, const QString &table, const QString &column) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); QString in = ids.join(QStringLiteral(",")); SqlQuery q(db); q.prepare(QStringLiteral("SELECT %3.ROWID, %2, %3.%4 FROM %3, %1 WHERE %3.%4 IN (in) AND %1.ROWID = %3.ROWID AND unavailable = 0").arg(songs_table_, Song::kColumnSpec, table, column, in)); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } QVector ret(ids.count()); while (q.next()) { const QString foreign_id = q.value(static_cast(Song::kColumns.count()) + 1).toString(); const qint64 index = ids.indexOf(foreign_id); if (index == -1) continue; ret[index].InitFromQuery(q, true); } return ret.toList(); } Song CollectionBackend::GetSongById(const int id, QSqlDatabase &db) { SongList list = GetSongsById(QStringList() << QString::number(id), db); if (list.isEmpty()) return Song(); return list.first(); } SongList CollectionBackend::GetSongsById(const QStringList &ids, QSqlDatabase &db) { QString in = ids.join(QStringLiteral(",")); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE ROWID IN (%3)").arg(Song::kColumnSpec, songs_table_, in)); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } SongList ret; while (q.next()) { Song song(source_); song.InitFromQuery(q, true); ret << song; } return ret; } Song CollectionBackend::GetSongByUrl(const QUrl &url, const qint64 beginning) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND beginning = :beginning AND unavailable = 0").arg(Song::kColumnSpec, 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(":beginning"), beginning); if (!q.Exec()) { db_->ReportErrors(q); return Song(); } if (!q.next()) { return Song(); } Song song(source_); song.InitFromQuery(q, true); return song; } Song CollectionBackend::GetSongByUrlAndTrack(const QUrl &url, const int track) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND track = :track AND unavailable = 0").arg(Song::kColumnSpec, 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(":track"), track); if (!q.Exec()) { db_->ReportErrors(q); return Song(); } if (!q.next()) { return Song(); } Song song(source_); song.InitFromQuery(q, true); return song; } SongList CollectionBackend::GetSongsByUrl(const QUrl &url, const bool unavailable) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = :unavailable").arg(Song::kColumnSpec, 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(":unavailable"), (unavailable ? 1 : 0)); SongList songs; if (q.Exec()) { while (q.next()) { Song song(source_); song.InitFromQuery(q, true); songs << song; } } else { db_->ReportErrors(q); } return songs; } Song CollectionBackend::GetSongBySongId(const QString &song_id) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); return GetSongBySongId(song_id, db); } SongList CollectionBackend::GetSongsBySongId(const QStringList &song_ids) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); return GetSongsBySongId(song_ids, db); } Song CollectionBackend::GetSongBySongId(const QString &song_id, QSqlDatabase &db) { SongList list = GetSongsBySongId(QStringList() << song_id, db); if (list.isEmpty()) return Song(); return list.first(); } SongList CollectionBackend::GetSongsBySongId(const QStringList &song_ids, QSqlDatabase &db) { QStringList song_ids2; song_ids2.reserve(song_ids.count()); for (const QString &song_id : song_ids) { song_ids2 << QLatin1Char('\'') + song_id + QLatin1Char('\''); } QString in = song_ids2.join(QLatin1Char(',')); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE SONG_ID IN (%3)").arg(Song::kColumnSpec, songs_table_, in)); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } SongList ret; while (q.next()) { Song song(source_); song.InitFromQuery(q, true); ret << song; } return ret; } SongList CollectionBackend::GetSongsByFingerprint(const QString &fingerprint) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE fingerprint = :fingerprint").arg(Song::kColumnSpec, songs_table_)); q.BindValue(QStringLiteral(":fingerprint"), fingerprint); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } SongList songs; while (q.next()) { Song song(source_); song.InitFromQuery(q, true); songs << song; } return songs; } CollectionBackend::AlbumList CollectionBackend::GetCompilationAlbums(const CollectionFilterOptions &opt) { return GetAlbums(QString(), true, opt); } SongList CollectionBackend::GetCompilationSongs(const QString &album, const CollectionFilterOptions &opt) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); CollectionQuery query(db, songs_table_, fts_table_, opt); query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec); query.AddCompilationRequirement(true); query.AddWhere(QStringLiteral("album"), album); if (!query.Exec()) { ReportErrors(query); return SongList(); } SongList ret; while (query.Next()) { Song song(source_); song.InitFromQuery(query, true); ret << song; } return ret; } void CollectionBackend::CompilationsNeedUpdating() { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); // Look for albums that have songs by more than one 'effective album artist' in the same directory SqlQuery q(db); q.prepare(QStringLiteral("SELECT effective_albumartist, album, url, compilation_detected FROM %1 WHERE unavailable = 0 ORDER BY album").arg(songs_table_)); if (!q.Exec()) { db_->ReportErrors(q); return; } QMap compilation_info; while (q.next()) { QString artist = q.value(0).toString(); QString album = q.value(1).toString(); QUrl url = QUrl::fromEncoded(q.value(2).toString().toUtf8()); bool compilation_detected = q.value(3).toBool(); // Ignore songs that don't have an album field set if (album.isEmpty()) continue; // Find the directory the song is in QString directory = url.toString(QUrl::PreferLocalFile | QUrl::RemoveFilename); CompilationInfo &info = compilation_info[directory + album]; info.urls << url; if (!info.artists.contains(artist)) { info.artists << artist; } if (compilation_detected) info.has_compilation_detected++; else info.has_not_compilation_detected++; } // Now mark the songs that we think are in compilations SongList deleted_songs; SongList added_songs; ScopedTransaction transaction(&db); QMap::const_iterator it = compilation_info.constBegin(); for (; it != compilation_info.constEnd(); ++it) { const CompilationInfo &info = it.value(); // If there were more than one 'effective album artist' for this album directory, then it's a compilation. 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); } } else { if (info.has_compilation_detected > 0) { UpdateCompilations(db, deleted_songs, added_songs, url, false); } } } } transaction.Commit(); if (!deleted_songs.isEmpty()) { emit SongsDeleted(deleted_songs); emit SongsDiscovered(added_songs); } } bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_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 ROWID, %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(Song::kColumnSpec, 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()); 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; } } else { db_->ReportErrors(q); return false; } } // Update the song 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(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()); if (!q.Exec()) { db_->ReportErrors(q); return false; } return true; } CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, const bool compilation_required, const CollectionFilterOptions &opt) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); CollectionQuery query(db, songs_table_, fts_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")); if (compilation_required) { query.AddCompilationRequirement(true); } else if (!artist.isEmpty()) { query.AddCompilationRequirement(false); query.AddWhere(QStringLiteral("effective_albumartist"), artist); } if (!query.Exec()) { ReportErrors(query); return AlbumList(); } QMap albums; while (query.Next()) { Album album_info; QUrl url = QUrl::fromEncoded(query.Value(0).toByteArray()); album_info.filetype = static_cast(query.Value(1).toInt()); const QString filetype = Song::TextForFiletype(album_info.filetype); album_info.cue_path = query.Value(2).toString(); const bool is_compilation = query.Value(5).toBool(); if (!is_compilation) { album_info.album_artist = query.Value(3).toString(); } album_info.album = query.Value(4).toString(); album_info.art_embedded = query.Value(6).toBool(); const QString art_automatic = query.Value(7).toString(); if (art_automatic.contains(QRegularExpression(QStringLiteral("..+:.*")))) { album_info.art_automatic = QUrl::fromEncoded(art_automatic.toUtf8()); } else { album_info.art_automatic = QUrl::fromLocalFile(art_automatic); } const QString art_manual = query.Value(8).toString(); if (art_manual.contains(QRegularExpression(QStringLiteral("..+:.*")))) { album_info.art_manual = QUrl::fromEncoded(art_manual.toUtf8()); } else { album_info.art_manual = QUrl::fromLocalFile(art_manual); } album_info.art_unset = query.Value(9).toBool(); QString key; if (!album_info.album_artist.isEmpty()) { key.append(album_info.album_artist); } if (!album_info.album.isEmpty()) { if (!key.isEmpty()) key.append(QLatin1Char('-')); key.append(album_info.album); } if (!filetype.isEmpty()) { key.append(filetype); } if (key.isEmpty()) continue; if (albums.contains(key)) { albums[key].urls.append(url); } else { album_info.urls << url; albums.insert(key, album_info); } } return albums.values(); } CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective_albumartist, const QString &album) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); Album ret; ret.album = album; ret.album_artist = effective_albumartist; CollectionQuery query(db, songs_table_, fts_table_); query.SetColumnSpec(QStringLiteral("url, art_embedded, art_automatic, art_manual, art_unset")); if (!effective_albumartist.isEmpty()) { query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); } query.AddWhere(QStringLiteral("album"), album); if (!query.Exec()) { ReportErrors(query); return ret; } if (query.Next()) { ret.urls << QUrl::fromEncoded(query.Value(0).toByteArray()); ret.art_embedded = query.Value(1).toInt() == 1; ret.art_automatic = QUrl::fromEncoded(query.Value(2).toByteArray()); ret.art_manual = QUrl::fromEncoded(query.Value(3).toByteArray()); ret.art_unset = query.Value(4).toInt() == 1; } return ret; } void CollectionBackend::UpdateEmbeddedAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_embedded) { QMetaObject::invokeMethod(this, "UpdateEmbeddedAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(bool, art_embedded)); } void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); // Get the songs before they're updated CollectionQuery query(db, songs_table_, fts_table_); query.SetColumnSpec(QStringLiteral("ROWID, ") + Song::kColumnSpec); query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); query.AddWhere(QStringLiteral("album"), album); if (!query.Exec()) { ReportErrors(query); return; } SongList deleted_songs; while (query.Next()) { Song song(source_); song.InitFromQuery(query, true); deleted_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); } } void CollectionBackend::UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) { QMetaObject::invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, art_manual)); } void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); CollectionQuery query(db, songs_table_, fts_table_); query.SetColumnSpec(QStringLiteral("ROWID, ") + Song::kColumnSpec); query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); query.AddWhere(QStringLiteral("album"), album); if (!query.Exec()) { ReportErrors(query); return; } SongList deleted_songs; while (query.Next()) { Song song(source_); song.InitFromQuery(query, true); deleted_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); } } void CollectionBackend::UnsetAlbumArtAsync(const QString &effective_albumartist, const QString &album) { QMetaObject::invokeMethod(this, "UnsetAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album)); } void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, const QString &album) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); CollectionQuery query(db, songs_table_, fts_table_); query.SetColumnSpec(QStringLiteral("ROWID, ") + Song::kColumnSpec); query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); query.AddWhere(QStringLiteral("album"), album); if (!query.Exec()) { ReportErrors(query); return; } SongList deleted_songs; while (query.Next()) { Song song(source_); song.InitFromQuery(query, true); deleted_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); } } void CollectionBackend::ClearAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool unset) { QMetaObject::invokeMethod(this, "ClearAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(bool, unset)); } void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_unset) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); CollectionQuery query(db, songs_table_, fts_table_); query.SetColumnSpec(QStringLiteral("ROWID, ") + Song::kColumnSpec); query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist); query.AddWhere(QStringLiteral("album"), album); if (!query.Exec()) { ReportErrors(query); return; } SongList deleted_songs; while (query.Next()) { Song song(source_); song.InitFromQuery(query, true); deleted_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); } } void CollectionBackend::ForceCompilation(const QString &album, const QList &artists, const bool on) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SongList deleted_songs, added_songs; for (const QString &artist : artists) { // Get the songs before they're updated CollectionQuery query(db, songs_table_, fts_table_); query.SetColumnSpec(QStringLiteral("ROWID, ") + Song::kColumnSpec); 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_)); if (!artist.isEmpty()) sql += QLatin1String(" AND artist = :artist"); SqlQuery q(db); q.prepare(sql); q.BindValue(QStringLiteral(":compilation_on"), on ? 1 : 0); q.BindValue(QStringLiteral(":compilation_off"), on ? 0 : 1); q.BindValue(QStringLiteral(":album"), album); if (!artist.isEmpty()) q.BindValue(QStringLiteral(":artist"), artist); if (!q.Exec()) { db_->ReportErrors(q); return; } // Now get the updated songs if (!query.Exec()) { ReportErrors(query); return; } 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); } } void CollectionBackend::IncrementPlayCount(const int id) { if (id == -1) return; QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET playcount = playcount + 1, lastplayed = :now WHERE ROWID = :id").arg(songs_table_)); q.BindValue(QStringLiteral(":now"), QDateTime::currentDateTime().toSecsSinceEpoch()); q.BindValue(QStringLiteral(":id"), id); if (!q.Exec()) { db_->ReportErrors(q); return; } Song new_song = GetSongById(id, db); emit SongsStatisticsChanged(SongList() << new_song); } void CollectionBackend::IncrementSkipCount(const int id, const float progress) { Q_UNUSED(progress); if (id == -1) return; QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET skipcount = skipcount + 1 WHERE ROWID = :id").arg(songs_table_)); q.BindValue(QStringLiteral(":id"), id); if (!q.Exec()) { db_->ReportErrors(q); return; } Song new_song = GetSongById(id, db); emit SongsStatisticsChanged(SongList() << new_song); } void CollectionBackend::ResetPlayStatistics(const int id, const bool save_tags) { if (id == -1) return; ResetPlayStatistics(QList() << id, save_tags); } void CollectionBackend::ResetPlayStatistics(const QList &id_list, const bool save_tags) { if (id_list.isEmpty()) return; QStringList id_str_list; id_str_list.reserve(id_list.count()); for (const int id : id_list) { id_str_list << QString::number(id); } const bool success = ResetPlayStatistics(id_str_list); if (success) { const SongList songs = GetSongsById(id_list); emit SongsStatisticsChanged(songs, save_tags); } } bool CollectionBackend::ResetPlayStatistics(const QStringList &id_str_list) { if (id_str_list.isEmpty()) return false; QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID IN (:ids)").arg(songs_table_)); q.BindValue(QStringLiteral(":ids"), id_str_list.join(QStringLiteral(","))); if (!q.Exec()) { db_->ReportErrors(q); return false; } return true; } void CollectionBackend::DeleteAllAsync() { QMetaObject::invokeMethod(this, &CollectionBackend::DeleteAll, Qt::QueuedConnection); } void CollectionBackend::DeleteAll() { { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); ScopedTransaction t(&db); { SqlQuery q(db); q.prepare(QStringLiteral("DELETE FROM ") + songs_table_); if (!q.Exec()) { db_->ReportErrors(q); return; } } { SqlQuery q(db); q.prepare(QStringLiteral("DELETE FROM ") + fts_table_); if (!q.Exec()) { db_->ReportErrors(q); return; } } t.Commit(); } emit DatabaseReset(); } SongList CollectionBackend::SmartPlaylistsFindSongs(const SmartPlaylistSearch &search) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); // Build the query QString sql = search.ToSql(songs_table()); // Run the query SongList ret; SqlQuery query(db); query.prepare(sql); if (!query.Exec()) { db_->ReportErrors(query); return ret; } // Read the results while (query.next()) { Song song; song.InitFromQuery(query, true); ret << song; } return ret; } SongList CollectionBackend::SmartPlaylistsGetAllSongs() { // Get all the songs! return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::SearchType::All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::SortType::FieldAsc, SmartPlaylistSearchTerm::Field::Artist, -1)); } SongList CollectionBackend::GetSongsBy(const QString &artist, const QString &album, const QString &title) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SongList songs; SqlQuery q(db); if (album.isEmpty()) { q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE artist = :artist COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(Song::kColumnSpec, songs_table_)); } else { q.prepare(QStringLiteral("SELECT ROWID, %1 FROM %2 WHERE artist = :artist COLLATE NOCASE AND album = :album COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(Song::kColumnSpec, songs_table_)); } q.BindValue(QStringLiteral(":artist"), artist); if (!album.isEmpty()) q.BindValue(QStringLiteral(":album"), album); q.BindValue(QStringLiteral(":title"), title); if (!q.Exec()) { db_->ReportErrors(q); return SongList(); } while (q.next()) { Song song(source_); song.InitFromQuery(q, true); songs << song; } return songs; } void CollectionBackend::UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const qint64 lastplayed) { SongList songs = GetSongsBy(artist, album, title); if (songs.isEmpty()) { qLog(Debug) << "Could not find a matching song in the database for" << artist << album << title; return; } QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); for (const Song &song : songs) { if (song.lastplayed() >= lastplayed) { continue; } SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET lastplayed = :lastplayed WHERE ROWID = :id").arg(songs_table_)); q.BindValue(QStringLiteral(":lastplayed"), lastplayed); q.BindValue(QStringLiteral(":id"), song.id()); if (!q.Exec()) { db_->ReportErrors(q); continue; } } emit SongsStatisticsChanged(SongList() << songs); } void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount, const bool save_tags) { SongList songs = GetSongsBy(artist, QString(), title); if (songs.isEmpty()) { qLog(Debug) << "Could not find a matching song in the database for" << artist << title; return; } QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); for (const Song &song : songs) { SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET playcount = :playcount WHERE ROWID = :id").arg(songs_table_)); q.BindValue(QStringLiteral(":playcount"), playcount); q.BindValue(QStringLiteral(":id"), song.id()); if (!q.Exec()) { db_->ReportErrors(q); return; } } emit SongsStatisticsChanged(SongList() << songs, save_tags); } void CollectionBackend::UpdateSongRating(const int id, const float rating, const bool save_tags) { if (id == -1) return; UpdateSongsRating(QList() << id, rating, save_tags); } void CollectionBackend::UpdateSongsRating(const QList &id_list, const float rating, const bool save_tags) { if (id_list.isEmpty()) return; QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); QStringList id_str_list; id_str_list.reserve(id_list.count()); for (int i : id_list) { id_str_list << QString::number(i); } QString ids = id_str_list.join(QStringLiteral(",")); SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET rating = :rating WHERE ROWID IN (%2)").arg(songs_table_, ids)); q.BindValue(QStringLiteral(":rating"), rating); if (!q.Exec()) { db_->ReportErrors(q); return; } SongList new_song_list = GetSongsById(id_str_list, db); emit SongsRatingChanged(new_song_list, save_tags); } void CollectionBackend::UpdateSongRatingAsync(const int id, const float rating, const bool save_tags) { QMetaObject::invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, rating), Q_ARG(bool, save_tags)); } void CollectionBackend::UpdateSongsRatingAsync(const QList &ids, const float rating, const bool save_tags) { QMetaObject::invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList, ids), Q_ARG(float, rating), Q_ARG(bool, save_tags)); } void CollectionBackend::UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days) { { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("UPDATE %1 SET lastseen = :lastseen WHERE directory_id = :directory_id AND unavailable = 0").arg(songs_table_)); q.BindValue(QStringLiteral(":lastseen"), QDateTime::currentDateTime().toSecsSinceEpoch()); q.BindValue(QStringLiteral(":directory_id"), directory_id); if (!q.Exec()) { db_->ReportErrors(q); return; } } if (expire_unavailable_songs_days > 0) ExpireSongs(directory_id, expire_unavailable_songs_days); } void CollectionBackend::ExpireSongs(const int directory_id, const int expire_unavailable_songs_days) { SongList songs; { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); SqlQuery q(db); q.prepare(QStringLiteral("SELECT %1.ROWID, ").arg(songs_table_) + Song::JoinSpec(songs_table_) + QStringLiteral(" FROM %1 LEFT JOIN playlist_items ON %1.ROWID = playlist_items.collection_id WHERE %1.directory_id = :directory_id AND %1.unavailable = 1 AND %1.lastseen > 0 AND %1.lastseen < :time AND playlist_items.collection_id IS NULL").arg(songs_table_)); q.BindValue(QStringLiteral(":directory_id"), directory_id); q.BindValue(QStringLiteral(":time"), QDateTime::currentDateTime().toSecsSinceEpoch() - (expire_unavailable_songs_days * 86400)); if (!q.Exec()) { db_->ReportErrors(q); return; } while (q.next()) { Song song(source_); song.InitFromQuery(q, true); songs << song; } } if (!songs.isEmpty()) DeleteSongs(songs); }