Mixxx

/home/maxime/Projets/Mixxx/1.10/mixxx/src/library/libraryscanner.cpp

Go to the documentation of this file.
00001 /***************************************************************************
00002                           libraryscanner.cpp  -  scans library in a thread
00003                              -------------------
00004     begin                : 11/27/2007
00005     copyright            : (C) 2007 Albert Santoni
00006     email                : gamegod \a\t users.sf.net
00007 ***************************************************************************/
00008 
00009 /***************************************************************************
00010 *                                                                         *
00011 *   This program is free software; you can redistribute it and/or modify  *
00012 *   it under the terms of the GNU General Public License as published by  *
00013 *   the Free Software Foundation; either version 2 of the License, or     *
00014 *   (at your option) any later version.                                   *
00015 *                                                                         *
00016 ***************************************************************************/
00017 
00018 #include <QtCore>
00019 #include <QtDebug>
00020 #include <QDesktopServices>
00021 
00022 #include "soundsourceproxy.h"
00023 #include "library/legacylibraryimporter.h"
00024 #include "libraryscanner.h"
00025 #include "libraryscannerdlg.h"
00026 #include "trackinfoobject.h"
00027 
00028 LibraryScanner::LibraryScanner(TrackCollection* collection) :
00029     m_pCollection(collection),
00030     m_pProgress(NULL),
00031     m_libraryHashDao(m_database),
00032     m_cueDao(m_database),
00033     m_playlistDao(m_database),
00034     m_crateDao(m_database),
00035     m_trackDao(m_database, m_cueDao, m_playlistDao, m_crateDao),
00036     //Don't initialize m_database here, we need to do it in run() so the DB conn is in
00037     //the right thread.
00038     nameFilters(SoundSourceProxy::supportedFileExtensionsString().split(" "))
00039 {
00040 
00041     qDebug() << "Constructed LibraryScanner";
00042     resetCancel();
00043 
00044     // Force the GUI thread's TrackInfoObject cache to be cleared when a library
00045     // scan is finished, because we might have modified the database directly
00046     // when we detected moved files, and the TIOs corresponding to the moved
00047     // files would then have the wrong track location.
00048     connect(this, SIGNAL(scanFinished()),
00049             &(collection->getTrackDAO()), SLOT(clearCache()));
00050 
00051     /* The "Album Artwork" folder within iTunes stores Album Arts.
00052      * It has numerous hundreds of sub folders but no audio files
00053      * We put this folder on a "black list"
00054      * On Windows, the iTunes folder is contained within the standard music folder
00055      * Hence, Mixxx will scan the "Album Arts folder" for standard users which is wasting time
00056      */
00057     QString iTunesArtFolder = "";
00058 #if defined(__WINDOWS__)
00059     iTunesArtFolder = QDesktopServices::storageLocation(QDesktopServices::MusicLocation) + "\\iTunes\\Album Artwork";
00060     iTunesArtFolder.replace(QString("\\"), QString("/"));
00061 #elif defined(__APPLE__)
00062     iTunesArtFolder = QDesktopServices::storageLocation(QDesktopServices::MusicLocation) + "/iTunes/Album Artwork";
00063 #endif
00064     m_directoriesBlacklist << iTunesArtFolder;
00065     qDebug() << "iTunes Album Art path is:" << iTunesArtFolder;
00066 
00067 #ifdef __WINDOWS__
00068     //Blacklist the _Serato_ directory that pollutes "My Music" on Windows.
00069     QString seratoDir = QDesktopServices::storageLocation(QDesktopServices::MusicLocation) + "\\_Serato_";
00070     m_directoriesBlacklist << seratoDir;
00071 #endif
00072 }
00073 
00074 LibraryScanner::~LibraryScanner()
00075 {
00076     //IMPORTANT NOTE: This code runs in the GUI thread, so it should _NOT_ use
00077     //                the m_trackDao that lives inside this class. It should use
00078     //                the DAOs that live in m_pTrackCollection.
00079 
00080     if (isRunning()) {
00081         //Cancel any running library scan...
00082         m_pCollection->slotCancelLibraryScan();
00083         this->cancel();
00084 
00085         wait(); //Wait for thread to finish
00086     }
00087 
00088     //Do housekeeping on the LibraryHashes table.
00089     m_pCollection->getDatabase().transaction();
00090 
00091     //Mark the corresponding file locations in the track_locations table as deleted
00092     //if we find one or more deleted directories.
00093     QStringList deletedDirs;
00094     QSqlQuery query(m_pCollection->getDatabase());
00095     query.prepare("SELECT directory_path FROM LibraryHashes "
00096                   "WHERE directory_deleted=1");
00097     if (query.exec()) {
00098         while (query.next()) {
00099             QString directory = query.value(query.record().indexOf("directory_path")).toString();
00100             deletedDirs << directory;
00101         }
00102     } else {
00103         qDebug() << "Couldn't SELECT deleted directories" << query.lastError();
00104     }
00105 
00106     //Delete any directories that have been marked as deleted...
00107     query.finish();
00108     query.exec("DELETE FROM LibraryHashes "
00109                "WHERE directory_deleted=1");
00110 
00111     //Print out any SQL error, if there was one.
00112     if (query.lastError().isValid()) {
00113         qDebug() << query.lastError();
00114     }
00115 
00116     QString dir;
00117     foreach(dir, deletedDirs) {
00118         m_pCollection->getTrackDAO().markTrackLocationsAsDeleted(dir);
00119     }
00120 
00121     m_pCollection->getDatabase().commit();
00122 
00123     //Close our database connection
00124     Q_ASSERT(!m_database.rollback()); //Rollback any uncommitted transaction
00125     //The above is an ASSERT because there should never be an outstanding
00126     //transaction when this code is called. If there is, it means we probably
00127     //aren't committing a transaction somewhere that should be.
00128     if (m_database.isOpen())
00129         m_database.close();
00130 
00131     qDebug() << "LibraryScanner destroyed";
00132 }
00133 
00134 void LibraryScanner::run()
00135 {
00136     unsigned static id = 0; //the id of this thread, for debugging purposes //XXX copypasta (should factor this out somehow), -kousu 2/2009
00137     QThread::currentThread()->setObjectName(QString("LibraryScanner %1").arg(++id));
00138     //m_pProgress->slotStartTiming();
00139 
00140     //Lower our priority to help not grind crappy computers.
00141     setPriority(QThread::LowPriority);
00142 
00143 
00144     if (!m_database.isValid()) {
00145        m_database = QSqlDatabase::addDatabase("QSQLITE", "LIBRARY_SCANNER");
00146     }
00147 
00148     if (!m_database.isOpen()) {
00149         m_database.setHostName("localhost");
00150         m_database.setDatabaseName(MIXXX_DB_PATH);
00151         m_database.setUserName("mixxx");
00152         m_database.setPassword("mixxx");
00153 
00154         //Open the database connection in this thread.
00155         if (!m_database.open()) {
00156             qDebug() << "Failed to open database from library scanner thread." << m_database.lastError();
00157             return;
00158         }
00159     }
00160 
00161     m_libraryHashDao.setDatabase(m_database);
00162     m_cueDao.setDatabase(m_database);
00163     m_trackDao.setDatabase(m_database);
00164     m_playlistDao.setDatabase(m_database);
00165 
00166     m_libraryHashDao.initialize();
00167     m_cueDao.initialize();
00168     m_trackDao.initialize();
00169     m_playlistDao.initialize();
00170 
00171     m_pCollection->resetLibaryCancellation();
00172 
00173     QTime t2;
00174     t2.start();
00175 
00176     //Try to upgrade the library from 1.7 (XML) to 1.8+ (DB) if needed. If the
00177     //upgrade_filename already exists, then do not try to upgrade since we have
00178     //already done it.
00179     QString upgrade_filename = QDir::homePath().append("/").append(SETTINGS_PATH).append("DBUPGRADED");
00180     qDebug() << "upgrade filename is " << upgrade_filename;
00181     QFile upgradefile(upgrade_filename);
00182     if (!upgradefile.exists())
00183     {
00184         LegacyLibraryImporter libImport(m_trackDao, m_playlistDao);
00185         connect(&libImport, SIGNAL(progress(QString)),
00186                 m_pProgress, SLOT(slotUpdate(QString)),
00187                 Qt::BlockingQueuedConnection);
00188         m_database.transaction();
00189         libImport.import();
00190         m_database.commit();
00191         qDebug("Legacy importer took %d ms", t2.elapsed());
00192 
00193     }
00194 
00195     //Refresh the name filters in case we loaded new
00196     //SoundSource plugins.
00197     nameFilters = SoundSourceProxy::supportedFileExtensionsString().split(" ");
00198 
00199     // Time the library scanner.
00200     QTime t;
00201     t.start();
00202 
00203     //First, we're going to mark all the directories that we've
00204     //previously hashed as needing verification. As we search through the directory tree
00205     //when we rescan, we'll mark any directory that does still exist as verified.
00206     m_libraryHashDao.invalidateAllDirectories();
00207 
00208     //Mark all the tracks in the library as needing
00209     //verification of their existance...
00210     //(ie. we want to check they're still on your hard drive where
00211     //we think they are)
00212     m_trackDao.invalidateTrackLocationsInLibrary(m_qLibraryPath);
00213 
00214     qDebug() << "Recursively scanning library.";
00215     //Start scanning the library.
00216     //THIS SHOULD NOT BE IN A TRANSACTION! Each addTrack() call from inside
00217     //recursiveScan() handles it's own transactions.
00218 
00219     QList<TrackInfoObject*> tracksToAdd;
00220 
00221     bool bScanFinishedCleanly = recursiveScan(m_qLibraryPath, tracksToAdd);
00222 
00223     if (!bScanFinishedCleanly) {
00224         qDebug() << "Recursive scan interrupted.";
00225     } else {
00226         qDebug() << "Recursive scan finished cleanly.";
00227     }
00228         /*
00229      * We store the scanned files in the database: Note that the recursiveScan()
00230      * method used TrackCollection::importDirectory() to scan the folders. The
00231      * method TrackCollection::importDirectory() added all the files to the
00232      * 'tracksToAdd' list.
00233      *
00234      * The following statement writes all the scanned tracks in the list to the
00235      * database at once. We don't care if the scan has been cancelled or not.
00236      *
00237      * This new approach accelerates the scanning process massively by a factor
00238      * of 3-4 !!!
00239      */
00240 
00241     // Runs inside a transaction. Do not unremove files.
00242     m_trackDao.addTracks(tracksToAdd, false);
00243 
00244     QMutableListIterator<TrackInfoObject*> it(tracksToAdd);
00245     while (it.hasNext()) {
00246         TrackInfoObject* pTrack = it.next();
00247         it.remove();
00248         delete pTrack;
00249     }
00250 
00251     //Start a transaction for all the library hashing (moved file detection)
00252     //stuff.
00253     m_database.transaction();
00254 
00255     //At the end of a scan, mark all tracks and directories that
00256     //weren't "verified" as "deleted" (as long as the scan wasn't cancelled
00257     //half way through. This condition is important because our rescanning
00258     //algorithm starts by marking all tracks and dirs as unverified, so a
00259     //cancelled scan might leave half of your library as unverified. Don't
00260     //want to mark those tracks/dirs as deleted in that case) :)
00261     if (bScanFinishedCleanly)
00262     {
00263         qDebug() << "Marking unverified tracks as deleted.";
00264         m_trackDao.markUnverifiedTracksAsDeleted();
00265         qDebug() << "Marking unverified directories as deleted.";
00266         m_libraryHashDao.markUnverifiedDirectoriesAsDeleted();
00267 
00268         //Check to see if the "deleted" tracks showed up in another location,
00269         //and if so, do some magic to update all our tables.
00270         qDebug() << "Detecting moved files.";
00271         m_trackDao.detectMovedFiles();
00272 
00273         //Remove the hashes for any directories that have been
00274         //marked as deleted to clean up. We need to do this otherwise
00275         //we can skip over songs if you move a set of songs from directory
00276         //A to B, then back to A.
00277         m_libraryHashDao.removeDeletedDirectoryHashes();
00278 
00279         m_database.commit();
00280         qDebug() << "Scan finished cleanly";
00281     }
00282     else {
00283         m_database.rollback();
00284         qDebug() << "Scan cancelled";
00285     }
00286 
00287     qDebug("Scan took: %d ms", t.elapsed());
00288 
00289     //m_pProgress->slotStopTiming();
00290 
00291     Q_ASSERT(!m_database.rollback()); //Rollback any uncommitted transaction
00292     //The above is an ASSERT because there should never be an outstanding
00293     //transaction when this code is called. If there is, it means we probably
00294     //aren't committing a transaction somewhere that should be.
00295     m_database.close();
00296 
00297     resetCancel();
00298     emit(scanFinished());
00299 }
00300 
00301 void LibraryScanner::scan(QString libraryPath)
00302 {
00303     m_qLibraryPath = libraryPath;
00304     m_pProgress = new LibraryScannerDlg();
00305     m_pProgress->setAttribute(Qt::WA_DeleteOnClose);
00306 
00307     //The important part here is that we need to use
00308     //Qt::BlockingQueuedConnection, because we're sending these signals across
00309     //threads. Normally you'd use regular QueuedConnections for this, but since
00310     //we don't have an event loop running and we need the signals to get
00311     //processed immediately, we have to use
00312     //BlockingQueuedConnection. (DirectConnection isn't an option for sending
00313     //signals across threads.)
00314     connect(m_pCollection, SIGNAL(progressLoading(QString)),
00315             m_pProgress, SLOT(slotUpdate(QString)));
00316             //Qt::BlockingQueuedConnection);
00317     connect(this, SIGNAL(progressHashing(QString)),
00318             m_pProgress, SLOT(slotUpdate(QString)));
00319             //Qt::BlockingQueuedConnection);
00320     connect(this, SIGNAL(scanFinished()),
00321             m_pProgress, SLOT(slotScanFinished()));
00322     connect(m_pProgress, SIGNAL(scanCancelled()),
00323             m_pCollection, SLOT(slotCancelLibraryScan()));
00324     connect(m_pProgress, SIGNAL(scanCancelled()),
00325             this, SLOT(cancel()));
00326     scan();
00327 }
00328 
00329 void LibraryScanner::cancel()
00330 {
00331     m_libraryScanMutex.lock();
00332     m_bCancelLibraryScan = 1;
00333     m_libraryScanMutex.unlock();
00334 }
00335 
00336 void LibraryScanner::resetCancel()
00337 {
00338     m_libraryScanMutex.lock();
00339     m_bCancelLibraryScan = 0;
00340     m_libraryScanMutex.unlock();
00341 }
00342 
00343 void LibraryScanner::scan()
00344 {
00345     start(); //Starts the thread by calling run()
00346 }
00347 
00352 bool LibraryScanner::recursiveScan(QString dirPath, QList<TrackInfoObject*>& tracksToAdd)
00353 {
00354     QDirIterator fileIt(dirPath, nameFilters, QDir::Files | QDir::NoDotAndDotDot);
00355     QString currentFile;
00356     bool bScanFinishedCleanly = true;
00357 
00358     //qDebug() << "Scanning dir:" << dirPath;
00359 
00360     QString newHashStr;
00361     bool prevHashExists = false;
00362     int newHash = -1;
00363     int prevHash = -1;
00364     //Note: A hash of "0" is a real hash if the directory contains no files!
00365 
00366     while (fileIt.hasNext())
00367     {
00368             currentFile = fileIt.next();
00369             //qDebug() << currentFile;
00370             newHashStr += currentFile;
00371     }
00372 
00373     //Calculate a hash of the directory's file list.
00374     newHash = qHash(newHashStr);
00375 
00376     //Try to retrieve a hash from the last time that directory was scanned.
00377     prevHash = m_libraryHashDao.getDirectoryHash(dirPath);
00378     prevHashExists = (prevHash == -1) ? false : true;
00379 
00380     //Compare the hashes, and if they don't match, rescan the files in that directory!
00381     if (prevHash != newHash)
00382     {
00383         //If we didn't know about this directory before...
00384         if (!prevHashExists) {
00385             m_libraryHashDao.saveDirectoryHash(dirPath, newHash);
00386         }
00387         else //Contents of a known directory have changed.
00388              //Just need to update the old hash in the database and then rescan it.
00389         {
00390             qDebug() << "old hash was" << prevHash << "and new hash is" << newHash;
00391             m_libraryHashDao.updateDirectoryHash(dirPath, newHash, 0);
00392         }
00393 
00394         //Rescan that mofo!
00395         bScanFinishedCleanly = m_pCollection->importDirectory(dirPath, m_trackDao, tracksToAdd);
00396     }
00397     else //prevHash == newHash
00398     {
00399         //The files in the directory haven't changed, so we don't need to do anything, right?
00400         //Wrong! We need to mark the directory in the database as "existing", so that we can
00401         //keep track of directories that have been deleted to stop the database from keeping
00402         //rows about deleted directories around. :)
00403         //qDebug() << "prevHash == newHash";
00404 
00405         // Mark the directory as verified and not deleted.
00406         // m_libraryHashDao.markAsExisting(dirPath);
00407         // m_libraryHashDao.markAsVerified(dirPath);
00408         m_libraryHashDao.updateDirectoryStatus(dirPath, false, true);
00409 
00410         //We also need to mark the tracks _inside_ this directory as verified.
00411         //Note that this doesn't mark the tracks as existing, just that they're in
00412         //the same state as the last time we scanned this directory.
00413         m_trackDao.markTracksInDirectoryAsVerified(dirPath);
00414 
00415         emit(progressHashing(dirPath));
00416     }
00417 
00418     //Let us break out of library directory hashing (the actual file scanning
00419     //stuff is in TrackCollection::importDirectory)
00420     m_libraryScanMutex.lock();
00421     bool cancel = m_bCancelLibraryScan;
00422     m_libraryScanMutex.unlock();
00423     if (cancel)
00424         return false;
00425 
00426 
00427     //Look at all the subdirectories and scan them recursively...
00428     QDirIterator dirIt(dirPath, QDir::Dirs | QDir::NoDotAndDotDot);
00429     while (dirIt.hasNext() && bScanFinishedCleanly)
00430     {
00431         QString nextPath = dirIt.next();
00432 
00433         // Skip the iTunes Album Art Folder since it is probably a waste of
00434         // time.
00435         if (m_directoriesBlacklist.contains(nextPath))
00436             continue;
00437 
00438         if (!recursiveScan(nextPath, tracksToAdd))
00439             bScanFinishedCleanly = false;
00440     }
00441 
00442     return bScanFinishedCleanly;
00443 }
00444 
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines