1 /***************************************************************************
2 * Copyright (C) 2003-2005 by The Amarok Developers *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
18 ***************************************************************************/
20 #define DEBUG_PREFIX "CollectionScanner"
23 #include "collectionscanner.h"
29 #include <limits.h> //PATH_MAX
30 #include <stdlib.h> //realpath
32 #include <audioproperties.h>
47 #include <amarok_collection_interface.h>
49 CollectionScanner::CollectionScanner( const QStringList
& folders
,
54 : KApplication( /*GUIenabled*/ false )
55 , m_importPlaylists( importPlaylists
)
56 , m_folders( folders
)
57 , m_recursively( recursive
)
58 , m_incremental( incremental
)
59 , m_restart( restart
)
60 , m_logfile( Amarok::saveLocation( QString() ) + "collection_scan.log" )
62 amarokCollectionInterface
= new OrgKdeAmarokCollectionInterface("org.kde.amarok", "/Collection", QDBusConnection::sessionBus());
63 kapp
->setObjectName( QString( "amarokcollectionscanner" ).toAscii() );
65 QFile::remove( m_logfile
);
67 QTimer::singleShot( 0, this, SLOT( doJob() ) );
71 CollectionScanner::~CollectionScanner()
74 delete amarokCollectionInterface
;
79 CollectionScanner::doJob() //SLOT
81 std::cout
<< "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>";
82 std::cout
<< "<scanner>";
88 QFile
logFile( m_logfile
);
90 if ( !logFile
.open( QIODevice::ReadOnly
) )
91 warning() << "Failed to open log file " << logFile
.fileName() << " read-only"
94 QTextStream logStream
;
95 logStream
.setDevice(&logFile
);
96 logStream
.setCodec(QTextCodec::codecForName( "UTF-8" ) );
97 lastFile
= logStream
.readAll();
101 QFile
folderFile( Amarok::saveLocation( QString() ) + "collection_scan.files" );
102 if ( !folderFile
.open( QIODevice::ReadOnly
) )
103 warning() << "Failed to open folder file " << folderFile
.fileName()
106 QTextStream folderStream
;
107 folderStream
.setDevice(&folderFile
);
108 folderStream
.setCodec( QTextCodec::codecForName( "UTF-8" ) );
109 entries
= folderStream
.readAll().split( "\n" );
112 for( int count
= entries
.indexOf( lastFile
) + 1; count
; --count
)
117 foreach( QString dir
, m_folders
) {
119 //apparently somewhere empty strings get into the mix
120 //which results in a full-system scan! Which we can't allow
123 if( !dir
.endsWith( '/' ) )
125 readDir( dir
, entries
);
128 QFile
folderFile( Amarok::saveLocation( QString() ) + "collection_scan.files" );
129 folderFile
.open( QIODevice::WriteOnly
);
130 QTextStream
stream( &folderFile
);
131 stream
.setCodec( QTextCodec::codecForName("UTF-8") );
132 stream
<< entries
.join( "\n" );
136 if( !entries
.isEmpty() ) {
138 AttributeMap attributes
;
139 attributes
["count"] = QString::number( entries
.count() );
140 writeElement( "itemcount", attributes
);
143 scanFiles( entries
);
146 std::cout
<< "</scanner>" << std::endl
;
152 CollectionScanner::readDir( const QString
& dir
, QStringList
& entries
)
154 // linux specific, but this fits the 90% rule
155 if( dir
.startsWith( "/dev" ) || dir
.startsWith( "/sys" ) || dir
.startsWith( "/proc" ) )
158 m_scannedFolders
<< d
.canonicalPath();
163 AttributeMap attributes
;
164 attributes
["path"] = dir
;
165 writeElement( "folder", attributes
);
166 d
.setFilter( QDir::NoDotAndDotDot
| QDir::Dirs
| QDir::Files
| QDir::Readable
);
167 foreach( QFileInfo f
, d
.entryInfoList() )
173 f
= QFileInfo(f
.symLinkTarget() );
175 if( f
.isDir() && m_recursively
&& !m_scannedFolders
.contains( f
.canonicalFilePath() ) )
177 readDir( f
.absoluteFilePath() + '/', entries
);
179 else if( f
.isFile() )
180 entries
.append( f
.absoluteFilePath() );
186 CollectionScanner::scanFiles( const QStringList
& entries
)
190 typedef QPair
<QString
, QString
> CoverBundle
;
192 QStringList validImages
; validImages
<< "jpg" << "png" << "gif" << "jpeg";
193 QStringList validPlaylists
; validPlaylists
<< "m3u" << "pls";
195 QList
<CoverBundle
> covers
;
200 oldForeachType( QStringList
, entries
) {
201 const QString path
= *it
;
202 const QString ext
= extension( path
);
203 const QString dir
= directory( path
);
207 // Write path to logfile
208 if( !m_logfile
.isEmpty() ) {
209 QFile
log( m_logfile
);
210 if( log
.open( QIODevice::WriteOnly
) ) {
211 QByteArray cPath
= path
.toUtf8();
212 log
.write( cPath
, cPath
.length() );
217 if( validImages
.contains( ext
) )
220 else if( m_importPlaylists
&& validPlaylists
.contains( ext
) ) {
221 AttributeMap attributes
;
222 attributes
["path"] = path
;
223 writeElement( "playlist", attributes
);
227 MetaBundle::EmbeddedImageList images
;
228 MetaBundle
mb( KUrl( path
), true, TagLib::AudioProperties::Fast
, &images
);
229 const AttributeMap attributes
= readTags( mb
);
231 if( !attributes
.empty() ) {
232 writeElement( "tags", attributes
);
234 CoverBundle
cover( attributes
["artist"], attributes
["album"] );
236 if( !covers
.contains( cover
) )
239 oldForeachType( MetaBundle::EmbeddedImageList
, images
) {
240 AttributeMap attributes
;
241 attributes
["path"] = path
;
242 attributes
["hash"] = (*it
).hash();
243 attributes
["description"] = (*it
).description();
244 writeElement( "embed", attributes
);
249 // Update Compilation-flag, when this is the last loop-run
250 // or we're going to switch to another dir in the next run
251 QStringList::ConstIterator
itTemp( it
);
253 if( path
== entries
.last() || dir
!= directory( *itTemp
) )
255 // we entered the next directory
256 oldForeachType( QStringList
, images
) {
257 // Serialize CoverBundle list with AMAROK_MAGIC as separator
260 for( QList
<CoverBundle
>::ConstIterator it2
= covers
.begin(); it2
!= covers
.end(); ++it2
)
261 string
+= (*it2
).first
+ "AMAROK_MAGIC" + (*it2
).second
+ "AMAROK_MAGIC";
263 AttributeMap attributes
;
264 attributes
["path"] = *it
;
265 attributes
["list"] = string
;
266 writeElement( "image", attributes
);
269 AttributeMap attributes
;
270 attributes
["path"] = dir
;
271 writeElement( "compilation", attributes
);
273 // clear now because we've processed them
282 CollectionScanner::readTags( const MetaBundle
& mb
)
284 // Tests reveal the following:
286 // TagLib::AudioProperties Relative Time Taken
288 // No AudioProp Reading 1
293 AttributeMap attributes
;
295 if ( !mb
.isValidMedia() ) {
296 std::cout
<< "<dud/>";
300 attributes
["path"] = mb
.url().path();
301 attributes
["title"] = mb
.title();
302 attributes
["artist"] = mb
.artist();
303 attributes
["composer"]= mb
.composer();
304 attributes
["album"] = mb
.album();
305 attributes
["comment"] = mb
.comment();
306 attributes
["genre"] = mb
.genre();
307 attributes
["year"] = mb
.year() ? QString::number( mb
.year() ) : QString();
308 attributes
["track"] = mb
.track() ? QString::number( mb
.track() ) : QString();
309 attributes
["discnumber"] = mb
.discNumber() ? QString::number( mb
.discNumber() ) : QString();
310 attributes
["bpm"] = mb
.bpm() ? QString::number( mb
.bpm() ) : QString();
311 attributes
["filetype"] = QString::number( mb
.fileType() );
312 attributes
["uniqueid"] = mb
.uniqueId();
313 attributes
["compilation"] = QString::number( mb
.compilation() );
315 if ( mb
.audioPropertiesUndetermined() )
316 attributes
["audioproperties"] = "false";
318 attributes
["audioproperties"] = "true";
319 attributes
["bitrate"] = QString::number( mb
.bitrate() );
320 attributes
["length"] = QString::number( mb
.length() );
321 attributes
["samplerate"] = QString::number( mb
.sampleRate() );
324 if ( mb
.filesize() >= 0 )
325 attributes
["filesize"] = QString::number( mb
.filesize() );
332 CollectionScanner::writeElement( const QString
& name
, const AttributeMap
& attributes
)
334 QDomDocument doc
; // A dummy. We don't really use DOM, but SAX2
335 QDomElement element
= doc
.createElement( name
);
337 oldForeachType( AttributeMap
, attributes
)
339 // There are at least some characters that Qt cannot categorize which make the resulting
340 // xml document ill-formed and prevent the parser from processing the remaining document.
341 // Because of this we skip attributes containing characters not belonging to any category.
342 QString data
= it
.value();
343 const unsigned len
= data
.length();
344 bool nonPrint
= false;
345 for( unsigned i
= 0; i
< len
; i
++ )
347 if( data
[i
].category() == QChar::NoCategory
)
357 element
.setAttribute( it
.key(), it
.value() );
361 QTextStream
stream( &text
, QIODevice::WriteOnly
);
362 element
.save( stream
, 0 );
365 std::cout
<< text
.toUtf8().data() << std::endl
;
369 #include "collectionscanner.moc"