00001
00002 #include <sys/stat.h>
00003
00004
00005 #include <QApplication>
00006 #include <QDir>
00007
00008
00009 #include <mythcontext.h>
00010 #include <mythdb.h>
00011 #include <mythdialogs.h>
00012 #include <mythscreenstack.h>
00013 #include <mythprogressdialog.h>
00014
00015
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
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
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
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
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
00097
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
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
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
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
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
00299 data->dumpToDatabase();
00300
00301
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
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
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
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
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
00525 disk_meta->dumpToDatabase();
00526
00527
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
00602
00603
00604
00605
00606
00607
00608
00609
00610
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
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 }