PageRenderTime 150ms CodeModel.GetById 40ms app.highlight 65ms RepoModel.GetById 36ms app.codeStats 4ms

/src/musicscanner.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 380 lines | 280 code | 76 blank | 24 comment | 34 complexity | 99fcd872b9ced23d3cd3bb5dbf1d8321 MD5 | raw file
  1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2 *
  3 *   Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
  4 *
  5 *   Tomahawk is free software: you can redistribute it and/or modify
  6 *   it under the terms of the GNU General Public License as published by
  7 *   the Free Software Foundation, either version 3 of the License, or
  8 *   (at your option) any later version.
  9 *
 10 *   Tomahawk is distributed in the hope that it will be useful,
 11 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 12 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13 *   GNU General Public License for more details.
 14 *
 15 *   You should have received a copy of the GNU General Public License
 16 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 17 */
 18
 19#include "musicscanner.h"
 20
 21#include <QtCore/QCoreApplication>
 22
 23#include "utils/tomahawkutils.h"
 24#include "tomahawksettings.h"
 25#include "sourcelist.h"
 26#include "database/database.h"
 27#include "database/databasecommand_dirmtimes.h"
 28#include "database/databasecommand_filemtimes.h"
 29#include "database/databasecommand_collectionstats.h"
 30#include "database/databasecommand_addfiles.h"
 31#include "database/databasecommand_deletefiles.h"
 32#include "taghandlers/tag.h"
 33
 34#include "utils/logger.h"
 35
 36using namespace Tomahawk;
 37
 38
 39void
 40DirLister::go()
 41{
 42    foreach ( const QString& dir, m_dirs )
 43    {
 44        m_opcount++;
 45        QMetaObject::invokeMethod( this, "scanDir", Qt::QueuedConnection, Q_ARG( QDir, QDir( dir, 0 ) ), Q_ARG( int, 0 ) );
 46    }
 47}
 48
 49
 50void
 51DirLister::scanDir( QDir dir, int depth )
 52{
 53    if ( isDeleting() )
 54    {
 55        m_opcount--;
 56        if ( m_opcount == 0 )
 57            emit finished();
 58
 59        return;
 60    }
 61
 62    tDebug( LOGVERBOSE ) << "DirLister::scanDir scanning:" << dir.canonicalPath();
 63    if ( !dir.exists() )
 64    {
 65        tDebug( LOGVERBOSE ) << "Dir no longer exists, not scanning";
 66
 67        m_opcount--;
 68        if ( m_opcount == 0 )
 69            emit finished();
 70
 71        return;
 72    }
 73
 74    QFileInfoList dirs;
 75
 76    dir.setFilter( QDir::Files | QDir::Readable | QDir::NoDotAndDotDot );
 77    dir.setSorting( QDir::Name );
 78    dirs = dir.entryInfoList();
 79
 80    foreach ( const QFileInfo& di, dirs )
 81        emit fileToScan( di );
 82
 83    dir.setFilter( QDir::Dirs | QDir::Readable | QDir::NoDotAndDotDot );
 84    dirs = dir.entryInfoList();
 85
 86    foreach ( const QFileInfo& di, dirs )
 87    {
 88        const QString canonical = di.canonicalFilePath();
 89        m_opcount++;
 90        QMetaObject::invokeMethod( this, "scanDir", Qt::QueuedConnection, Q_ARG( QDir, di.canonicalFilePath() ), Q_ARG( int, depth + 1 ) );
 91    }
 92
 93    m_opcount--;
 94    if ( m_opcount == 0 )
 95    {
 96        tDebug() << Q_FUNC_INFO << "emitting finished";
 97        emit finished();
 98    }
 99}
100
101
102MusicScanner::MusicScanner( const QStringList& dirs, quint32 bs )
103    : QObject()
104    , m_dirs( dirs )
105    , m_batchsize( bs )
106    , m_dirListerThreadController( 0 )
107{
108    m_ext2mime.insert( "mp3", TomahawkUtils::extensionToMimetype( "mp3" ) );
109    m_ext2mime.insert( "ogg", TomahawkUtils::extensionToMimetype( "ogg" ) );
110    m_ext2mime.insert( "oga", TomahawkUtils::extensionToMimetype( "oga" ) );
111    m_ext2mime.insert( "mpc", TomahawkUtils::extensionToMimetype( "mpc" ) );
112    m_ext2mime.insert( "wma", TomahawkUtils::extensionToMimetype( "wma" ) );
113    m_ext2mime.insert( "aac", TomahawkUtils::extensionToMimetype( "aac" ) );
114    m_ext2mime.insert( "m4a", TomahawkUtils::extensionToMimetype( "m4a" ) );
115    m_ext2mime.insert( "mp4", TomahawkUtils::extensionToMimetype( "mp4" ) );
116    m_ext2mime.insert( "flac", TomahawkUtils::extensionToMimetype( "flac" ) );
117}
118
119
120MusicScanner::~MusicScanner()
121{
122    tDebug() << Q_FUNC_INFO;
123
124    if ( !m_dirLister.isNull() )
125    {
126        m_dirListerThreadController->quit();;
127        m_dirListerThreadController->wait( 60000 );
128
129        delete m_dirLister.data();
130        delete m_dirListerThreadController;
131        m_dirListerThreadController = 0;
132    }
133}
134
135
136void
137MusicScanner::startScan()
138{
139    tDebug( LOGVERBOSE ) << "Loading mtimes...";
140    m_scanned = m_skipped = m_cmdQueue = 0;
141    m_skippedFiles.clear();
142
143    SourceList::instance()->getLocal()->scanningProgress( m_scanned );
144
145    // trigger the scan once we've loaded old filemtimes
146    //FIXME: For multiple collection support make sure the right prefix gets passed in...or not...
147    //bear in mind that simply passing in the top-level of a defined collection means it will not return items that need
148    //to be removed that aren't in that root any longer -- might have to do the filtering in setMTimes based on strings
149    DatabaseCommand_FileMtimes *cmd = new DatabaseCommand_FileMtimes();
150    connect( cmd, SIGNAL( done( QMap< QString, QMap< unsigned int, unsigned int > > ) ),
151                    SLOT( setFileMtimes( QMap< QString, QMap< unsigned int, unsigned int > > ) ) );
152
153    Database::instance()->enqueue( QSharedPointer< DatabaseCommand >( cmd ) );
154    return;
155}
156
157
158void
159MusicScanner::setFileMtimes( const QMap< QString, QMap< unsigned int, unsigned int > >& m )
160{
161    tDebug( LOGVERBOSE ) << Q_FUNC_INFO << m.count();
162    m_filemtimes = m;
163    scan();
164}
165
166
167void
168MusicScanner::scan()
169{
170    tDebug( LOGEXTRA ) << "Num saved file mtimes from last scan:" << m_filemtimes.size();
171
172    connect( this, SIGNAL( batchReady( QVariantList, QVariantList ) ),
173                     SLOT( commitBatch( QVariantList, QVariantList ) ), Qt::DirectConnection );
174
175    m_dirListerThreadController = new QThread( this );
176
177    m_dirLister = QWeakPointer< DirLister >( new DirLister( m_dirs ) );
178    m_dirLister.data()->moveToThread( m_dirListerThreadController );
179
180    connect( m_dirLister.data(), SIGNAL( fileToScan( QFileInfo ) ),
181                                   SLOT( scanFile( QFileInfo ) ), Qt::QueuedConnection );
182
183    // queued, so will only fire after all dirs have been scanned:
184    connect( m_dirLister.data(), SIGNAL( finished() ),
185                                   SLOT( listerFinished() ), Qt::QueuedConnection );
186
187    m_dirListerThreadController->start();
188    QMetaObject::invokeMethod( m_dirLister.data(), "go" );
189}
190
191
192void
193MusicScanner::listerFinished()
194{
195    tDebug( LOGVERBOSE ) << Q_FUNC_INFO;
196
197    // any remaining stuff that wasnt emitted as a batch:
198    foreach( const QString& key, m_filemtimes.keys() )
199        m_filesToDelete << m_filemtimes[ key ].keys().first();
200
201    tDebug() << "Lister finished: to delete:" << m_filesToDelete;
202
203    if ( m_filesToDelete.length() || m_scannedfiles.length() )
204    {
205        commitBatch( m_scannedfiles, m_filesToDelete );
206        m_scannedfiles.clear();
207        m_filesToDelete.clear();
208
209        tDebug( LOGINFO ) << "Scanning complete, saving to database. ( scanned" << m_scanned << "skipped" << m_skipped << ")";
210        tDebug( LOGEXTRA ) << "Skipped the following files (no tags / no valid audio):";
211        foreach ( const QString& s, m_skippedFiles )
212            tDebug( LOGEXTRA ) << s;
213    }
214    else
215        cleanup();
216}
217
218
219void
220MusicScanner::cleanup()
221{
222    if ( !m_dirLister.isNull() )
223    {
224        m_dirListerThreadController->quit();;
225        m_dirListerThreadController->wait( 60000 );
226
227        delete m_dirLister.data();
228        delete m_dirListerThreadController;
229        m_dirListerThreadController = 0;
230    }
231
232    tDebug() << Q_FUNC_INFO << "emitting finished!";
233    emit finished();
234}
235
236
237void
238MusicScanner::commitBatch( const QVariantList& tracks, const QVariantList& deletethese )
239{
240    if ( deletethese.length() )
241    {
242        tDebug( LOGINFO ) << Q_FUNC_INFO << "deleting" << deletethese.length() << "tracks";
243        executeCommand( QSharedPointer<DatabaseCommand>( new DatabaseCommand_DeleteFiles( deletethese, SourceList::instance()->getLocal() ) ) );
244    }
245
246    if ( tracks.length() )
247    {
248        tDebug( LOGINFO ) << Q_FUNC_INFO << "adding" << tracks.length() << "tracks";
249        executeCommand( QSharedPointer<DatabaseCommand>( new DatabaseCommand_AddFiles( tracks, SourceList::instance()->getLocal() ) ) );
250    }
251}
252
253
254void
255MusicScanner::executeCommand( QSharedPointer< DatabaseCommand > cmd )
256{
257    tDebug() << Q_FUNC_INFO << m_cmdQueue;
258    m_cmdQueue++;
259    connect( cmd.data(), SIGNAL( finished() ), SLOT( commandFinished() ) );
260    Database::instance()->enqueue( cmd );
261}
262
263
264void
265MusicScanner::commandFinished()
266{
267    tDebug() << Q_FUNC_INFO << m_cmdQueue;
268
269    if ( --m_cmdQueue == 0 )
270        cleanup();
271}
272
273
274void
275MusicScanner::scanFile( const QFileInfo& fi )
276{
277    if ( m_filemtimes.contains( "file://" + fi.canonicalFilePath() ) )
278    {
279        if ( fi.lastModified().toUTC().toTime_t() == m_filemtimes.value( "file://" + fi.canonicalFilePath() ).values().first() )
280        {
281            m_filemtimes.remove( "file://" + fi.canonicalFilePath() );
282            return;
283        }
284
285        m_filesToDelete << m_filemtimes.value( "file://" + fi.canonicalFilePath() ).keys().first();
286        m_filemtimes.remove( "file://" + fi.canonicalFilePath() );
287    }
288
289    tDebug( LOGVERBOSE ) << Q_FUNC_INFO << "Scanning file:" << fi.canonicalFilePath();
290    QVariant m = readFile( fi );
291    if ( m.toMap().isEmpty() )
292        return;
293
294    m_scannedfiles << m;
295    if ( m_batchsize != 0 && (quint32)m_scannedfiles.length() >= m_batchsize )
296    {
297        emit batchReady( m_scannedfiles, m_filesToDelete );
298        m_scannedfiles.clear();
299        m_filesToDelete.clear();
300    }
301}
302
303
304QVariant
305MusicScanner::readFile( const QFileInfo& fi )
306{
307    const QString suffix = fi.suffix().toLower();
308
309    if ( !m_ext2mime.contains( suffix ) )
310    {
311        return QVariantMap(); // invalid extension
312    }
313
314    if ( m_scanned )
315        if ( m_scanned % 3 == 0 )
316            SourceList::instance()->getLocal()->scanningProgress( m_scanned );
317    if ( m_scanned % 100 == 0 )
318        tDebug( LOGINFO ) << "Scan progress:" << m_scanned << fi.canonicalFilePath();
319
320    #ifdef COMPLEX_TAGLIB_FILENAME
321        const wchar_t *encodedName = reinterpret_cast< const wchar_t * >( fi.canonicalFilePath().utf16() );
322    #else
323        QByteArray fileName = QFile::encodeName( fi.canonicalFilePath() );
324        const char *encodedName = fileName.constData();
325    #endif
326
327    TagLib::FileRef f( encodedName );
328    if ( f.isNull() || !f.tag() )
329    {
330        m_skippedFiles << fi.canonicalFilePath();
331        m_skipped++;
332        return QVariantMap();
333    }
334
335    int bitrate = 0;
336    int duration = 0;
337
338    Tag *tag = Tag::fromFile( f );
339
340    if ( f.audioProperties() )
341    {
342        TagLib::AudioProperties *properties = f.audioProperties();
343        duration = properties->length();
344        bitrate = properties->bitrate();
345    }
346
347    QString artist = tag->artist().trimmed();
348    QString album  = tag->album().trimmed();
349    QString track  = tag->title().trimmed();
350    if ( artist.isEmpty() || track.isEmpty() )
351    {
352        // FIXME: do some clever filename guessing
353        m_skippedFiles << fi.canonicalFilePath();
354        m_skipped++;
355        return QVariantMap();
356    }
357
358    QString mimetype = m_ext2mime.value( suffix );
359    QString url( "file://%1" );
360
361    QVariantMap m;
362    m["url"]          = url.arg( fi.canonicalFilePath() );
363    m["mtime"]        = fi.lastModified().toUTC().toTime_t();
364    m["size"]         = (unsigned int)fi.size();
365    m["mimetype"]     = mimetype;
366    m["duration"]     = duration;
367    m["bitrate"]      = bitrate;
368    m["artist"]       = artist;
369    m["album"]        = album;
370    m["track"]        = track;
371    m["albumpos"]     = tag->track();
372    m["year"]         = tag->year();
373    m["albumartist"]  = tag->albumArtist();
374    m["composer"]     = tag->composer();
375    m["discnumber"]   = tag->discNumber();
376    m["hash"]         = ""; // TODO
377
378    m_scanned++;
379    return m;
380}