![]() |
Mixxx
|
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