|
MythTV
0.26-pre
|
00001 // POSIX headers 00002 #include <sys/stat.h> 00003 00004 // Qt headers 00005 #include <QApplication> 00006 #include <QDir> 00007 00008 // MythTV headers 00009 #include <mythcontext.h> 00010 #include <mythdb.h> 00011 #include <mythdialogs.h> 00012 #include <mythscreenstack.h> 00013 #include <mythprogressdialog.h> 00014 00015 // MythMusic headers 00016 #include "decoder.h" 00017 #include "filescanner.h" 00018 #include "metadata.h" 00019 #include "metaio.h" 00020 00021 FileScanner::FileScanner() : m_decoder(NULL) 00022 { 00023 MSqlQuery query(MSqlQuery::InitCon()); 00024 00025 // Cache the directory ids from the database 00026 query.prepare("SELECT directory_id, path FROM music_directories"); 00027 if (query.exec()) 00028 { 00029 while(query.next()) 00030 { 00031 m_directoryid[query.value(1).toString()] = query.value(0).toInt(); 00032 } 00033 } 00034 00035 // Cache the genre ids from the database 00036 query.prepare("SELECT genre_id, LOWER(genre) FROM music_genres"); 00037 if (query.exec()) 00038 { 00039 while(query.next()) 00040 { 00041 m_genreid[query.value(1).toString()] = query.value(0).toInt(); 00042 } 00043 } 00044 00045 // Cache the artist ids from the database 00046 query.prepare("SELECT artist_id, LOWER(artist_name) FROM music_artists"); 00047 if (query.exec() || query.isActive()) 00048 { 00049 while(query.next()) 00050 { 00051 m_artistid[query.value(1).toString()] = query.value(0).toInt(); 00052 } 00053 } 00054 00055 // Cache the album ids from the database 00056 query.prepare("SELECT album_id, artist_id, LOWER(album_name) FROM music_albums"); 00057 if (query.exec()) 00058 { 00059 while(query.next()) 00060 { 00061 m_albumid[query.value(1).toString() + "#" + query.value(2).toString()] = query.value(0).toInt(); 00062 } 00063 } 00064 } 00065 00066 FileScanner::~FileScanner () 00067 { 00068 00069 } 00070 00082 void FileScanner::BuildFileList(QString &directory, MusicLoadedMap &music_files, int parentid) 00083 { 00084 QDir d(directory); 00085 00086 if (!d.exists()) 00087 return; 00088 00089 QFileInfoList list = d.entryInfoList(); 00090 if (list.isEmpty()) 00091 return; 00092 00093 QFileInfoList::const_iterator it = list.begin(); 00094 const QFileInfo *fi; 00095 00096 /* Recursively traverse directory, calling QApplication::processEvents() 00097 every now and then to ensure the UI updates */ 00098 int update_interval = 0; 00099 int newparentid = 0; 00100 while (it != list.end()) 00101 { 00102 fi = &(*it); 00103 ++it; 00104 if (fi->fileName() == "." || fi->fileName() == "..") 00105 continue; 00106 QString filename = fi->absoluteFilePath(); 00107 if (fi->isDir()) 00108 { 00109 00110 QString dir(filename); 00111 dir.remove(0, m_startdir.length()); 00112 00113 newparentid = m_directoryid[dir]; 00114 00115 if (newparentid == 0) 00116 { 00117 int id = GetDirectoryId(dir, parentid); 00118 m_directoryid[dir] = id; 00119 00120 if (id > 0) 00121 { 00122 newparentid = id; 00123 } 00124 else 00125 { 00126 LOG(VB_GENERAL, LOG_ERR, 00127 QString("Failed to get directory id for path %1") 00128 .arg(dir)); 00129 } 00130 } 00131 00132 BuildFileList(filename, music_files, newparentid); 00133 00134 qApp->processEvents (); 00135 } 00136 else 00137 { 00138 if (++update_interval > 100) 00139 { 00140 qApp->processEvents(); 00141 update_interval = 0; 00142 } 00143 00144 music_files[filename] = kFileSystem; 00145 } 00146 } 00147 } 00148 00159 int FileScanner::GetDirectoryId(const QString &directory, const int &parentid) 00160 { 00161 if (directory.isEmpty()) 00162 return 0; 00163 00164 MSqlQuery query(MSqlQuery::InitCon()); 00165 00166 // Load the directory id or insert it and get the id 00167 query.prepare("SELECT directory_id FROM music_directories " 00168 "WHERE path = :DIRECTORY ;"); 00169 query.bindValue(":DIRECTORY", directory); 00170 00171 if (query.exec() && query.next()) 00172 { 00173 return query.value(0).toInt(); 00174 } 00175 else 00176 { 00177 query.prepare("INSERT INTO music_directories (path, parent_id) " 00178 "VALUES (:DIRECTORY, :PARENTID);"); 00179 query.bindValue(":DIRECTORY", directory); 00180 query.bindValue(":PARENTID", parentid); 00181 00182 if (!query.exec() || !query.isActive() 00183 || query.numRowsAffected() <= 0) 00184 { 00185 MythDB::DBError("music insert directory", query); 00186 return -1; 00187 } 00188 return query.lastInsertId().toInt(); 00189 } 00190 00191 MythDB::DBError("music select directory id", query); 00192 return -1; 00193 } 00194 00203 bool FileScanner::HasFileChanged(const QString &filename, const QString &date_modified) 00204 { 00205 struct stat stbuf; 00206 00207 QByteArray fname = filename.toLocal8Bit(); 00208 if (stat(fname.constData(), &stbuf) == 0) 00209 { 00210 if (date_modified.isEmpty() || 00211 stbuf.st_mtime > 00212 (time_t)QDateTime::fromString(date_modified, 00213 Qt::ISODate).toTime_t()) 00214 { 00215 return true; 00216 } 00217 } 00218 else { 00219 LOG(VB_GENERAL, LOG_ERR, QString("Failed to stat file: %1") 00220 .arg(filename)); 00221 } 00222 return false; 00223 } 00224 00237 void FileScanner::AddFileToDB(const QString &filename) 00238 { 00239 QString extension = filename.section( '.', -1 ) ; 00240 QString directory = filename; 00241 directory.remove(0, m_startdir.length()); 00242 directory = directory.section( '/', 0, -2); 00243 00244 QString nameFilter = gCoreContext->GetSetting("AlbumArtFilter", "*.png;*.jpg;*.jpeg;*.gif;*.bmp"); 00245 00246 // If this file is an image, insert the details into the music_albumart table 00247 if (nameFilter.indexOf(extension.toLower()) > -1) 00248 { 00249 QString name = filename.section( '/', -1); 00250 00251 MSqlQuery query(MSqlQuery::InitCon()); 00252 query.prepare("INSERT INTO music_albumart SET filename = :FILE, " 00253 "directory_id = :DIRID, imagetype = :TYPE;"); 00254 query.bindValue(":FILE", name); 00255 query.bindValue(":DIRID", m_directoryid[directory]); 00256 query.bindValue(":TYPE", AlbumArtImages::guessImageType(name)); 00257 00258 if (!query.exec() || query.numRowsAffected() <= 0) 00259 { 00260 MythDB::DBError("music insert artwork", query); 00261 } 00262 return; 00263 } 00264 00265 Decoder *decoder = Decoder::create(filename, NULL, NULL, true); 00266 00267 if (decoder) 00268 { 00269 LOG(VB_FILE, LOG_INFO, 00270 QString("Reading metadata from %1").arg(filename)); 00271 Metadata *data = decoder->readMetadata(); 00272 if (data) 00273 { 00274 QString album_cache_string; 00275 00276 // Set values from cache 00277 int did = m_directoryid[directory]; 00278 if (did > 0) 00279 data->setDirectoryId(did); 00280 00281 int aid = m_artistid[data->Artist().toLower()]; 00282 if (aid > 0) 00283 { 00284 data->setArtistId(aid); 00285 00286 // The album cache depends on the artist id 00287 album_cache_string = data->getArtistId() + "#" 00288 + data->Album().toLower(); 00289 00290 if (m_albumid[album_cache_string] > 0) 00291 data->setAlbumId(m_albumid[album_cache_string]); 00292 } 00293 00294 int gid = m_genreid[data->Genre().toLower()]; 00295 if (gid > 0) 00296 data->setGenreId(gid); 00297 00298 // Commit track info to database 00299 data->dumpToDatabase(); 00300 00301 // Update the cache 00302 m_artistid[data->Artist().toLower()] = 00303 data->getArtistId(); 00304 00305 m_genreid[data->Genre().toLower()] = 00306 data->getGenreId(); 00307 00308 album_cache_string = data->getArtistId() + "#" 00309 + data->Album().toLower(); 00310 m_albumid[album_cache_string] = data->getAlbumId(); 00311 00312 // read any embedded images from the tag 00313 MetaIO *tagger = data->getTagger(); 00314 if (tagger && tagger->supportsEmbeddedImages()) 00315 { 00316 AlbumArtList artList = tagger->getAlbumArtList(data->Filename()); 00317 data->setEmbeddedAlbumArt(artList); 00318 data->getAlbumArtImages()->dumpToDatabase(); 00319 } 00320 00321 delete data; 00322 } 00323 00324 delete decoder; 00325 } 00326 } 00327 00334 void FileScanner::cleanDB() 00335 { 00336 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack"); 00337 00338 QString message = QObject::tr("Cleaning music database"); 00339 MythUIProgressDialog *clean_progress = new MythUIProgressDialog(message, 00340 popupStack, 00341 "cleaningprogressdialog"); 00342 00343 if (clean_progress->Create()) 00344 { 00345 popupStack->AddScreen(clean_progress, false); 00346 clean_progress->SetTotal(4); 00347 } 00348 else 00349 { 00350 delete clean_progress; 00351 clean_progress = NULL; 00352 } 00353 00354 uint counter = 0; 00355 00356 MSqlQuery query(MSqlQuery::InitCon()); 00357 MSqlQuery deletequery(MSqlQuery::InitCon()); 00358 00359 if (!query.exec("SELECT g.genre_id FROM music_genres g " 00360 "LEFT JOIN music_songs s ON g.genre_id=s.genre_id " 00361 "WHERE s.genre_id IS NULL;")) 00362 MythDB::DBError("FileScanner::cleanDB - select music_genres", query); 00363 while (query.next()) 00364 { 00365 int genreid = query.value(0).toInt(); 00366 deletequery.prepare("DELETE FROM music_genres WHERE genre_id=:GENREID"); 00367 deletequery.bindValue(":GENREID", genreid); 00368 if (!deletequery.exec()) 00369 MythDB::DBError("FileScanner::cleanDB - delete music_genres", 00370 deletequery); 00371 } 00372 00373 if (clean_progress) 00374 clean_progress->SetProgress(++counter); 00375 00376 if (!query.exec("SELECT a.album_id FROM music_albums a " 00377 "LEFT JOIN music_songs s ON a.album_id=s.album_id " 00378 "WHERE s.album_id IS NULL;")) 00379 MythDB::DBError("FileScanner::cleanDB - select music_albums", query); 00380 while (query.next()) 00381 { 00382 int albumid = query.value(0).toInt(); 00383 deletequery.prepare("DELETE FROM music_albums WHERE album_id=:ALBUMID"); 00384 deletequery.bindValue(":ALBUMID", albumid); 00385 if (!deletequery.exec()) 00386 MythDB::DBError("FileScanner::cleanDB - delete music_albums", 00387 deletequery); 00388 } 00389 00390 if (clean_progress) 00391 clean_progress->SetProgress(++counter); 00392 00393 if (!query.exec("SELECT a.artist_id FROM music_artists a " 00394 "LEFT JOIN music_songs s ON a.artist_id=s.artist_id " 00395 "LEFT JOIN music_albums l ON a.artist_id=l.artist_id " 00396 "WHERE s.artist_id IS NULL AND l.artist_id IS NULL")) 00397 MythDB::DBError("FileScanner::cleanDB - select music_artists", query); 00398 while (query.next()) 00399 { 00400 int artistid = query.value(0).toInt(); 00401 deletequery.prepare("DELETE FROM music_artists WHERE artist_id=:ARTISTID"); 00402 deletequery.bindValue(":ARTISTID", artistid); 00403 if (!deletequery.exec()) 00404 MythDB::DBError("FileScanner::cleanDB - delete music_artists", 00405 deletequery); 00406 } 00407 00408 if (clean_progress) 00409 clean_progress->SetProgress(++counter); 00410 00411 if (!query.exec("SELECT a.albumart_id FROM music_albumart a LEFT JOIN " 00412 "music_songs s ON a.song_id=s.song_id WHERE " 00413 "embedded='1' AND s.song_id IS NULL;")) 00414 MythDB::DBError("FileScanner::cleanDB - select music_albumart", query); 00415 while (query.next()) 00416 { 00417 int albumartid = query.value(0).toInt(); 00418 deletequery.prepare("DELETE FROM music_albumart WHERE albumart_id=:ALBUMARTID"); 00419 deletequery.bindValue(":ALBUMARTID", albumartid); 00420 if (!deletequery.exec()) 00421 MythDB::DBError("FileScanner::cleanDB - delete music_albumart", 00422 deletequery); 00423 } 00424 00425 if (clean_progress) 00426 { 00427 clean_progress->SetProgress(++counter); 00428 clean_progress->Close(); 00429 } 00430 } 00431 00439 void FileScanner::RemoveFileFromDB (const QString &filename) 00440 { 00441 QString sqlfilename(filename); 00442 sqlfilename.remove(0, m_startdir.length()); 00443 // We know that the filename will not contain :// as the SQL limits this 00444 QString directory = sqlfilename.section( '/', 0, -2 ) ; 00445 sqlfilename = sqlfilename.section( '/', -1 ) ; 00446 00447 QString extension = sqlfilename.section( '.', -1 ) ; 00448 00449 QString nameFilter = gCoreContext->GetSetting("AlbumArtFilter", 00450 "*.png;*.jpg;*.jpeg;*.gif;*.bmp"); 00451 00452 if (nameFilter.indexOf(extension) > -1) 00453 { 00454 MSqlQuery query(MSqlQuery::InitCon()); 00455 query.prepare("DELETE FROM music_albumart WHERE filename= :FILE AND " 00456 "directory_id= :DIRID;"); 00457 query.bindValue(":FILE", sqlfilename); 00458 query.bindValue(":DIRID", m_directoryid[directory]); 00459 00460 if (!query.exec() || query.numRowsAffected() <= 0) 00461 { 00462 MythDB::DBError("music delete artwork", query); 00463 } 00464 return; 00465 } 00466 00467 MSqlQuery query(MSqlQuery::InitCon()); 00468 query.prepare("DELETE FROM music_songs WHERE filename = :NAME ;"); 00469 query.bindValue(":NAME", sqlfilename); 00470 if (!query.exec()) 00471 MythDB::DBError("FileScanner::RemoveFileFromDB - deleting music_songs", 00472 query); 00473 } 00474 00482 void FileScanner::UpdateFileInDB(const QString &filename) 00483 { 00484 QString directory = filename; 00485 directory.remove(0, m_startdir.length()); 00486 directory = directory.section( '/', 0, -2); 00487 00488 Decoder *decoder = Decoder::create(filename, NULL, NULL, true); 00489 00490 if (decoder) 00491 { 00492 Metadata *db_meta = decoder->getMetadata(); 00493 Metadata *disk_meta = decoder->readMetadata(); 00494 00495 if (db_meta && disk_meta) 00496 { 00497 disk_meta->setID(db_meta->ID()); 00498 disk_meta->setRating(db_meta->Rating()); 00499 00500 QString album_cache_string; 00501 00502 // Set values from cache 00503 int did = m_directoryid[directory]; 00504 if (did > 0) 00505 disk_meta->setDirectoryId(did); 00506 00507 int aid = m_artistid[disk_meta->Artist().toLower()]; 00508 if (aid > 0) 00509 { 00510 disk_meta->setArtistId(aid); 00511 00512 // The album cache depends on the artist id 00513 album_cache_string = disk_meta->getArtistId() + "#" + 00514 disk_meta->Album().toLower(); 00515 00516 if (m_albumid[album_cache_string] > 0) 00517 disk_meta->setAlbumId(m_albumid[album_cache_string]); 00518 } 00519 00520 int gid = m_genreid[disk_meta->Genre().toLower()]; 00521 if (gid > 0) 00522 disk_meta->setGenreId(gid); 00523 00524 // Commit track info to database 00525 disk_meta->dumpToDatabase(); 00526 00527 // Update the cache 00528 m_artistid[disk_meta->Artist().toLower()] 00529 = disk_meta->getArtistId(); 00530 m_genreid[disk_meta->Genre().toLower()] 00531 = disk_meta->getGenreId(); 00532 album_cache_string = disk_meta->getArtistId() + "#" + 00533 disk_meta->Album().toLower(); 00534 m_albumid[album_cache_string] = disk_meta->getAlbumId(); 00535 } 00536 00537 if (disk_meta) 00538 delete disk_meta; 00539 00540 if (db_meta) 00541 delete db_meta; 00542 00543 delete decoder; 00544 } 00545 } 00546 00556 void FileScanner::SearchDir(QString &directory) 00557 { 00558 00559 m_startdir = directory; 00560 00561 MusicLoadedMap music_files; 00562 MusicLoadedMap::Iterator iter; 00563 00564 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack"); 00565 00566 QString message = QObject::tr("Searching for music files"); 00567 00568 MythUIBusyDialog *busy = new MythUIBusyDialog(message, popupStack, 00569 "musicscanbusydialog"); 00570 00571 if (busy->Create()) 00572 popupStack->AddScreen(busy, false); 00573 else 00574 busy = NULL; 00575 00576 BuildFileList(m_startdir, music_files, 0); 00577 00578 if (busy) 00579 busy->Close(); 00580 00581 ScanMusic(music_files); 00582 ScanArtwork(music_files); 00583 00584 message = QObject::tr("Updating music database"); 00585 MythUIProgressDialog *file_checking = new MythUIProgressDialog(message, 00586 popupStack, 00587 "scalingprogressdialog"); 00588 00589 if (file_checking->Create()) 00590 { 00591 popupStack->AddScreen(file_checking, false); 00592 file_checking->SetTotal(music_files.size()); 00593 } 00594 else 00595 { 00596 delete file_checking; 00597 file_checking = NULL; 00598 } 00599 00600 /* 00601 This can be optimised quite a bit by consolidating all commands 00602 via a lot of refactoring. 00603 00604 1) group all files of the same decoder type, and don't 00605 create/delete a Decoder pr. AddFileToDB. Or make Decoders be 00606 singletons, it should be a fairly simple change. 00607 00608 2) RemoveFileFromDB should group the remove into one big SQL. 00609 00610 3) UpdateFileInDB, same as 1. 00611 */ 00612 00613 uint counter = 0; 00614 for (iter = music_files.begin(); iter != music_files.end(); iter++) 00615 { 00616 if (*iter == kFileSystem) 00617 AddFileToDB(iter.key()); 00618 else if (*iter == kDatabase) 00619 RemoveFileFromDB(iter.key ()); 00620 else if (*iter == kNeedUpdate) 00621 UpdateFileInDB(iter.key()); 00622 00623 if (file_checking) 00624 { 00625 file_checking->SetProgress(++counter); 00626 qApp->processEvents(); 00627 } 00628 } 00629 if (file_checking) 00630 file_checking->Close(); 00631 00632 // Cleanup orphaned entries from the database 00633 cleanDB(); 00634 } 00635 00643 void FileScanner::ScanMusic(MusicLoadedMap &music_files) 00644 { 00645 MusicLoadedMap::Iterator iter; 00646 00647 MSqlQuery query(MSqlQuery::InitCon()); 00648 if (!query.exec("SELECT CONCAT_WS('/', path, filename), date_modified " 00649 "FROM music_songs LEFT JOIN music_directories ON " 00650 "music_songs.directory_id=music_directories.directory_id " 00651 "WHERE filename NOT LIKE ('%://%')")) 00652 MythDB::DBError("FileScanner::ScanMusic", query); 00653 00654 uint counter = 0; 00655 00656 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack"); 00657 00658 QString message = QObject::tr("Scanning music files"); 00659 MythUIProgressDialog *file_checking = new MythUIProgressDialog(message, 00660 popupStack, 00661 "scalingprogressdialog"); 00662 00663 if (file_checking->Create()) 00664 { 00665 popupStack->AddScreen(file_checking, false); 00666 file_checking->SetTotal(query.size()); 00667 } 00668 else 00669 { 00670 delete file_checking; 00671 file_checking = NULL; 00672 } 00673 00674 QString name; 00675 00676 if (query.isActive() && query.size() > 0) 00677 { 00678 while (query.next()) 00679 { 00680 name = m_startdir + query.value(0).toString(); 00681 00682 if (name != QString::null) 00683 { 00684 if ((iter = music_files.find(name)) != music_files.end()) 00685 { 00686 if (music_files[name] == kDatabase) 00687 { 00688 if (file_checking) 00689 { 00690 file_checking->SetProgress(++counter); 00691 qApp->processEvents(); 00692 } 00693 continue; 00694 } 00695 else if (HasFileChanged(name, query.value(1).toString())) 00696 music_files[name] = kNeedUpdate; 00697 else 00698 music_files.erase(iter); 00699 } 00700 else 00701 { 00702 music_files[name] = kDatabase; 00703 } 00704 } 00705 00706 if (file_checking) 00707 { 00708 file_checking->SetProgress(++counter); 00709 qApp->processEvents(); 00710 } 00711 } 00712 } 00713 00714 if (file_checking) 00715 file_checking->Close(); 00716 } 00717 00725 void FileScanner::ScanArtwork(MusicLoadedMap &music_files) 00726 { 00727 MusicLoadedMap::Iterator iter; 00728 00729 MSqlQuery query(MSqlQuery::InitCon()); 00730 if (!query.exec("SELECT CONCAT_WS('/', path, filename) " 00731 "FROM music_albumart LEFT JOIN music_directories ON " 00732 "music_albumart.directory_id=music_directories.directory_id" 00733 " WHERE music_albumart.embedded=0")) 00734 MythDB::DBError("FileScanner::ScanArtwork", query); 00735 00736 uint counter = 0; 00737 00738 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack"); 00739 00740 QString message = QObject::tr("Scanning Album Artwork"); 00741 MythUIProgressDialog *file_checking = new MythUIProgressDialog(message, 00742 popupStack, 00743 "albumprogressdialog"); 00744 00745 if (file_checking->Create()) 00746 { 00747 popupStack->AddScreen(file_checking, false); 00748 file_checking->SetTotal(query.size()); 00749 } 00750 else 00751 { 00752 delete file_checking; 00753 file_checking = NULL; 00754 } 00755 00756 if (query.isActive() && query.size() > 0) 00757 { 00758 while (query.next()) 00759 { 00760 QString name; 00761 00762 name = m_startdir + query.value(0).toString(); 00763 00764 if (name != QString::null) 00765 { 00766 if ((iter = music_files.find(name)) != music_files.end()) 00767 { 00768 if (music_files[name] == kDatabase) 00769 { 00770 if (file_checking) 00771 { 00772 file_checking->SetProgress(++counter); 00773 qApp->processEvents(); 00774 } 00775 continue; 00776 } 00777 else 00778 music_files.erase(iter); 00779 } 00780 else 00781 { 00782 music_files[name] = kDatabase; 00783 } 00784 } 00785 if (file_checking) 00786 { 00787 file_checking->SetProgress(++counter); 00788 qApp->processEvents(); 00789 } 00790 } 00791 } 00792 00793 if (file_checking) 00794 file_checking->Close(); 00795 }
1.7.6.1