/* * Cantata * * Copyright (c) 2017-2022 Craig Drummond * * ---- * * 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 #include #include #include #include #include #include #include #include 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; ib.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.discb.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; i0) { 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; ibindValue(":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::getGenres() { DBUG; QList genres; if (!db) { return genres; } QMap > map; if (0!=currentVersion && db) { QString queryStr("distinct "); for (int i=0; i >::ConstIterator it=map.constBegin(); QMap >::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::getArtists(const QString &genre) { DBUG << genre; QList artists; if (!db) { return artists; } QMap sortMap; QMap 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::ConstIterator it=albumMap.constBegin(); QMap::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::getAlbums(const QString &artistId, const QString &genre, AlbumSort sort) { timer.start(); DBUG << artistId << genre; QList 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 entries; QMap > 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::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 >::iterator aIt = albumIdArtists.find(key); if (aIt == albumIdArtists.end()) { albumIdArtists.insert(key, QSet() << artist); } else { aIt.value().insert(artist); } } } QMap >::ConstIterator aIt = albumIdArtists.constBegin(); QMap >::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 LibraryDb::getTracks(const QString &artistId, const QString &albumId, const QString &genre, AlbumSort sort, bool useFilter, int maxTracks) { DBUG << artistId << albumId << genre << sort; QList 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 LibraryDb::getTracks(int rowFrom, int count) { QList 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 LibraryDb::songs(const QStringList &files, bool allowPlaylists) const { QList 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::getAlbumsWithArtistOrComposer(const QString &artist) { QList 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 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 LibraryDb::get(const QString &type) { if (detailsCache.contains(type)) { return detailsCache[type]; } QSet set; if (!db) { return set; } QStringList columns; bool isGenre="genre"==type; bool isAlbum="album"==type; if (isGenre) { for (int i=0; i &artists, QSet &albumArtists, QSet &composers, QSet &albums, QSet &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 replaceChars=QList() << 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 *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; iclose(); } 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"