Clementine-audio-player-Mac.../src/library/librarybackend.cpp

1232 lines
36 KiB
C++

/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "librarybackend.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QSettings>
#include <QVariant>
#include <QtDebug>
#include "core/application.h"
#include "core/database.h"
#include "core/scopedtransaction.h"
#include "core/tagreaderclient.h"
#include "core/utilities.h"
#include "libraryquery.h"
#include "smartplaylists/search.h"
#include "sqlrow.h"
const char* LibraryBackend::kSettingsGroup = "LibraryBackend";
const char* LibraryBackend::kNewScoreSql =
"case when playcount <= 0 then (%1 * 100 + score) / 2"
" else (score * (playcount + skipcount) + %1 * 100) / (playcount + "
"skipcount + 1)"
" end";
LibraryBackend::LibraryBackend(QObject* parent)
: LibraryBackendInterface(parent),
save_statistics_in_file_(false),
save_ratings_in_file_(false) {}
void LibraryBackend::Init(Database* db, const QString& songs_table,
const QString& fts_table) {
db_ = db;
songs_table_ = songs_table;
fts_table_ = fts_table;
}
void LibraryBackend::Init(Database* db, const QString& songs_table,
const QString& dirs_table,
const QString& subdirs_table,
const QString& fts_table) {
Init(db, songs_table, fts_table);
dirs_table_ = dirs_table;
subdirs_table_ = subdirs_table;
}
void LibraryBackend::LoadDirectoriesAsync() {
metaObject()->invokeMethod(this, "LoadDirectories", Qt::QueuedConnection);
}
void LibraryBackend::UpdateTotalSongCountAsync() {
metaObject()->invokeMethod(this, "UpdateTotalSongCount",
Qt::QueuedConnection);
}
void LibraryBackend::IncrementPlayCountAsync(int id) {
metaObject()->invokeMethod(this, "IncrementPlayCount", Qt::QueuedConnection,
Q_ARG(int, id));
}
void LibraryBackend::IncrementSkipCountAsync(int id, float progress) {
metaObject()->invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection,
Q_ARG(int, id), Q_ARG(float, progress));
}
void LibraryBackend::ResetStatisticsAsync(int id) {
metaObject()->invokeMethod(this, "ResetStatistics", Qt::QueuedConnection,
Q_ARG(int, id));
}
void LibraryBackend::UpdateSongRatingAsync(int id, float rating) {
metaObject()->invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection,
Q_ARG(int, id), Q_ARG(float, rating));
}
void LibraryBackend::UpdateSongsRatingAsync(const QList<int>& ids,
float rating) {
metaObject()->invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection,
Q_ARG(const QList<int>&, ids),
Q_ARG(float, rating));
}
void LibraryBackend::LoadDirectories() {
DirectoryList dirs = GetAllDirectories();
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
for (const Directory& dir : dirs) {
emit DirectoryDiscovered(dir, SubdirsInDirectory(dir.id, db));
}
}
void LibraryBackend::ChangeDirPath(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
QSqlQuery q(db);
q.prepare(
QString("UPDATE %1 SET path=:path WHERE ROWID=:id").arg(dirs_table_));
q.bindValue(":path", new_path);
q.bindValue(":id", id);
q.exec();
if (db_->CheckErrors(q)) return;
const QByteArray old_url = QUrl::fromLocalFile(old_path).toEncoded();
const QByteArray new_url = QUrl::fromLocalFile(new_path).toEncoded();
const int path_len = old_url.length();
// Do the subdirs table
q = QSqlQuery(db);
q.prepare(QString("UPDATE %1 SET path=:path || substr(path, %2)"
" WHERE directory=:id")
.arg(subdirs_table_)
.arg(path_len));
q.bindValue(":path", new_url);
q.bindValue(":id", id);
q.exec();
if (db_->CheckErrors(q)) return;
// Do the songs table
q = QSqlQuery(db);
q.prepare(QString("UPDATE %1 SET filename=:path || substr(filename, %2)"
" WHERE directory=:id")
.arg(songs_table_)
.arg(path_len));
q.bindValue(":path", new_url);
q.bindValue(":id", id);
q.exec();
if (db_->CheckErrors(q)) return;
t.Commit();
}
DirectoryList LibraryBackend::GetAllDirectories() {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
DirectoryList ret;
QSqlQuery q(db);
q.prepare(QString("SELECT ROWID, path FROM %1").arg(dirs_table_));
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
Directory dir;
dir.id = q.value(0).toInt();
dir.path = q.value(1).toString();
ret << dir;
}
return ret;
}
SubdirectoryList LibraryBackend::SubdirsInDirectory(int id) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db = db_->Connect();
return SubdirsInDirectory(id, db);
}
SubdirectoryList LibraryBackend::SubdirsInDirectory(int id, QSqlDatabase& db) {
QSqlQuery q(db);
q.prepare(QString("SELECT path, mtime FROM %1"
" WHERE directory = :dir")
.arg(subdirs_table_));
q.bindValue(":dir", id);
q.exec();
if (db_->CheckErrors(q)) return SubdirectoryList();
SubdirectoryList subdirs;
while (q.next()) {
Subdirectory subdir;
subdir.directory_id = id;
subdir.path = q.value(0).toString();
subdir.mtime = q.value(1).toUInt();
subdirs << subdir;
}
return subdirs;
}
void LibraryBackend::UpdateTotalSongCount() {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare(QString("SELECT COUNT(*) FROM %1 WHERE unavailable = 0")
.arg(songs_table_));
q.exec();
if (db_->CheckErrors(q)) return;
if (!q.next()) return;
emit TotalSongCountUpdated(q.value(0).toInt());
}
void LibraryBackend::AddDirectory(const QString& path) {
QString canonical_path = QFileInfo(path).canonicalFilePath();
QString db_path = canonical_path;
if (Application::kIsPortable && Utilities::UrlOnSameDriveAsClementine(
QUrl::fromLocalFile(canonical_path))) {
db_path = Utilities::GetRelativePathToClementineBin(db_path);
qLog(Debug) << "db_path" << db_path;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare(QString("INSERT INTO %1 (path, subdirs)"
" VALUES (:path, 1)")
.arg(dirs_table_));
q.bindValue(":path", db_path);
q.exec();
if (db_->CheckErrors(q)) return;
Directory dir;
dir.path = canonical_path;
dir.id = q.lastInsertId().toInt();
emit DirectoryDiscovered(dir, SubdirectoryList());
}
void LibraryBackend::RemoveDirectory(int dir_id) {
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
QSqlQuery q(db);
q.prepare(
QString("DELETE FROM %1 WHERE directory = :id").arg(subdirs_table_));
q.bindValue(":id", dir_id);
q.exec();
if (db_->CheckErrors(q)) return;
// Now remove the directory itself
q = QSqlQuery(db);
q.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(dirs_table_));
q.bindValue(":id", dir_id);
q.exec();
if (db_->CheckErrors(q)) return;
emit DirectoryDeleted(dir_id);
transaction.Commit();
}
SongList LibraryBackend::FindSongsInDirectory(int id) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec +
" FROM %1 WHERE directory = :directory")
.arg(songs_table_));
q.bindValue(":directory", id);
q.exec();
if (db_->CheckErrors(q)) return SongList();
SongList ret;
while (q.next()) {
Song song;
song.InitFromQuery(q, true);
ret << song;
}
return ret;
}
void LibraryBackend::SongPathChanged(const Song& song,
const QFileInfo& new_file) {
// Take a song and update its path
Song updated_song = song;
updated_song.InitFromFilePartial(new_file.absoluteFilePath());
SongList updated_songs;
updated_songs << updated_song;
AddOrUpdateSongs(updated_songs);
}
void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery find_query(db);
find_query.prepare(QString("SELECT ROWID FROM %1"
" WHERE directory = :id AND path = :path")
.arg(subdirs_table_));
QSqlQuery add_query(db);
add_query.prepare(QString("INSERT INTO %1 (directory, path, mtime)"
" VALUES (:id, :path, :mtime)")
.arg(subdirs_table_));
QSqlQuery update_query(db);
update_query.prepare(QString("UPDATE %1 SET mtime = :mtime"
" WHERE directory = :id AND path = :path")
.arg(subdirs_table_));
QSqlQuery delete_query(db);
delete_query.prepare(QString("DELETE FROM %1"
" WHERE directory = :id AND path = :path")
.arg(subdirs_table_));
ScopedTransaction transaction(&db);
for (const Subdirectory& subdir : subdirs) {
if (subdir.mtime == 0) {
// Delete the subdirectory
delete_query.bindValue(":id", subdir.directory_id);
delete_query.bindValue(":path", subdir.path);
delete_query.exec();
db_->CheckErrors(delete_query);
} else {
// See if this subdirectory already exists in the database
find_query.bindValue(":id", subdir.directory_id);
find_query.bindValue(":path", subdir.path);
find_query.exec();
if (db_->CheckErrors(find_query)) continue;
if (find_query.next()) {
update_query.bindValue(":mtime", subdir.mtime);
update_query.bindValue(":id", subdir.directory_id);
update_query.bindValue(":path", subdir.path);
update_query.exec();
db_->CheckErrors(update_query);
} else {
add_query.bindValue(":id", subdir.directory_id);
add_query.bindValue(":path", subdir.path);
add_query.bindValue(":mtime", subdir.mtime);
add_query.exec();
db_->CheckErrors(add_query);
}
}
}
transaction.Commit();
}
void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery check_dir(db);
check_dir.prepare(
QString("SELECT ROWID FROM %1 WHERE ROWID = :id").arg(dirs_table_));
QSqlQuery add_song(db);
add_song.prepare(QString("INSERT INTO %1 (" + Song::kColumnSpec +
")"
" VALUES (" +
Song::kBindSpec + ")")
.arg(songs_table_));
QSqlQuery update_song(db);
update_song.prepare(
QString("UPDATE %1 SET " + Song::kUpdateSpec + " WHERE ROWID = :id")
.arg(songs_table_));
QSqlQuery add_song_fts(db);
add_song_fts.prepare(QString("INSERT INTO %1 (ROWID, " +
Song::kFtsColumnSpec +
")"
" VALUES (:id, " +
Song::kFtsBindSpec + ")")
.arg(fts_table_));
QSqlQuery update_song_fts(db);
update_song_fts.prepare(
QString("UPDATE %1 SET " + Song::kFtsUpdateSpec + " WHERE ROWID = :id")
.arg(fts_table_));
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 LibraryWatcher is scanning it.
if (!dirs_table_.isEmpty()) {
check_dir.bindValue(":id", song.directory_id());
check_dir.exec();
if (db_->CheckErrors(check_dir)) continue;
if (!check_dir.next()) continue; // Directory didn't exist
}
if (song.id() == -1) {
// Create
// Insert the row and create a new ID
song.BindToQuery(&add_song);
add_song.exec();
if (db_->CheckErrors(add_song)) continue;
// Get the new ID
const int id = add_song.lastInsertId().toInt();
// Add to the FTS index
add_song_fts.bindValue(":id", id);
song.BindToFtsQuery(&add_song_fts);
add_song_fts.exec();
if (db_->CheckErrors(add_song_fts)) continue;
Song copy(song);
copy.set_id(id);
added_songs << copy;
} else {
// Get the previous song data first
Song old_song(GetSongById(song.id()));
if (!old_song.is_valid()) continue;
// Update
song.BindToQuery(&update_song);
update_song.bindValue(":id", song.id());
update_song.exec();
if (db_->CheckErrors(update_song)) continue;
song.BindToFtsQuery(&update_song_fts);
update_song_fts.bindValue(":id", song.id());
update_song_fts.exec();
if (db_->CheckErrors(update_song_fts)) continue;
deleted_songs << old_song;
added_songs << song;
}
}
transaction.Commit();
if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs);
if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs);
UpdateTotalSongCountAsync();
}
void LibraryBackend::UpdateMTimesOnly(const SongList& songs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id")
.arg(songs_table_));
ScopedTransaction transaction(&db);
for (const Song& song : songs) {
q.bindValue(":mtime", song.mtime());
q.bindValue(":id", song.id());
q.exec();
db_->CheckErrors(q);
}
transaction.Commit();
}
void LibraryBackend::DeleteSongs(const SongList& songs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery remove(db);
remove.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
QSqlQuery remove_fts(db);
remove_fts.prepare(
QString("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_));
ScopedTransaction transaction(&db);
for (const Song& song : songs) {
remove.bindValue(":id", song.id());
remove.exec();
db_->CheckErrors(remove);
remove_fts.bindValue(":id", song.id());
remove_fts.exec();
db_->CheckErrors(remove_fts);
}
transaction.Commit();
emit SongsDeleted(songs);
UpdateTotalSongCountAsync();
}
void LibraryBackend::MarkSongsUnavailable(const SongList& songs,
bool unavailable) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery remove(db);
remove.prepare(QString("UPDATE %1 SET unavailable = %2 WHERE ROWID = :id")
.arg(songs_table_)
.arg(int(unavailable)));
ScopedTransaction transaction(&db);
for (const Song& song : songs) {
remove.bindValue(":id", song.id());
remove.exec();
db_->CheckErrors(remove);
}
transaction.Commit();
emit SongsDeleted(songs);
UpdateTotalSongCountAsync();
}
QStringList LibraryBackend::GetAll(const QString& column,
const QueryOptions& opt) {
LibraryQuery query(opt);
query.SetColumnSpec("DISTINCT " + column);
query.AddCompilationRequirement(false);
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query)) return QStringList();
QStringList ret;
while (query.Next()) {
ret << query.Value(0).toString();
}
return ret;
}
QStringList LibraryBackend::GetAllArtists(const QueryOptions& opt) {
return GetAll("artist", opt);
}
QStringList LibraryBackend::GetAllArtistsWithAlbums(const QueryOptions& opt) {
// Albums with 'albumartist' field set:
LibraryQuery query(opt);
query.SetColumnSpec("DISTINCT albumartist");
query.AddCompilationRequirement(false);
query.AddWhere("album", "", "!=");
// Albums with no 'albumartist' (extract 'artist'):
LibraryQuery query2(opt);
query2.SetColumnSpec("DISTINCT artist");
query2.AddCompilationRequirement(false);
query2.AddWhere("album", "", "!=");
query2.AddWhere("albumartist", "", "=");
{
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query) || !ExecQuery(&query2)) {
return QStringList();
}
}
QSet<QString> artists;
while (query.Next()) {
artists << query.Value(0).toString();
}
while (query2.Next()) {
artists << query2.Value(0).toString();
}
return QStringList(artists.values());
}
LibraryBackend::AlbumList LibraryBackend::GetAllAlbums(
const QueryOptions& opt) {
return GetAlbums(QString(), QString(), false, opt);
}
LibraryBackend::AlbumList LibraryBackend::GetAlbumsByArtist(
const QString& artist, const QueryOptions& opt) {
return GetAlbums(artist, QString(), false, opt);
}
LibraryBackend::AlbumList LibraryBackend::GetAlbumsByAlbumArtist(
const QString& album_artist, const QueryOptions& opt) {
return GetAlbums(QString(), album_artist, false, opt);
}
SongList LibraryBackend::GetSongsByAlbum(const QString& album,
const QueryOptions& opt) {
LibraryQuery query(opt);
query.AddCompilationRequirement(false);
query.AddWhere("album", album);
return ExecLibraryQuery(&query);
}
SongList LibraryBackend::GetSongs(const QString& artist, const QString& album,
const QueryOptions& opt) {
LibraryQuery query(opt);
query.AddCompilationRequirement(false);
query.AddWhere("artist", artist);
query.AddWhere("album", album);
return ExecLibraryQuery(&query);
}
SongList LibraryBackend::ExecLibraryQuery(LibraryQuery* query) {
query->SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
QMutexLocker l(db_->Mutex());
if (!ExecQuery(query)) return SongList();
SongList ret;
while (query->Next()) {
Song song;
song.InitFromQuery(*query, true);
ret << song;
}
return ret;
}
Song LibraryBackend::GetSongById(int id) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
return GetSongById(id, db);
}
SongList LibraryBackend::GetSongsById(const QList<int>& ids) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QStringList str_ids;
for (int id : ids) {
str_ids << QString::number(id);
}
return GetSongsById(str_ids, db);
}
SongList LibraryBackend::GetSongsById(const QStringList& ids) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
return GetSongsById(ids, db);
}
SongList LibraryBackend::GetSongsByForeignId(const QStringList& ids,
const QString& table,
const QString& column) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QString in = ids.join(",");
QSqlQuery q(db);
q.prepare(
QString(
"SELECT %2.ROWID, " + Song::kColumnSpec +
", %2.%3"
" FROM %2, %1"
" WHERE %2.%3 IN (%4) AND %1.ROWID = %2.ROWID AND unavailable = 0")
.arg(songs_table_, table, column, in));
q.exec();
if (db_->CheckErrors(q)) return SongList();
QVector<Song> ret(ids.count());
while (q.next()) {
const QString foreign_id = q.value(Song::kColumns.count() + 1).toString();
const int index = ids.indexOf(foreign_id);
if (index == -1) continue;
ret[index].InitFromQuery(q, true);
}
return ret.toList();
}
Song LibraryBackend::GetSongById(int id, QSqlDatabase& db) {
SongList list = GetSongsById(QStringList() << QString::number(id), db);
if (list.isEmpty()) return Song();
return list.first();
}
SongList LibraryBackend::GetSongsById(const QStringList& ids,
QSqlDatabase& db) {
QString in = ids.join(",");
QSqlQuery q(db);
q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec +
" FROM %1"
" WHERE ROWID IN (%2)")
.arg(songs_table_, in));
q.exec();
if (db_->CheckErrors(q)) return SongList();
SongList ret;
while (q.next()) {
Song song;
song.InitFromQuery(q, true);
ret << song;
}
return ret;
}
Song LibraryBackend::GetSongByUrl(const QUrl& url, qint64 beginning) {
LibraryQuery query;
query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
query.AddWhere("filename", url.toEncoded());
query.AddWhere("beginning", beginning);
Song song;
if (ExecQuery(&query) && query.Next()) {
song.InitFromQuery(query, true);
}
return song;
}
SongList LibraryBackend::GetSongsByUrl(const QUrl& url) {
LibraryQuery query;
query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
query.AddWhere("filename", url.toEncoded());
SongList songlist;
if (ExecQuery(&query)) {
while (query.Next()) {
Song song;
song.InitFromQuery(query, true);
songlist << song;
}
}
return songlist;
}
LibraryBackend::AlbumList LibraryBackend::GetCompilationAlbums(
const QueryOptions& opt) {
return GetAlbums(QString(), QString(), true, opt);
}
SongList LibraryBackend::GetCompilationSongs(const QString& album,
const QueryOptions& opt) {
LibraryQuery query(opt);
query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
query.AddCompilationRequirement(true);
query.AddWhere("album", album);
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query)) return SongList();
SongList ret;
while (query.Next()) {
Song song;
song.InitFromQuery(query, true);
ret << song;
}
return ret;
}
void LibraryBackend::UpdateCompilations() {
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
QSqlQuery q(db);
q.prepare(QString("SELECT effective_albumartist, album, filename, sampler "
"FROM %1 WHERE unavailable = 0 ORDER BY album")
.arg(songs_table_));
q.exec();
if (db_->CheckErrors(q)) return;
QMap<QString, CompilationInfo> compilation_info;
while (q.next()) {
QString artist = q.value(0).toString();
QString album = q.value(1).toString();
QString filename = q.value(2).toString();
bool sampler = 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
QUrl url = QUrl::fromEncoded(filename.toUtf8());
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 (sampler)
++info.has_samplers;
else
++info.has_not_samplers;
}
// Now mark the songs that we think are in compilations
SongList deleted_songs;
SongList added_songs;
ScopedTransaction transaction(&db);
QMap<QString, CompilationInfo>::const_iterator it =
compilation_info.constBegin();
for (; it != compilation_info.constEnd(); ++it) {
const CompilationInfo& info = it.value();
// If there were more then 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_samplers > 0)
UpdateCompilations(db, deleted_songs, added_songs, url, true);
} else {
if (info.has_samplers > 0)
UpdateCompilations(db, deleted_songs, added_songs, url, false);
}
}
}
transaction.Commit();
if (!deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
}
}
void LibraryBackend::UpdateCompilations(const QSqlDatabase& db,
SongList& deleted_songs,
SongList& added_songs, const QUrl& url,
const bool sampler) {
QSqlQuery find_song(db);
find_song.prepare(QString("SELECT ROWID, " + Song::kColumnSpec +
" FROM %1"
" WHERE filename = :filename AND unavailable = 0")
.arg(songs_table_));
QSqlQuery update_song(db);
update_song.prepare(
QString("UPDATE %1"
" SET sampler = :sampler,"
" effective_compilation = ((compilation OR :sampler OR "
"forced_compilation_on) AND NOT forced_compilation_off) + 0"
" WHERE filename = :filename AND unavailable = 0")
.arg(songs_table_));
// Get song, so we can tell the model its updated
find_song.bindValue(":filename", url.toEncoded());
find_song.exec();
while (find_song.next()) {
Song song;
song.InitFromQuery(find_song, true);
deleted_songs << song;
song.set_sampler(sampler);
added_songs << song;
}
// Update the song
update_song.bindValue(":sampler", int(sampler));
update_song.bindValue(":filename", url.toEncoded());
update_song.exec();
db_->CheckErrors(update_song);
}
LibraryBackend::AlbumList LibraryBackend::GetAlbums(const QString& artist,
const QString& album_artist,
bool compilation,
const QueryOptions& opt) {
AlbumList ret;
LibraryQuery query(opt);
query.SetColumnSpec(
"album, artist, albumartist, compilation, sampler, art_automatic, "
"art_manual, filename");
query.SetOrderBy("album");
if (compilation) {
query.AddCompilationRequirement(true);
} else if (!album_artist.isNull()) {
query.AddCompilationRequirement(false);
query.AddWhere("albumartist", album_artist);
} else if (!artist.isNull()) {
query.AddCompilationRequirement(false);
query.AddWhere("artist", artist);
}
{
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query)) return ret;
}
QString last_album;
QString last_artist;
QString last_album_artist;
while (query.Next()) {
bool compilation = query.Value(3).toBool() || query.Value(4).toBool();
Album info;
info.artist = compilation ? QString() : query.Value(1).toString();
info.album_artist = compilation ? QString() : query.Value(2).toString();
info.album_name = query.Value(0).toString();
info.art_automatic = query.Value(5).toString();
info.art_manual = query.Value(6).toString();
info.first_url = QUrl::fromEncoded(query.Value(7).toByteArray());
if ((info.artist == last_artist ||
info.album_artist == last_album_artist) &&
info.album_name == last_album)
continue;
ret << info;
last_album = info.album_name;
last_artist = info.artist;
last_album_artist = info.album_artist;
}
return ret;
}
LibraryBackend::Album LibraryBackend::GetAlbumArt(const QString& artist,
const QString& albumartist,
const QString& album) {
Album ret;
ret.album_name = album;
ret.artist = artist;
ret.album_artist = albumartist;
LibraryQuery query = LibraryQuery(QueryOptions());
query.SetColumnSpec("art_automatic, art_manual, filename");
if (!albumartist.isEmpty()) {
query.AddWhere("albumartist", albumartist);
} else if (!artist.isEmpty()) {
query.AddWhere("artist", artist);
}
query.AddWhere("album", album);
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query)) return ret;
if (query.Next()) {
ret.art_automatic = query.Value(0).toString();
ret.art_manual = query.Value(1).toString();
ret.first_url = QUrl::fromEncoded(query.Value(2).toByteArray());
}
return ret;
}
void LibraryBackend::UpdateManualAlbumArtAsync(const QString& artist,
const QString& albumartist,
const QString& album,
const QString& art) {
metaObject()->invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection,
Q_ARG(QString, artist),
Q_ARG(QString, albumartist), Q_ARG(QString, album),
Q_ARG(QString, art));
}
void LibraryBackend::UpdateManualAlbumArt(const QString& artist,
const QString& albumartist,
const QString& album,
const QString& art) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
// Get the songs before they're updated
LibraryQuery query;
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("album", album);
if (!albumartist.isNull() && !albumartist.isEmpty()) {
query.AddWhere("albumartist", albumartist);
} else if (!artist.isNull()) {
query.AddWhere("artist", artist);
}
if (!ExecQuery(&query)) return;
SongList deleted_songs;
while (query.Next()) {
Song song;
song.InitFromQuery(query, true);
deleted_songs << song;
}
// Update the songs
QString sql(QString("UPDATE %1 SET art_manual = :art"
" WHERE album = :album AND unavailable = 0")
.arg(songs_table_));
if (!albumartist.isNull() && !albumartist.isEmpty()) {
sql += " AND albumartist = :albumartist";
} else if (!artist.isNull()) {
sql += " AND artist = :artist";
}
QSqlQuery q(db);
q.prepare(sql);
q.bindValue(":art", art);
q.bindValue(":album", album);
if (!albumartist.isNull() && !albumartist.isEmpty()) {
q.bindValue(":albumartist", albumartist);
} else if (!artist.isNull()) {
q.bindValue(":artist", artist);
}
q.exec();
db_->CheckErrors(q);
// Now get the updated songs
if (!ExecQuery(&query)) return;
SongList added_songs;
while (query.Next()) {
Song song;
song.InitFromQuery(query, true);
added_songs << song;
}
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
}
}
void LibraryBackend::ForceCompilation(const QString& album,
const QList<QString>& artists, 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
LibraryQuery query;
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("album", album);
if (!artist.isNull()) query.AddWhere("artist", artist);
if (!ExecQuery(&query)) return;
while (query.Next()) {
Song song;
song.InitFromQuery(query, true);
deleted_songs << song;
}
// Update the songs
QString sql(
QString(
"UPDATE %1 SET forced_compilation_on = :forced_compilation_on,"
" forced_compilation_off = :forced_compilation_off,"
" effective_compilation = ((compilation OR sampler OR "
":forced_compilation_on) AND NOT :forced_compilation_off) + 0"
" WHERE album = :album AND unavailable = 0")
.arg(songs_table_));
if (!artist.isEmpty()) sql += " AND artist = :artist";
QSqlQuery q(db);
q.prepare(sql);
q.bindValue(":forced_compilation_on", on ? 1 : 0);
q.bindValue(":forced_compilation_off", on ? 0 : 1);
q.bindValue(":album", album);
if (!artist.isEmpty()) q.bindValue(":artist", artist);
q.exec();
db_->CheckErrors(q);
// Now get the updated songs
if (!ExecQuery(&query)) return;
while (query.Next()) {
Song song;
song.InitFromQuery(query, true);
added_songs << song;
}
}
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
}
}
bool LibraryBackend::ExecQuery(LibraryQuery* q) {
return !db_->CheckErrors(q->Exec(db_->Connect(), songs_table_, fts_table_));
}
SongList LibraryBackend::FindSongs(const smart_playlists::Search& search) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
// Build the query
QString sql = search.ToSql(songs_table());
// Run the query
SongList ret;
QSqlQuery query(db);
query.prepare(sql);
query.exec();
if (db_->CheckErrors(query)) return ret;
// Read the results
while (query.next()) {
Song song;
song.InitFromQuery(query, true);
ret << song;
}
return ret;
}
SongList LibraryBackend::GetAllSongs() {
// Get all the songs!
return FindSongs(smart_playlists::Search(
smart_playlists::Search::Type_All, smart_playlists::Search::TermList(),
smart_playlists::Search::Sort_FieldAsc,
smart_playlists::SearchTerm::Field_Artist, -1));
}
void LibraryBackend::IncrementPlayCount(int id) {
if (id == -1) return;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET playcount = playcount + 1,"
" lastplayed = :now,"
" score = " +
QString(kNewScoreSql).arg("1.0") + " WHERE ROWID = :id")
.arg(songs_table_));
q.bindValue(":now", QDateTime::currentDateTime().toTime_t());
q.bindValue(":id", id);
q.exec();
if (db_->CheckErrors(q)) return;
Song new_song = GetSongById(id, db);
emit SongsStatisticsChanged(SongList() << new_song);
}
void LibraryBackend::IncrementSkipCount(int id, float progress) {
if (id == -1) return;
progress = qBound(0.0f, progress, 1.0f);
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET skipcount = skipcount + 1,"
" score = " +
QString(kNewScoreSql).arg(progress) + " WHERE ROWID = :id")
.arg(songs_table_));
q.bindValue(":id", id);
q.exec();
if (db_->CheckErrors(q)) return;
Song new_song = GetSongById(id, db);
emit SongsStatisticsChanged(SongList() << new_song);
}
void LibraryBackend::ResetStatistics(int id) {
if (id == -1) return;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0,"
" lastplayed = -1, score = 0"
" WHERE ROWID = :id")
.arg(songs_table_));
q.bindValue(":id", id);
q.exec();
if (db_->CheckErrors(q)) return;
Song new_song = GetSongById(id, db);
emit SongsStatisticsChanged(SongList() << new_song);
}
void LibraryBackend::UpdateSongRating(int id, float rating) {
if (id == -1) return;
QList<int> id_list;
id_list << id;
UpdateSongsRating(id_list, rating);
}
void LibraryBackend::UpdateSongsRating(const QList<int>& id_list,
float rating) {
if (id_list.isEmpty()) return;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QStringList id_str_list;
for (int i : id_list) {
id_str_list << QString::number(i);
}
QString ids = id_str_list.join(",");
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET rating = :rating"
" WHERE ROWID IN (%2)")
.arg(songs_table_, ids));
q.bindValue(":rating", rating);
q.exec();
if (db_->CheckErrors(q)) return;
SongList new_song_list = GetSongsById(id_str_list, db);
emit SongsRatingChanged(new_song_list);
}
void LibraryBackend::DeleteAll() {
{
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
QSqlQuery q("DELETE FROM " + songs_table_, db);
q.exec();
if (db_->CheckErrors(q)) return;
q = QSqlQuery("DELETE FROM " + fts_table_, db);
q.exec();
if (db_->CheckErrors(q)) return;
t.Commit();
}
emit DatabaseReset();
}