1 /* This file is part of Clementine.
2 Copyright 2010, David Sansome <me@davidsansome.com>
3
4 Clementine is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 Clementine is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "librarywatcher.h"
19
20 #include "librarybackend.h"
21 #include "core/filesystemwatcherinterface.h"
22 #include "core/logging.h"
23 #include "core/tagreaderclient.h"
24 #include "core/taskmanager.h"
25 #include "core/utilities.h"
26 #include "playlistparsers/cueparser.h"
27
28 #include <QDateTime>
29 #include <QDirIterator>
30 #include <QtDebug>
31 #include <QThread>
32 #include <QDateTime>
33 #include <QHash>
34 #include <QSet>
35 #include <QSettings>
36 #include <QTimer>
37
38 #include <fileref.h>
39 #include <tag.h>
40
41 // This is defined by one of the windows headers that is included by taglib.
42 #ifdef RemoveDirectory
43 #undef RemoveDirectory
44 #endif
45
46 namespace {
47 static const char *kNoMediaFile = ".nomedia";
48 static const char *kNoMusicFile = ".nomusic";
49 }
50
51 QStringList LibraryWatcher::sValidImages;
52
53 const char* LibraryWatcher::kSettingsGroup = "LibraryWatcher";
54
LibraryWatcher(QObject * parent)55 LibraryWatcher::LibraryWatcher(QObject* parent)
56 : QObject(parent),
57 backend_(nullptr),
58 task_manager_(nullptr),
59 fs_watcher_(FileSystemWatcherInterface::Create(this)),
60 stop_requested_(false),
61 scan_on_startup_(true),
62 monitor_(true),
63 rescan_timer_(new QTimer(this)),
64 rescan_paused_(false),
65 total_watches_(0),
66 cue_parser_(new CueParser(backend_, this)) {
67 rescan_timer_->setInterval(1000);
68 rescan_timer_->setSingleShot(true);
69
70 if (sValidImages.isEmpty()) {
71 sValidImages << "jpg"
72 << "png"
73 << "gif"
74 << "jpeg";
75 }
76
77 ReloadSettings();
78
79 connect(rescan_timer_, SIGNAL(timeout()), SLOT(RescanPathsNow()));
80 }
81
ScanTransaction(LibraryWatcher * watcher,int dir,bool incremental,bool ignores_mtime)82 LibraryWatcher::ScanTransaction::ScanTransaction(LibraryWatcher* watcher,
83 int dir, bool incremental,
84 bool ignores_mtime)
85 : progress_(0),
86 progress_max_(0),
87 dir_(dir),
88 incremental_(incremental),
89 ignores_mtime_(ignores_mtime),
90 watcher_(watcher),
91 cached_songs_dirty_(true),
92 known_subdirs_dirty_(true) {
93 QString description;
94 if (watcher_->device_name_.isEmpty())
95 description = tr("Updating library");
96 else
97 description = tr("Updating %1").arg(watcher_->device_name_);
98
99 task_id_ = watcher_->task_manager_->StartTask(description);
100 emit watcher_->ScanStarted(task_id_);
101 }
102
~ScanTransaction()103 LibraryWatcher::ScanTransaction::~ScanTransaction() {
104 // If we're stopping then don't commit the transaction
105 if (watcher_->stop_requested_) return;
106
107 if (!new_songs.isEmpty()) emit watcher_->NewOrUpdatedSongs(new_songs);
108
109 if (!touched_songs.isEmpty()) emit watcher_->SongsMTimeUpdated(touched_songs);
110
111 if (!deleted_songs.isEmpty()) emit watcher_->SongsDeleted(deleted_songs);
112
113 if (!readded_songs.isEmpty()) emit watcher_->SongsReadded(readded_songs);
114
115 if (!new_subdirs.isEmpty()) emit watcher_->SubdirsDiscovered(new_subdirs);
116
117 if (!touched_subdirs.isEmpty())
118 emit watcher_->SubdirsMTimeUpdated(touched_subdirs);
119
120 watcher_->task_manager_->SetTaskFinished(task_id_);
121
122 for (const Subdirectory& subdir : deleted_subdirs) {
123 if (watcher_->watched_dirs_.contains(dir_)) {
124 watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
125 }
126 }
127
128 if (watcher_->monitor_) {
129 // Watch the new subdirectories
130 for (const Subdirectory& subdir : new_subdirs) {
131 watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
132 }
133 }
134 }
135
AddToProgress(int n)136 void LibraryWatcher::ScanTransaction::AddToProgress(int n) {
137 progress_ += n;
138 watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
139 }
140
AddToProgressMax(int n)141 void LibraryWatcher::ScanTransaction::AddToProgressMax(int n) {
142 progress_max_ += n;
143 watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
144 }
145
FindSongsInSubdirectory(const QString & path)146 SongList LibraryWatcher::ScanTransaction::FindSongsInSubdirectory(
147 const QString& path) {
148 if (cached_songs_dirty_) {
149 cached_songs_ = watcher_->backend_->FindSongsInDirectory(dir_);
150 cached_songs_dirty_ = false;
151 }
152
153 // TODO: Make this faster
154 SongList ret;
155 for (const Song& song : cached_songs_) {
156 if (song.url().toLocalFile().section('/', 0, -2) == path) ret << song;
157 }
158 return ret;
159 }
160
SetKnownSubdirs(const SubdirectoryList & subdirs)161 void LibraryWatcher::ScanTransaction::SetKnownSubdirs(
162 const SubdirectoryList& subdirs) {
163 known_subdirs_ = subdirs;
164 known_subdirs_dirty_ = false;
165 }
166
HasSeenSubdir(const QString & path)167 bool LibraryWatcher::ScanTransaction::HasSeenSubdir(const QString& path) {
168 if (known_subdirs_dirty_)
169 SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
170
171 for (const Subdirectory& subdir : known_subdirs_) {
172 if (subdir.path == path && subdir.mtime != 0) return true;
173 }
174 return false;
175 }
176
GetImmediateSubdirs(const QString & path)177 SubdirectoryList LibraryWatcher::ScanTransaction::GetImmediateSubdirs(
178 const QString& path) {
179 if (known_subdirs_dirty_)
180 SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
181
182 SubdirectoryList ret;
183 for (const Subdirectory& subdir : known_subdirs_) {
184 if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path &&
185 subdir.mtime != 0) {
186 ret << subdir;
187 }
188 }
189
190 return ret;
191 }
192
GetAllSubdirs()193 SubdirectoryList LibraryWatcher::ScanTransaction::GetAllSubdirs() {
194 if (known_subdirs_dirty_)
195 SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
196 return known_subdirs_;
197 }
198
AddDirectory(const Directory & dir,const SubdirectoryList & subdirs)199 void LibraryWatcher::AddDirectory(const Directory& dir,
200 const SubdirectoryList& subdirs) {
201 watched_dirs_[dir.id] = dir;
202
203 if (subdirs.isEmpty()) {
204 // This is a new directory that we've never seen before.
205 // Scan it fully.
206 ScanTransaction transaction(this, dir.id, false);
207 transaction.SetKnownSubdirs(subdirs);
208 transaction.AddToProgressMax(1);
209 ScanSubdirectory(dir.path, Subdirectory(), &transaction);
210 } else {
211 // We can do an incremental scan - looking at the mtimes of each
212 // subdirectory and only rescan if the directory has changed.
213 ScanTransaction transaction(this, dir.id, true);
214 transaction.SetKnownSubdirs(subdirs);
215 transaction.AddToProgressMax(subdirs.count());
216 for (const Subdirectory& subdir : subdirs) {
217 if (stop_requested_) return;
218
219 if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, &transaction);
220
221 if (monitor_) AddWatch(dir, subdir.path);
222 }
223 }
224
225 emit CompilationsNeedUpdating();
226 }
227
ScanSubdirectory(const QString & path,const Subdirectory & subdir,ScanTransaction * t,bool force_noincremental)228 void LibraryWatcher::ScanSubdirectory(const QString& path,
229 const Subdirectory& subdir,
230 ScanTransaction* t,
231 bool force_noincremental) {
232 QFileInfo path_info(path);
233 QDir path_dir(path);
234
235 // Do not scan symlinked dirs that are already in collection
236 if (path_info.isSymLink()) {
237 QString real_path = path_info.symLinkTarget();
238 for (const Directory& dir : watched_dirs_) {
239 if (real_path.startsWith(dir.path)) {
240 t->AddToProgress(1);
241 return;
242 }
243 }
244 }
245
246 // Do not scan directories containing a .nomedia or .nomusic file
247 if (path_dir.exists(kNoMediaFile) ||
248 path_dir.exists(kNoMusicFile)) {
249 t->AddToProgress(1);
250 return;
251 }
252
253 if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() &&
254 subdir.mtime == path_info.lastModified().toTime_t()) {
255 // The directory hasn't changed since last time
256 t->AddToProgress(1);
257 return;
258 }
259
260 QMap<QString, QStringList> album_art;
261 QStringList files_on_disk;
262 SubdirectoryList my_new_subdirs;
263
264 // If a directory is moved then only its parent gets a changed notification,
265 // so we need to look and see if any of our children don't exist any more.
266 // If one has been removed, "rescan" it to get the deleted songs
267 SubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
268 for (const Subdirectory& subdir : previous_subdirs) {
269 if (!QFile::exists(subdir.path) && subdir.path != path) {
270 t->AddToProgressMax(1);
271 ScanSubdirectory(subdir.path, subdir, t, true);
272 }
273 }
274
275 // First we "quickly" get a list of the files in the directory that we
276 // think might be music. While we're here, we also look for new
277 // subdirectories
278 // and possible album artwork.
279 QDirIterator it(
280 path, QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot);
281 while (it.hasNext()) {
282 if (stop_requested_) return;
283
284 QString child(it.next());
285 QFileInfo child_info(child);
286
287 if (child_info.isDir()) {
288 if (!child_info.isHidden() && !t->HasSeenSubdir(child)) {
289 // We haven't seen this subdirectory before - add it to a list and
290 // later we'll tell the backend about it and scan it.
291 Subdirectory new_subdir;
292 new_subdir.directory_id = -1;
293 new_subdir.path = child;
294 new_subdir.mtime = child_info.lastModified().toTime_t();
295 my_new_subdirs << new_subdir;
296 }
297 } else {
298 QString ext_part(ExtensionPart(child));
299 QString dir_part(DirectoryPart(child));
300
301 if (sValidImages.contains(ext_part))
302 album_art[dir_part] << child;
303 else if (!child_info.isHidden())
304 files_on_disk << child;
305 }
306 }
307
308 if (stop_requested_) return;
309
310 // Ask the database for a list of files in this directory
311 SongList songs_in_db = t->FindSongsInSubdirectory(path);
312
313 QSet<QString> cues_processed;
314
315 // Now compare the list from the database with the list of files on disk
316 for (const QString& file : files_on_disk) {
317 if (stop_requested_) return;
318
319 // associated cue
320 QString matching_cue = NoExtensionPart(file) + ".cue";
321
322 Song matching_song;
323 if (FindSongByPath(songs_in_db, file, &matching_song)) {
324 uint matching_cue_mtime = GetMtimeForCue(matching_cue);
325
326 // The song is in the database and still on disk.
327 // Check the mtime to see if it's been changed since it was added.
328 QFileInfo file_info(file);
329
330 if (!file_info.exists()) {
331 // Partially fixes race condition - if file was removed between being
332 // added to the list and now.
333 files_on_disk.removeAll(file);
334 continue;
335 }
336
337 // cue sheet's path from library (if any)
338 QString song_cue = matching_song.cue_path();
339 uint song_cue_mtime = GetMtimeForCue(song_cue);
340
341 bool cue_deleted = song_cue_mtime == 0 && matching_song.has_cue();
342 bool cue_added = matching_cue_mtime != 0 && !matching_song.has_cue();
343
344 // watch out for cue songs which have their mtime equal to
345 // qMax(media_file_mtime, cue_sheet_mtime)
346 bool changed =
347 (matching_song.mtime() !=
348 qMax(file_info.lastModified().toTime_t(), song_cue_mtime)) ||
349 cue_deleted || cue_added;
350
351 // Also want to look to see whether the album art has changed
352 QString image = ImageForSong(file, album_art);
353 if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) ||
354 (!matching_song.art_automatic().isEmpty() &&
355 !matching_song.has_embedded_cover() &&
356 !QFile::exists(matching_song.art_automatic()))) {
357 changed = true;
358 }
359
360 // the song's changed - reread the metadata from file
361 if (t->ignores_mtime() || changed) {
362 qLog(Debug) << file << "changed";
363
364 // if cue associated...
365 if (!cue_deleted && (matching_song.has_cue() || cue_added)) {
366 UpdateCueAssociatedSongs(file, path, matching_cue, image, t);
367 // if no cue or it's about to lose it...
368 } else {
369 UpdateNonCueAssociatedSong(file, matching_song, image, cue_deleted,
370 t);
371 }
372 }
373
374 // nothing has changed - mark the song available without re-scanning
375 if (matching_song.is_unavailable()) t->readded_songs << matching_song;
376
377 } else {
378 // The song is on disk but not in the DB
379 SongList song_list =
380 ScanNewFile(file, path, matching_cue, &cues_processed);
381
382 if (song_list.isEmpty()) {
383 continue;
384 }
385
386 qLog(Debug) << file << "created";
387 // choose an image for the song(s)
388 QString image = ImageForSong(file, album_art);
389
390 for (Song song : song_list) {
391 song.set_directory_id(t->dir());
392 if (song.art_automatic().isEmpty()) song.set_art_automatic(image);
393
394 t->new_songs << song;
395 }
396 }
397 }
398
399 // Look for deleted songs
400 for (const Song& song : songs_in_db) {
401 if (!song.is_unavailable() &&
402 !files_on_disk.contains(song.url().toLocalFile())) {
403 qLog(Debug) << "Song deleted from disk:" << song.url().toLocalFile();
404 t->deleted_songs << song;
405 }
406 }
407
408 // Add this subdir to the new or touched list
409 Subdirectory updated_subdir;
410 updated_subdir.directory_id = t->dir();
411 updated_subdir.mtime =
412 path_info.exists() ? path_info.lastModified().toTime_t() : 0;
413 updated_subdir.path = path;
414
415 if (subdir.directory_id == -1)
416 t->new_subdirs << updated_subdir;
417 else
418 t->touched_subdirs << updated_subdir;
419
420 if (updated_subdir.mtime ==
421 0) { // Subdirectory deleted, mark it for removal from the watcher.
422 t->deleted_subdirs << updated_subdir;
423 }
424
425 t->AddToProgress(1);
426
427 // Recurse into the new subdirs that we found
428 t->AddToProgressMax(my_new_subdirs.count());
429 for (const Subdirectory& my_new_subdir : my_new_subdirs) {
430 if (stop_requested_) return;
431 ScanSubdirectory(my_new_subdir.path, my_new_subdir, t, true);
432 }
433 }
434
UpdateCueAssociatedSongs(const QString & file,const QString & path,const QString & matching_cue,const QString & image,ScanTransaction * t)435 void LibraryWatcher::UpdateCueAssociatedSongs(const QString& file,
436 const QString& path,
437 const QString& matching_cue,
438 const QString& image,
439 ScanTransaction* t) {
440 QFile cue(matching_cue);
441 cue.open(QIODevice::ReadOnly);
442
443 SongList old_sections = backend_->GetSongsByUrl(QUrl::fromLocalFile(file));
444
445 QHash<quint64, Song> sections_map;
446 for (const Song& song : old_sections) {
447 sections_map[song.beginning_nanosec()] = song;
448 }
449
450 QSet<int> used_ids;
451
452 // update every song that's in the cue and library
453 for (Song cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
454 cue_song.set_directory_id(t->dir());
455
456 Song matching = sections_map[cue_song.beginning_nanosec()];
457 // a new section
458 if (!matching.is_valid()) {
459 t->new_songs << cue_song;
460 // changed section
461 } else {
462 PreserveUserSetData(file, image, matching, &cue_song, t);
463 used_ids.insert(matching.id());
464 }
465 }
466
467 // sections that are now missing
468 for (const Song& matching : old_sections) {
469 if (!used_ids.contains(matching.id())) {
470 t->deleted_songs << matching;
471 }
472 }
473 }
474
UpdateNonCueAssociatedSong(const QString & file,const Song & matching_song,const QString & image,bool cue_deleted,ScanTransaction * t)475 void LibraryWatcher::UpdateNonCueAssociatedSong(const QString& file,
476 const Song& matching_song,
477 const QString& image,
478 bool cue_deleted,
479 ScanTransaction* t) {
480 // if a cue got deleted, we turn it's first section into the new
481 // 'raw' (cueless) song and we just remove the rest of the sections
482 // from the library
483 if (cue_deleted) {
484 for (const Song& song :
485 backend_->GetSongsByUrl(QUrl::fromLocalFile(file))) {
486 if (!song.IsMetadataEqual(matching_song)) {
487 t->deleted_songs << song;
488 }
489 }
490 }
491
492 Song song_on_disk;
493 song_on_disk.set_directory_id(t->dir());
494 TagReaderClient::Instance()->ReadFileBlocking(file, &song_on_disk);
495
496 if (song_on_disk.is_valid()) {
497 PreserveUserSetData(file, image, matching_song, &song_on_disk, t);
498 }
499 }
500
ScanNewFile(const QString & file,const QString & path,const QString & matching_cue,QSet<QString> * cues_processed)501 SongList LibraryWatcher::ScanNewFile(const QString& file, const QString& path,
502 const QString& matching_cue,
503 QSet<QString>* cues_processed) {
504 SongList song_list;
505
506 uint matching_cue_mtime = GetMtimeForCue(matching_cue);
507 // if it's a cue - create virtual tracks
508 if (matching_cue_mtime) {
509 // don't process the same cue many times
510 if (cues_processed->contains(matching_cue)) return song_list;
511
512 QFile cue(matching_cue);
513 cue.open(QIODevice::ReadOnly);
514
515 // Ignore FILEs pointing to other media files. Also, watch out for incorrect
516 // media files. Playlist parser for CUEs considers every entry in sheet
517 // valid and we don't want invalid media getting into library!
518 QString file_nfd = file.normalized(QString::NormalizationForm_D);
519 for (const Song& cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
520 if (cue_song.url().toLocalFile().normalized(QString::NormalizationForm_D) == file_nfd) {
521 if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
522 song_list << cue_song;
523 }
524 }
525 }
526
527 if (!song_list.isEmpty()) {
528 *cues_processed << matching_cue;
529 }
530
531 // it's a normal media file
532 } else {
533 Song song;
534 TagReaderClient::Instance()->ReadFileBlocking(file, &song);
535
536 if (song.is_valid()) {
537 song_list << song;
538 }
539 }
540
541 return song_list;
542 }
543
PreserveUserSetData(const QString & file,const QString & image,const Song & matching_song,Song * out,ScanTransaction * t)544 void LibraryWatcher::PreserveUserSetData(const QString& file,
545 const QString& image,
546 const Song& matching_song, Song* out,
547 ScanTransaction* t) {
548 out->set_id(matching_song.id());
549
550 // Previous versions of Clementine incorrectly overwrote this and
551 // stored it in the DB, so we can't rely on matching_song to
552 // know if it has embedded artwork or not, but we can check here.
553 if (!out->has_embedded_cover()) out->set_art_automatic(image);
554
555 out->MergeUserSetData(matching_song);
556
557 // The song was deleted from the database (e.g. due to an unmounted
558 // filesystem), but has been restored.
559 if (matching_song.is_unavailable()) {
560 qLog(Debug) << file << " unavailable song restored";
561
562 t->new_songs << *out;
563 } else if (!matching_song.IsMetadataEqual(*out)) {
564 qLog(Debug) << file << "metadata changed";
565
566 // Update the song in the DB
567 t->new_songs << *out;
568 } else {
569 // Only the mtime's changed
570 t->touched_songs << *out;
571 }
572 }
573
GetMtimeForCue(const QString & cue_path)574 uint LibraryWatcher::GetMtimeForCue(const QString& cue_path) {
575 // slight optimisation
576 if (cue_path.isEmpty()) {
577 return 0;
578 }
579
580 const QFileInfo file_info(cue_path);
581 if (!file_info.exists()) {
582 return 0;
583 }
584
585 const QDateTime cue_last_modified = file_info.lastModified();
586
587 return cue_last_modified.isValid() ? cue_last_modified.toTime_t() : 0;
588 }
589
AddWatch(const Directory & dir,const QString & path)590 void LibraryWatcher::AddWatch(const Directory& dir, const QString& path) {
591 if (!QFile::exists(path)) return;
592
593 connect(fs_watcher_, SIGNAL(PathChanged(const QString&)), this,
594 SLOT(DirectoryChanged(const QString&)), Qt::UniqueConnection);
595 fs_watcher_->AddPath(path);
596 subdir_mapping_[path] = dir;
597 }
598
RemoveWatch(const Directory & dir,const Subdirectory & subdir)599 void LibraryWatcher::RemoveWatch(const Directory& dir,
600 const Subdirectory& subdir) {
601 for (const QString& subdir_path : subdir_mapping_.keys(dir)) {
602 if (subdir_path != subdir.path) continue;
603 fs_watcher_->RemovePath(subdir_path);
604 subdir_mapping_.remove(subdir_path);
605 break;
606 }
607 }
608
RemoveDirectory(const Directory & dir)609 void LibraryWatcher::RemoveDirectory(const Directory& dir) {
610 rescan_queue_.remove(dir.id);
611 watched_dirs_.remove(dir.id);
612
613 // Stop watching the directory's subdirectories
614 for (const QString& subdir_path : subdir_mapping_.keys(dir)) {
615 fs_watcher_->RemovePath(subdir_path);
616 subdir_mapping_.remove(subdir_path);
617 }
618 }
619
FindSongByPath(const SongList & list,const QString & path,Song * out)620 bool LibraryWatcher::FindSongByPath(const SongList& list, const QString& path,
621 Song* out) {
622 // TODO: Make this faster
623 for (const Song& song : list) {
624 if (song.url().toLocalFile() == path) {
625 *out = song;
626 return true;
627 }
628 }
629 return false;
630 }
631
DirectoryChanged(const QString & subdir)632 void LibraryWatcher::DirectoryChanged(const QString& subdir) {
633 // Find what dir it was in
634 QHash<QString, Directory>::const_iterator it =
635 subdir_mapping_.constFind(subdir);
636 if (it == subdir_mapping_.constEnd()) {
637 return;
638 }
639 Directory dir = *it;
640
641 qLog(Debug) << "Subdir" << subdir << "changed under directory" << dir.path
642 << "id" << dir.id;
643
644 // Queue the subdir for rescanning
645 if (!rescan_queue_[dir.id].contains(subdir)) rescan_queue_[dir.id] << subdir;
646
647 if (!rescan_paused_) rescan_timer_->start();
648 }
649
RescanPathsNow()650 void LibraryWatcher::RescanPathsNow() {
651 for (int dir : rescan_queue_.keys()) {
652 if (stop_requested_) return;
653 ScanTransaction transaction(this, dir, false);
654 transaction.AddToProgressMax(rescan_queue_[dir].count());
655
656 for (const QString& path : rescan_queue_[dir]) {
657 if (stop_requested_) return;
658 Subdirectory subdir;
659 subdir.directory_id = dir;
660 subdir.mtime = 0;
661 subdir.path = path;
662 ScanSubdirectory(path, subdir, &transaction);
663 }
664 }
665
666 rescan_queue_.clear();
667
668 emit CompilationsNeedUpdating();
669 }
670
PickBestImage(const QStringList & images)671 QString LibraryWatcher::PickBestImage(const QStringList& images) {
672 // This is used when there is more than one image in a directory.
673 // Pick the biggest image that matches the most important filter
674
675 QStringList filtered;
676
677 for (const QString& filter_text : best_image_filters_) {
678 // the images in the images list are represented by a full path,
679 // so we need to isolate just the filename
680 for (const QString& image : images) {
681 QFileInfo file_info(image);
682 QString filename(file_info.fileName());
683 if (filename.contains(filter_text, Qt::CaseInsensitive))
684 filtered << image;
685 }
686
687 /* We assume the filters are give in the order best to worst, so
688 if we've got a result, we go with it. Otherwise we might
689 start capturing more generic rules */
690 if (!filtered.isEmpty()) break;
691 }
692
693 if (filtered.isEmpty()) {
694 // the filter was too restrictive, just use the original list
695 filtered = images;
696 }
697
698 int biggest_size = 0;
699 QString biggest_path;
700
701 for (const QString& path : filtered) {
702 if (stop_requested_) return "";
703
704 QImage image(path);
705 if (image.isNull()) continue;
706
707 int size = image.width() * image.height();
708 if (size > biggest_size) {
709 biggest_size = size;
710 biggest_path = path;
711 }
712 }
713
714 return biggest_path;
715 }
716
ImageForSong(const QString & path,QMap<QString,QStringList> & album_art)717 QString LibraryWatcher::ImageForSong(const QString& path,
718 QMap<QString, QStringList>& album_art) {
719 QString dir(DirectoryPart(path));
720
721 if (album_art.contains(dir)) {
722 if (album_art[dir].count() == 1)
723 return album_art[dir][0];
724 else {
725 QString best_image = PickBestImage(album_art[dir]);
726 album_art[dir] = QStringList() << best_image;
727 return best_image;
728 }
729 }
730 return QString();
731 }
732
ReloadSettingsAsync()733 void LibraryWatcher::ReloadSettingsAsync() {
734 QMetaObject::invokeMethod(this, "ReloadSettings", Qt::QueuedConnection);
735 }
736
ReloadSettings()737 void LibraryWatcher::ReloadSettings() {
738 const bool was_monitoring_before = monitor_;
739
740 QSettings s;
741 s.beginGroup(kSettingsGroup);
742 scan_on_startup_ = s.value("startup_scan", true).toBool();
743 monitor_ = s.value("monitor", true).toBool();
744
745 best_image_filters_.clear();
746 QStringList filters =
747 s.value("cover_art_patterns", QStringList() << "front"
748 << "cover").toStringList();
749 for (const QString& filter : filters) {
750 QString s = filter.trimmed();
751 if (!s.isEmpty()) best_image_filters_ << s;
752 }
753
754 if (!monitor_ && was_monitoring_before) {
755 fs_watcher_->Clear();
756 } else if (monitor_ && !was_monitoring_before) {
757 // Add all directories to all QFileSystemWatchers again
758 for (const Directory& dir : watched_dirs_.values()) {
759 SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
760 for (const Subdirectory& subdir : subdirs) {
761 AddWatch(dir, subdir.path);
762 }
763 }
764 }
765 }
766
SetRescanPausedAsync(bool pause)767 void LibraryWatcher::SetRescanPausedAsync(bool pause) {
768 QMetaObject::invokeMethod(this, "SetRescanPaused", Qt::QueuedConnection,
769 Q_ARG(bool, pause));
770 }
771
SetRescanPaused(bool pause)772 void LibraryWatcher::SetRescanPaused(bool pause) {
773 rescan_paused_ = pause;
774 if (!rescan_paused_ && !rescan_queue_.isEmpty()) RescanPathsNow();
775 }
776
IncrementalScanAsync()777 void LibraryWatcher::IncrementalScanAsync() {
778 QMetaObject::invokeMethod(this, "IncrementalScanNow", Qt::QueuedConnection);
779 }
780
FullScanAsync()781 void LibraryWatcher::FullScanAsync() {
782 QMetaObject::invokeMethod(this, "FullScanNow", Qt::QueuedConnection);
783 }
784
IncrementalScanNow()785 void LibraryWatcher::IncrementalScanNow() { PerformScan(true, false); }
786
FullScanNow()787 void LibraryWatcher::FullScanNow() { PerformScan(false, true); }
788
PerformScan(bool incremental,bool ignore_mtimes)789 void LibraryWatcher::PerformScan(bool incremental, bool ignore_mtimes) {
790 for (const Directory& dir : watched_dirs_.values()) {
791 ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes);
792 SubdirectoryList subdirs(transaction.GetAllSubdirs());
793 transaction.AddToProgressMax(subdirs.count());
794
795 for (const Subdirectory& subdir : subdirs) {
796 if (stop_requested_) return;
797
798 ScanSubdirectory(subdir.path, subdir, &transaction);
799 }
800 }
801
802 emit CompilationsNeedUpdating();
803 }
804