/*************************************************************************** * Copyright (C) 2003-2005 by The Amarok Developers * * * * This program 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 2 of the License, or * * (at your option) any later version. * * * * This program 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 this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Steet, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #define DEBUG_PREFIX "ScanController" #include "amarok.h" #include "amarokconfig.h" #include "collectiondb.h" #include "debug.h" #include "metabundle.h" #include "mountpointmanager.h" #include "playlist.h" #include "playlistbrowser.h" #include "scancontroller.h" #include "statusbar.h" #include #include #include #include #include #include #include //////////////////////////////////////////////////////////////////////////////// // class ScanController //////////////////////////////////////////////////////////////////////////////// ScanController* ScanController::currController = 0; ScanController* ScanController::instance() { return currController; } void ScanController::setInstance( ScanController* curr ) { currController = curr; } ScanController::ScanController( CollectionDB* parent, bool incremental, const TQStringList& folders ) : DependentJob( parent, "CollectionScanner" ) , TQXmlDefaultHandler() , m_scanner( new Amarok::ProcIO() ) , m_folders( TQDeepCopy( folders ) ) , m_incremental( incremental ) , m_hasChanged( false ) , m_source( new TQXmlInputSource() ) , m_reader( new TQXmlSimpleReader() ) , m_waitingBundle( 0 ) , m_lastCommandPaused( false ) , m_isPaused( false ) , m_tablesCreated( false ) , m_scanCount( 0 ) { DEBUG_BLOCK ScanController::setInstance( this ); m_reader->setContentHandler( this ); m_reader->parse( m_source, true ); connect( this, TQT_SIGNAL( scanDone( bool ) ), MountPointManager::instance(), TQT_SLOT( updateStatisticsURLs( bool ) ) ); connect( m_scanner, TQT_SIGNAL( readReady( KProcIO* ) ), TQT_SLOT( slotReadReady() ) ); *m_scanner << "amarokcollectionscanner"; *m_scanner << "--nocrashhandler"; // We want to be able to catch SIGSEGV // TDEProcess must be started from the GUI thread, so we're invoking the scanner // here in the ctor: if( incremental ) { setDescription( i18n( "Updating Collection" ) ); initIncremental(); } else { setDescription( i18n( "Building Collection" ) ); *m_scanner << "-p"; if( AmarokConfig::scanRecursively() ) *m_scanner << "-r"; *m_scanner << m_folders; m_scanner->start(); } } ScanController::~ScanController() { DEBUG_BLOCK if( !isAborted() && !m_crashedFiles.empty() ) { KMessageBox::information( 0, i18n( "

The Collection Scanner was unable to process these files:

" ) + "" + m_crashedFiles.join( "
" ) + "
", i18n( "Collection Scan Report" ) ); } else if( m_crashedFiles.size() >= MAX_RESTARTS ) { KMessageBox::error( 0, i18n( "

Sorry, the Collection Scan was aborted, since too many problems were encountered.

" ) + "

Advice: A common source for this problem is a broken 'TagLib' package on your computer. Replacing this package may help fixing the issue.

" "

The following files caused problems:

" + "" + m_crashedFiles.join( "
" ) + "
", i18n( "Collection Scan Error" ) ); } m_scanner->kill(); delete m_scanner; delete m_reader; delete m_source; ScanController::setInstance( 0 ); } // Cause the CollectionDB to emit fileDeleted() signals void ScanController::completeJob( void ) { m_fileMapsMutex.lock(); TQMap::Iterator it; if( !m_incremental ) { CollectionDB::instance()->emitFilesAdded( m_filesAdded ); } else { for( it = m_filesAdded.begin(); it != m_filesAdded.end(); ++it ) { if( m_filesDeleted.contains( it.key() ) ) m_filesDeleted.remove( it.key() ); } for( it = m_filesAdded.begin(); it != m_filesAdded.end(); ++it ) CollectionDB::instance()->emitFileAdded( it.data(), it.key() ); for( it = m_filesDeleted.begin(); it != m_filesDeleted.end(); ++it ) CollectionDB::instance()->emitFileDeleted( it.data(), it.key() ); } m_fileMapsMutex.unlock(); emit scanDone( !m_incremental || m_hasChanged ); ThreadManager::DependentJob::completeJob(); } /** * The Incremental Scanner works as follows: Here we check the mtime of every directory in the "directories" * table and store all changed directories in m_folders. * * These directories are then scanned in CollectionReader::doJob(), with m_recursively set according to the * user's preference, so the user can add directories or whole directory trees, too. Since we don't want to * rescan unchanged subdirectories, CollectionReader::readDir() checks if we are scanning recursively and * prevents that. */ void ScanController::initIncremental() { DEBUG_BLOCK connect( CollectionDB::instance(), TQT_SIGNAL( fileMoved( const TQString &, const TQString & ) ), TQT_SLOT( slotFileMoved( const TQString &, const TQString & ) ) ); connect( CollectionDB::instance(), TQT_SIGNAL( fileMoved( const TQString &, const TQString &, const TQString & ) ), TQT_SLOT( slotFileMoved( const TQString &, const TQString & ) ) ); IdList list = MountPointManager::instance()->getMountedDeviceIds(); TQString deviceIds; foreachType( IdList, list ) { if ( !deviceIds.isEmpty() ) deviceIds += ','; deviceIds += TQString::number(*it); } const TQStringList values = CollectionDB::instance()->query( TQString( "SELECT deviceid, dir, changedate FROM directories WHERE deviceid IN (%1);" ) .arg( deviceIds ) ); foreach( values ) { int id = (*it).toInt(); const TQString folder = MountPointManager::instance()->getAbsolutePath( id, (*++it) ); const TQString mtime = *++it; const TQFileInfo info( folder ); if( info.exists() ) { if( info.lastModified().toTime_t() != mtime.toUInt() ) { m_folders << folder; debug() << "Collection dir changed: " << folder << endl; } } else { // this folder has been removed m_folders << folder; debug() << "Collection dir removed: " << folder << endl; } kapp->processEvents(); // Don't block the GUI } if ( !m_folders.isEmpty() ) { debug() << "Collection was modified." << endl; m_hasChanged = true; Amarok::StatusBar::instance()->shortMessage( i18n( "Updating Collection..." ) ); // Start scanner process if( AmarokConfig::scanRecursively() ) *m_scanner << "-r"; *m_scanner << "-i"; *m_scanner << m_folders; m_scanner->start(); } } bool ScanController::doJob() { DEBUG_BLOCK if( !CollectionDB::instance()->isConnected() ) return false; if( m_incremental && !m_hasChanged ) return true; CollectionDB::instance()->createTables( true ); m_tablesCreated = true; //For a full rescan, we might not have cleared tags table (for devices not plugged //in), so preserve the necessary other tables (eg artist) CollectionDB::instance()->prepareTempTables(); CollectionDB::instance()->invalidateArtistAlbumCache(); main_loop: uint delayCount = 100; /// Main Loop while( !isAborted() ) { if( m_xmlData.isNull() ) { if( !m_scanner->isRunning() ) delayCount--; // Wait a bit after process has exited, so that we have time to parse all data if( delayCount == 0 ) break; msleep( 15 ); } else { m_dataMutex.lock(); TQDeepCopy data = m_xmlData; m_source->setData( data ); m_xmlData = TQString(); m_dataMutex.unlock(); if( !m_reader->parseContinue() ) ::warning() << "parseContinue() failed: " << errorString() << endl << data << endl; } } if( !isAborted() ) { if( m_scanner->normalExit() && !m_scanner->signalled() ) { CollectionDB::instance()->sanitizeCompilations(); if ( m_incremental ) { m_foldersToRemove += m_folders; foreach( m_foldersToRemove ) { m_fileMapsMutex.lock(); CollectionDB::instance()->removeSongsInDir( *it, &m_filesDeleted ); m_fileMapsMutex.unlock(); CollectionDB::instance()->removeDirFromCollection( *it ); } CollectionDB::instance()->removeOrphanedEmbeddedImages(); } else CollectionDB::instance()->clearTables( false ); // empty permanent tables CollectionDB::instance()->copyTempTables(); // copy temp into permanent tables //Clean up unused entries in the main tables (eg artist, composer) CollectionDB::instance()->deleteAllRedundant( "artist" ); CollectionDB::instance()->deleteAllRedundant( "composer" ); CollectionDB::instance()->deleteAllRedundant( "year" ); CollectionDB::instance()->deleteAllRedundant( "genre" ); CollectionDB::instance()->deleteAllRedundant( "album" ); //Remove free space and fragmentation in the DB. Used to run on shutdown, but //that took too long, sometimes causing Amarok to be killed. CollectionDB::instance()->vacuum(); } else { if( m_crashedFiles.size() <= MAX_RESTARTS || m_crashedFiles.size() <= (m_scanCount * MAX_FAILURE_PERCENTAGE) / 100 ) { kapp->postEvent( this, new RestartEvent() ); sleep( 3 ); } else m_aborted = true; goto main_loop; } } if( CollectionDB::instance()->isConnected() ) { m_tablesCreated = false; CollectionDB::instance()->dropTables( true ); // drop temp tables } setProgress100Percent(); return !isAborted(); } void ScanController::slotReadReady() { TQString line; m_dataMutex.lock(); while( m_scanner->readln( line, true, 0 ) != -1 ) { if( !line.startsWith( "exepath=" ) ) // skip binary location info from scanner m_xmlData += line; } m_dataMutex.unlock(); } bool ScanController::requestPause() { DEBUG_BLOCK debug() << "Attempting to pause the collection scanner..." << endl; DCOPRef dcopRef( "amarokcollectionscanner", "scanner" ); m_lastCommandPaused = true; return dcopRef.send( "pause" ); } bool ScanController::requestUnpause() { DEBUG_BLOCK debug() << "Attempting to unpause the collection scanner..." << endl; DCOPRef dcopRef( "amarokcollectionscanner", "scanner" ); m_lastCommandPaused = false; return dcopRef.send( "unpause" ); } void ScanController::requestAcknowledged() { DEBUG_BLOCK if( m_waitingBundle ) m_waitingBundle->scannerAcknowledged(); if( m_lastCommandPaused ) m_isPaused = true; else m_isPaused = false; } void ScanController::slotFileMoved( const TQString &/*src*/, const TQString &/*dest*/) { //why is this needed? TQBob, take a look at this /* if( m_incremental ) // pedantry { m_fileMapsMutex.lock(); m_filesFound[ src ] = true; m_fileMapsMutex.unlock(); } */ } void ScanController::notifyThisBundle( MetaBundle* bundle ) { DEBUG_BLOCK m_waitingBundle = bundle; debug() << "will notify " << m_waitingBundle << endl; } bool ScanController::startElement( const TQString&, const TQString& localName, const TQString&, const TQXmlAttributes& attrs ) { // List of entity names: // // itemcount Number of files overall // folder Folder which is being processed // dud Invalid audio file // tags Valid audio file with metadata // playlist Playlist file // image Cover image // compilation Folder to check for compilation // filesize Size of the track in bytes if( localName == "dud" || localName == "tags" || localName == "playlist" ) { incrementProgress(); } if( localName == "itemcount") { const int totalSteps = attrs.value( "count" ).toInt(); debug() << "itemcount event: " << totalSteps << endl; setProgressTotalSteps( totalSteps ); } else if( localName == "tags") { MetaBundle bundle; bundle.setPath ( attrs.value( "path" ) ); bundle.setTitle ( attrs.value( "title" ) ); bundle.setArtist ( attrs.value( "artist" ) ); bundle.setComposer ( attrs.value( "composer" ) ); bundle.setAlbum ( attrs.value( "album" ) ); bundle.setComment ( attrs.value( "comment" ) ); bundle.setGenre ( attrs.value( "genre" ) ); bundle.setYear ( attrs.value( "year" ).toInt() ); bundle.setTrack ( attrs.value( "track" ).toInt() ); bundle.setDiscNumber( attrs.value( "discnumber" ).toInt() ); bundle.setBpm ( attrs.value( "bpm" ).toFloat() ); bundle.setFileType( attrs.value( "filetype" ).toInt() ); bundle.setUniqueId( attrs.value( "uniqueid" ) ); bundle.setCompilation( attrs.value( "compilation" ).toInt() ); if( attrs.value( "audioproperties" ) == "true" ) { bundle.setBitrate ( attrs.value( "bitrate" ).toInt() ); bundle.setLength ( attrs.value( "length" ).toInt() ); bundle.setSampleRate( attrs.value( "samplerate" ).toInt() ); } if( !attrs.value( "filesize" ).isNull() && !attrs.value( "filesize" ).isEmpty() ) { bundle.setFilesize( attrs.value( "filesize" ).toInt() ); } CollectionDB::instance()->addSong( &bundle, m_incremental ); if( !bundle.uniqueId().isEmpty() ) { m_fileMapsMutex.lock(); m_filesAdded[bundle.uniqueId()] = bundle.url().path(); m_fileMapsMutex.unlock(); } m_scanCount++; } else if( localName == "folder" ) { const TQString folder = attrs.value( "path" ); const TQFileInfo info( folder ); // Update dir statistics for rescanning purposes if( info.exists() ) CollectionDB::instance()->updateDirStats( folder, info.lastModified().toTime_t(), true); if( m_incremental ) { m_foldersToRemove += folder; } } else if( localName == "playlist" ) TQApplication::postEvent( PlaylistBrowser::instance(), new PlaylistFoundEvent( attrs.value( "path" ) ) ); else if( localName == "compilation" ) CollectionDB::instance()->checkCompilations( attrs.value( "path" ), !m_incremental); else if( localName == "image" ) { // Deserialize CoverBundle list TQStringList list = TQStringList::split( "AMAROK_MAGIC", attrs.value( "list" ), true ); TQValueList< TQPair > covers; for( uint i = 0; i < list.count(); ) { covers += tqMakePair( list[i], list[i + 1] ); i += 2; } CollectionDB::instance()->addImageToAlbum( attrs.value( "path" ), covers, CollectionDB::instance()->isConnected() ); } else if( localName == "embed" ) { CollectionDB::instance()->addEmbeddedImage( attrs.value( "path" ), attrs.value( "hash" ), attrs.value( "description" ) ); } return true; } void ScanController::customEvent( TQCustomEvent* e ) { if( e->type() == RestartEventType ) { debug() << "RestartEvent received." << endl; TQFile log( Amarok::saveLocation( TQString() ) + "collection_scan.log" ); if ( !log.open( IO_ReadOnly ) ) ::warning() << "Failed opening log file " << log.name() << endl; else { TQCString path = TQCString(log.readAll()); m_crashedFiles << TQString::fromUtf8( path, path.length() ); } m_dataMutex.lock(); m_xmlData = TQString(); delete m_source; m_source = new TQXmlInputSource(); m_dataMutex.unlock(); delete m_reader; m_reader = new TQXmlSimpleReader(); m_reader->setContentHandler( this ); m_reader->parse( m_source, true ); delete m_scanner; // Reusing doesn't work, so we have to destroy and reinstantiate m_scanner = new Amarok::ProcIO(); connect( m_scanner, TQT_SIGNAL( readReady( KProcIO* ) ), TQT_SLOT( slotReadReady() ) ); *m_scanner << "amarokcollectionscanner"; *m_scanner << "--nocrashhandler"; // We want to be able to catch SIGSEGV if( m_incremental ) *m_scanner << "-i"; *m_scanner << "-p"; *m_scanner << "-s"; m_scanner->start(); } else ThreadManager::Job::customEvent( e ); } #include "scancontroller.moc"