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