/src/musicscanner.cpp

http://github.com/tomahawk-player/tomahawk · C++ · 380 lines · 280 code · 76 blank · 24 comment · 34 complexity · 99fcd872b9ced23d3cd3bb5dbf1d8321 MD5 · raw file

  1. /* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2. *
  3. * Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
  4. *
  5. * Tomahawk is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * Tomahawk is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. #include "musicscanner.h"
  19. #include <QtCore/QCoreApplication>
  20. #include "utils/tomahawkutils.h"
  21. #include "tomahawksettings.h"
  22. #include "sourcelist.h"
  23. #include "database/database.h"
  24. #include "database/databasecommand_dirmtimes.h"
  25. #include "database/databasecommand_filemtimes.h"
  26. #include "database/databasecommand_collectionstats.h"
  27. #include "database/databasecommand_addfiles.h"
  28. #include "database/databasecommand_deletefiles.h"
  29. #include "taghandlers/tag.h"
  30. #include "utils/logger.h"
  31. using namespace Tomahawk;
  32. void
  33. DirLister::go()
  34. {
  35. foreach ( const QString& dir, m_dirs )
  36. {
  37. m_opcount++;
  38. QMetaObject::invokeMethod( this, "scanDir", Qt::QueuedConnection, Q_ARG( QDir, QDir( dir, 0 ) ), Q_ARG( int, 0 ) );
  39. }
  40. }
  41. void
  42. DirLister::scanDir( QDir dir, int depth )
  43. {
  44. if ( isDeleting() )
  45. {
  46. m_opcount--;
  47. if ( m_opcount == 0 )
  48. emit finished();
  49. return;
  50. }
  51. tDebug( LOGVERBOSE ) << "DirLister::scanDir scanning:" << dir.canonicalPath();
  52. if ( !dir.exists() )
  53. {
  54. tDebug( LOGVERBOSE ) << "Dir no longer exists, not scanning";
  55. m_opcount--;
  56. if ( m_opcount == 0 )
  57. emit finished();
  58. return;
  59. }
  60. QFileInfoList dirs;
  61. dir.setFilter( QDir::Files | QDir::Readable | QDir::NoDotAndDotDot );
  62. dir.setSorting( QDir::Name );
  63. dirs = dir.entryInfoList();
  64. foreach ( const QFileInfo& di, dirs )
  65. emit fileToScan( di );
  66. dir.setFilter( QDir::Dirs | QDir::Readable | QDir::NoDotAndDotDot );
  67. dirs = dir.entryInfoList();
  68. foreach ( const QFileInfo& di, dirs )
  69. {
  70. const QString canonical = di.canonicalFilePath();
  71. m_opcount++;
  72. QMetaObject::invokeMethod( this, "scanDir", Qt::QueuedConnection, Q_ARG( QDir, di.canonicalFilePath() ), Q_ARG( int, depth + 1 ) );
  73. }
  74. m_opcount--;
  75. if ( m_opcount == 0 )
  76. {
  77. tDebug() << Q_FUNC_INFO << "emitting finished";
  78. emit finished();
  79. }
  80. }
  81. MusicScanner::MusicScanner( const QStringList& dirs, quint32 bs )
  82. : QObject()
  83. , m_dirs( dirs )
  84. , m_batchsize( bs )
  85. , m_dirListerThreadController( 0 )
  86. {
  87. m_ext2mime.insert( "mp3", TomahawkUtils::extensionToMimetype( "mp3" ) );
  88. m_ext2mime.insert( "ogg", TomahawkUtils::extensionToMimetype( "ogg" ) );
  89. m_ext2mime.insert( "oga", TomahawkUtils::extensionToMimetype( "oga" ) );
  90. m_ext2mime.insert( "mpc", TomahawkUtils::extensionToMimetype( "mpc" ) );
  91. m_ext2mime.insert( "wma", TomahawkUtils::extensionToMimetype( "wma" ) );
  92. m_ext2mime.insert( "aac", TomahawkUtils::extensionToMimetype( "aac" ) );
  93. m_ext2mime.insert( "m4a", TomahawkUtils::extensionToMimetype( "m4a" ) );
  94. m_ext2mime.insert( "mp4", TomahawkUtils::extensionToMimetype( "mp4" ) );
  95. m_ext2mime.insert( "flac", TomahawkUtils::extensionToMimetype( "flac" ) );
  96. }
  97. MusicScanner::~MusicScanner()
  98. {
  99. tDebug() << Q_FUNC_INFO;
  100. if ( !m_dirLister.isNull() )
  101. {
  102. m_dirListerThreadController->quit();;
  103. m_dirListerThreadController->wait( 60000 );
  104. delete m_dirLister.data();
  105. delete m_dirListerThreadController;
  106. m_dirListerThreadController = 0;
  107. }
  108. }
  109. void
  110. MusicScanner::startScan()
  111. {
  112. tDebug( LOGVERBOSE ) << "Loading mtimes...";
  113. m_scanned = m_skipped = m_cmdQueue = 0;
  114. m_skippedFiles.clear();
  115. SourceList::instance()->getLocal()->scanningProgress( m_scanned );
  116. // trigger the scan once we've loaded old filemtimes
  117. //FIXME: For multiple collection support make sure the right prefix gets passed in...or not...
  118. //bear in mind that simply passing in the top-level of a defined collection means it will not return items that need
  119. //to be removed that aren't in that root any longer -- might have to do the filtering in setMTimes based on strings
  120. DatabaseCommand_FileMtimes *cmd = new DatabaseCommand_FileMtimes();
  121. connect( cmd, SIGNAL( done( QMap< QString, QMap< unsigned int, unsigned int > > ) ),
  122. SLOT( setFileMtimes( QMap< QString, QMap< unsigned int, unsigned int > > ) ) );
  123. Database::instance()->enqueue( QSharedPointer< DatabaseCommand >( cmd ) );
  124. return;
  125. }
  126. void
  127. MusicScanner::setFileMtimes( const QMap< QString, QMap< unsigned int, unsigned int > >& m )
  128. {
  129. tDebug( LOGVERBOSE ) << Q_FUNC_INFO << m.count();
  130. m_filemtimes = m;
  131. scan();
  132. }
  133. void
  134. MusicScanner::scan()
  135. {
  136. tDebug( LOGEXTRA ) << "Num saved file mtimes from last scan:" << m_filemtimes.size();
  137. connect( this, SIGNAL( batchReady( QVariantList, QVariantList ) ),
  138. SLOT( commitBatch( QVariantList, QVariantList ) ), Qt::DirectConnection );
  139. m_dirListerThreadController = new QThread( this );
  140. m_dirLister = QWeakPointer< DirLister >( new DirLister( m_dirs ) );
  141. m_dirLister.data()->moveToThread( m_dirListerThreadController );
  142. connect( m_dirLister.data(), SIGNAL( fileToScan( QFileInfo ) ),
  143. SLOT( scanFile( QFileInfo ) ), Qt::QueuedConnection );
  144. // queued, so will only fire after all dirs have been scanned:
  145. connect( m_dirLister.data(), SIGNAL( finished() ),
  146. SLOT( listerFinished() ), Qt::QueuedConnection );
  147. m_dirListerThreadController->start();
  148. QMetaObject::invokeMethod( m_dirLister.data(), "go" );
  149. }
  150. void
  151. MusicScanner::listerFinished()
  152. {
  153. tDebug( LOGVERBOSE ) << Q_FUNC_INFO;
  154. // any remaining stuff that wasnt emitted as a batch:
  155. foreach( const QString& key, m_filemtimes.keys() )
  156. m_filesToDelete << m_filemtimes[ key ].keys().first();
  157. tDebug() << "Lister finished: to delete:" << m_filesToDelete;
  158. if ( m_filesToDelete.length() || m_scannedfiles.length() )
  159. {
  160. commitBatch( m_scannedfiles, m_filesToDelete );
  161. m_scannedfiles.clear();
  162. m_filesToDelete.clear();
  163. tDebug( LOGINFO ) << "Scanning complete, saving to database. ( scanned" << m_scanned << "skipped" << m_skipped << ")";
  164. tDebug( LOGEXTRA ) << "Skipped the following files (no tags / no valid audio):";
  165. foreach ( const QString& s, m_skippedFiles )
  166. tDebug( LOGEXTRA ) << s;
  167. }
  168. else
  169. cleanup();
  170. }
  171. void
  172. MusicScanner::cleanup()
  173. {
  174. if ( !m_dirLister.isNull() )
  175. {
  176. m_dirListerThreadController->quit();;
  177. m_dirListerThreadController->wait( 60000 );
  178. delete m_dirLister.data();
  179. delete m_dirListerThreadController;
  180. m_dirListerThreadController = 0;
  181. }
  182. tDebug() << Q_FUNC_INFO << "emitting finished!";
  183. emit finished();
  184. }
  185. void
  186. MusicScanner::commitBatch( const QVariantList& tracks, const QVariantList& deletethese )
  187. {
  188. if ( deletethese.length() )
  189. {
  190. tDebug( LOGINFO ) << Q_FUNC_INFO << "deleting" << deletethese.length() << "tracks";
  191. executeCommand( QSharedPointer<DatabaseCommand>( new DatabaseCommand_DeleteFiles( deletethese, SourceList::instance()->getLocal() ) ) );
  192. }
  193. if ( tracks.length() )
  194. {
  195. tDebug( LOGINFO ) << Q_FUNC_INFO << "adding" << tracks.length() << "tracks";
  196. executeCommand( QSharedPointer<DatabaseCommand>( new DatabaseCommand_AddFiles( tracks, SourceList::instance()->getLocal() ) ) );
  197. }
  198. }
  199. void
  200. MusicScanner::executeCommand( QSharedPointer< DatabaseCommand > cmd )
  201. {
  202. tDebug() << Q_FUNC_INFO << m_cmdQueue;
  203. m_cmdQueue++;
  204. connect( cmd.data(), SIGNAL( finished() ), SLOT( commandFinished() ) );
  205. Database::instance()->enqueue( cmd );
  206. }
  207. void
  208. MusicScanner::commandFinished()
  209. {
  210. tDebug() << Q_FUNC_INFO << m_cmdQueue;
  211. if ( --m_cmdQueue == 0 )
  212. cleanup();
  213. }
  214. void
  215. MusicScanner::scanFile( const QFileInfo& fi )
  216. {
  217. if ( m_filemtimes.contains( "file://" + fi.canonicalFilePath() ) )
  218. {
  219. if ( fi.lastModified().toUTC().toTime_t() == m_filemtimes.value( "file://" + fi.canonicalFilePath() ).values().first() )
  220. {
  221. m_filemtimes.remove( "file://" + fi.canonicalFilePath() );
  222. return;
  223. }
  224. m_filesToDelete << m_filemtimes.value( "file://" + fi.canonicalFilePath() ).keys().first();
  225. m_filemtimes.remove( "file://" + fi.canonicalFilePath() );
  226. }
  227. tDebug( LOGVERBOSE ) << Q_FUNC_INFO << "Scanning file:" << fi.canonicalFilePath();
  228. QVariant m = readFile( fi );
  229. if ( m.toMap().isEmpty() )
  230. return;
  231. m_scannedfiles << m;
  232. if ( m_batchsize != 0 && (quint32)m_scannedfiles.length() >= m_batchsize )
  233. {
  234. emit batchReady( m_scannedfiles, m_filesToDelete );
  235. m_scannedfiles.clear();
  236. m_filesToDelete.clear();
  237. }
  238. }
  239. QVariant
  240. MusicScanner::readFile( const QFileInfo& fi )
  241. {
  242. const QString suffix = fi.suffix().toLower();
  243. if ( !m_ext2mime.contains( suffix ) )
  244. {
  245. return QVariantMap(); // invalid extension
  246. }
  247. if ( m_scanned )
  248. if ( m_scanned % 3 == 0 )
  249. SourceList::instance()->getLocal()->scanningProgress( m_scanned );
  250. if ( m_scanned % 100 == 0 )
  251. tDebug( LOGINFO ) << "Scan progress:" << m_scanned << fi.canonicalFilePath();
  252. #ifdef COMPLEX_TAGLIB_FILENAME
  253. const wchar_t *encodedName = reinterpret_cast< const wchar_t * >( fi.canonicalFilePath().utf16() );
  254. #else
  255. QByteArray fileName = QFile::encodeName( fi.canonicalFilePath() );
  256. const char *encodedName = fileName.constData();
  257. #endif
  258. TagLib::FileRef f( encodedName );
  259. if ( f.isNull() || !f.tag() )
  260. {
  261. m_skippedFiles << fi.canonicalFilePath();
  262. m_skipped++;
  263. return QVariantMap();
  264. }
  265. int bitrate = 0;
  266. int duration = 0;
  267. Tag *tag = Tag::fromFile( f );
  268. if ( f.audioProperties() )
  269. {
  270. TagLib::AudioProperties *properties = f.audioProperties();
  271. duration = properties->length();
  272. bitrate = properties->bitrate();
  273. }
  274. QString artist = tag->artist().trimmed();
  275. QString album = tag->album().trimmed();
  276. QString track = tag->title().trimmed();
  277. if ( artist.isEmpty() || track.isEmpty() )
  278. {
  279. // FIXME: do some clever filename guessing
  280. m_skippedFiles << fi.canonicalFilePath();
  281. m_skipped++;
  282. return QVariantMap();
  283. }
  284. QString mimetype = m_ext2mime.value( suffix );
  285. QString url( "file://%1" );
  286. QVariantMap m;
  287. m["url"] = url.arg( fi.canonicalFilePath() );
  288. m["mtime"] = fi.lastModified().toUTC().toTime_t();
  289. m["size"] = (unsigned int)fi.size();
  290. m["mimetype"] = mimetype;
  291. m["duration"] = duration;
  292. m["bitrate"] = bitrate;
  293. m["artist"] = artist;
  294. m["album"] = album;
  295. m["track"] = track;
  296. m["albumpos"] = tag->track();
  297. m["year"] = tag->year();
  298. m["albumartist"] = tag->albumArtist();
  299. m["composer"] = tag->composer();
  300. m["discnumber"] = tag->discNumber();
  301. m["hash"] = ""; // TODO
  302. m_scanned++;
  303. return m;
  304. }