/*************************************************************************** * 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 "CollectionScanner" #include "amarok.h" #include "collectionscanner.h" #include "collectionscannerdcophandler.h" #include "debug.h" #include #include #include //stat #include //PATH_MAX #include //realpath #include #include #include #include #include #include #include #include #include #include CollectionScanner::CollectionScanner( const TQStringList& folders, bool recursive, bool incremental, bool importPlaylists, bool restart ) : TDEApplication( /*allowStyles*/ false, /*GUIenabled*/ false ) , m_importPlaylists( importPlaylists ) , m_folders( folders ) , m_recursively( recursive ) , m_incremental( incremental ) , m_restart( restart ) , m_logfile( Amarok::saveLocation( TQString() ) + "collection_scan.log" ) , m_pause( false ) { DcopCollectionScannerHandler* dcsh = new DcopCollectionScannerHandler(); connect( dcsh, TQT_SIGNAL(pauseRequest()), this, TQT_SLOT(pause()) ); connect( dcsh, TQT_SIGNAL(unpauseRequest()), this, TQT_SLOT(resume()) ); kapp->setName( TQString( "amarokcollectionscanner" ).ascii() ); if( !restart ) TQFile::remove( m_logfile ); TQTimer::singleShot( 0, this, TQT_SLOT( doJob() ) ); } CollectionScanner::~CollectionScanner() { DEBUG_BLOCK } void CollectionScanner::pause() { DEBUG_BLOCK m_pause = true; } void CollectionScanner::resume() { DEBUG_BLOCK m_pause = false; } void CollectionScanner::doJob() //SLOT { std::cout << ""; std::cout << ""; TQStringList entries; if( m_restart ) { TQFile logFile( m_logfile ); TQString lastFile; if ( !logFile.open( IO_ReadOnly ) ) warning() << "Failed to open log file " << logFile.name() << " read-only" << endl; else { TQTextStream logStream; logStream.setDevice(TQT_TQIODEVICE(&logFile)); logStream.setEncoding(TQTextStream::UnicodeUTF8); lastFile = logStream.read(); logFile.close(); } TQFile folderFile( Amarok::saveLocation( TQString() ) + "collection_scan.files" ); if ( !folderFile.open( IO_ReadOnly ) ) warning() << "Failed to open folder file " << folderFile.name() << " read-only" << endl; else { TQTextStream folderStream; folderStream.setDevice(TQT_TQIODEVICE(&folderFile)); folderStream.setEncoding(TQTextStream::UnicodeUTF8); entries = TQStringList::split( "\n", folderStream.read() ); } for( int count = entries.findIndex( lastFile ) + 1; count; --count ) entries.pop_front(); } else { foreachType( TQStringList, m_folders ) { if( (*it).isEmpty() ) //apparently somewhere empty strings get into the mix //which results in a full-system scan! Which we can't allow continue; TQString dir = *it; if( !dir.endsWith( "/" ) ) dir += '/'; readDir( dir, entries ); } TQFile folderFile( Amarok::saveLocation( TQString() ) + "collection_scan.files" ); if ( !folderFile.open( IO_WriteOnly ) ) warning() << "Failed to open folder file " << folderFile.name() << " read-only" << endl; else { TQTextStream stream( &folderFile ); stream.setEncoding(TQTextStream::UnicodeUTF8); stream << entries.join( "\n" ); folderFile.close(); } } if( !entries.isEmpty() ) { if( !m_restart ) { AttributeMap attributes; attributes["count"] = TQString::number( entries.count() ); writeElement( "itemcount", attributes ); } scanFiles( entries ); } std::cout << "" << std::endl; quit(); } void CollectionScanner::readDir( const TQString& dir, TQStringList& entries ) { static DCOPRef dcopRef( "amarok", "collection" ); // linux specific, but this fits the 90% rule if( dir.startsWith( "/dev" ) || dir.startsWith( "/sys" ) || dir.startsWith( "/proc" ) ) return; const TQCString dir8Bit = TQFile::encodeName( dir ); DIR *d = opendir( dir8Bit ); if( d == NULL ) { warning() << "Skipping, " << strerror(errno) << ": " << dir << endl; return; } #ifdef USE_SOLARIS int dfd = d->d_fd; #else int dfd = dirfd(d); #endif if (dfd == -1) { warning() << "Skipping, unable to obtain file descriptor: " << dir << endl; closedir(d); return; } struct stat statBuf; struct stat statBuf_symlink; fstat( dfd, &statBuf ); struct direntry de; memset(&de, 0, sizeof(struct direntry)); de.dev = statBuf.st_dev; de.ino = statBuf.st_ino; int f = -1; #if __GNUC__ < 4 for( unsigned int i = 0; i < m_processedDirs.size(); ++i ) if( memcmp( &m_processedDirs[i], &de, sizeof( direntry ) ) == 0 ) { f = i; break; } #else f = m_processedDirs.find( de ); #endif if ( ! S_ISDIR( statBuf.st_mode ) || f != -1 ) { debug() << "Skipping, already scanned: " << dir << endl; closedir(d); return; } AttributeMap attributes; attributes["path"] = dir; writeElement( "folder", attributes ); m_processedDirs.resize( m_processedDirs.size() + 1 ); m_processedDirs[m_processedDirs.size() - 1] = de; for( dirent *ent; ( ent = readdir( d ) ); ) { TQCString entry (ent->d_name); TQCString entryname (ent->d_name); if ( entry == "." || entry == ".." ) continue; entry.prepend( dir8Bit ); if ( stat( entry, &statBuf ) != 0 ) continue; if ( lstat( entry, &statBuf_symlink ) != 0 ) continue; // loop protection if ( ! ( S_ISDIR( statBuf.st_mode ) || S_ISREG( statBuf.st_mode ) ) ) continue; if ( S_ISDIR( statBuf.st_mode ) && m_recursively && entry.length() && entryname[0] != '.' ) { if ( S_ISLNK( statBuf_symlink.st_mode ) ) { char nosymlink[PATH_MAX]; if ( realpath( entry, nosymlink ) ) { debug() << entry << " is a symlink. Using: " << nosymlink << endl; entry = nosymlink; } } const TQString file = TQFile::decodeName( entry ); bool isInCollection = false; if( m_incremental ) dcopRef.call( "isDirInCollection", file ).get( isInCollection ); if( !m_incremental || !isInCollection ) // we MUST add a '/' after the dirname readDir( file + '/', entries ); } else if( S_ISREG( statBuf.st_mode ) ) entries.append( TQFile::decodeName( entry ) ); } closedir( d ); } void CollectionScanner::scanFiles( const TQStringList& entries ) { DEBUG_BLOCK typedef TQPair CoverBundle; TQStringList validImages; validImages << "jpg" << "png" << "gif" << "jpeg"; TQStringList validPlaylists; validPlaylists << "m3u" << "pls"; TQValueList covers; TQStringList images; int itemCount = 0; DCOPRef dcopRef( "amarok", "collection" ); foreachType( TQStringList, entries ) { const TQString path = *it; const TQString ext = extension( path ); const TQString dir = directory( path ); itemCount++; // Write path to logfile if( !m_logfile.isEmpty() ) { TQFile log( m_logfile ); if( log.open( IO_WriteOnly ) ) { TQCString cPath = path.utf8(); log.writeBlock( cPath, cPath.length() ); log.close(); } } if( validImages.contains( ext ) ) images += path; else if( m_importPlaylists && validPlaylists.contains( ext ) ) { AttributeMap attributes; attributes["path"] = path; writeElement( "playlist", attributes ); } else { MetaBundle::EmbeddedImageList images; MetaBundle mb( KURL::fromPathOrURL( path ), true, TagLib::AudioProperties::Fast, &images ); const AttributeMap attributes = readTags( mb ); if( !attributes.empty() ) { writeElement( "tags", attributes ); CoverBundle cover( attributes["artist"], attributes["album"] ); if( !covers.contains( cover ) ) covers += cover; foreachType( MetaBundle::EmbeddedImageList, images ) { AttributeMap attributes; attributes["path"] = path; attributes["hash"] = (*it).hash(); attributes["description"] = (*it).description(); writeElement( "embed", attributes ); } } } // Update Compilation-flag, when this is the last loop-run // or we're going to switch to another dir in the next run TQStringList::ConstIterator itTemp( it ); ++itTemp; if( path == entries.last() || dir != directory( *itTemp ) ) { // we entered the next directory foreachType( TQStringList, images ) { // Serialize CoverBundle list with AMAROK_MAGIC as separator TQString string; for( TQValueList::ConstIterator it2 = covers.begin(); it2 != covers.end(); ++it2 ) string += (*it2).first + "AMAROK_MAGIC" + (*it2).second + "AMAROK_MAGIC"; AttributeMap attributes; attributes["path"] = *it; attributes["list"] = string; writeElement( "image", attributes ); } AttributeMap attributes; attributes["path"] = dir; writeElement( "compilation", attributes ); // clear now because we've processed them covers.clear(); images.clear(); } if( itemCount % 20 == 0 ) { kapp->processEvents(); // let DCOP through! if( m_pause ) { dcopRef.send( "scannerAcknowledged" ); while( m_pause ) { sleep( 1 ); kapp->processEvents(); } dcopRef.send( "scannerAcknowledged" ); } } } } AttributeMap CollectionScanner::readTags( const MetaBundle& mb ) { // Tests reveal the following: // // TagLib::AudioProperties Relative Time Taken // // No AudioProp Reading 1 // Fast 1.18 // Average Untested // Accurate Untested AttributeMap attributes; if ( !mb.isValidMedia() ) { std::cout << ""; return attributes; } attributes["path"] = mb.url().path(); attributes["title"] = mb.title(); attributes["artist"] = mb.artist(); attributes["composer"]= mb.composer(); attributes["album"] = mb.album(); attributes["comment"] = mb.comment(); attributes["genre"] = mb.genre(); attributes["year"] = mb.year() ? TQString::number( mb.year() ) : TQString(); attributes["track"] = mb.track() ? TQString::number( mb.track() ) : TQString(); attributes["discnumber"] = mb.discNumber() ? TQString::number( mb.discNumber() ) : TQString(); attributes["bpm"] = mb.bpm() ? TQString::number( mb.bpm() ) : TQString(); attributes["filetype"] = TQString::number( mb.fileType() ); attributes["uniqueid"] = mb.uniqueId(); attributes["compilation"] = TQString::number( mb.compilation() ); if ( mb.audioPropertiesUndetermined() ) attributes["audioproperties"] = "false"; else { attributes["audioproperties"] = "true"; attributes["bitrate"] = TQString::number( mb.bitrate() ); attributes["length"] = TQString::number( mb.length() ); attributes["samplerate"] = TQString::number( mb.sampleRate() ); } if ( mb.filesize() >= 0 ) attributes["filesize"] = TQString::number( mb.filesize() ); return attributes; } void CollectionScanner::writeElement( const TQString& name, const AttributeMap& attributes ) { TQDomDocument doc; // A dummy. We don't really use DOM, but SAX2 TQDomElement element = doc.createElement( name ); foreachType( AttributeMap, attributes ) { // There are at least some characters that TQt cannot categorize which make the resulting // xml document ill-formed and prevent the parser from processing the remaining document. // Because of this we skip attributes containing characters not belonging to any category. TQString data = it.data(); const unsigned len = data.length(); bool nonPrint = false; for( unsigned i = 0; i < len; i++ ) { if( data.ref( i ).category() == TQChar::NoCategory ) { nonPrint = true; break; } } if( nonPrint ) continue; element.setAttribute( it.key(), it.data() ); } TQString text; TQTextStream stream( &text, IO_WriteOnly ); element.save( stream, 0 ); std::cout << text.utf8().data() << std::endl; } #include "collectionscanner.moc"