MythTV master
musicfilescanner.cpp
Go to the documentation of this file.
1// POSIX headers
2#include <sys/stat.h>
3#include <unistd.h>
4
5// Qt headers
6#include <QDir>
7
8// MythTV headers
11#include "libmythbase/mythdb.h"
13
14#include "musicmetadata.h"
15#include "metaio.h"
16#include "musicfilescanner.h"
17
19{
21
22 // Cache the directory ids from the database
23 query.prepare("SELECT directory_id, path FROM music_directories");
24 if (query.exec())
25 {
26 while(query.next())
27 {
28 m_directoryid[query.value(1).toString()] = query.value(0).toInt();
29 }
30 }
31
32 // Cache the genre ids from the database
33 query.prepare("SELECT genre_id, LOWER(genre) FROM music_genres");
34 if (query.exec())
35 {
36 while(query.next())
37 {
38 m_genreid[query.value(1).toString()] = query.value(0).toInt();
39 }
40 }
41
42 // Cache the artist ids from the database
43 query.prepare("SELECT artist_id, LOWER(artist_name) FROM music_artists");
44 if (query.exec() || query.isActive())
45 {
46 while(query.next())
47 {
48 m_artistid[query.value(1).toString()] = query.value(0).toInt();
49 }
50 }
51
52 // Cache the album ids from the database
53 query.prepare("SELECT album_id, artist_id, LOWER(album_name) FROM music_albums");
54 if (query.exec())
55 {
56 while(query.next())
57 {
58 m_albumid[query.value(1).toString() + "#" + query.value(2).toString()] = query.value(0).toInt();
59 }
60 }
61}
62
75void MusicFileScanner::BuildFileList(QString &directory, MusicLoadedMap &music_files, MusicLoadedMap &art_files, int parentid)
76{
77 QDir d(directory);
78
79 if (!d.exists())
80 return;
81
82 d.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
83
84 QFileInfoList list = d.entryInfoList();
85 if (list.isEmpty())
86 return;
87
88 // Recursively traverse directory
89 int newparentid = 0;
90 for (const auto& fi : std::as_const(list))
91 {
92 QString filename = fi.absoluteFilePath();
93 if (fi.isDir())
94 {
95
96 QString dir(filename);
97 dir.remove(0, m_startDirs.last().length());
98
99 newparentid = m_directoryid[dir];
100
101 if (newparentid == 0)
102 {
103 int id = GetDirectoryId(dir, parentid);
104 m_directoryid[dir] = id;
105
106 if (id > 0)
107 {
108 newparentid = id;
109 }
110 else
111 {
112 LOG(VB_GENERAL, LOG_ERR,
113 QString("Failed to get directory id for path %1")
114 .arg(dir));
115 }
116 }
117
118 BuildFileList(filename, music_files, art_files, newparentid);
119 }
120 else
121 {
122 if (IsArtFile(filename))
123 {
124 MusicFileData fdata;
125 fdata.startDir = m_startDirs.last();
127 art_files[filename] = fdata;
128 }
129 else if (IsMusicFile(filename))
130 {
131 MusicFileData fdata;
132 fdata.startDir = m_startDirs.last();
134 music_files[filename] = fdata;
135 }
136 else
137 {
138 LOG(VB_GENERAL, LOG_INFO,
139 QString("Found file with unsupported extension %1")
140 .arg(filename));
141 }
142 }
143 }
144}
145
147{
148 QFileInfo fi(filename);
149 QString extension = fi.suffix().toLower();
150 QString nameFilter = gCoreContext->GetSetting("AlbumArtFilter", "*.png;*.jpg;*.jpeg;*.gif;*.bmp");
151
152
153 return !extension.isEmpty() && nameFilter.indexOf(extension.toLower()) > -1;
154}
155
157{
158 QFileInfo fi(filename);
159 QString extension = fi.suffix().toLower();
160 QString nameFilter = MetaIO::kValidFileExtensions;
161
162 return !extension.isEmpty() && nameFilter.indexOf(extension.toLower()) > -1;
163}
164
175int MusicFileScanner::GetDirectoryId(const QString &directory, int parentid)
176{
177 if (directory.isEmpty())
178 return 0;
179
181
182 // Load the directory id or insert it and get the id
183 query.prepare("SELECT directory_id FROM music_directories "
184 "WHERE path = BINARY :DIRECTORY ;");
185 query.bindValue(":DIRECTORY", directory);
186
187 if (!query.exec())
188 {
189 MythDB::DBError("music select directory id", query);
190 return -1;
191 }
192
193 if (query.next())
194 {
195 // we have found the directory already in the DB
196 return query.value(0).toInt();
197 }
198
199 // directory is not in the DB so insert it
200 query.prepare("INSERT INTO music_directories (path, parent_id) "
201 "VALUES (:DIRECTORY, :PARENTID);");
202 query.bindValue(":DIRECTORY", directory);
203 query.bindValue(":PARENTID", parentid);
204
205 if (!query.exec() || !query.isActive() || query.numRowsAffected() <= 0)
206 {
207 MythDB::DBError("music insert directory", query);
208 return -1;
209 }
210
211 return query.lastInsertId().toInt();
212}
213
223 const QString &filename, const QString &date_modified)
224{
225 QFileInfo fi(filename);
226 QDateTime dt = fi.lastModified();
227 if (dt.isValid())
228 {
229 QDateTime old_dt = MythDate::fromString(date_modified);
230 return !old_dt.isValid() || (dt > old_dt);
231 }
232 LOG(VB_GENERAL, LOG_ERR, QString("Failed to stat file: %1")
233 .arg(filename));
234 return false;
235}
236
252void MusicFileScanner::AddFileToDB(const QString &filename, const QString &startDir)
253{
254 QString extension = filename.section( '.', -1 ) ;
255 QString directory = filename;
256 directory.remove(0, startDir.length());
257 directory = directory.section( '/', 0, -2);
258
259 QString nameFilter = gCoreContext->GetSetting("AlbumArtFilter", "*.png;*.jpg;*.jpeg;*.gif;*.bmp");
260
261 // If this file is an image, insert the details into the music_albumart table
262 if (nameFilter.indexOf(extension.toLower()) > -1)
263 {
264 QString name = filename.section( '/', -1);
265
267 query.prepare("INSERT INTO music_albumart "
268 "SET filename = :FILE, directory_id = :DIRID, "
269 "imagetype = :TYPE, hostname = :HOSTNAME;");
270
271 query.bindValue(":FILE", name);
272 query.bindValue(":DIRID", m_directoryid[directory]);
273 query.bindValue(":TYPE", AlbumArtImages::guessImageType(name));
274 query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
275
276 if (!query.exec() || query.numRowsAffected() <= 0)
277 {
278 MythDB::DBError("music insert artwork", query);
279 }
280
282
283 return;
284 }
285
286 if (extension.isEmpty() || !MetaIO::kValidFileExtensions.contains(extension.toLower()))
287 {
288 LOG(VB_GENERAL, LOG_WARNING, QString("Ignoring filename with unsupported filename: '%1'").arg(filename));
289 return;
290 }
291
292 LOG(VB_FILE, LOG_INFO, QString("Reading metadata from %1").arg(filename));
294 if (data)
295 {
296 data->setFileSize((quint64)QFileInfo(filename).size());
298
299 QString album_cache_string;
300
301 // Set values from cache
302 int did = m_directoryid[directory];
303 if (did >= 0)
304 data->setDirectoryId(did);
305
306 int aid = m_artistid[data->Artist().toLower()];
307 if (aid > 0)
308 {
309 data->setArtistId(aid);
310
311 // The album cache depends on the artist id
312 album_cache_string = QString::number(data->getArtistId()) + "#"
313 + data->Album().toLower();
314
315 if (m_albumid[album_cache_string] > 0)
316 data->setAlbumId(m_albumid[album_cache_string]);
317 }
318
319 int caid = m_artistid[data->CompilationArtist().toLower()];
320 if (caid > 0)
321 data->setCompilationArtistId(caid);
322
323 int gid = m_genreid[data->Genre().toLower()];
324 if (gid > 0)
325 data->setGenreId(gid);
326
327 // Commit track info to database
328 data->dumpToDatabase();
329
330 // Update the cache
331 m_artistid[data->Artist().toLower()] =
332 data->getArtistId();
333
334 m_artistid[data->CompilationArtist().toLower()] =
336
337 m_genreid[data->Genre().toLower()] =
338 data->getGenreId();
339
340 album_cache_string = QString::number(data->getArtistId()) + "#"
341 + data->Album().toLower();
342 m_albumid[album_cache_string] = data->getAlbumId();
343
344 // read any embedded images from the tag
346
347 if (tagger)
348 {
349 if (tagger->supportsEmbeddedImages())
350 {
351 AlbumArtList artList = tagger->getAlbumArtList(data->Filename());
352 data->setEmbeddedAlbumArt(artList);
354 }
355 delete tagger;
356 }
357
358 delete data;
359
361 }
362}
363
371{
372 LOG(VB_GENERAL, LOG_INFO, "Cleaning old entries from music database");
373
375 MSqlQuery deletequery(MSqlQuery::InitCon());
376
377 // delete unused genre_ids from music_genres
378 if (!query.exec("SELECT g.genre_id FROM music_genres g "
379 "LEFT JOIN music_songs s ON g.genre_id=s.genre_id "
380 "WHERE s.genre_id IS NULL;"))
381 MythDB::DBError("MusicFileScanner::cleanDB - select music_genres", query);
382
383 deletequery.prepare("DELETE FROM music_genres WHERE genre_id=:GENREID");
384 while (query.next())
385 {
386 int genreid = query.value(0).toInt();
387 deletequery.bindValue(":GENREID", genreid);
388 if (!deletequery.exec())
389 MythDB::DBError("MusicFileScanner::cleanDB - delete music_genres",
390 deletequery);
391 }
392
393 // delete unused album_ids from music_albums
394 if (!query.exec("SELECT a.album_id FROM music_albums a "
395 "LEFT JOIN music_songs s ON a.album_id=s.album_id "
396 "WHERE s.album_id IS NULL;"))
397 MythDB::DBError("MusicFileScanner::cleanDB - select music_albums", query);
398
399 deletequery.prepare("DELETE FROM music_albums WHERE album_id=:ALBUMID");
400 while (query.next())
401 {
402 int albumid = query.value(0).toInt();
403 deletequery.bindValue(":ALBUMID", albumid);
404 if (!deletequery.exec())
405 MythDB::DBError("MusicFileScanner::cleanDB - delete music_albums",
406 deletequery);
407 }
408
409 // delete unused artist_ids from music_artists
410 if (!query.exec("SELECT a.artist_id FROM music_artists a "
411 "LEFT JOIN music_songs s ON a.artist_id=s.artist_id "
412 "LEFT JOIN music_albums l ON a.artist_id=l.artist_id "
413 "WHERE s.artist_id IS NULL AND l.artist_id IS NULL"))
414 MythDB::DBError("MusicFileScanner::cleanDB - select music_artists", query);
415
416
417 deletequery.prepare("DELETE FROM music_artists WHERE artist_id=:ARTISTID");
418 while (query.next())
419 {
420 int artistid = query.value(0).toInt();
421 deletequery.bindValue(":ARTISTID", artistid);
422 if (!deletequery.exec())
423 MythDB::DBError("MusicFileScanner::cleanDB - delete music_artists",
424 deletequery);
425 }
426
427 // delete unused directory_ids from music_directories
428 //
429 // Get a list of directory_ids not referenced in music_songs.
430 // This list will contain any directory that is only used for
431 // organization. I.E. If your songs are organized by artist and
432 // then by album, this will contain all of the artist directories.
433 if (!query.exec("SELECT d.directory_id, d.parent_id FROM music_directories d "
434 "LEFT JOIN music_songs s ON d.directory_id=s.directory_id "
435 "WHERE s.directory_id IS NULL ORDER BY directory_id DESC;"))
436 MythDB::DBError("MusicFileScanner::cleanDB - select music_directories", query);
437
438 deletequery.prepare("DELETE FROM music_directories WHERE directory_id=:DIRECTORYID");
439
440 MSqlQuery parentquery(MSqlQuery::InitCon());
441 parentquery.prepare("SELECT COUNT(*) FROM music_directories "
442 "WHERE parent_id=:DIRECTORYID ");
443
444 MSqlQuery dirnamequery(MSqlQuery::InitCon());
445 dirnamequery.prepare("SELECT path FROM music_directories "
446 "WHERE directory_id=:DIRECTORYID ");
447
448 int deletedCount = 1;
449
450 while (deletedCount > 0)
451 {
452 deletedCount = 0;
453 query.seek(-1);
454
455 // loop through the list of unused directory_ids deleting any which
456 // aren't referenced by any other directories parent_id
457 while (query.next())
458 {
459 int directoryid = query.value(0).toInt();
460
461 // have we still got references to this directory_id from other directories
462 parentquery.bindValue(":DIRECTORYID", directoryid);
463 if (!parentquery.exec())
464 {
465 MythDB::DBError("MusicFileScanner::cleanDB - get parent directory count",
466 parentquery);
467 continue;
468 }
469 if (!parentquery.next())
470 continue;
471 int parentCount = parentquery.value(0).toInt();
472 if (parentCount != 0)
473 // Still has child directories
474 continue;
475 if(VERBOSE_LEVEL_CHECK(VB_GENERAL, LOG_DEBUG))
476 {
477 dirnamequery.bindValue(":DIRECTORYID", directoryid);
478 if (dirnamequery.exec() && dirnamequery.next())
479 {
480 LOG(VB_GENERAL, LOG_DEBUG,
481 QString("MusicFileScanner deleted directory %1 %2")
482 .arg(directoryid,5).arg(dirnamequery.value(0).toString()));
483 }
484 }
485 deletequery.bindValue(":DIRECTORYID", directoryid);
486 if (!deletequery.exec())
487 MythDB::DBError("MusicFileScanner::cleanDB - delete music_directories",
488 deletequery);
489 deletedCount += deletequery.numRowsAffected();
490 }
491 LOG(VB_GENERAL, LOG_INFO,
492 QString("MusicFileScanner deleted %1 directory entries")
493 .arg(deletedCount));
494 }
495
496 // delete unused albumart_ids from music_albumart (embedded images)
497 if (!query.exec("SELECT a.albumart_id FROM music_albumart a LEFT JOIN "
498 "music_songs s ON a.song_id=s.song_id WHERE "
499 "embedded='1' AND s.song_id IS NULL;"))
500 MythDB::DBError("MusicFileScanner::cleanDB - select music_albumart", query);
501
502 deletequery.prepare("DELETE FROM music_albumart WHERE albumart_id=:ALBUMARTID");
503 while (query.next())
504 {
505 int albumartid = query.value(0).toInt();
506 deletequery.bindValue(":ALBUMARTID", albumartid);
507 if (!deletequery.exec())
508 MythDB::DBError("MusicFileScanner::cleanDB - delete music_albumart",
509 deletequery);
510 }
511}
512
523void MusicFileScanner::RemoveFileFromDB(const QString &filename, const QString &startDir)
524{
525 QString sqlfilename(filename);
526 sqlfilename.remove(0, startDir.length());
527 // We know that the filename will not contain :// as the SQL limits this
528 QString directory = sqlfilename.section( '/', 0, -2 ) ;
529 sqlfilename = sqlfilename.section( '/', -1 ) ;
530
531 QString extension = sqlfilename.section( '.', -1 ) ;
532
533 QString nameFilter = gCoreContext->GetSetting("AlbumArtFilter",
534 "*.png;*.jpg;*.jpeg;*.gif;*.bmp");
535
536 if (nameFilter.indexOf(extension.toLower()) > -1)
537 {
539 query.prepare("DELETE FROM music_albumart WHERE filename= :FILE AND "
540 "directory_id= :DIRID;");
541 query.bindValue(":FILE", sqlfilename);
542 query.bindValue(":DIRID", m_directoryid[directory]);
543
544 if (!query.exec() || query.numRowsAffected() <= 0)
545 {
546 MythDB::DBError("music delete artwork", query);
547 }
548
550
551 return;
552 }
553
555 query.prepare("DELETE FROM music_songs WHERE filename = :NAME ;");
556 query.bindValue(":NAME", sqlfilename);
557 if (!query.exec())
558 MythDB::DBError("MusicFileScanner::RemoveFileFromDB - deleting music_songs",
559 query);
560
562}
563
574void MusicFileScanner::UpdateFileInDB(const QString &filename, const QString &startDir)
575{
576 QString dbFilename = filename;
577 dbFilename.remove(0, startDir.length());
578
579 QString directory = filename;
580 directory.remove(0, startDir.length());
581 directory = directory.section( '/', 0, -2);
582
583 MusicMetadata *db_meta = MetaIO::getMetadata(dbFilename);
585
586 if (db_meta && disk_meta)
587 {
588 if (db_meta->ID() <= 0)
589 {
590 LOG(VB_GENERAL, LOG_ERR, QString("Asked to update track with "
591 "invalid ID - %1")
592 .arg(db_meta->ID()));
593 delete disk_meta;
594 delete db_meta;
595 return;
596 }
597
598 disk_meta->setID(db_meta->ID());
599 disk_meta->setRating(db_meta->Rating());
600 if (db_meta->PlayCount() > disk_meta->PlayCount())
601 disk_meta->setPlaycount(db_meta->Playcount());
602
603 QString album_cache_string;
604
605 // Set values from cache
606 int did = m_directoryid[directory];
607 if (did > 0)
608 disk_meta->setDirectoryId(did);
609
610 int aid = m_artistid[disk_meta->Artist().toLower()];
611 if (aid > 0)
612 {
613 disk_meta->setArtistId(aid);
614
615 // The album cache depends on the artist id
616 album_cache_string = QString::number(disk_meta->getArtistId()) + "#" +
617 disk_meta->Album().toLower();
618
619 if (m_albumid[album_cache_string] > 0)
620 disk_meta->setAlbumId(m_albumid[album_cache_string]);
621 }
622
623 int caid = m_artistid[disk_meta->CompilationArtist().toLower()];
624 if (caid > 0)
625 disk_meta->setCompilationArtistId(caid);
626
627 int gid = m_genreid[disk_meta->Genre().toLower()];
628 if (gid > 0)
629 disk_meta->setGenreId(gid);
630
631 disk_meta->setFileSize((quint64)QFileInfo(filename).size());
632
633 disk_meta->setHostname(gCoreContext->GetHostName());
634
635 // Commit track info to database
636 disk_meta->dumpToDatabase();
637
638 // Update the cache
639 m_artistid[disk_meta->Artist().toLower()]
640 = disk_meta->getArtistId();
641 m_artistid[disk_meta->CompilationArtist().toLower()]
642 = disk_meta->getCompilationArtistId();
643 m_genreid[disk_meta->Genre().toLower()]
644 = disk_meta->getGenreId();
645 album_cache_string = QString::number(disk_meta->getArtistId()) + "#" +
646 disk_meta->Album().toLower();
647 m_albumid[album_cache_string] = disk_meta->getAlbumId();
648 }
649
650 delete disk_meta;
651 delete db_meta;
652}
653
663void MusicFileScanner::SearchDirs(const QStringList &dirList)
664{
665 QString host = gCoreContext->GetHostName();
666
667 if (IsRunning())
668 {
669 // check how long the scanner has been running
670 // if it's more than 60 minutes assume something went wrong
671 QString lastRun = gCoreContext->GetSetting("MusicScannerLastRunStart", "");
672 if (!lastRun.isEmpty())
673 {
674 QDateTime dtLastRun = QDateTime::fromString(lastRun, Qt::ISODate);
675 if (dtLastRun.isValid())
676 {
677 static constexpr int64_t kOneHour {60LL * 60};
678 if (MythDate::current() > dtLastRun.addSecs(kOneHour))
679 {
680 LOG(VB_GENERAL, LOG_INFO, "Music file scanner has been running for more than 60 minutes. Lets reset and try again");
681 gCoreContext->SendMessage(QString("MUSIC_SCANNER_ERROR %1 %2").arg(host, "Stalled"));
682
683 // give the user time to read the notification before restarting the scan
684 sleep(5);
685 }
686 else
687 {
688 LOG(VB_GENERAL, LOG_INFO, "Music file scanner is already running");
689 gCoreContext->SendMessage(QString("MUSIC_SCANNER_ERROR %1 %2").arg(host, "Already_Running"));
690 return;
691 }
692 }
693 }
694 }
695
696 //TODO: could sanity check the directory exists and is readable here?
697
698 LOG(VB_GENERAL, LOG_INFO, "Music file scanner started");
699 gCoreContext->SendMessage(QString("MUSIC_SCANNER_STARTED %1").arg(host));
700
702 QString status = QString("running");
703 updateLastRunStatus(status);
704
707
708 MusicLoadedMap music_files;
709 MusicLoadedMap art_files;
710 MusicLoadedMap::Iterator iter;
711
712 for (int x = 0; x < dirList.count(); x++)
713 {
714 QString startDir = dirList[x];
715 m_startDirs.append(startDir + '/');
716 LOG(VB_GENERAL, LOG_INFO, QString("Searching '%1' for music files").arg(startDir));
717
718 BuildFileList(startDir, music_files, art_files, 0);
719 }
720
721 m_tracksTotal = music_files.count();
722 m_coverartTotal = art_files.count();
723
724 ScanMusic(music_files);
725 ScanArtwork(art_files);
726
727 LOG(VB_GENERAL, LOG_INFO, "Updating database");
728
729 /*
730 This can be optimised quite a bit by consolidating all commands
731 via a lot of refactoring.
732
733 1) group all files of the same decoder type, and don't
734 create/delete a Decoder pr. AddFileToDB. Or make Decoders be
735 singletons, it should be a fairly simple change.
736
737 2) RemoveFileFromDB should group the remove into one big SQL.
738
739 3) UpdateFileInDB, same as 1.
740 */
741
742 for (iter = music_files.begin(); iter != music_files.end(); iter++)
743 {
744 if ((*iter).location == MusicFileScanner::kFileSystem)
745 AddFileToDB(iter.key(), (*iter).startDir);
746 else if ((*iter).location == MusicFileScanner::kDatabase)
747 RemoveFileFromDB(iter.key(), (*iter).startDir);
748 else if ((*iter).location == MusicFileScanner::kNeedUpdate)
749 {
750 UpdateFileInDB(iter.key(), (*iter).startDir);
752 }
753 }
754
755 for (iter = art_files.begin(); iter != art_files.end(); iter++)
756 {
757 if ((*iter).location == MusicFileScanner::kFileSystem)
758 AddFileToDB(iter.key(), (*iter).startDir);
759 else if ((*iter).location == MusicFileScanner::kDatabase)
760 RemoveFileFromDB(iter.key(), (*iter).startDir);
761 else if ((*iter).location == MusicFileScanner::kNeedUpdate)
762 {
763 UpdateFileInDB(iter.key(), (*iter).startDir);
765 }
766 }
767
768 // Cleanup orphaned entries from the database
769 cleanDB();
770
771 QString trackStatus = QString("total tracks found: %1 (unchanged: %2, added: %3, removed: %4, updated %5)")
774 QString coverartStatus = QString("total coverart found: %1 (unchanged: %2, added: %3, removed: %4, updated %5)")
777
778
779 LOG(VB_GENERAL, LOG_INFO, "Music file scanner finished ");
780 LOG(VB_GENERAL, LOG_INFO, trackStatus);
781 LOG(VB_GENERAL, LOG_INFO, coverartStatus);
782
783 gCoreContext->SendMessage(QString("MUSIC_SCANNER_FINISHED %1 %2 %3 %4 %5")
784 .arg(host).arg(m_tracksTotal).arg(m_tracksAdded)
786
788 status = QString("success - %1 - %2").arg(trackStatus, coverartStatus);
789 updateLastRunStatus(status);
790}
791
800{
801 MusicLoadedMap::Iterator iter;
802
804 query.prepare("SELECT CONCAT_WS('/', path, filename), date_modified "
805 "FROM music_songs LEFT JOIN music_directories ON "
806 "music_songs.directory_id=music_directories.directory_id "
807 "WHERE filename NOT LIKE BINARY ('%://%') "
808 "AND hostname = :HOSTNAME");
809
810 query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
811
812 if (!query.exec())
813 MythDB::DBError("MusicFileScanner::ScanMusic", query);
814
815 LOG(VB_GENERAL, LOG_INFO, "Checking tracks");
816
817 QString name;
818
819 if (query.isActive() && query.size() > 0)
820 {
821 while (query.next())
822 {
823 for (int x = 0; x < m_startDirs.count(); x++)
824 {
825 name = m_startDirs[x] + query.value(0).toString();
826 iter = music_files.find(name);
827 if (iter != music_files.end())
828 break;
829 }
830
831 if (iter != music_files.end())
832 {
833 if (music_files[name].location == MusicFileScanner::kDatabase)
834 continue;
835 if (m_forceupdate || HasFileChanged(name, query.value(1).toString()))
836 music_files[name].location = MusicFileScanner::kNeedUpdate;
837 else
838 {
840 music_files.erase(iter);
841 }
842 }
843 else
844 {
845 music_files[name].location = MusicFileScanner::kDatabase;
846 }
847 }
848 }
849}
850
859{
860 MusicLoadedMap::Iterator iter;
861
863 query.prepare("SELECT CONCAT_WS('/', path, filename) "
864 "FROM music_albumart "
865 "LEFT JOIN music_directories ON music_albumart.directory_id=music_directories.directory_id "
866 "WHERE music_albumart.embedded = 0 "
867 "AND music_albumart.hostname = :HOSTNAME");
868
869 query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
870
871 if (!query.exec())
872 MythDB::DBError("MusicFileScanner::ScanArtwork", query);
873
874 LOG(VB_GENERAL, LOG_INFO, "Checking artwork");
875
876 QString name;
877
878 if (query.isActive() && query.size() > 0)
879 {
880 while (query.next())
881 {
882 for (int x = 0; x < m_startDirs.count(); x++)
883 {
884 name = m_startDirs[x] + query.value(0).toString();
885 iter = music_files.find(name);
886 if (iter != music_files.end())
887 break;
888 }
889
890 if (iter != music_files.end())
891 {
892 if (music_files[name].location == MusicFileScanner::kDatabase)
893 continue;
895 music_files.erase(iter);
896 }
897 else
898 {
899 music_files[name].location = MusicFileScanner::kDatabase;
900 }
901 }
902 }
903}
904
905// static
907{
908 return gCoreContext->GetSetting("MusicScannerLastRunStatus", "") == "running";
909}
910
912{
913 QDateTime qdtNow = MythDate::current();
914 gCoreContext->SaveSetting("MusicScannerLastRunEnd", qdtNow.toString(Qt::ISODate));
915}
916
918{
919 QDateTime qdtNow = MythDate::current();
920 gCoreContext->SaveSetting("MusicScannerLastRunStart", qdtNow.toString(Qt::ISODate));
921}
922
924{
925 gCoreContext->SaveSetting("MusicScannerLastRunStatus", status);
926}
void dumpToDatabase(void)
saves or updates the image details in the DB
static ImageType guessImageType(const QString &filename)
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:837
QVariant value(int i) const
Definition: mythdbcon.h:204
int size(void) const
Definition: mythdbcon.h:214
int numRowsAffected() const
Definition: mythdbcon.h:217
bool isActive(void) const
Definition: mythdbcon.h:215
bool seek(int where, bool relative=false)
Wrap QSqlQuery::seek(int,bool)
Definition: mythdbcon.cpp:832
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:618
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:888
QVariant lastInsertId()
Return the id of the last inserted row.
Definition: mythdbcon.cpp:935
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:812
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:550
Definition: metaio.h:18
static MusicMetadata * getMetadata(const QString &filename)
Get the metadata for filename.
Definition: metaio.cpp:90
static const QString kValidFileExtensions
Definition: metaio.h:160
static MusicMetadata * readMetadata(const QString &filename)
Read the metadata from filename directly.
Definition: metaio.cpp:62
virtual bool supportsEmbeddedImages(void)
Does the tag support embedded cover art.
Definition: metaio.h:57
static MetaIO * createTagger(const QString &filename)
Finds an appropriate tagger for the given file.
Definition: metaio.cpp:31
virtual AlbumArtList getAlbumArtList(const QString &filename)
Reads the list of embedded images in the tag.
Definition: metaio.h:68
static void updateLastRunEnd(void)
QMap< QString, MusicFileData > MusicLoadedMap
void UpdateFileInDB(const QString &filename, const QString &startDir)
Updates a file in the database.
void RemoveFileFromDB(const QString &filename, const QString &startDir)
Removes a file from the database.
static bool IsArtFile(const QString &filename)
static void updateLastRunStart(void)
QStringList m_startDirs
void SearchDirs(const QStringList &dirList)
Scan a list of directories recursively for music and albumart. Inserts, updates and removes any files...
static bool HasFileChanged(const QString &filename, const QString &date_modified)
Check if file has been modified since given date/time.
static int GetDirectoryId(const QString &directory, int parentid)
Get an ID for the given directory from the database. If it doesn't already exist in the database,...
MusicFileScanner(bool force=false)
void ScanMusic(MusicLoadedMap &music_files)
Check a list of files against musics files already in the database.
void BuildFileList(QString &directory, MusicLoadedMap &music_files, MusicLoadedMap &art_files, int parentid)
Builds a list of all the files found descending recursively into the given directory.
void AddFileToDB(const QString &filename, const QString &startDir)
Insert file details into database. If it is an audio file, read the metadata and insert that informat...
static bool IsRunning(void)
static void updateLastRunStatus(QString &status)
static bool IsMusicFile(const QString &filename)
static void cleanDB()
Clear orphaned entries from the genre, artist, album and albumart tables.
void ScanArtwork(MusicLoadedMap &music_files)
Check a list of files against images already in the database.
void setDirectoryId(int ldirectoryid)
void setArtistId(int lartistid)
void setID(IdType lid)
void setHostname(const QString &host)
QString CompilationArtist() const
void setCompilationArtistId(int lartistid)
void setAlbumId(int lalbumid)
int Playcount() const
void setEmbeddedAlbumArt(AlbumArtList &albumart)
IdType ID() const
QString Filename(bool find=true)
QString Artist() const
void setRating(int lrating)
void setPlaycount(int lplaycount)
int Rating() const
void setGenreId(int lgenreid)
int PlayCount() const
QString Genre() const
int getCompilationArtistId()
AlbumArtImages * getAlbumArtImages(void)
QString Album() const
void setFileSize(uint64_t lfilesize)
void dumpToDatabase(void)
QString GetHostName(void)
void SaveSetting(const QString &key, int newValue)
QString GetSetting(const QString &key, const QString &defaultval="")
void SendMessage(const QString &message)
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:226
static const iso6937table * d
QList< AlbumArtImage * > AlbumArtList
Definition: musicmetadata.h:62
bool force
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
@ ISODate
Default UTC.
Definition: mythdate.h:17
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15