MythTV  0.26-pre
filescanner.cpp
Go to the documentation of this file.
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 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends