|
|
|
/***************************************************************************
|
|
|
|
* 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 <kglobal.h>
|
|
|
|
#include <klocale.h>
|
|
|
|
|
|
|
|
|
|
|
|
CollectionScanner::CollectionScanner( const TQStringList& folders,
|
|
|
|
bool recursive,
|
|
|
|
bool incremental,
|
|
|
|
bool importPlaylists,
|
|
|
|
bool restart )
|
|
|
|
: KApplication( /*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
|
|
|
|
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"
|
|
|
|
|