mediascanner.cpp 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #include "mediascanner.h"
  2. #include <QtSql>
  3. #include "mediainfo.h"
  4. #include "tagreader.h"
  5. #include "scopedtransaction.h"
  6. #define DEBUG if (0) qDebug() << __PRETTY_FUNCTION__
  7. const QString CONNECTION_NAME("MediaScanner");
  8. const int BULK_LIMIT = 10;
  9. MediaScanner::MediaScanner(const QSqlDatabase &db, QObject *parent)
  10. : QObject(parent), m_stop(false)
  11. {
  12. m_db = QSqlDatabase::cloneDatabase(db, CONNECTION_NAME);
  13. if (!m_db.open())
  14. DEBUG << "Erorr opening database" << m_db.lastError().text();
  15. QSqlQuery query(db);
  16. //query.exec("PRAGMA synchronous=OFF"); // dangerous, can corrupt db
  17. //query.exec("PRAGMA journal_mode=WAL");
  18. query.exec("PRAGMA count_changes=OFF");
  19. }
  20. MediaScanner::~MediaScanner()
  21. {
  22. QSqlDatabase::removeDatabase(CONNECTION_NAME);
  23. }
  24. void MediaScanner::addDirectory(const QString &_path)
  25. {
  26. QString path = QFileInfo(_path).absoluteFilePath();
  27. if (path.endsWith('/'))
  28. path.chop(1);
  29. QSqlQuery query(m_db);
  30. query.prepare("INSERT INTO directories (path) VALUES (:path)");
  31. query.bindValue(":path", path);
  32. if (!query.exec()) {
  33. m_errorString = query.lastError().text();
  34. DEBUG << m_errorString;
  35. return;
  36. }
  37. scan(path);
  38. }
  39. QHash<QString, MediaScanner::FileInfo> MediaScanner::findFilesByPath(const QString &path)
  40. {
  41. QHash<QString, MediaScanner::FileInfo> hash;
  42. QSqlQuery query(m_db);
  43. query.setForwardOnly(true);
  44. query.prepare("SELECT filepath, mtime, ctime, filesize FROM music WHERE directory=:path");
  45. query.bindValue(":path", path);
  46. if (!query.exec()) {
  47. m_errorString = query.lastError().text();
  48. DEBUG << m_errorString;
  49. return hash;
  50. }
  51. while (query.next()) {
  52. FileInfo fi;
  53. fi.name = query.value(0).toString();
  54. fi.mtime = query.value(1).toLongLong();
  55. fi.ctime = query.value(2).toLongLong();
  56. fi.size = query.value(3).toLongLong();
  57. hash.insert(fi.name, fi);
  58. }
  59. return hash;
  60. }
  61. QString cleanString(QString str)
  62. {
  63. str = str.simplified();
  64. if (!str.isEmpty())
  65. str[0] = str[0].toUpper();
  66. return str;
  67. }
  68. QString determineTitle(const TagReader &reader, const QFileInfo &fi)
  69. {
  70. QString title = reader.title();
  71. title = title.simplified();
  72. // Many mp3 state the title as 'Track xx' which is as good as empty
  73. if (title.startsWith("Track ", Qt::CaseInsensitive))
  74. title.clear();
  75. if (title.isEmpty())
  76. title = fi.baseName();
  77. title[0] = title[0].toUpper();
  78. return title;
  79. }
  80. QString determineAlbum(const TagReader &reader, const QFileInfo &fi)
  81. {
  82. QString album = reader.album();
  83. album = album.simplified();
  84. if (album.isEmpty())
  85. album = fi.dir().dirName();
  86. album[0] = album[0].toUpper();
  87. return album;
  88. }
  89. QByteArray determineThumbnail(const TagReader &reader, const QFileInfo &fi)
  90. {
  91. // Thumbnail is determined from following
  92. // 1. Embedded thumbnail
  93. // 2. foo.mp3 -> foo.{jpg,png,gif,bmp}
  94. // 3. {id3_album, cover, album, folder}.{jpg, png, gif, bmp}
  95. // 4. default image (empty)
  96. // 1
  97. QByteArray ba = reader.thumbnail();
  98. if (!ba.isNull())
  99. return ba;
  100. QDir dir = fi.absoluteDir();
  101. const char *supportedExtensions[] = { ".jpg", ".png", ".gif", ".bmp" }; // prioritized
  102. // 2
  103. for (unsigned i = 0; i < sizeof(supportedExtensions)/sizeof(char *); i++) {
  104. if (dir.exists(fi.baseName() + supportedExtensions[i]))
  105. return QByteArray("file://") + QFile::encodeName(dir.absoluteFilePath(fi.baseName())) + supportedExtensions[i];
  106. }
  107. // 3
  108. QString album = reader.album().simplified();
  109. for (unsigned i = 0; i < sizeof(supportedExtensions)/sizeof(char *); i++) {
  110. if (dir.exists(album + supportedExtensions[i]))
  111. return QByteArray("file://") + QFile::encodeName(dir.absoluteFilePath(album)) + supportedExtensions[i];
  112. if (dir.exists(QString("album") + supportedExtensions[i]))
  113. return QByteArray("file://") + QFile::encodeName(dir.absoluteFilePath("album")) + supportedExtensions[i];
  114. if (dir.exists(QString("cover") + supportedExtensions[i]))
  115. return QByteArray("file://") + QFile::encodeName(dir.absoluteFilePath("cover")) + supportedExtensions[i];
  116. if (dir.exists(QString("folder") + supportedExtensions[i]))
  117. return QByteArray("file://") + QFile::encodeName(dir.absoluteFilePath("folder")) + supportedExtensions[i];
  118. }
  119. return QByteArray();
  120. }
  121. QString determineArtist(const TagReader &reader)
  122. {
  123. QString artist = cleanString(reader.artist());
  124. if (artist.isEmpty())
  125. return "Unknown Artist";
  126. return artist;
  127. }
  128. // ## See if DELETE+INSERT is the best approach. Sqlite3 supports INSERT OR IGNORE which could aslo be used
  129. // ## Also check other upsert methods
  130. void MediaScanner::updateMediaInfos(const QList<QFileInfo> &fis)
  131. {
  132. QList<QSqlRecord> records;
  133. QSqlQuery query(m_db);
  134. ScopedTransaction transaction(m_db);
  135. foreach(const QFileInfo &fi, fis) {
  136. DEBUG << "Updating " << fi.absoluteFilePath();
  137. TagReader reader(fi.absoluteFilePath());
  138. query.prepare("DELETE FROM music WHERE filepath=:filepath");
  139. query.bindValue(":filepath", fi.absoluteFilePath());
  140. if (!query.exec())
  141. DEBUG << query.lastError().text();
  142. if (!query.prepare("INSERT INTO music (filepath, title, album, artist, track, year, genre, comment, thumbnail, length, bitrate, samplerate, directory, mtime, ctime, filesize) "
  143. " VALUES (:filepath, :title, :album, :artist, :track, :year, :genre, :comment, :thumbnail, :length, :bitrate, :samplerate, :directory, :mtime, :ctime, :filesize)")) {
  144. DEBUG << query.lastError().text();
  145. return;
  146. }
  147. query.bindValue(":filepath", fi.absoluteFilePath());
  148. query.bindValue(":title", determineTitle(reader, fi));
  149. query.bindValue(":album", determineAlbum(reader, fi));
  150. query.bindValue(":artist", determineArtist(reader));
  151. query.bindValue(":track", reader.track());
  152. query.bindValue(":year", reader.year());
  153. query.bindValue(":genre", reader.genre());
  154. query.bindValue(":comment", reader.comment());
  155. query.bindValue(":thumbnail", determineThumbnail(reader, fi));
  156. query.bindValue(":length", reader.length());
  157. query.bindValue(":bitrate", reader.bitrate());
  158. query.bindValue(":samplerate", reader.sampleRate());
  159. query.bindValue(":directory", fi.absolutePath());
  160. query.bindValue(":mtime", fi.lastModified().toTime_t());
  161. query.bindValue(":ctime", fi.created().toTime_t());
  162. query.bindValue(":filesize", fi.size());
  163. if (!query.exec())
  164. DEBUG << query.lastError().text();
  165. QSqlRecord record;
  166. QMap<QString, QVariant> boundValues = query.boundValues();
  167. for (QMap<QString, QVariant>::const_iterator it = boundValues.constBegin(); it != boundValues.constEnd(); ++it) {
  168. QString key = it.key().mid(1); // remove the ':'
  169. record.append(QSqlField(key, (QVariant::Type) it.value().type()));
  170. record.setValue(key, it.value());
  171. }
  172. records.append(record);
  173. if (m_stop)
  174. break;
  175. }
  176. emit databaseUpdated(records);
  177. }
  178. static bool isMediaFile(const QFileInfo &info)
  179. {
  180. return info.suffix() == "mp3" || info.suffix() == "m4a";
  181. }
  182. void MediaScanner::scan(const QString &path)
  183. {
  184. QQueue<QString> dirQ;
  185. dirQ.enqueue(path);
  186. QList<QFileInfo> diskFileInfos;
  187. while (!dirQ.isEmpty() && !m_stop) {
  188. QString curdir = dirQ.dequeue();
  189. QFileInfoList fileInfosInDisk = QDir(curdir).entryInfoList(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot|QDir::NoSymLinks);
  190. QHash<QString, FileInfo> fileInfosInDb = findFilesByPath(curdir);
  191. DEBUG << "Scanning " << curdir << fileInfosInDisk.count() << " files in disk and " << fileInfosInDb.count() << "in database";
  192. foreach(const QFileInfo &diskFileInfo, fileInfosInDisk) {
  193. FileInfo dbFileInfo = fileInfosInDb.take(diskFileInfo.absoluteFilePath());
  194. if (diskFileInfo.isFile()) {
  195. if (!isMediaFile(diskFileInfo))
  196. continue;
  197. if (diskFileInfo.lastModified().toTime_t() == dbFileInfo.mtime
  198. && diskFileInfo.created().toTime_t() == dbFileInfo.ctime
  199. && diskFileInfo.size() == dbFileInfo.size) {
  200. DEBUG << diskFileInfo.absoluteFilePath() << " : no change";
  201. continue;
  202. }
  203. diskFileInfos.append(diskFileInfo);
  204. if (diskFileInfos.count() > BULK_LIMIT) {
  205. updateMediaInfos(diskFileInfos);
  206. diskFileInfos.clear();
  207. }
  208. DEBUG << diskFileInfo.absoluteFilePath() << " : added";
  209. } else if (diskFileInfo.isDir()) {
  210. dirQ.enqueue(diskFileInfo.absoluteFilePath());
  211. }
  212. if (m_stop)
  213. break;
  214. }
  215. // ## remove the files from the db in the fileInfosInDb hash now?
  216. }
  217. if (!diskFileInfos.isEmpty())
  218. updateMediaInfos(diskFileInfos);
  219. }
  220. void MediaScanner::refresh()
  221. {
  222. QSqlQuery query(m_db);
  223. query.setForwardOnly(true);
  224. query.exec("SELECT path FROM directories");
  225. QStringList dirs;
  226. while (query.next()) {
  227. dirs << query.value(0).toString();
  228. }
  229. for (int i = 0; i < dirs.count(); i++) {
  230. if (!m_stop)
  231. scan(dirs[i]);
  232. }
  233. }