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/collectionscanner/collectionscanner.cpp

484 lines
15 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 "CollectionScanner"
#include "amarok.h"
#include "collectionscanner.h"
#include "collectionscannerdcophandler.h"
#include "debug.h"
#include <cerrno>
#include <iostream>
#include <dirent.h> //stat
#include <limits.h> //PATH_MAX
#include <stdlib.h> //realpath
#include <taglib/audioproperties.h>
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <taglib/tstring.h>
#include <tqdom.h>
#include <tqfile.h>
#include <tqtimer.h>
#include <dcopref.h>
#include <tdeglobal.h>
#include <tdelocale.h>
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 << "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>";
std::cout << "<scanner>";
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 << "</scanner>" << 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
if (m_processedDirs.count() > 0) {
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<TQString, TQString> CoverBundle;
TQStringList validImages; validImages << "jpg" << "png" << "gif" << "jpeg";
TQStringList validPlaylists; validPlaylists << "m3u" << "pls";
TQValueList<CoverBundle> 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<CoverBundle>::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 << "<dud/>";
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"