1 /***************************************************************************
2 * Copyright (C) 2003-2007 by The Amarok Developers *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
18 ***************************************************************************/
20 #define DEBUG_PREFIX "ScanController"
22 #include "scancontroller.h"
25 #include "amarokconfig.h"
26 #include "collectiondb.h"
28 #include "metabundle.h"
29 #include "mountpointmanager.h"
30 #include "statusbar.h"
32 #include <KApplication>
34 #include <KMessageBox>
40 ////////////////////////////////////////////////////////////////////////////////
41 // class ScanController
42 ////////////////////////////////////////////////////////////////////////////////
44 ScanController
* ScanController::currController
= 0;
46 ScanController
* ScanController::instance()
48 return currController
;
51 void ScanController::setInstance( ScanController
* curr
)
53 currController
= curr
;
56 ScanController::ScanController( CollectionDB
* parent
, bool incremental
, const QStringList
& folders
)
57 : DependentJob( parent
, "CollectionScanner" )
58 , QXmlDefaultHandler()
59 , m_scanner( new Amarok::ProcIO() )
60 , m_folders( folders
)
61 , m_incremental( incremental
)
62 , m_hasChanged( false )
63 , m_source( new QXmlInputSource() )
64 , m_reader( new QXmlSimpleReader() )
65 , m_tablesCreated( false )
70 ScanController::setInstance( this );
71 m_reader
->setContentHandler( this );
73 connect( this, SIGNAL( scanDone( bool ) ), MountPointManager::instance(), SLOT( updateStatisticsURLs( bool ) ) );
75 connect( m_scanner
, SIGNAL( readReady( K3ProcIO
* ) ), SLOT( slotReadReady() ) );
77 *m_scanner
<< "amarokcollectionscanner";
78 *m_scanner
<< "--nocrashhandler"; // We want to be able to catch SIGSEGV
80 // K3Process must be started from the GUI thread, so we're invoking the scanner
84 setDescription( i18n( "Updating Collection" ) );
89 setDescription( i18n( "Building Collection" ) );
91 if( AmarokConfig::scanRecursively() ) *m_scanner
<< "-r";
92 *m_scanner
<< m_folders
;
98 ScanController::~ScanController()
102 if( !isAborted() && !m_crashedFiles
.empty() ) {
103 KMessageBox::information( 0, i18n( "<p>The Collection Scanner was unable to process these files:</p>" ) +
104 "<i>" + m_crashedFiles
.join( "<br>" ) + "</i>",
105 i18n( "Collection Scan Report" ) );
107 else if( m_crashedFiles
.size() >= MAX_RESTARTS
) {
108 KMessageBox::error( 0, i18n( "<p>Sorry, the Collection Scan was aborted, since too many problems were encountered.</p>" ) +
109 "<p>Advice: A common source for this problem is a broken 'TagLib' package on your computer. Replacing this package may help fixing the issue.</p>"
110 "<p>The following files caused problems:</p>" +
111 "<i>" + m_crashedFiles
.join( "<br>" ) + "</i>",
112 i18n( "Collection Scan Error" ) );
119 ScanController::setInstance( 0 );
123 // Cause the CollectionDB to emit fileDeleted() signals
125 ScanController::completeJob( void )
127 m_fileMapsMutex
.lock();
129 QMap
<QString
,QString
>::Iterator it
;
132 CollectionDB::instance()->emitFilesAdded( m_filesAdded
);
136 for( it
= m_filesAdded
.begin(); it
!= m_filesAdded
.end(); ++it
)
138 if( m_filesDeleted
.contains( it
.key() ) )
139 m_filesDeleted
.remove( it
.key() );
141 for( it
= m_filesAdded
.begin(); it
!= m_filesAdded
.end(); ++it
)
142 CollectionDB::instance()->emitFileAdded( it
.value(), it
.key() );
143 for( it
= m_filesDeleted
.begin(); it
!= m_filesDeleted
.end(); ++it
)
144 CollectionDB::instance()->emitFileDeleted( it
.value(), it
.key() );
147 m_fileMapsMutex
.unlock();
149 emit
scanDone( !m_incremental
|| m_hasChanged
);
151 ThreadManager::DependentJob::completeJob();
156 * The Incremental Scanner works as follows: Here we check the mtime of every directory in the "directories"
157 * table and store all changed directories in m_folders.
159 * These directories are then scanned in CollectionReader::doJob(), with m_recursively set according to the
160 * user's preference, so the user can add directories or whole directory trees, too. Since we don't want to
161 * rescan unchanged subdirectories, CollectionReader::readDir() checks if we are scanning recursively and
165 ScanController::initIncremental()
169 connect( CollectionDB::instance(),
170 SIGNAL( fileMoved( const QString
&, const QString
& ) ),
171 SLOT( slotFileMoved( const QString
&, const QString
& ) ) );
172 connect( CollectionDB::instance(),
173 SIGNAL( fileMoved( const QString
&, const QString
&, const QString
& ) ),
174 SLOT( slotFileMoved( const QString
&, const QString
& ) ) );
176 IdList list
= MountPointManager::instance()->getMountedDeviceIds();
178 foreach( int id
, list
)
180 if ( !deviceIds
.isEmpty() ) deviceIds
+= ',';
181 deviceIds
+= QString::number( id
);
184 const QStringList values
= CollectionDB::instance()->query(
185 QString( "SELECT deviceid, dir, changedate FROM directories WHERE deviceid IN (%1);" )
190 int id
= (*it
).toInt();
191 const QString folder
= MountPointManager::instance()->getAbsolutePath( id
, (*++it
) );
192 const QString mtime
= *++it
;
194 const QFileInfo
info( folder
);
197 if( info
.lastModified().toTime_t() != mtime
.toUInt() )
200 debug() << "Collection dir changed: " << folder
;
205 // this folder has been removed
207 debug() << "Collection dir removed: " << folder
;
210 kapp
->processEvents(); // Don't block the GUI
213 if ( !m_folders
.isEmpty() )
215 debug() << "Collection was modified.";
217 Amarok::StatusBar::instance()->shortMessage( i18n( "Updating Collection..." ) );
219 // Start scanner process
220 if( AmarokConfig::scanRecursively() ) *m_scanner
<< "-r";
222 *m_scanner
<< m_folders
;
229 ScanController::doJob()
233 if( !CollectionDB::instance()->isConnected() )
235 if( m_incremental
&& !m_hasChanged
)
238 CollectionDB::instance()->createTables( true );
239 m_tablesCreated
= true;
241 //For a full rescan, we might not have cleared tags table (for devices not plugged
242 //in), so preserve the necessary other tables (eg artist)
243 CollectionDB::instance()->prepareTempTables();
245 CollectionDB::instance()->invalidateArtistAlbumCache();
246 setProgressTotalSteps( 100 );
249 uint delayCount
= 100;
251 bool sessionStarted
= false;
254 while( !isAborted() ) {
255 if( m_xmlData
.isNull() ) {
256 if( !m_scanner
->isRunning() )
258 // Wait a bit after process has exited, so that we have time to parse all data
259 if( delayCount
== 0 )
266 QString data
= m_xmlData
;
267 m_source
->setData( data
);
270 m_dataMutex
.unlock();
271 if ( !sessionStarted
)
272 if ( m_reader
->parse( m_source
, true ) ) { //start a new session
273 sessionStarted
= true;
276 debug() << "Incremental parsing failed: " << errorString() << endl
<< QString( data
);
277 else if( !m_reader
->parseContinue() ) {
278 debug() << "parseContinue() failed: " << errorString() << endl
<< QString( data
);
284 if( m_scanner
->normalExit() && !m_scanner
->signalled() ) {
285 CollectionDB::instance()->sanitizeCompilations();
286 if ( m_incremental
) {
287 m_foldersToRemove
+= m_folders
;
288 foreach( QString str
, m_foldersToRemove
) {
289 m_fileMapsMutex
.lock();
290 CollectionDB::instance()->removeSongsInDir( str
, &m_filesDeleted
);
291 m_fileMapsMutex
.unlock();
292 CollectionDB::instance()->removeDirFromCollection( str
);
294 CollectionDB::instance()->removeOrphanedEmbeddedImages();
297 CollectionDB::instance()->clearTables( false ); // empty permanent tables
299 CollectionDB::instance()->copyTempTables(); // copy temp into permanent tables
301 //Clean up unused entries in the main tables (eg artist, composer)
302 CollectionDB::instance()->deleteAllRedundant( "artist" );
303 CollectionDB::instance()->deleteAllRedundant( "composer" );
304 CollectionDB::instance()->deleteAllRedundant( "year" );
305 CollectionDB::instance()->deleteAllRedundant( "genre" );
306 CollectionDB::instance()->deleteAllRedundant( "album" );
308 //Remove free space and fragmentation in the DB. Used to run on shutdown, but
309 //that took too long, sometimes causing Amarok to be killed.
310 CollectionDB::instance()->vacuum();
314 if( m_crashedFiles
.size() <= MAX_RESTARTS
||
315 m_crashedFiles
.size() <= (m_scanCount
* MAX_FAILURE_PERCENTAGE
) / 100 ) {
316 kapp
->postEvent( this, new RestartEvent() );
326 if( CollectionDB::instance()->isConnected() )
328 m_tablesCreated
= false;
329 CollectionDB::instance()->dropTables( true ); // drop temp tables
337 ScanController::slotReadReady()
343 while( m_scanner
->readln( line
, true, 0 ) != -1 ) {
344 if( !line
.startsWith( "exepath=" ) ) // skip binary location info from scanner
347 m_dataMutex
.unlock();
352 ScanController::slotFileMoved( const QString
&/*src*/, const QString
&/*dest*/)
354 //why is this needed? QBob, take a look at this
356 if( m_incremental ) // pedantry
358 m_fileMapsMutex.lock();
359 m_filesFound[ src ] = true;
360 m_fileMapsMutex.unlock();
367 ScanController::startElement( const QString
&, const QString
& localName
, const QString
&, const QXmlAttributes
& attrs
)
369 // List of entity names:
371 // itemcount Number of files overall
372 // folder Folder which is being processed
373 // dud Invalid audio file
374 // tags Valid audio file with metadata
375 // playlist Playlist file
377 // compilation Folder to check for compilation
378 // filesize Size of the track in bytes
380 if( localName
== "dud" || localName
== "tags" || localName
== "playlist" ) {
384 if( localName
== "itemcount") {
385 const int totalSteps
= attrs
.value( "count" ).toInt();
386 debug() << "itemcount event: " << totalSteps
;
387 setProgressTotalSteps( totalSteps
);
390 else if( localName
== "tags") {
392 bundle
.setPath ( attrs
.value( "path" ) );
393 bundle
.setTitle ( attrs
.value( "title" ) );
394 bundle
.setArtist ( attrs
.value( "artist" ) );
395 bundle
.setComposer ( attrs
.value( "composer" ) );
396 bundle
.setAlbum ( attrs
.value( "album" ) );
397 bundle
.setComment ( attrs
.value( "comment" ) );
398 bundle
.setGenre ( attrs
.value( "genre" ) );
399 bundle
.setYear ( attrs
.value( "year" ).toInt() );
400 bundle
.setTrack ( attrs
.value( "track" ).toInt() );
401 bundle
.setDiscNumber( attrs
.value( "discnumber" ).toInt() );
402 bundle
.setBpm ( attrs
.value( "bpm" ).toFloat() );
403 bundle
.setFileType( attrs
.value( "filetype" ).toInt() );
404 bundle
.setUniqueId( attrs
.value( "uniqueid" ) );
405 bundle
.setCompilation( attrs
.value( "compilation" ).toInt() );
407 if( attrs
.value( "audioproperties" ) == "true" ) {
408 bundle
.setBitrate ( attrs
.value( "bitrate" ).toInt() );
409 bundle
.setLength ( attrs
.value( "length" ).toInt() );
410 bundle
.setSampleRate( attrs
.value( "samplerate" ).toInt() );
413 if( !attrs
.value( "filesize" ).isNull()
414 && !attrs
.value( "filesize" ).isEmpty() )
416 bundle
.setFilesize( attrs
.value( "filesize" ).toInt() );
419 CollectionDB::instance()->addSong( &bundle
, m_incremental
);
420 if( !bundle
.uniqueId().isEmpty() )
422 m_fileMapsMutex
.lock();
423 m_filesAdded
[bundle
.uniqueId()] = bundle
.url().path();
424 m_fileMapsMutex
.unlock();
430 else if( localName
== "folder" ) {
431 const QString folder
= attrs
.value( "path" );
432 const QFileInfo
info( folder
);
434 // Update dir statistics for rescanning purposes
436 CollectionDB::instance()->updateDirStats( folder
, info
.lastModified().toTime_t(), true);
438 if( m_incremental
) {
439 m_foldersToRemove
+= folder
;
444 // else if( localName == "playlist" )
445 // QApplication::postEvent( PlaylistBrowser::instance(), new PlaylistFoundEvent( attrs.value( "path" ) ) );
447 else if( localName
== "compilation" )
448 CollectionDB::instance()->checkCompilations( attrs
.value( "path" ), !m_incremental
);
450 else if( localName
== "image" ) {
451 // Deserialize CoverBundle list
452 QString data
= attrs
.value( "list" );
453 QStringList list
= data
.split( "AMAROK_MAGIC" );
454 QList
< QPair
<QString
, QString
> > covers
;
456 for( int i
= 0; i
+ 1 < list
.count(); ) {
457 covers
+= qMakePair( list
[i
], list
[i
+ 1] );
461 CollectionDB::instance()->addImageToAlbum( attrs
.value( "path" ), covers
, CollectionDB::instance()->isConnected() );
464 else if( localName
== "embed" ) {
465 CollectionDB::instance()->addEmbeddedImage( attrs
.value( "path" ), attrs
.value( "hash" ), attrs
.value( "description" ) );
473 ScanController::customEvent( QEvent
* e
)
475 if( e
->type() == RestartEventType
)
477 debug() << "RestartEvent received.";
479 QFile
log( Amarok::saveLocation( QString() ) + "collection_scan.log" );
480 if ( !log
.open( QIODevice::ReadOnly
) )
481 ::warning() << "Failed opening log file " << log
.fileName();
483 QByteArray path
= QByteArray(log
.readAll());
484 m_crashedFiles
<< QString::fromUtf8( path
, path
.length() );
491 m_source
= new QXmlInputSource();
492 m_dataMutex
.unlock();
495 m_reader
= new QXmlSimpleReader();
497 m_reader
->setContentHandler( this );
498 m_reader
->parse( m_source
, true );
500 delete m_scanner
; // Reusing doesn't work, so we have to destroy and reinstantiate
501 m_scanner
= new Amarok::ProcIO();
502 connect( m_scanner
, SIGNAL( readReady( K3ProcIO
* ) ), SLOT( slotReadReady() ) );
504 *m_scanner
<< "amarokcollectionscanner";
505 *m_scanner
<< "--nocrashhandler"; // We want to be able to catch SIGSEGV
514 ThreadManager::Job::customEvent( e
);
518 #include "scancontroller.moc"