Files
cantata/db/librarydb.cpp
Craig Drummond b6bd94c236 Update (c) year
2022-01-08 21:24:07 +00:00

1363 lines
41 KiB
C++

/*
* Cantata
*
* Copyright (c) 2017-2022 Craig Drummond <craig.p.drummond@gmail.com>
*
* ----
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "librarydb.h"
#include "support/utils.h"
#include <QCoreApplication>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QFile>
#include <QRegExp>
#include <QRandomGenerator>
#include <QDebug>
#include <algorithm>
static const int constSchemaVersion=5;
bool LibraryDb::dbgEnabled=false;
#define DBUG if (dbgEnabled) qWarning() << metaObject()->className() << __FUNCTION__ << (void *)this
const QLatin1String LibraryDb::constFileExt(".sql");
const QLatin1String LibraryDb::constNullGenre("-");
LibraryDb::AlbumSort LibraryDb::toAlbumSort(const QString &str)
{
for (int i=0; i<AS_Count; ++i) {
if (albumSortStr((AlbumSort)i)==str) {
return (AlbumSort)i;
}
}
return AS_AlArYr;
}
QString LibraryDb::albumSortStr(AlbumSort m)
{
switch(m) {
case AS_ArAlYr: return "artist";
default:
case AS_AlArYr: return "album";
case AS_YrAlAr: return "year";
case AS_Modified: return "modified";
// Re-added in 2.1
case AS_ArYrAl: return "aryral";
case AS_AlYrAr: return "alyral";
case AS_YrArAl: return "yraral";
}
}
static bool albumsSortAlArYr(const LibraryDb::Album &a, const LibraryDb::Album &b)
{
const QString &an=a.sort.isEmpty() ? a.name : a.sort;
const QString &bn=b.sort.isEmpty() ? b.name : b.sort;
int cmp=Utils::compare(an, bn);
if (cmp!=0) {
return cmp<0;
}
const QString &aa=a.artistSort.isEmpty() ? a.artist : a.artistSort;
const QString &ba=b.artistSort.isEmpty() ? b.artist : b.artistSort;
cmp=Utils::compare(aa, ba);
if (cmp!=0) {
return cmp<0;
}
return a.year<b.year;
}
static bool albumsSortArAlYr(const LibraryDb::Album &a, const LibraryDb::Album &b)
{
const QString &aa=a.artistSort.isEmpty() ? a.artist : a.artistSort;
const QString &ba=b.artistSort.isEmpty() ? b.artist : b.artistSort;
int cmp=Utils::compare(aa, ba);
if (cmp!=0) {
return cmp<0;
}
const QString &an=a.sort.isEmpty() ? a.name : a.sort;
const QString &bn=b.sort.isEmpty() ? b.name : b.sort;
cmp=Utils::compare(an, bn);
if (cmp!=0) {
return cmp<0;
}
return a.year<b.year;
}
static bool albumsSortAlYrAr(const LibraryDb::Album &a, const LibraryDb::Album &b)
{
const QString &an=a.sort.isEmpty() ? a.name : a.sort;
const QString &bn=b.sort.isEmpty() ? b.name : b.sort;
int cmp=Utils::compare(an, bn);
if (cmp!=0) {
return cmp<0;
}
if (a.year!=b.year) {
return a.year<b.year;
}
const QString &aa=a.artistSort.isEmpty() ? a.artist : a.artistSort;
const QString &ba=b.artistSort.isEmpty() ? b.artist : b.artistSort;
return Utils::compare(aa, ba)<0;
}
static bool albumsSortArYrAl(const LibraryDb::Album &a, const LibraryDb::Album &b)
{
const QString &aa=a.artistSort.isEmpty() ? a.artist : a.artistSort;
const QString &ba=b.artistSort.isEmpty() ? b.artist : b.artistSort;
int cmp=Utils::compare(aa, ba);
if (cmp!=0) {
return cmp<0;
}
if (a.year!=b.year) {
return a.year<b.year;
}
const QString &an=a.sort.isEmpty() ? a.name : a.sort;
const QString &bn=b.sort.isEmpty() ? b.name : b.sort;
return Utils::compare(an, bn)<0;
}
static bool albumsSortYrAlAr(const LibraryDb::Album &a, const LibraryDb::Album &b)
{
if (a.year!=b.year) {
return a.year<b.year;
}
const QString &an=a.sort.isEmpty() ? a.name : a.sort;
const QString &bn=b.sort.isEmpty() ? b.name : b.sort;
int cmp=Utils::compare(an, bn);
if (cmp!=0) {
return cmp<0;
}
const QString &aa=a.artistSort.isEmpty() ? a.artist : a.artistSort;
const QString &ba=b.artistSort.isEmpty() ? b.artist : b.artistSort;
return Utils::compare(aa, ba)<0;
}
static bool albumsSortYrArAl(const LibraryDb::Album &a, const LibraryDb::Album &b)
{
if (a.year!=b.year) {
return a.year<b.year;
}
const QString &aa=a.artistSort.isEmpty() ? a.artist : a.artistSort;
const QString &ba=b.artistSort.isEmpty() ? b.artist : b.artistSort;
int cmp=Utils::compare(aa, ba);
if (cmp!=0) {
return cmp<0;
}
const QString &an=a.sort.isEmpty() ? a.name : a.sort;
const QString &bn=b.sort.isEmpty() ? b.name : b.sort;
return Utils::compare(an, bn)<0;
}
static bool albumsSortModified(const LibraryDb::Album &a, const LibraryDb::Album &b)
{
if (a.lastModified==b.lastModified) {
return albumsSortAlArYr(a, b);
}
return a.lastModified>b.lastModified;
}
static bool songSort(const Song &a, const Song &b)
{
if (Song::SingleTracks==a.type && Song::SingleTracks==b.type) {
int cmp=Utils::compare(a.title, b.title);
if (0!=cmp) {
return cmp<0;
}
cmp=Utils::compare(a.artist, b.artist);
if (0!=cmp) {
return cmp<0;
}
}
if (a.disc!=b.disc) {
return a.disc<b.disc;
}
if (a.track!=b.track) {
return a.track<b.track;
}
if (a.displayYear()!=b.displayYear()) {
return a.displayYear()<b.displayYear();
}
int cmp=Utils::compare(a.title, b.title);
if (0!=cmp) {
return cmp<0;
}
cmp=Utils::compare(a.name(), b.name());
if (0!=cmp) {
return cmp<0;
}
cmp=Utils::compare(a.displayGenre(), b.displayGenre());
if (0!=cmp) {
return cmp<0;
}
if (a.time!=b.time) {
return a.time<b.time;
}
return a.file.compare(b.file)<0;
}
static bool songsSortAlAr(const Song &a, const Song &b)
{
const QString an=a.hasAlbumSort() ? a.albumSort() : a.album;
const QString bn=b.hasAlbumSort() ? b.albumSort() : b.album;
int cmp=Utils::compare(an, bn);
if (cmp!=0) {
return cmp<0;
}
const QString aa=a.hasArtistSort() ? a.artistSort() : a.albumArtist();
const QString ba=b.hasArtistSort() ? b.artistSort() : b.albumArtist();
cmp=Utils::compare(aa, ba);
if (cmp!=0) {
return cmp<0;
}
// if (a.displayYear()!=b.displayYear()) {
// return a.displayYear()<b.displayYear();
// }
return songSort(a, b);
}
static bool songsSortArAl(const Song &a, const Song &b)
{
const QString aa=a.hasArtistSort() ? a.artistSort() : a.albumArtist();
const QString ba=b.hasArtistSort() ? b.artistSort() : b.albumArtist();
int cmp=Utils::compare(aa, ba);
if (cmp!=0) {
return cmp<0;
}
const QString an=a.hasAlbumSort() ? a.albumSort() : a.album;
const QString bn=b.hasAlbumSort() ? b.albumSort() : b.album;
cmp=Utils::compare(an, bn);
if (cmp!=0) {
return cmp<0;
}
// if (a.displayYear()!=b.displayYear()) {
// return a.displayYear()<b.displayYear();
// }
return songSort(a, b);
}
//static bool songsSortAlYrAr(const Song &a, const Song &b)
//{
// const QString an=a.hasAlbumSort() ? a.albumSort() : a.album;
// const QString bn=b.hasAlbumSort() ? b.albumSort() : b.album;
// int cmp=Utils::compare(an, bn);
// if (cmp!=0) {
// return cmp<0;
// }
// if (a.displayYear()!=b.displayYear()) {
// return a.displayYear()<b.displayYear();
// }
// const QString aa=a.hasArtistSort() ? a.artistSort() : a.albumArtist();
// const QString ba=b.hasArtistSort() ? b.artistSort() : b.albumArtist();
// cmp=Utils::compare(aa, ba);
// if (cmp!=0) {
// return cmp<0;
// }
// return songSort(a, b);
//}
//static bool songsSortArYrAl(const Song &a, const Song &b)
//{
// const QString aa=a.hasArtistSort() ? a.artistSort() : a.albumArtist();
// const QString ba=b.hasArtistSort() ? b.artistSort() : b.albumArtist();
// int cmp=Utils::compare(aa, ba);
// if (cmp!=0) {
// return cmp<0;
// }
// if (a.displayYear()!=b.displayYear()) {
// return a.displayYear()<b.displayYear();
// }
// const QString an=a.hasAlbumSort() ? a.albumSort() : a.album;
// const QString bn=b.hasAlbumSort() ? b.albumSort() : b.album;
// cmp=Utils::compare(an, bn);
// if (cmp!=0) {
// return cmp<0;
// }
// return songSort(a, b);
//}
//static bool songsSortYrAlAr(const Song &a, const Song &b)
//{
// if (a.displayYear()!=b.displayYear()) {
// return a.displayYear()<b.displayYear();
// }
// const QString an=a.hasAlbumSort() ? a.albumSort() : a.album;
// const QString bn=b.hasAlbumSort() ? b.albumSort() : b.album;
// int cmp=Utils::compare(an, bn);
// if (cmp!=0) {
// return cmp<0;
// }
// const QString aa=a.hasArtistSort() ? a.artistSort() : a.albumArtist();
// const QString ba=b.hasArtistSort() ? b.artistSort() : b.albumArtist();
// cmp=Utils::compare(aa, ba);
// if (cmp!=0) {
// return cmp<0;
// }
// return songSort(a, b);
//}
//static bool songsSortYrArAl(const Song &a, const Song &b)
//{
// if (a.displayYear()!=b.displayYear()) {
// return a.displayYear()<b.displayYear();
// }
// const QString aa=a.hasArtistSort() ? a.artistSort() : a.albumArtist();
// const QString ba=b.hasArtistSort() ? b.artistSort() : b.albumArtist();
// int cmp=Utils::compare(aa, ba);
// if (cmp!=0) {
// return cmp<0;
// }
// const QString an=a.hasAlbumSort() ? a.albumSort() : a.album;
// const QString bn=b.hasAlbumSort() ? b.albumSort() : b.album;
// cmp=Utils::compare(an, bn);
// if (cmp!=0) {
// return cmp<0;
// }
// return songSort(a, b);
//}
//static bool songsSortModified(const Song &a, const Song &b)
//{
// if (a.lastModified==b.lastModified) {
// return songsSortAlArYr(a, b);
// }
// return a.lastModified>b.lastModified;
//}
static QString artistSort(const Song &s)
{
if (s.useComposer() && !s.composer().isEmpty()) {
return s.composer();
}
if (!s.artistSortString().isEmpty()) {
return s.artistSortString();
}
return Song::sortString(s.albumArtist());
}
static QString albumSort(const Song &s)
{
if (!s.albumSort().isEmpty()) {
return s.albumSort();
}
return Song::sortString(s.album);
}
// Code taken from Clementine's LibraryQuery
class SqlQuery
{
public:
SqlQuery(const QString &colSpec, QSqlDatabase &database)
: db(database)
, fts(false)
, columSpec(colSpec)
, limit(0)
{
}
void addWhere(const QString &column, const QVariant &value, const QString &op="=")
{
// ignore 'literal' for IN
if (!op.compare("IN", Qt::CaseInsensitive)) {
QStringList final;
for(const QString &singleValue: value.toStringList()) {
final.append("?");
boundValues << singleValue;
}
whereClauses << QString("%1 IN (" + final.join(",") + ")").arg(column);
} else {
// Do integers inline - sqlite seems to get confused when you pass integers
// to bound parameters
if (QVariant::Int==value.type()) {
whereClauses << QString("%1 %2 %3").arg(column, op, value.toString());
} else if ("genre"==column) {
QString clause("(");
for (int i=0; i<Song::constNumGenres; ++i) {
clause+=QString("%1 %2 ?").arg(column+QString::number(i+1), op);
if (i<(Song::constNumGenres-1)) {
clause+=" OR ";
}
boundValues << value;
}
clause+=")";
whereClauses << clause;
} else {
whereClauses << QString("%1 %2 ?").arg(column, op);
boundValues << value;
}
}
}
void setFilter(const QString &filter, const QString yearFilter)
{
if (!filter.isEmpty()) {
whereClauses << "songs_fts match ?";
boundValues << "\'"+filter+"\'";
fts=true;
}
if (!yearFilter.isEmpty()) {
whereClauses << yearFilter;
}
}
void setOrder(const QString &o)
{
order=o;
}
void setLimit(int l)
{
limit=l;
}
bool exec()
{
QString sql=fts
? QString("SELECT %1 FROM songs INNER JOIN songs_fts AS fts ON songs.ROWID = fts.ROWID").arg(columSpec)
: QString("SELECT %1 FROM songs").arg(columSpec);
if (!whereClauses.isEmpty()) {
sql+=" WHERE " + whereClauses.join(" AND ");
}
if (!order.isEmpty()) {
sql+=" ORDER by "+order;
}
if (limit>0) {
sql+=" LIMIT "+QString::number(limit);
}
query = QSqlQuery(db);
query.prepare(sql);
for (const QVariant &value: boundValues) {
query.addBindValue(value);
}
return query.exec();
}
bool next() { return query.next(); }
QString executedQuery() const { return query.executedQuery(); }
int size() const { return query.size(); }
QVariant value(int col) const { return query.value(col); }
const QSqlQuery & realQuery() const { return query; }
private:
QSqlDatabase &db;
QSqlQuery query;
bool fts;
QString columSpec;
QStringList whereClauses;
QVariantList boundValues;
QString order;
int limit;
};
LibraryDb::LibraryDb(QObject *p, const QString &name)
: QObject(p)
, dbName(name)
, currentVersion(0)
, newVersion(0)
, db(nullptr)
, insertSongQuery(nullptr)
{
DBUG;
}
LibraryDb::~LibraryDb()
{
reset();
}
void LibraryDb::clear()
{
if (db) {
DBUG;
erase();
currentVersion=0;
init(dbFileName);
}
}
void LibraryDb::erase()
{
reset();
if (!dbFileName.isEmpty() && QFile::exists(dbFileName)) {
QFile::remove(dbFileName);
}
}
enum SongFields {
SF_file,
SF_artist ,
SF_artistId,
SF_albumArtist,
SF_artistSort,
SF_composer,
SF_album,
SF_albumId,
SF_albumSort,
SF_title,
SF_genre1,
SF_genre2,
SF_genre3,
SF_genre4,
SF_track,
SF_disc,
SF_time,
SF_year,
SF_origYear,
SF_type,
SF_lastModified
};
bool LibraryDb::init(const QString &dbFile)
{
if (dbFile!=dbFileName) {
reset();
dbFileName=dbFile;
}
if (db) {
return true;
}
DBUG << dbFile << dbName;
currentVersion=0;
db=new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", dbName.isEmpty() ? QLatin1String(QSqlDatabase::defaultConnection) : dbName));
if (!db || !db->isValid()) {
emit error(tr("Database error - please check Qt SQLite driver is installed"));
return false;
}
db->setDatabaseName(dbFile);
DBUG << (void *)db;
if (!db->open()) {
delete db;
db=nullptr;
DBUG << "Failed to open";
return false;
}
if (!createTable("versions(collection integer, schema integer)")) {
DBUG << "Failed to create versions table";
return false;
}
QSqlQuery query("select collection, schema from versions", *db);
int schemaVersion=0;
if (query.next()) {
currentVersion=query.value(0).toUInt();
schemaVersion=query.value(1).toUInt();
}
if (schemaVersion>0 && schemaVersion!=constSchemaVersion) {
DBUG << "Scheme version changed";
currentVersion=0;
erase();
return init(dbFile);
}
if (0==currentVersion || (schemaVersion>0 && schemaVersion!=constSchemaVersion)) {
QSqlQuery(*db).exec("delete from versions");
QSqlQuery(*db).exec("insert into versions (collection, schema) values(0, "+QString::number(constSchemaVersion)+")");
}
DBUG << "Current version" << currentVersion;
// NOTE: The order here MUST match SongFields enum above!!!
if (createTable("songs ("
"file text, "
"artist text, "
"artistId text, "
"albumArtist text, "
"artistSort text, "
"composer text, "
"album text, "
"albumId text, "
"albumSort text, "
"title text, "
"genre1 text, "
"genre2 text, "
"genre3 text, "
"genre4 text, "
"track integer, "
"disc integer, "
"time integer, "
"year integer, "
"origYear integer, "
"type integer, "
"lastModified integer, "
"primary key (file))")) {
QSqlQuery fts(*db);
if (!fts.exec("create virtual table if not exists songs_fts using fts4(fts_artist, fts_artistId, fts_album, fts_albumId, fts_title, tokenize=unicode61)")) {
DBUG << "Failed to create FTS table" << fts.lastError().text() << "trying again with simple tokenizer";
if (!fts.exec("create virtual table if not exists songs_fts using fts4(fts_artist, fts_artistId, fts_album, fts_albumId, fts_title, tokenize=simple)")) {
DBUG << "Failed to create FTS table" << fts.lastError().text();
}
}
} else {
DBUG << "Failed to create songs table";
return false;
}
emit libraryUpdated();
DBUG << "Created";
return true;
}
void LibraryDb::insertSong(const Song &s)
{
if (!db) {
return;
}
if (!insertSongQuery) {
insertSongQuery=new QSqlQuery(*db);
insertSongQuery->prepare("insert into songs(file, artist, artistId, albumArtist, artistSort, composer, album, albumId, albumSort, title, genre1, genre2, genre3, genre4, track, disc, time, year, origYear, type, lastModified) "
"values(:file, :artist, :artistId, :albumArtist, :artistSort, :composer, :album, :albumId, :albumSort, :title, :genre1, :genre2, :genre3, :genre4, :track, :disc, :time, :year, :origYear, :type, :lastModified)");
}
QString albumId=s.albumId();
insertSongQuery->bindValue(":file", s.file);
insertSongQuery->bindValue(":artist", s.artist);
insertSongQuery->bindValue(":artistId", s.albumArtistOrComposer());
insertSongQuery->bindValue(":albumArtist", s.albumartist);
insertSongQuery->bindValue(":artistSort", artistSort(s));
insertSongQuery->bindValue(":composer", s.composer());
insertSongQuery->bindValue(":album", s.album==albumId ? QString() : s.album);
insertSongQuery->bindValue(":albumId", albumId);
insertSongQuery->bindValue(":albumSort", albumSort(s));
insertSongQuery->bindValue(":title", s.title);
for (int i=0; i<Song::constNumGenres; ++i) {
insertSongQuery->bindValue(":genre"+QString::number(i+1), s.genres[i].isEmpty() ? constNullGenre : s.genres[i]);
}
insertSongQuery->bindValue(":track", s.track);
insertSongQuery->bindValue(":disc", s.disc);
insertSongQuery->bindValue(":time", s.time);
insertSongQuery->bindValue(":year", s.year);
insertSongQuery->bindValue(":origYear", s.origYear);
insertSongQuery->bindValue(":type", s.type);
insertSongQuery->bindValue(":lastModified", s.lastModified);
if (!insertSongQuery->exec()) {
qWarning() << "insert failed" << insertSongQuery->lastError().text() << newVersion << s.file;
}
}
QList<LibraryDb::Genre> LibraryDb::getGenres()
{
DBUG;
QList<LibraryDb::Genre> genres;
if (!db) {
return genres;
}
QMap<QString, QSet<QString> > map;
if (0!=currentVersion && db) {
QString queryStr("distinct ");
for (int i=0; i<Song::constNumGenres; ++i) {
queryStr+="genre"+QString::number(i+1)+", ";
}
queryStr+="artistId";
SqlQuery query(queryStr, *db);
query.setFilter(filter, yearFilter);
query.exec();
DBUG << query.executedQuery();
while (query.next()) {
for (int i=0; i<Song::constNumGenres; ++i) {
QString genre=query.value(i).toString();
if (!genre.isEmpty() && genre!=constNullGenre) {
map[genre].insert(query.value(Song::constNumGenres).toString());
}
}
}
}
QMap<QString, QSet<QString> >::ConstIterator it=map.constBegin();
QMap<QString, QSet<QString> >::ConstIterator end=map.constEnd();
for (; it!=end; ++it) {
DBUG << it.key();
genres.append(Genre(it.key(), it.value().size()));
}
std::sort(genres.begin(), genres.end());
return genres;
}
QList<LibraryDb::Artist> LibraryDb::getArtists(const QString &genre)
{
DBUG << genre;
QList<LibraryDb::Artist> artists;
if (!db) {
return artists;
}
QMap<QString, QString> sortMap;
QMap<QString, int> albumMap;
if (0!=currentVersion && db) {
SqlQuery query("distinct artistId, albumId, artistSort", *db);
query.setFilter(filter, yearFilter);
if (!genre.isEmpty()) {
query.addWhere("genre", genre);
} else if (!genreFilter.isEmpty()) {
query.addWhere("genre", genreFilter);
}
query.exec();
DBUG << query.executedQuery();
while (query.next()) {
QString artist=query.value(0).toString();
albumMap[artist]++;
sortMap[artist]=query.value(2).toString();
}
}
QMap<QString, int>::ConstIterator it=albumMap.constBegin();
QMap<QString, int>::ConstIterator end=albumMap.constEnd();
for (; it!=end; ++it) {
// DBUG << it.key();
artists.append(Artist(it.key(), sortMap[it.key()], it.value()));
}
std::sort(artists.begin(), artists.end());
return artists;
}
QList<LibraryDb::Album> LibraryDb::getAlbums(const QString &artistId, const QString &genre, AlbumSort sort)
{
timer.start();
DBUG << artistId << genre;
QList<Album> albums;
if (!db) {
return albums;
}
if (0!=currentVersion && db) {
bool wantModified=AS_Modified==sort;
bool wantArtist=artistId.isEmpty();
QString queryString="album, albumId, albumSort, artist, albumArtist, composer";
for (int i=0; i<Song::constNumGenres; ++i) {
queryString+=", genre"+QString::number(i+1);
}
queryString+=", type, year, origYear, time";
if (wantModified) {
queryString+=", lastModified";
}
if (wantArtist) {
queryString+=", artistId, artistSort";
}
SqlQuery query(queryString, *db);
query.setFilter(filter, yearFilter);
if (!artistId.isEmpty()) {
query.addWhere("artistId", artistId);
}
if (!genre.isEmpty()) {
query.addWhere("genre", genre);
} else if (!genreFilter.isEmpty()) {
query.addWhere("genre", genreFilter);
}
query.exec();
int count=0;
QMap<QString, Album> entries;
QMap<QString, QSet<QString> > albumIdArtists; // Map of albumId -> albumartists/composers
while (query.next()) {
count++;
int col=0;
QString album=query.value(col++).toString();
QString albumId=query.value(col++).toString();
QString albumSort=query.value(col++).toString();
Song s;
s.artist=query.value(col++).toString();
s.albumartist=query.value(col++).toString();
s.setComposer(query.value(col++).toString());
s.album=album.isEmpty() ? albumId : album;
for (int i=0; i<Song::constNumGenres; ++i) {
QString genre=query.value(col++).toString();
if (genre!=constNullGenre) {
s.addGenre(genre);
}
}
s.type=(Song::Type)query.value(col++).toInt();
s.year=query.value(col++).toInt();
s.origYear=query.value(col++).toInt();
if (Song::SingleTracks==s.type) {
s.album=Song::singleTracks();
s.albumartist=Song::variousArtists();
s.year = s.origYear = 0;
}
album=s.albumName();
int time=query.value(col++).toInt();
int lastModified=wantModified ? query.value(col++).toInt() : 0;
QString artist=wantArtist ? query.value(col++).toString() : QString();
QString artistSort=wantArtist ? query.value(col++).toString() : QString();
// If listing albums not filtered on artist, then if we have a unqique id for the album use that.
// This will allow us to grouup albums with different composers when the composer tweak is set
// Issue #1025
bool haveUniqueId=wantArtist && !albumId.isEmpty() && !album.isEmpty() && albumId!=album;
QString key=haveUniqueId ? albumId : ('{'+albumId+"}{"+(wantArtist ? artist : artistId)+'}');
QMap<QString, Album>::iterator it=entries.find(key);
if (it==entries.end()) {
entries.insert(key, Album(album.isEmpty() ? albumId : album, albumId, albumSort, artist, artistSort, s.displayYear(), 1, time, lastModified, haveUniqueId));
} else {
Album &al=it.value();
if (wantModified) {
al.lastModified=qMax(al.lastModified, lastModified);
}
al.year=qMax(al.year, (int)s.displayYear());
al.duration+=time;
al.trackCount++;
}
if (haveUniqueId) {
QMap<QString, QSet<QString> >::iterator aIt = albumIdArtists.find(key);
if (aIt == albumIdArtists.end()) {
albumIdArtists.insert(key, QSet<QString>() << artist);
} else {
aIt.value().insert(artist);
}
}
}
QMap<QString, QSet<QString> >::ConstIterator aIt = albumIdArtists.constBegin();
QMap<QString, QSet<QString> >::ConstIterator aEnd = albumIdArtists.constEnd();
for(; aIt!=aEnd; ++aIt) {
if (aIt.value().count()>1) {
QStringList artists = aIt.value().values();
artists.sort();
Album &al = entries.find(aIt.key()).value();
al.artist = artists.join(", ");
al.artistSort = QString();
}
}
albums=entries.values();
DBUG << count << albums.count();
}
DBUG << "After select" << timer.elapsed();
switch(sort) {
case AS_AlArYr:
std::sort(albums.begin(), albums.end(), albumsSortAlArYr);
break;
case AS_AlYrAr:
std::sort(albums.begin(), albums.end(), albumsSortAlYrAr);
break;
case AS_ArAlYr:
std::sort(albums.begin(), albums.end(), albumsSortArAlYr);
break;
case AS_ArYrAl:
std::sort(albums.begin(), albums.end(), albumsSortArYrAl);
break;
case AS_YrAlAr:
std::sort(albums.begin(), albums.end(), albumsSortYrAlAr);
break;
case AS_YrArAl:
std::sort(albums.begin(), albums.end(), albumsSortYrArAl);
break;
case AS_Modified:
std::sort(albums.begin(), albums.end(), albumsSortModified);
break;
default:
break;
}
DBUG << "After sort" << timer.elapsed();
return albums;
}
QList<Song> LibraryDb::getTracks(const QString &artistId, const QString &albumId, const QString &genre, AlbumSort sort, bool useFilter, int maxTracks)
{
DBUG << artistId << albumId << genre << sort;
QList<Song> songs;
if (!db) {
return songs;
}
if (0!=currentVersion && db) {
SqlQuery query("*", *db);
if (useFilter) {
query.setFilter(filter, yearFilter);
}
if (!artistId.isEmpty()) {
query.addWhere("artistId", artistId);
}
if (!albumId.isEmpty()) {
query.addWhere("albumId", albumId);
}
if (!genre.isEmpty()) {
query.addWhere("genre", genre);
} else if (useFilter && !genreFilter.isEmpty()) {
query.addWhere("genre", genreFilter);
}
if (maxTracks != 1) {
query.setLimit(maxTracks);
}
query.exec();
DBUG << query.executedQuery();
while (query.next()) {
songs.append(getSong(query.realQuery()));
}
}
switch(sort) {
case AS_AlArYr:
std::sort(songs.begin(), songs.end(), songsSortAlAr);
break;
case AS_ArAlYr:
std::sort(songs.begin(), songs.end(), songsSortArAl);
break;
// case AS_Year:
// std::sort(songs.begin(), songs.end(), songsSortYrAlAr);
// break;
// case AS_Modified:
// std::sort(songs.begin(), songs.end(), songsSortModified);
// break;
default:
std::sort(songs.begin(), songs.end(), songSort);
break;
}
return songs;
}
QList<Song> LibraryDb::getTracks(int rowFrom, int count)
{
QList<Song> songList;
if (db) {
SqlQuery query("*", *db);
query.addWhere("rowid", rowFrom, ">");
query.addWhere("rowid", rowFrom+count, "<=");
query.addWhere("type", 0);
query.exec();
DBUG << query.executedQuery();
while (query.next()) {
songList.append(getSong(query.realQuery()));
}
}
return songList;
}
int LibraryDb::trackCount()
{
if (!db) {
return 0;
}
SqlQuery query("(count())", *db);
query.addWhere("type", 0);
query.exec();
DBUG << query.executedQuery();
int numTracks=query.next() ? query.value(0).toInt() : 0;
DBUG << numTracks;
return numTracks;
}
QList<Song> LibraryDb::songs(const QStringList &files, bool allowPlaylists) const
{
QList<Song> songList;
if (0!=currentVersion && db) {
for (const QString &f: files) {
SqlQuery query("*", *db);
query.addWhere("file", f);
query.exec();
DBUG << query.executedQuery();
if (query.next()) {
Song song=getSong(query.realQuery());
if (allowPlaylists || Song::Playlist!=song.type) {
songList.append(song);
}
}
}
}
return songList;
}
QList<LibraryDb::Album> LibraryDb::getAlbumsWithArtistOrComposer(const QString &artist)
{
QList<LibraryDb::Album> albums;
if (0!=currentVersion && db) {
SqlQuery query("distinct album, albumId, albumSort", *db);
query.addWhere("artist", artist);
query.exec();
DBUG << query.executedQuery();
while (query.next()) {
QString album=query.value(0).toString();
QString albumId=query.value(1).toString();
albums.append(Album(album.isEmpty() ? albumId : album, albumId, query.value(2).toString(), artist));
}
}
if (albums.isEmpty()) {
// No artist albums? Try composer...
SqlQuery query("distinct album, albumId, albumSort", *db);
query.addWhere("composer", artist);
query.exec();
DBUG << query.executedQuery();
while (query.next()) {
QString album=query.value(0).toString();
QString albumId=query.value(1).toString();
albums.append(Album(album.isEmpty() ? albumId : album, albumId, query.value(2).toString(), artist));
}
}
std::sort(albums.begin(), albums.end(), albumsSortArAlYr);
return albums;
}
LibraryDb::Album LibraryDb::getRandomAlbum(const QString &genre, const QString &artist)
{
Album al;
if (0!=currentVersion && db) {
SqlQuery query("artistId, albumId", *db);
query.setOrder("random()");
query.setLimit(1);
if (!artist.isEmpty()) {
query.addWhere("artistId", artist);
}
if (!genre.isEmpty()) {
query.addWhere("genre", genre);
} else if (!genreFilter.isEmpty()) {
query.addWhere("genre", genreFilter);
}
if (!yearFilter.isEmpty()) {
query.addWhere("year", yearFilter);
}
query.exec();
DBUG << query.executedQuery();
if (query.next()) {
al.artist=query.value(0).toString();
al.id=query.value(1).toString();
}
}
return al;
}
LibraryDb::Album LibraryDb::getRandomAlbum(const QStringList &genres, const QStringList &artists)
{
if (genres.isEmpty() && artists.isEmpty()) {
return getRandomAlbum(QString(), QString());
}
QList<Album> albums;
for (const QString &genre: genres) {
albums.append(getRandomAlbum(genre, QString()));
}
for (const QString &artist: artists) {
albums.append(getRandomAlbum(QString(), artist));
}
if (albums.isEmpty()) {
return Album();
}
return albums.at(QRandomGenerator::global()->bounded(albums.count()));
}
QSet<QString> LibraryDb::get(const QString &type)
{
if (detailsCache.contains(type)) {
return detailsCache[type];
}
QSet<QString> set;
if (!db) {
return set;
}
QStringList columns;
bool isGenre="genre"==type;
bool isAlbum="album"==type;
if (isGenre) {
for (int i=0; i<Song::constNumGenres; ++i) {
columns << type+QString::number(i+1);
}
} else if (isAlbum) {
columns << type << "albumId";
} else {
columns << type;
}
for (const QString &col: columns) {
SqlQuery query("distinct "+col, *db);
query.exec();
DBUG << query.executedQuery();
while (query.next()) {
if (isGenre) {
for (int i=0; i<Song::constNumGenres; ++i) {
QString val=query.value(i).toString();
if (!val.isEmpty() && constNullGenre!=val) {
set.insert(val);
}
}
} else if (isAlbum) {
// For albums, we have album and albumId
// album is only stored if it is different to albumId - in this case albumId would be the musicbrainz id
QString val=query.value(0).toString();
if (val.isEmpty()) {
val=query.value(1).toString();
}
if (!val.isEmpty()) {
set.insert(val);
}
} else {
QString val=query.value(0).toString();
if (!val.isEmpty()) {
set.insert(val);
}
}
}
}
detailsCache[type]=set;
return set;
}
void LibraryDb::getDetails(QSet<QString> &artists, QSet<QString> &albumArtists, QSet<QString> &composers, QSet<QString> &albums, QSet<QString> &genres)
{
artists=get("artist");
albumArtists=get("albumArtist");
composers=get("composer");
albums=get("album");
genres=get("genre");
}
bool LibraryDb::songExists(const Song &song)
{
if (!db) {
return false;
}
SqlQuery query("file", *db);
query.addWhere("artistId", song.albumArtistOrComposer());
query.addWhere("albumId", song.albumId());
query.addWhere("title", song.title);
query.addWhere("track", song.track);
query.addWhere("disc", song.disc);
query.exec();
return query.next();
}
static const quint16 constMinYear=1500;
static const quint16 constMaxYear=2500; // 2500 (bit hopeful here :-) )
bool LibraryDb::setFilter(const QString &f, const QString &genre)
{
QString newFilter=f.trimmed().toLower();
QString year;
if (!f.isEmpty()) {
QStringList strings(newFilter.split(QRegExp("\\s+")));
static QList<QLatin1Char> replaceChars=QList<QLatin1Char>() << QLatin1Char('(') << QLatin1Char(')') << QLatin1Char('"')
<< QLatin1Char(':') << QLatin1Char('-') << QLatin1Char('#');
QStringList tokens;
for (QString str: strings) {
if (str.startsWith('#')) {
QStringList parts=str.mid(1).split('-');
if (1==parts.length()) {
int val=parts.at(0).simplified().toUInt();
if (val>=constMinYear && val<=constMaxYear) {
if (Song::useOriginalYear()) {
year = QString("( (origYear = %1) OR (origYear = 0 AND year = %1) )").arg(val);
} else {
year = QString("year = %1").arg(val);
}
continue;
}
} else if (2==parts.length()) {
int from=parts.at(0).simplified().toUInt();
int to=parts.at(1).simplified().toUInt();
if (from>=constMinYear && from<=constMaxYear && to>=constMinYear && to<=constMaxYear) {
if (Song::useOriginalYear()) {
year = QString("( (origYear >= %1 AND origYear <= %2) OR (origYear = 0 AND year >= %1 AND year <= %2))")
.arg(qMin(from, to)).arg(qMax(from, to));
} else {
year = QString("year >= %1 AND year <= %2").arg(qMin(from, to)).arg(qMax(from, to));
}
continue;
}
}
}
for (const QLatin1Char ch: replaceChars) {
str.replace(ch, '?');
}
if (str.length()>0) {
tokens.append(str+QLatin1String("*"));
}
}
newFilter=tokens.join(" ");
DBUG << newFilter;
}
bool modified=newFilter!=filter || genre!=genreFilter || year!=yearFilter;
filter=newFilter;
genreFilter=genre;
yearFilter=year;
return modified;
}
void LibraryDb::updateStarted(time_t ver)
{
DBUG << (void *)db;
if (!db) {
return;
}
newVersion=ver;
timer.start();
db->transaction();
if (currentVersion>0) {
clearSongs(false);
}
}
void LibraryDb::insertSongs(QList<Song> *songs)
{
DBUG << (int)(songs ? songs->size() : -1);
if (!songs) {
return;
}
for (const Song &s: *songs) {
insertSong(s);
}
delete songs;
}
void LibraryDb::updateFinished()
{
if (!db) {
return;
}
DBUG << timer.elapsed();
DBUG << "update fts" << timer.elapsed();
QSqlQuery(*db).exec("insert into songs_fts(fts_artist, fts_artistId, fts_album, fts_albumId, fts_title) "
"select artist, artistId, album, albumId, title from songs");
QSqlQuery(*db).exec("update versions set collection ="+QString::number(newVersion));
DBUG << "commit" << timer.elapsed();
db->commit();
currentVersion=newVersion;
DBUG << "complete" << timer.elapsed();
emit libraryUpdated();
}
void LibraryDb::abortUpdate()
{
if (db) {
db->rollback();
}
}
bool LibraryDb::createTable(const QString &q)
{
if (!db) {
return false;
}
QSqlQuery query(*db);
if (!query.exec("create table if not exists "+q)) {
qWarning() << "Failed to create table" << query.lastError().text();
return false;
}
return true;
}
Song LibraryDb::getSong(const QSqlQuery &query)
{
Song s;
s.file=query.value(SF_file).toString();
s.artist=query.value(SF_artist).toString();
s.albumartist=query.value(SF_albumArtist).toString();
s.setComposer(query.value(SF_composer).toString());
s.album=query.value(SF_album).toString();
QString val=query.value(SF_albumId).toString();
if (s.album.isEmpty()) {
s.album=val;
val=QString();
}
if (!val.isEmpty() && val!=s.album) {
s.setMbAlbumId(val);
}
s.title=query.value(SF_title).toString();
for (int i=SF_genre1; i<SF_genre1+Song::constNumGenres; ++i) {
QString genre=query.value(i).toString();
if (genre!=constNullGenre) {
s.addGenre(genre);
}
}
s.track=query.value(SF_track).toUInt();
s.disc=query.value(SF_disc).toUInt();
s.time=query.value(SF_time).toUInt();
s.year=query.value(SF_year).toUInt();
s.origYear=query.value(SF_origYear).toUInt();
s.type=(Song::Type)query.value(SF_type).toUInt();
val=query.value(SF_artistSort).toString();
if (!val.isEmpty() && val!=s.albumArtist()) {
s.setArtistSort(val);
}
val=query.value(SF_albumSort).toString();
if (!val.isEmpty() && val!=s.album) {
s.setAlbumSort(val);
}
s.lastModified=query.value(SF_lastModified).toUInt();
return s;
}
void LibraryDb::reset()
{
bool removeDb=nullptr!=db;
delete insertSongQuery;
if (db) {
db->close();
}
delete db;
insertSongQuery=nullptr;
db=nullptr;
if (removeDb) {
QSqlDatabase::removeDatabase(dbName);
}
}
void LibraryDb::clearSongs(bool startTransaction)
{
if (!db) {
return;
}
if (startTransaction) {
db->transaction();
}
QSqlQuery(*db).exec("delete from songs");
QSqlQuery(*db).exec("delete from songs_fts");
detailsCache.clear();
if (startTransaction) {
db->commit();
}
}
#include "moc_librarydb.cpp"