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