2018-02-27 18:06:05 +01:00
/*
* Strawberry Music Player
* This file was part of Clementine .
* Copyright 2010 , David Sansome < me @ davidsansome . com >
2021-03-20 21:14:47 +01:00
* Copyright 2018 - 2021 , Jonas Kvinge < jonas @ jkvinge . net >
2018-02-27 18:06:05 +01:00
*
* 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/>.
2018-08-09 18:10:03 +02:00
*
2018-02-27 18:06:05 +01:00
*/
2020-02-12 00:07:05 +01:00
# include "config.h"
2018-02-27 18:06:05 +01:00
# include <memory>
# include <functional>
2020-06-14 23:54:18 +02:00
# include <cassert>
2018-02-27 18:06:05 +01:00
2018-05-01 00:41:33 +02:00
# include <QObject>
2019-07-24 19:16:51 +02:00
# include <QApplication>
# include <QThread>
2018-05-01 00:41:33 +02:00
# include <QMutex>
# include <QIODevice>
# include <QDir>
2018-02-27 18:06:05 +01:00
# include <QFile>
2018-05-01 00:41:33 +02:00
# include <QByteArray>
2018-02-27 18:06:05 +01:00
# include <QHash>
2018-05-01 00:41:33 +02:00
# include <QList>
# include <QVariant>
# include <QString>
# include <QStringBuilder>
# include <QStringList>
# include <QUrl>
# include <QSqlDatabase>
2018-02-27 18:06:05 +01:00
# include <QSqlQuery>
# include <QtDebug>
# include "core/application.h"
# include "core/database.h"
# include "core/logging.h"
# include "core/scopedtransaction.h"
# include "core/song.h"
# include "collection/collectionbackend.h"
# include "collection/sqlrow.h"
2018-05-01 00:41:33 +02:00
# include "playlistitem.h"
# include "songplaylistitem.h"
# include "playlistbackend.h"
2018-02-27 18:06:05 +01:00
# include "playlistparsers/cueparser.h"
2020-09-17 17:50:17 +02:00
# include "smartplaylists/playlistgenerator.h"
2018-02-27 18:06:05 +01:00
const int PlaylistBackend : : kSongTableJoins = 2 ;
2018-05-01 00:41:33 +02:00
PlaylistBackend : : PlaylistBackend ( Application * app , QObject * parent )
2021-07-11 07:40:57 +02:00
: QObject ( parent ) ,
app_ ( app ) ,
db_ ( app_ - > database ( ) ) ,
original_thread_ ( nullptr ) {
2019-07-24 19:16:51 +02:00
original_thread_ = thread ( ) ;
}
void PlaylistBackend : : Close ( ) {
if ( db_ ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
db_ - > Close ( ) ;
}
}
void PlaylistBackend : : ExitAsync ( ) {
metaObject ( ) - > invokeMethod ( this , " Exit " , Qt : : QueuedConnection ) ;
}
void PlaylistBackend : : Exit ( ) {
assert ( QThread : : currentThread ( ) = = thread ( ) ) ;
moveToThread ( original_thread_ ) ;
emit ExitFinished ( ) ;
}
2018-02-27 18:06:05 +01:00
PlaylistBackend : : PlaylistList PlaylistBackend : : GetAllPlaylists ( ) {
return GetPlaylists ( GetPlaylists_All ) ;
}
PlaylistBackend : : PlaylistList PlaylistBackend : : GetAllOpenPlaylists ( ) {
return GetPlaylists ( GetPlaylists_OpenInUi ) ;
}
PlaylistBackend : : PlaylistList PlaylistBackend : : GetAllFavoritePlaylists ( ) {
return GetPlaylists ( GetPlaylists_Favorite ) ;
}
PlaylistBackend : : PlaylistList PlaylistBackend : : GetPlaylists ( GetPlaylistsFlags flags ) {
2018-10-02 00:38:52 +02:00
2018-02-27 18:06:05 +01:00
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
PlaylistList ret ;
QStringList condition_list ;
if ( flags & GetPlaylists_OpenInUi ) {
condition_list < < " ui_order != -1 " ;
}
if ( flags & GetPlaylists_Favorite ) {
condition_list < < " is_favorite != 0 " ;
}
QString condition ;
if ( ! condition_list . isEmpty ( ) ) {
condition = " WHERE " + condition_list . join ( " OR " ) ;
}
QSqlQuery q ( db ) ;
2020-09-17 17:50:17 +02:00
q . prepare ( " SELECT ROWID, name, last_played, special_type, ui_path, is_favorite, dynamic_playlist_type, dynamic_playlist_data, dynamic_playlist_backend FROM playlists " + condition + " ORDER BY ui_order " ) ;
2018-02-27 18:06:05 +01:00
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return ret ;
while ( q . next ( ) ) {
Playlist p ;
p . id = q . value ( 0 ) . toInt ( ) ;
p . name = q . value ( 1 ) . toString ( ) ;
p . last_played = q . value ( 2 ) . toInt ( ) ;
p . special_type = q . value ( 3 ) . toString ( ) ;
p . ui_path = q . value ( 4 ) . toString ( ) ;
p . favorite = q . value ( 5 ) . toBool ( ) ;
2020-09-17 17:50:17 +02:00
p . dynamic_type = PlaylistGenerator : : Type ( q . value ( 6 ) . toInt ( ) ) ;
p . dynamic_data = q . value ( 7 ) . toByteArray ( ) ;
p . dynamic_backend = q . value ( 8 ) . toString ( ) ;
2018-02-27 18:06:05 +01:00
ret < < p ;
}
return ret ;
}
PlaylistBackend : : Playlist PlaylistBackend : : GetPlaylist ( int id ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
QSqlQuery q ( db ) ;
2020-09-17 17:50:17 +02:00
q . prepare ( " SELECT ROWID, name, last_played, special_type, ui_path, is_favorite, dynamic_playlist_type, dynamic_playlist_data, dynamic_playlist_backend FROM playlists WHERE ROWID=:id " ) ;
2018-02-27 18:06:05 +01:00
q . bindValue ( " :id " , id ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return Playlist ( ) ;
q . next ( ) ;
Playlist p ;
p . id = q . value ( 0 ) . toInt ( ) ;
p . name = q . value ( 1 ) . toString ( ) ;
p . last_played = q . value ( 2 ) . toInt ( ) ;
p . special_type = q . value ( 3 ) . toString ( ) ;
p . ui_path = q . value ( 4 ) . toString ( ) ;
p . favorite = q . value ( 5 ) . toBool ( ) ;
2020-09-17 17:50:17 +02:00
p . dynamic_type = PlaylistGenerator : : Type ( q . value ( 6 ) . toInt ( ) ) ;
p . dynamic_data = q . value ( 7 ) . toByteArray ( ) ;
p . dynamic_backend = q . value ( 8 ) . toString ( ) ;
2018-02-27 18:06:05 +01:00
return p ;
2018-03-04 20:13:05 +01:00
2018-02-27 18:06:05 +01:00
}
QSqlQuery PlaylistBackend : : GetPlaylistRows ( int playlist ) {
2018-03-04 20:13:05 +01:00
2018-02-27 18:06:05 +01:00
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
2020-11-22 03:36:46 +01:00
QString query = " SELECT songs.ROWID, " + Song : : JoinSpec ( " songs " ) + " , p.ROWID, " + Song : : JoinSpec ( " p " ) + " , p.type FROM playlist_items AS p LEFT JOIN songs ON p.collection_id = songs.ROWID WHERE p.playlist = :playlist " ;
2018-02-27 18:06:05 +01:00
QSqlQuery q ( db ) ;
// Forward iterations only may be faster
q . setForwardOnly ( true ) ;
q . prepare ( query ) ;
q . bindValue ( " :playlist " , playlist ) ;
q . exec ( ) ;
return q ;
}
QList < PlaylistItemPtr > PlaylistBackend : : GetPlaylistItems ( int playlist ) {
QList < PlaylistItemPtr > playlistitems ;
2019-07-25 17:56:28 +02:00
{
QSqlQuery q = GetPlaylistRows ( playlist ) ;
// Note that as this only accesses the query, not the db, we don't need the mutex.
if ( db_ - > CheckErrors ( q ) ) return QList < PlaylistItemPtr > ( ) ;
// it's probable that we'll have a few songs associated with the same CUE so we're caching results of parsing CUEs
2021-06-30 16:14:58 +02:00
std : : shared_ptr < NewSongFromQueryState > state_ptr = std : : make_shared < NewSongFromQueryState > ( ) ;
2019-07-25 17:56:28 +02:00
while ( q . next ( ) ) {
playlistitems < < NewPlaylistItemFromQuery ( SqlRow ( q ) , state_ptr ) ;
}
2018-02-27 18:06:05 +01:00
}
2019-07-24 21:37:09 +02:00
if ( QThread : : currentThread ( ) ! = thread ( ) & & QThread : : currentThread ( ) ! = qApp - > thread ( ) ) {
2019-07-25 17:56:28 +02:00
Close ( ) ;
2019-07-24 21:37:09 +02:00
}
2018-02-27 18:06:05 +01:00
return playlistitems ;
}
QList < Song > PlaylistBackend : : GetPlaylistSongs ( int playlist ) {
2019-07-25 17:56:28 +02:00
SongList songs ;
{
QSqlQuery q = GetPlaylistRows ( playlist ) ;
// Note that as this only accesses the query, not the db, we don't need the mutex.
if ( db_ - > CheckErrors ( q ) ) return QList < Song > ( ) ;
// it's probable that we'll have a few songs associated with the same CUE so we're caching results of parsing CUEs
2021-06-30 16:14:58 +02:00
std : : shared_ptr < NewSongFromQueryState > state_ptr = std : : make_shared < NewSongFromQueryState > ( ) ;
2019-07-25 17:56:28 +02:00
while ( q . next ( ) ) {
songs < < NewSongFromQuery ( SqlRow ( q ) , state_ptr ) ;
}
2018-02-27 18:06:05 +01:00
}
2019-07-24 21:37:09 +02:00
if ( QThread : : currentThread ( ) ! = thread ( ) & & QThread : : currentThread ( ) ! = qApp - > thread ( ) ) {
2019-07-25 17:56:28 +02:00
Close ( ) ;
2019-07-24 21:37:09 +02:00
}
2018-02-27 18:06:05 +01:00
return songs ;
}
PlaylistItemPtr PlaylistBackend : : NewPlaylistItemFromQuery ( const SqlRow & row , std : : shared_ptr < NewSongFromQueryState > state ) {
// The song tables get joined first, plus one each for the song ROWIDs
2021-03-21 18:53:02 +01:00
const int playlist_row = static_cast < int > ( Song : : kColumns . count ( ) + 1 ) * kSongTableJoins ;
2018-02-27 18:06:05 +01:00
2018-09-08 12:38:02 +02:00
PlaylistItemPtr item ( PlaylistItem : : NewFromSource ( Song : : Source ( row . value ( playlist_row ) . toInt ( ) ) ) ) ;
2018-02-27 18:06:05 +01:00
if ( item ) {
item - > InitFromQuery ( row ) ;
return RestoreCueData ( item , state ) ;
}
else {
return item ;
}
}
Song PlaylistBackend : : NewSongFromQuery ( const SqlRow & row , std : : shared_ptr < NewSongFromQueryState > state ) {
2018-10-02 00:38:52 +02:00
2018-02-27 18:06:05 +01:00
return NewPlaylistItemFromQuery ( row , state ) - > Metadata ( ) ;
2018-10-02 00:38:52 +02:00
2018-02-27 18:06:05 +01:00
}
// If song had a CUE and the CUE still exists, the metadata from it will be applied here.
PlaylistItemPtr PlaylistBackend : : RestoreCueData ( PlaylistItemPtr item , std : : shared_ptr < NewSongFromQueryState > state ) {
2018-10-02 00:38:52 +02:00
2018-09-08 12:38:02 +02:00
// We need collection to run a CueParser; also, this method applies only to file-type PlaylistItems
if ( item - > source ( ) ! = Song : : Source_LocalFile ) return item ;
2018-02-27 18:06:05 +01:00
CueParser cue_parser ( app_ - > collection_backend ( ) ) ;
Song song = item - > Metadata ( ) ;
2021-04-25 21:16:44 +02:00
// We're only interested in .cue songs here
2018-02-27 18:06:05 +01:00
if ( ! song . has_cue ( ) ) return item ;
QString cue_path = song . cue_path ( ) ;
2021-04-25 21:16:44 +02:00
// If .cue was deleted - reload the song
2018-02-27 18:06:05 +01:00
if ( ! QFile : : exists ( cue_path ) ) {
item - > Reload ( ) ;
return item ;
}
SongList song_list ;
{
QMutexLocker locker ( & state - > mutex_ ) ;
if ( ! state - > cached_cues_ . contains ( cue_path ) ) {
2021-04-25 21:16:44 +02:00
QFile cue_file ( cue_path ) ;
if ( ! cue_file . open ( QIODevice : : ReadOnly ) ) return item ;
2018-02-27 18:06:05 +01:00
2021-04-25 21:16:44 +02:00
song_list = cue_parser . Load ( & cue_file , cue_path , QDir ( cue_path . section ( ' / ' , 0 , - 2 ) ) ) ;
2018-02-27 18:06:05 +01:00
state - > cached_cues_ [ cue_path ] = song_list ;
2019-04-08 23:00:07 +02:00
}
else {
2018-02-27 18:06:05 +01:00
song_list = state - > cached_cues_ [ cue_path ] ;
}
}
2019-04-08 23:00:07 +02:00
for ( const Song & from_list : song_list ) {
2018-05-01 00:41:33 +02:00
if ( from_list . url ( ) . toEncoded ( ) = = song . url ( ) . toEncoded ( ) & & from_list . beginning_nanosec ( ) = = song . beginning_nanosec ( ) ) {
2021-04-25 21:16:44 +02:00
// We found a matching section; replace the input item with a new one containing CUE metadata
2021-07-11 19:57:18 +02:00
return std : : make_shared < SongPlaylistItem > ( from_list ) ;
2018-02-27 18:06:05 +01:00
}
}
2021-04-25 21:16:44 +02:00
// There's no such section in the related .cue -> reload the song
2018-02-27 18:06:05 +01:00
item - > Reload ( ) ;
2021-04-25 21:16:44 +02:00
2018-02-27 18:06:05 +01:00
return item ;
}
2020-09-17 17:50:17 +02:00
void PlaylistBackend : : SavePlaylistAsync ( int playlist , const PlaylistItemList & items , int last_played , PlaylistGeneratorPtr dynamic ) {
2018-02-27 18:06:05 +01:00
2020-09-17 17:50:17 +02:00
metaObject ( ) - > invokeMethod ( this , " SavePlaylist " , Qt : : QueuedConnection , Q_ARG ( int , playlist ) , Q_ARG ( PlaylistItemList , items ) , Q_ARG ( int , last_played ) , Q_ARG ( PlaylistGeneratorPtr , dynamic ) ) ;
2018-02-27 18:06:05 +01:00
}
2020-09-17 17:50:17 +02:00
void PlaylistBackend : : SavePlaylist ( int playlist , const PlaylistItemList & items , int last_played , PlaylistGeneratorPtr dynamic ) {
2018-02-27 18:06:05 +01:00
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
qLog ( Debug ) < < " Saving playlist " < < playlist ;
QSqlQuery clear ( db ) ;
clear . prepare ( " DELETE FROM playlist_items WHERE playlist = :playlist " ) ;
QSqlQuery insert ( db ) ;
2018-09-08 12:38:02 +02:00
insert . prepare ( " INSERT INTO playlist_items (playlist, type, collection_id, " + Song : : kColumnSpec + " ) VALUES (:playlist, :type, :collection_id, " + Song : : kBindSpec + " ) " ) ;
2018-02-27 18:06:05 +01:00
QSqlQuery update ( db ) ;
2020-09-17 17:50:17 +02:00
update . prepare ( " UPDATE playlists SET last_played=:last_played, dynamic_playlist_type=:dynamic_type, dynamic_playlist_data=:dynamic_data, dynamic_playlist_backend=:dynamic_backend WHERE ROWID=:playlist " ) ;
2018-02-27 18:06:05 +01:00
ScopedTransaction transaction ( & db ) ;
// Clear the existing items in the playlist
clear . bindValue ( " :playlist " , playlist ) ;
clear . exec ( ) ;
if ( db_ - > CheckErrors ( clear ) ) return ;
// Save the new ones
2021-06-20 19:04:08 +02:00
for ( PlaylistItemPtr item : items ) { // clazy:exclude=range-loop
2018-02-27 18:06:05 +01:00
insert . bindValue ( " :playlist " , playlist ) ;
item - > BindToQuery ( & insert ) ;
insert . exec ( ) ;
db_ - > CheckErrors ( insert ) ;
}
// Update the last played track number
update . bindValue ( " :last_played " , last_played ) ;
2020-09-17 17:50:17 +02:00
if ( dynamic ) {
update . bindValue ( " :dynamic_type " , dynamic - > type ( ) ) ;
update . bindValue ( " :dynamic_data " , dynamic - > Save ( ) ) ;
update . bindValue ( " :dynamic_backend " , dynamic - > collection ( ) - > songs_table ( ) ) ;
}
else {
update . bindValue ( " :dynamic_type " , 0 ) ;
update . bindValue ( " :dynamic_data " , QByteArray ( ) ) ;
update . bindValue ( " :dynamic_backend " , QString ( ) ) ;
}
2018-02-27 18:06:05 +01:00
update . bindValue ( " :playlist " , playlist ) ;
update . exec ( ) ;
if ( db_ - > CheckErrors ( update ) ) return ;
transaction . Commit ( ) ;
}
2018-05-01 00:41:33 +02:00
int PlaylistBackend : : CreatePlaylist ( const QString & name , const QString & special_type ) {
2018-10-02 00:38:52 +02:00
2018-02-27 18:06:05 +01:00
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
QSqlQuery q ( db ) ;
q . prepare ( " INSERT INTO playlists (name, special_type) VALUES (:name, :special_type) " ) ;
q . bindValue ( " :name " , name ) ;
q . bindValue ( " :special_type " , special_type ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return - 1 ;
return q . lastInsertId ( ) . toInt ( ) ;
}
void PlaylistBackend : : RemovePlaylist ( int id ) {
2018-10-02 00:38:52 +02:00
2018-02-27 18:06:05 +01:00
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
QSqlQuery delete_playlist ( db ) ;
delete_playlist . prepare ( " DELETE FROM playlists WHERE ROWID=:id " ) ;
QSqlQuery delete_items ( db ) ;
delete_items . prepare ( " DELETE FROM playlist_items WHERE playlist=:id " ) ;
delete_playlist . bindValue ( " :id " , id ) ;
delete_items . bindValue ( " :id " , id ) ;
ScopedTransaction transaction ( & db ) ;
delete_playlist . exec ( ) ;
if ( db_ - > CheckErrors ( delete_playlist ) ) return ;
delete_items . exec ( ) ;
if ( db_ - > CheckErrors ( delete_items ) ) return ;
transaction . Commit ( ) ;
}
void PlaylistBackend : : RenamePlaylist ( int id , const QString & new_name ) {
2018-10-02 00:38:52 +02:00
2018-02-27 18:06:05 +01:00
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
QSqlQuery q ( db ) ;
q . prepare ( " UPDATE playlists SET name=:name WHERE ROWID=:id " ) ;
q . bindValue ( " :name " , new_name ) ;
q . bindValue ( " :id " , id ) ;
q . exec ( ) ;
db_ - > CheckErrors ( q ) ;
}
void PlaylistBackend : : FavoritePlaylist ( int id , bool is_favorite ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
QSqlQuery q ( db ) ;
q . prepare ( " UPDATE playlists SET is_favorite=:is_favorite WHERE ROWID=:id " ) ;
q . bindValue ( " :is_favorite " , is_favorite ? 1 : 0 ) ;
q . bindValue ( " :id " , id ) ;
q . exec ( ) ;
db_ - > CheckErrors ( q ) ;
}
void PlaylistBackend : : SetPlaylistOrder ( const QList < int > & ids ) {
2018-10-02 00:38:52 +02:00
2018-02-27 18:06:05 +01:00
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
ScopedTransaction transaction ( & db ) ;
QSqlQuery q ( db ) ;
q . prepare ( " UPDATE playlists SET ui_order=-1 " ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return ;
q . prepare ( " UPDATE playlists SET ui_order=:index WHERE ROWID=:id " ) ;
for ( int i = 0 ; i < ids . count ( ) ; + + i ) {
q . bindValue ( " :index " , i ) ;
q . bindValue ( " :id " , ids [ i ] ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return ;
}
transaction . Commit ( ) ;
}
void PlaylistBackend : : SetPlaylistUiPath ( int id , const QString & path ) {
QMutexLocker l ( db_ - > Mutex ( ) ) ;
QSqlDatabase db ( db_ - > Connect ( ) ) ;
QSqlQuery q ( db ) ;
q . prepare ( " UPDATE playlists SET ui_path=:path WHERE ROWID=:id " ) ;
ScopedTransaction transaction ( & db ) ;
q . bindValue ( " :path " , path ) ;
q . bindValue ( " :id " , id ) ;
q . exec ( ) ;
if ( db_ - > CheckErrors ( q ) ) return ;
transaction . Commit ( ) ;
}