File indexing completed on 2025-01-05 04:25:55

0001 /****************************************************************************************
0002  * Copyright (c) 2007 Maximilian Kossick <maximilian.kossick@googlemail.com>            *
0003  * Copyright (c) 2007 Casey Link <unnamedrambler@gmail.com>                             *
0004  * Copyright (c) 2008-2009 Jeff Mitchell <mitchell@kde.org>                             *
0005  * Copyright (c) 2013 Ralf Engels <ralf-engels@gmx.de>                                  *
0006  *                                                                                      *
0007  * This program is free software; you can redistribute it and/or modify it under        *
0008  * the terms of the GNU General Public License as published by the Free Software        *
0009  * Foundation; either version 2 of the License, or (at your option) any later           *
0010  * version.                                                                             *
0011  *                                                                                      *
0012  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0013  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0014  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0015  *                                                                                      *
0016  * You should have received a copy of the GNU General Public License along with         *
0017  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0018  ****************************************************************************************/
0019 
0020 #define DEBUG_PREFIX "SqlCollection"
0021 
0022 #include "SqlCollection.h"
0023 
0024 #include "DefaultSqlQueryMakerFactory.h"
0025 #include "DatabaseUpdater.h"
0026 #include "core/support/Amarok.h"
0027 #include "core/support/Debug.h"
0028 #include "core/capabilities/TranscodeCapability.h"
0029 #include "core/transcoding/TranscodingController.h"
0030 #include "core-impl/collections/db/MountPointManager.h"
0031 #include "scanner/GenericScanManager.h"
0032 #include "scanner/AbstractDirectoryWatcher.h"
0033 #include "dialogs/OrganizeCollectionDialog.h"
0034 #include "SqlCollectionLocation.h"
0035 #include "SqlQueryMaker.h"
0036 #include "SqlScanResultProcessor.h"
0037 #include "SvgHandler.h"
0038 #include "MainWindow.h"
0039 
0040 #include "collectionscanner/BatchFile.h"
0041 
0042 #include <QApplication>
0043 #include <QDir>
0044 #include <QMessageBox>
0045 #include <QStandardPaths>
0046 
0047 #include <KConfigGroup>
0048 #include <ThreadWeaver/ThreadWeaver>
0049 #include <ThreadWeaver/Queue>
0050 #include <ThreadWeaver/Job>
0051 
0052 
0053 /** Concrete implementation of the directory watcher */
0054 class SqlDirectoryWatcher : public AbstractDirectoryWatcher
0055 {
0056 public:
0057     SqlDirectoryWatcher( Collections::SqlCollection* collection )
0058         : AbstractDirectoryWatcher()
0059         , m_collection( collection )
0060     { }
0061 
0062     ~SqlDirectoryWatcher() override
0063     { }
0064 
0065 protected:
0066     QList<QString> collectionFolders() override
0067     { return m_collection->mountPointManager()->collectionFolders(); }
0068 
0069     Collections::SqlCollection* m_collection;
0070 };
0071 
0072 class SqlScanManager : public GenericScanManager
0073 {
0074 public:
0075     SqlScanManager( Collections::SqlCollection* collection, QObject* parent )
0076         : GenericScanManager( parent )
0077         , m_collection( collection )
0078     { }
0079 
0080     ~SqlScanManager() override
0081     { }
0082 
0083 protected:
0084     QList< QPair<QString, uint> > getKnownDirs()
0085     {
0086         QList< QPair<QString, uint> > result;
0087 
0088         // -- get all (mounted) mount points
0089         QList<int> idList = m_collection->mountPointManager()->getMountedDeviceIds();
0090 
0091         // -- query all known directories
0092         QString deviceIds;
0093         foreach( int id, idList )
0094         {
0095             if( !deviceIds.isEmpty() )
0096                 deviceIds += ',';
0097             deviceIds += QString::number( id );
0098         }
0099         QString query = QString( "SELECT deviceid, dir, changedate FROM directories WHERE deviceid IN (%1);" );
0100 
0101         QStringList values = m_collection->sqlStorage()->query( query.arg( deviceIds ) );
0102         for( QListIterator<QString> iter( values ); iter.hasNext(); )
0103         {
0104             int deviceid = iter.next().toInt();
0105             QString dir = iter.next();
0106             uint mtime = iter.next().toUInt();
0107 
0108             QString folder = m_collection->mountPointManager()->getAbsolutePath( deviceid, dir );
0109             result.append( QPair<QString, uint>( folder, mtime ) );
0110         }
0111 
0112         return result;
0113     }
0114 
0115     QString getBatchFile( const QStringList &scanDirsRequested ) override
0116     {
0117         // -- write the batch file
0118         // the batch file contains the known modification dates so that the scanner only
0119         // needs to report changed directories
0120         QList<QPair<QString, uint> > knownDirs = getKnownDirs();
0121         if( !knownDirs.isEmpty() )
0122         {
0123             QString path = QStandardPaths::writableLocation( QStandardPaths::GenericDataLocation ) + "/amarok/amarokcollectionscanner_batchscan.xml";
0124             while( QFile::exists( path ) )
0125                 path += '_';
0126 
0127             CollectionScanner::BatchFile batchfile;
0128             batchfile.setTimeDefinitions( knownDirs );
0129             batchfile.setDirectories( scanDirsRequested );
0130             if( !batchfile.write( path ) )
0131             {
0132                 warning() << "Failed to write batch file" << path;
0133                 return QString();
0134             }
0135             return path;
0136         }
0137         return QString();
0138     }
0139 
0140     Collections::SqlCollection* m_collection;
0141 };
0142 
0143 namespace Collections {
0144 
0145 class OrganizeCollectionDelegateImpl : public OrganizeCollectionDelegate
0146 {
0147 public:
0148     OrganizeCollectionDelegateImpl()
0149         : OrganizeCollectionDelegate()
0150         , m_dialog( nullptr )
0151         , m_organizing( false ) {}
0152     ~ OrganizeCollectionDelegateImpl() override { delete m_dialog; }
0153 
0154     void setTracks( const Meta::TrackList &tracks ) override { m_tracks = tracks; }
0155     void setFolders( const QStringList &folders ) override { m_folders = folders; }
0156     void setIsOrganizing( bool organizing ) override { m_organizing = organizing; }
0157     void setTranscodingConfiguration( const Transcoding::Configuration &configuration ) override
0158     { m_targetFileExtension =
0159       Amarok::Components::transcodingController()->format( configuration.encoder() )->fileExtension(); }
0160     void setCaption( const QString &caption ) override { m_caption = caption; }
0161 
0162     void show() override
0163     {
0164         m_dialog = new OrganizeCollectionDialog( m_tracks,
0165                     m_folders,
0166                     m_targetFileExtension,
0167                     The::mainWindow(), //parent
0168                     "", //name is unused
0169                     true, //modal
0170                     m_caption //caption
0171                 );
0172 
0173         connect( m_dialog, &OrganizeCollectionDialog::accepted,
0174                  this, &OrganizeCollectionDelegateImpl::accepted );
0175         connect( m_dialog, &OrganizeCollectionDialog::rejected,
0176                  this, &OrganizeCollectionDelegateImpl::rejected );
0177         m_dialog->show();
0178     }
0179 
0180     bool overwriteDestinations() const override { return m_dialog->overwriteDestinations(); }
0181     QMap<Meta::TrackPtr, QString> destinations() const override { return m_dialog->getDestinations(); }
0182 
0183 private:
0184     Meta::TrackList m_tracks;
0185     QStringList m_folders;
0186     OrganizeCollectionDialog *m_dialog;
0187     bool m_organizing;
0188     QString m_targetFileExtension;
0189     QString m_caption;
0190 };
0191 
0192 
0193 class OrganizeCollectionDelegateFactoryImpl : public OrganizeCollectionDelegateFactory
0194 {
0195 public:
0196     OrganizeCollectionDelegate* createDelegate() override { return new OrganizeCollectionDelegateImpl(); }
0197 };
0198 
0199 
0200 class SqlCollectionLocationFactoryImpl : public SqlCollectionLocationFactory
0201 {
0202 public:
0203     SqlCollectionLocationFactoryImpl( SqlCollection *collection )
0204         : SqlCollectionLocationFactory()
0205         , m_collection( collection ) {}
0206 
0207     SqlCollectionLocation *createSqlCollectionLocation() const override
0208     {
0209         Q_ASSERT( m_collection );
0210         SqlCollectionLocation *loc = new SqlCollectionLocation( m_collection );
0211         loc->setOrganizeCollectionDelegateFactory( new OrganizeCollectionDelegateFactoryImpl() );
0212         return loc;
0213     }
0214 
0215     Collections::SqlCollection *m_collection;
0216 };
0217 
0218 } //namespace Collections
0219 
0220 using namespace Collections;
0221 
0222 SqlCollection::SqlCollection( const QSharedPointer<SqlStorage> &storage )
0223     : DatabaseCollection()
0224     , m_registry( nullptr )
0225     , m_sqlStorage( storage )
0226     , m_scanProcessor( nullptr )
0227     , m_directoryWatcher( nullptr )
0228     , m_collectionLocationFactory( nullptr )
0229     , m_queryMakerFactory( nullptr )
0230 {
0231     qRegisterMetaType<TrackUrls>( "TrackUrls" );
0232     qRegisterMetaType<ChangedTrackUrls>( "ChangedTrackUrls" );
0233 
0234     // update database to current schema version; this must be run *before* MountPointManager
0235     // is initialized or its handlers may try to insert
0236     // into the database before it's created/updated!
0237     DatabaseUpdater updater( this );
0238     if( updater.needsUpdate() )
0239     {
0240         if( updater.schemaExists() ) // this is an update
0241         {
0242             QMessageBox dialog;
0243             dialog.setText( i18n( "Updating Amarok database schema. Please don't terminate "
0244                                               "Amarok now as it may result in database corruption." ) );
0245             dialog.setWindowTitle( i18n( "Updating Amarok database schema" ) );
0246 
0247             dialog.setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed );
0248             dialog.show();
0249             dialog.raise();
0250             // otherwise the splash screen doesn't load image and this dialog is not shown:
0251             qApp->processEvents();
0252 
0253             updater.update();
0254 
0255             dialog.hide();
0256             qApp->processEvents();
0257         }
0258         else // this is new schema creation
0259             updater.update();
0260     }
0261 
0262     //perform a quick check of the database
0263     updater.cleanupDatabase();
0264 
0265     m_registry = new SqlRegistry( this );
0266 
0267     m_collectionLocationFactory = new SqlCollectionLocationFactoryImpl( this );
0268     m_queryMakerFactory = new DefaultSqlQueryMakerFactory( this );
0269 
0270     // scanning
0271     m_scanManager = new SqlScanManager( this, this );
0272     m_scanProcessor = new SqlScanResultProcessor( m_scanManager, this, this );
0273     auto directoryWatcher = QSharedPointer<SqlDirectoryWatcher>::create( this );
0274     m_directoryWatcher = directoryWatcher.toWeakRef();
0275     connect( directoryWatcher.data(), &AbstractDirectoryWatcher::done,
0276              directoryWatcher.data(), &AbstractDirectoryWatcher::deleteLater ); // auto delete
0277     connect( directoryWatcher.data(), &AbstractDirectoryWatcher::requestScan,
0278              m_scanManager, &GenericScanManager::requestScan );
0279     ThreadWeaver::Queue::instance()->enqueue( directoryWatcher );
0280 }
0281 
0282 SqlCollection::~SqlCollection()
0283 {
0284     DEBUG_BLOCK
0285 
0286     if( auto directoryWatcher = m_directoryWatcher.toStrongRef() )
0287         directoryWatcher->requestAbort();
0288 
0289     delete m_scanProcessor; // this prevents any further commits from the scanner
0290     delete m_collectionLocationFactory;
0291     delete m_queryMakerFactory;
0292     delete m_registry;
0293 }
0294 
0295 QString
0296 SqlCollection::uidUrlProtocol() const
0297 {
0298     return "amarok-sqltrackuid";
0299 }
0300 
0301 QString
0302 SqlCollection::generateUidUrl( const QString &hash )
0303 {
0304     return uidUrlProtocol() + "://" + hash;
0305 }
0306 
0307 QueryMaker*
0308 SqlCollection::queryMaker()
0309 {
0310     Q_ASSERT( m_queryMakerFactory );
0311     return m_queryMakerFactory->createQueryMaker();
0312 }
0313 
0314 SqlRegistry*
0315 SqlCollection::registry() const
0316 {
0317     Q_ASSERT( m_registry );
0318     return m_registry;
0319 }
0320 
0321 QSharedPointer<SqlStorage>
0322 SqlCollection::sqlStorage() const
0323 {
0324     Q_ASSERT( m_sqlStorage );
0325     return m_sqlStorage;
0326 }
0327 
0328 bool
0329 SqlCollection::possiblyContainsTrack( const QUrl &url ) const
0330 {
0331     if( url.isLocalFile() )
0332     {
0333         foreach( const QString &folder, collectionFolders() )
0334         {
0335             QUrl q = QUrl::fromLocalFile( folder );
0336             if( q.isParentOf( url ) || q.matches( url , QUrl::StripTrailingSlash) )
0337                 return true;
0338         }
0339         return false;
0340     }
0341     else
0342         return url.scheme() == uidUrlProtocol();
0343 }
0344 
0345 Meta::TrackPtr
0346 SqlCollection::trackForUrl( const QUrl &url )
0347 {
0348     if( url.scheme() == uidUrlProtocol() )
0349         return m_registry->getTrackFromUid( url.url() );
0350     else if( url.scheme() == "file" )
0351         return m_registry->getTrack( url.path() );
0352     else
0353         return Meta::TrackPtr();
0354 }
0355 
0356 Meta::TrackPtr
0357 SqlCollection::getTrack( int deviceId, const QString &rpath, int directoryId, const QString &uidUrl )
0358 {
0359     return m_registry->getTrack( deviceId, rpath, directoryId, uidUrl );
0360 }
0361 
0362 Meta::TrackPtr
0363 SqlCollection::getTrackFromUid( const QString &uniqueid )
0364 {
0365     return m_registry->getTrackFromUid( uniqueid );
0366 }
0367 
0368 Meta::AlbumPtr
0369 SqlCollection::getAlbum( const QString &album, const QString &artist )
0370 {
0371     return m_registry->getAlbum( album, artist );
0372 }
0373 
0374 CollectionLocation*
0375 SqlCollection::location()
0376 {
0377     Q_ASSERT( m_collectionLocationFactory );
0378     return m_collectionLocationFactory->createSqlCollectionLocation();
0379 }
0380 
0381 void
0382 SqlCollection::slotDeviceAdded( int id )
0383 {
0384     QString query = "select count(*) from tracks inner join urls on tracks.url = urls.id where urls.deviceid = %1";
0385     QStringList rs = m_sqlStorage->query( query.arg( id ) );
0386     if( !rs.isEmpty() )
0387     {
0388         int count = rs.first().toInt();
0389         if( count > 0 )
0390         {
0391             collectionUpdated();
0392         }
0393     }
0394     else
0395     {
0396         warning() << "Query " << query << "did not return a result! Is the database available?";
0397     }
0398 }
0399 
0400 void
0401 SqlCollection::slotDeviceRemoved( int id )
0402 {
0403     QString query = "select count(*) from tracks inner join urls on tracks.url = urls.id where urls.deviceid = %1";
0404     QStringList rs = m_sqlStorage->query( query.arg( id ) );
0405     if( !rs.isEmpty() )
0406     {
0407         int count = rs.first().toInt();
0408         if( count > 0 )
0409         {
0410             collectionUpdated();
0411         }
0412     }
0413     else
0414     {
0415         warning() << "Query " << query << "did not return a result! Is the database available?";
0416     }
0417 }
0418 
0419 bool
0420 SqlCollection::hasCapabilityInterface( Capabilities::Capability::Type type ) const
0421 {
0422     switch( type )
0423     {
0424     case Capabilities::Capability::Transcode:
0425         return true;
0426     default:
0427         return DatabaseCollection::hasCapabilityInterface( type );
0428     }
0429 }
0430 
0431 Capabilities::Capability*
0432 SqlCollection::createCapabilityInterface( Capabilities::Capability::Type type )
0433 {
0434     switch( type )
0435     {
0436     case Capabilities::Capability::Transcode:
0437         return new SqlCollectionTranscodeCapability();
0438     default:
0439         return DatabaseCollection::createCapabilityInterface( type );
0440     }
0441 }
0442 
0443 void
0444 SqlCollection::dumpDatabaseContent()
0445 {
0446     DatabaseUpdater updater( this );
0447 
0448     QStringList tables = m_sqlStorage->query( "select table_name from INFORMATION_SCHEMA.tables WHERE table_schema='amarok'" );
0449     foreach( const QString &table, tables )
0450     {
0451         QString filePath =
0452             QDir::home().absoluteFilePath( table + '-' + QDateTime::currentDateTime().toString( Qt::ISODate ) + ".csv" );
0453         updater.writeCSVFile( table, filePath, true );
0454     }
0455 }
0456 
0457 // ---------- SqlCollectionTranscodeCapability -------------
0458 
0459 SqlCollectionTranscodeCapability::~SqlCollectionTranscodeCapability()
0460 {
0461     // nothing to do
0462 }
0463 
0464 Transcoding::Configuration
0465 SqlCollectionTranscodeCapability::savedConfiguration()
0466 {
0467     KConfigGroup transcodeGroup = Amarok::config( SQL_TRANSCODING_GROUP_NAME );
0468     return Transcoding::Configuration::fromConfigGroup( transcodeGroup );
0469 }
0470 
0471 void
0472 SqlCollectionTranscodeCapability::setSavedConfiguration( const Transcoding::Configuration &configuration )
0473 {
0474     KConfigGroup transcodeGroup = Amarok::config( SQL_TRANSCODING_GROUP_NAME );
0475     configuration.saveToConfigGroup( transcodeGroup );
0476     transcodeGroup.sync();
0477 }
0478