You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
amarok/amarok/src/scancontroller.cpp

554 lines
18 KiB

/***************************************************************************
* 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 <tqdeepcopy.h>
#include <tqfileinfo.h>
#include <tqtextcodec.h>
#include <dcopref.h>
#include <tdeapplication.h>
#include <tdelocale.h>
#include <tdemessagebox.h>
////////////////////////////////////////////////////////////////////////////////
// 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<TQStringList>( 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( "<p>The Collection Scanner was unable to process these files:</p>" ) +
"<i>" + m_crashedFiles.join( "<br>" ) + "</i>",
i18n( "Collection Scan Report" ) );
}
else if( m_crashedFiles.size() >= MAX_RESTARTS ) {
KMessageBox::error( 0, i18n( "<p>Sorry, the Collection Scan was aborted, since too many problems were encountered.</p>" ) +
"<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>"
"<p>The following files caused problems:</p>" +
"<i>" + m_crashedFiles.join( "<br>" ) + "</i>",
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<TQString,TQString>::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<TQString> 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<TQString, TQString> > 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"