2018-02-27 18:06:05 +01:00
/*
* Strawberry Music Player
* This file was part of Clementine .
* Copyright 2010 , David Sansome < me @ davidsansome . com >
*
* Strawberry is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* Strawberry is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with Strawberry . If not , see < http : //www.gnu.org/licenses/>.
*
*/
# include "config.h"
2018-05-01 00:41:33 +02:00
# include <QtGlobal>
# include <QObject>
# include <QMutex>
# include <QSet>
# include <QMap>
# include <QByteArray>
2018-02-27 18:06:05 +01:00
# include <QFileInfo>
2018-05-01 00:41:33 +02:00
# include <QDateTime>
2018-02-27 18:06:05 +01:00
# include <QVariant>
2018-05-01 00:41:33 +02:00
# include <QString>
# include <QStringList>
# include <QUrl>
# include <QSqlDatabase>
# include <QSqlQuery>
2018-02-27 18:06:05 +01:00
# include <QtDebug>
# include "core/application.h"
# include "core/database.h"
2018-05-01 00:41:33 +02:00
# include "core/logging.h"
2018-02-27 18:06:05 +01:00
# include "core/scopedtransaction.h"
# include "core/utilities.h"
2018-05-01 00:41:33 +02:00
# include "directory.h"
# include "collectionbackend.h"
# include "collectionquery.h"
# include "sqlrow.h"
2018-02-27 18:06:05 +01:00
const char * CollectionBackend : : kSettingsGroup = " Collection " ;
CollectionBackend : : CollectionBackend ( QObject * parent )
: CollectionBackendInterface ( parent )
{ }
void CollectionBackend : : Init ( Database * db , const QString & songs_table , const QString & dirs_table , const QString & subdirs_table , const QString & fts_table ) {
db_ = db ;
songs_table_ = songs_table ;
dirs_table_ = dirs_table ;
subdirs_table_ = subdirs_table ;
fts_table_ = fts_table ;
}
void CollectionBackend : : LoadDirectoriesAsync ( ) {
metaObject ( ) - > invokeMethod ( this , " LoadDirectories " , Qt : : QueuedConnection ) ;
}
void CollectionBackend : : UpdateTotalSongCountAsync ( ) {
metaObject ( ) - > invokeMethod ( this , " UpdateTotalSongCount " , Qt : : QueuedConnection ) ;
}
void CollectionBackend : : UpdateTotalArtistCountAsync ( ) {
metaObject ( ) - > invokeMethod ( this , " UpdateTotalArtistCount " , Qt : : QueuedConnection ) ;
}
void CollectionBackend : : UpdateTotalAlbumCountAsync ( ) {
metaObject ( ) - > invokeMethod ( this , " UpdateTotalAlbumCount " , Qt : : QueuedConnection ) ;
}
void CollectionBackend : : IncrementPlayCountAsync ( int id ) {
metaObject ( ) - > invokeMethod ( this , " IncrementPlayCount " , Qt : : QueuedConnection , Q_ARG ( int , id ) ) ;
}
void CollectionBackend : : IncrementSkipCountAsync ( int id , float progress ) {
metaObject ( ) - > invokeMethod ( this , " IncrementSkipCount " , Qt : : QueuedConnection , Q_ARG ( int , id ) , Q_ARG ( float , progress ) ) ;
}
void CollectionBackend : : ResetStatisticsAsync ( int id ) {
metaObject ( ) - > invokeMethod ( this , " ResetStatistics " , Qt : : QueuedConnection , Q_ARG ( int , id ) ) ;
}
void CollectionBackend : : 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 CollectionBackend : : 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 CollectionBackend : : 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 CollectionBackend : : SubdirsInDirectory ( int id ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db = db_ - > Connect ( ) ;
return SubdirsInDirectory ( id , db ) ;
}
SubdirectoryList CollectionBackend : : SubdirsInDirectory ( int id , QSqlDatabase & db ) {
QSqlQuery q ( db ) ;
q . prepare ( QString ( " SELECT path, mtime FROM %1 WHERE directory_id = :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 CollectionBackend : : 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 CollectionBackend : : UpdateTotalArtistCount ( ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
QSqlQuery q ( db ) ;
q . prepare ( QString ( " select COUNT(distinct artist) from %1 WHERE unavailable = 0 " ) . arg ( songs_table_ ) ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return ;
if ( ! q . next ( ) ) return ;
//qLog(Debug) << "TotalArtist: " << q.value(0).toInt();
emit TotalArtistCountUpdated ( q . value ( 0 ) . toInt ( ) ) ;
}
void CollectionBackend : : UpdateTotalAlbumCount ( ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
QSqlQuery q ( db ) ;
q . prepare ( QString ( " select COUNT(distinct album) from %1 WHERE unavailable = 0 " ) . arg ( songs_table_ ) ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return ;
if ( ! q . next ( ) ) return ;
//qLog(Debug) << "TotalAlbum: " << q.value(0).toInt();
emit TotalAlbumCountUpdated ( q . value ( 0 ) . toInt ( ) ) ;
}
void CollectionBackend : : AddDirectory ( const QString & path ) {
QString canonical_path = QFileInfo ( path ) . canonicalFilePath ( ) ;
QString db_path = canonical_path ;
if ( Application : : kIsPortable & & Utilities : : UrlOnSameDriveAsStrawberry ( QUrl : : fromLocalFile ( canonical_path ) ) ) {
db_path = Utilities : : GetRelativePathToStrawberryBin ( QUrl : : fromLocalFile ( db_path ) ) . toLocalFile ( ) ;
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 ( ) ) ;
2018-05-01 00:41:33 +02:00
2018-02-27 18:06:05 +01:00
}
void CollectionBackend : : RemoveDirectory ( const Directory & 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
QSqlQuery q ( db ) ;
q . prepare ( QString ( " DELETE FROM %1 WHERE directory_id = :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 ) ;
transaction . Commit ( ) ;
2018-05-01 00:41:33 +02:00
2018-02-27 18:06:05 +01:00
}
SongList CollectionBackend : : 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_id = :directory_id " ) . arg ( songs_table_ ) ) ;
q . bindValue ( " :directory_id " , id ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return SongList ( ) ;
SongList ret ;
while ( q . next ( ) ) {
Song song ;
song . InitFromQuery ( q , true ) ;
ret < < song ;
}
return ret ;
2018-05-01 00:41:33 +02:00
2018-02-27 18:06:05 +01:00
}
void CollectionBackend : : 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 = :id AND path = :path " ) . arg ( subdirs_table_ ) ) ;
QSqlQuery add_query ( db ) ;
add_query . prepare ( QString ( " INSERT INTO %1 (directory_id, 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 = :id AND path = :path " ) . arg ( subdirs_table_ ) ) ;
QSqlQuery delete_query ( db ) ;
delete_query . prepare ( QString ( " DELETE FROM %1 WHERE directory_id = :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 CollectionBackend : : 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
2018-05-01 00:41:33 +02:00
// This is to fix a possible race condition when a directory is removed while CollectionWatcher is scanning it.
2018-02-27 18:06:05 +01:00
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 ( ) ;
UpdateTotalArtistCountAsync ( ) ;
UpdateTotalAlbumCountAsync ( ) ;
}
void CollectionBackend : : 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 CollectionBackend : : 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 ( ) ;
UpdateTotalArtistCountAsync ( ) ;
UpdateTotalAlbumCountAsync ( ) ;
}
void CollectionBackend : : 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 ( ) ;
UpdateTotalArtistCountAsync ( ) ;
UpdateTotalAlbumCountAsync ( ) ;
}
QStringList CollectionBackend : : GetAll ( const QString & column , const QueryOptions & opt ) {
CollectionQuery 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 CollectionBackend : : GetAllArtists ( const QueryOptions & opt ) {
return GetAll ( " artist " , opt ) ;
}
QStringList CollectionBackend : : GetAllArtistsWithAlbums ( const QueryOptions & opt ) {
// Albums with 'albumartist' field set:
CollectionQuery query ( opt ) ;
// query.SetColumnSpec("DISTINCT artist");
query . SetColumnSpec ( " DISTINCT albumartist " ) ;
query . AddCompilationRequirement ( false ) ;
query . AddWhere ( " album " , " " , " != " ) ;
// Albums with no 'albumartist' (extract 'artist'):
CollectionQuery 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 ( ) ;
}
}
// QStringList ret;
QSet < QString > artists ;
while ( query . Next ( ) ) {
//ret << query.Value(0).toString();
artists < < query . Value ( 0 ) . toString ( ) ;
}
while ( query2 . Next ( ) ) {
artists < < query2 . Value ( 0 ) . toString ( ) ;
}
// return ret;
return QStringList ( artists . toList ( ) ) ;
}
CollectionBackend : : AlbumList CollectionBackend : : GetAllAlbums ( const QueryOptions & opt ) {
return GetAlbums ( QString ( ) , QString ( ) , false , opt ) ;
}
CollectionBackend : : AlbumList CollectionBackend : : GetAlbumsByArtist ( const QString & artist , const QueryOptions & opt ) {
return GetAlbums ( artist , QString ( ) , false , opt ) ;
}
CollectionBackend : : AlbumList CollectionBackend : : GetAlbumsByAlbumArtist (
const QString & album_artist , const QueryOptions & opt ) {
return GetAlbums ( QString ( ) , album_artist , false , opt ) ;
}
SongList CollectionBackend : : GetSongsByAlbum ( const QString & album , const QueryOptions & opt ) {
CollectionQuery query ( opt ) ;
query . AddCompilationRequirement ( false ) ;
query . AddWhere ( " album " , album ) ;
return ExecCollectionQuery ( & query ) ;
}
SongList CollectionBackend : : GetSongs ( const QString & artist , const QString & album , const QueryOptions & opt ) {
CollectionQuery query ( opt ) ;
query . AddCompilationRequirement ( false ) ;
query . AddWhere ( " artist " , artist ) ;
query . AddWhere ( " album " , album ) ;
return ExecCollectionQuery ( & query ) ;
}
SongList CollectionBackend : : ExecCollectionQuery ( CollectionQuery * 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 CollectionBackend : : GetSongById ( int id ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
return GetSongById ( id , db ) ;
}
SongList CollectionBackend : : 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 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 ( " , " ) ;
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 CollectionBackend : : GetSongById ( 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 ( " , " ) ;
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 CollectionBackend : : GetSongByUrl ( const QUrl & url , qint64 beginning ) {
CollectionQuery 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 CollectionBackend : : GetSongsByUrl ( const QUrl & url ) {
CollectionQuery 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 ;
}
CollectionBackend : : AlbumList CollectionBackend : : GetCompilationAlbums ( const QueryOptions & opt ) {
return GetAlbums ( QString ( ) , QString ( ) , true , opt ) ;
}
SongList CollectionBackend : : GetCompilationSongs ( const QString & album , const QueryOptions & opt ) {
CollectionQuery 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 CollectionBackend : : UpdateCompilations ( ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
2018-05-01 00:41:33 +02:00
// Look for albums that have songs by more than one 'effective album artist' in the same directory
2018-02-27 18:06:05 +01:00
QSqlQuery q ( db ) ;
q . prepare ( QString ( " SELECT effective_albumartist, album, filename, compilation_detected 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 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
int last_separator = filename . lastIndexOf ( ' / ' ) ;
if ( last_separator = = - 1 ) continue ;
CompilationInfo & info = compilation_info [ album ] ;
info . artists . insert ( artist ) ;
info . directories . insert ( filename . left ( last_separator ) ) ;
if ( compilation_detected ) info . has_compilation_detected = true ;
else info . has_not_compilation_detected = true ;
}
// Now mark the songs that we think are in compilations
QSqlQuery update ( db ) ;
update . prepare ( QString ( " UPDATE %1 SET compilation_detected = :compilation_detected, compilation_effective = ((compilation OR :compilation_detected OR compilation_on) AND NOT compilation_off) + 0 WHERE album = :album AND unavailable = 0 " ) . arg ( songs_table_ ) ) ;
QSqlQuery find_songs ( db ) ;
find_songs . prepare ( QString ( " SELECT ROWID, " + Song : : kColumnSpec + " FROM %1 WHERE album = :album AND compilation_detected = :compilation_detected AND unavailable = 0 " ) . arg ( songs_table_ ) ) ;
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 ( ) ;
QString album ( it . key ( ) ) ;
// If there were more 'effective album artists' than there were directories for this album then it's a compilation.
if ( info . artists . count ( ) > info . directories . count ( ) ) {
if ( info . has_not_compilation_detected )
UpdateCompilations ( find_songs , update , deleted_songs , added_songs , album , 1 ) ;
}
else {
if ( info . has_compilation_detected )
UpdateCompilations ( find_songs , update , deleted_songs , added_songs , album , 0 ) ;
}
}
transaction . Commit ( ) ;
if ( ! deleted_songs . isEmpty ( ) ) {
emit SongsDeleted ( deleted_songs ) ;
emit SongsDiscovered ( added_songs ) ;
}
}
void CollectionBackend : : UpdateCompilations ( QSqlQuery & find_songs , QSqlQuery & update , SongList & deleted_songs , SongList & added_songs , const QString & album , int compilation_detected ) {
2018-05-01 00:41:33 +02:00
// Get songs that were already in that album, so we can tell the model they've been updated
2018-02-27 18:06:05 +01:00
find_songs . bindValue ( " :album " , album ) ;
find_songs . bindValue ( " :compilation_detected " , int ( ! compilation_detected ) ) ;
find_songs . exec ( ) ;
while ( find_songs . next ( ) ) {
Song song ;
song . InitFromQuery ( find_songs , true ) ;
deleted_songs < < song ;
song . set_compilation_detected ( true ) ;
added_songs < < song ;
}
// Mark this album
update . bindValue ( " :compilation_detected " , compilation_detected ) ;
update . bindValue ( " :album " , album ) ;
update . exec ( ) ;
db_ - > CheckErrors ( update ) ;
}
CollectionBackend : : AlbumList CollectionBackend : : GetAlbums ( const QString & artist , const QString & album_artist , bool compilation , const QueryOptions & opt ) {
AlbumList ret ;
CollectionQuery query ( opt ) ;
query . SetColumnSpec ( " album, artist, albumartist, compilation, compilation_detected, art_automatic, art_manual, filename " ) ;
query . SetOrderBy ( " album " ) ;
if ( compilation ) {
query . AddCompilationRequirement ( true ) ;
}
2018-03-04 03:10:07 +01:00
else if ( ! album_artist . isNull ( ) & & ! album_artist . isEmpty ( ) ) {
2018-02-27 18:06:05 +01:00
query . AddCompilationRequirement ( false ) ;
query . AddWhere ( " albumartist " , album_artist ) ;
}
2018-03-04 03:10:07 +01:00
else if ( ! artist . isNull ( ) & & ! artist . isEmpty ( ) ) {
2018-02-27 18:06:05 +01:00
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 ;
}
CollectionBackend : : Album CollectionBackend : : GetAlbumArt ( const QString & artist , const QString & albumartist , const QString & album ) {
Album ret ;
ret . album_name = album ;
ret . artist = artist ;
ret . album_artist = albumartist ;
CollectionQuery query = CollectionQuery ( 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 CollectionBackend : : 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 CollectionBackend : : 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
CollectionQuery query ;
query . SetColumnSpec ( " ROWID, " + Song : : kColumnSpec ) ;
query . AddWhere ( " album " , album ) ;
2018-03-04 03:10:07 +01:00
if ( ! albumartist . isNull ( ) & & ! albumartist . isEmpty ( ) ) {
2018-02-27 18:06:05 +01:00
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_ ) ) ;
2018-03-04 03:10:07 +01:00
if ( ! albumartist . isNull ( ) & & ! albumartist . isEmpty ( ) ) {
2018-02-27 18:06:05 +01:00
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 ) ;
2018-03-04 03:10:07 +01:00
if ( ! albumartist . isNull ( ) & & ! albumartist . isEmpty ( ) ) {
2018-02-27 18:06:05 +01:00
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 CollectionBackend : : 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
CollectionQuery query ;
query . SetColumnSpec ( " ROWID, " + Song : : kColumnSpec ) ;
query . AddWhere ( " album " , album ) ;
2018-03-04 03:10:07 +01:00
if ( ! artist . isNull ( ) & & ! artist . isEmpty ( ) ) query . AddWhere ( " artist " , artist ) ;
2018-02-27 18:06:05 +01:00
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 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 + = " AND artist = :artist " ;
QSqlQuery q ( db ) ;
q . prepare ( sql ) ;
q . bindValue ( " :compilation_on " , on ? 1 : 0 ) ;
q . bindValue ( " :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 CollectionBackend : : ExecQuery ( CollectionQuery * q ) {
return ! db_ - > CheckErrors ( q - > Exec ( db_ - > Connect ( ) , songs_table_ , fts_table_ ) ) ;
}
void CollectionBackend : : 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 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 ) ;
}
void CollectionBackend : : 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 WHERE ROWID = :id " ) . arg ( songs_table_ ) ) ;
q . bindValue ( " :id " , id ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return ;
Song new_song = GetSongById ( id , db ) ;
}
void CollectionBackend : : 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 WHERE ROWID = :id " ) . arg ( songs_table_ ) ) ;
q . bindValue ( " :id " , id ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return ;
Song new_song = GetSongById ( id , db ) ;
}
void CollectionBackend : : DeleteAll ( ) {
2018-05-01 00:41:33 +02:00
2018-02-27 18:06:05 +01:00
{
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 ( ) ;
}