Add ability to cancel scan per directory.

Store a boolean along with watched directories to indicate active. Use this flag
to provide a mechanism to halt scans on a per-directory basis.
This commit is contained in:
Jim Broadus 2020-01-21 00:32:55 -08:00
parent 97ffae2e70
commit 4ebd3c8c0a
2 changed files with 106 additions and 49 deletions

View File

@ -27,13 +27,13 @@
#include <QDateTime> #include <QDateTime>
#include <QDirIterator> #include <QDirIterator>
#include <QtDebug>
#include <QThread>
#include <QDateTime>
#include <QHash> #include <QHash>
#include <QMutexLocker>
#include <QSet> #include <QSet>
#include <QSettings> #include <QSettings>
#include <QThread>
#include <QTimer> #include <QTimer>
#include <QtDebug>
#include <fileref.h> #include <fileref.h>
#include <tag.h> #include <tag.h>
@ -59,7 +59,6 @@ LibraryWatcher::LibraryWatcher(QObject* parent)
backend_(nullptr), backend_(nullptr),
task_manager_(nullptr), task_manager_(nullptr),
fs_watcher_(FileSystemWatcherInterface::Create(this)), fs_watcher_(FileSystemWatcherInterface::Create(this)),
stop_requested_(false),
scan_on_startup_(true), scan_on_startup_(true),
monitor_(true), monitor_(true),
rescan_timer_(new QTimer(this)), rescan_timer_(new QTimer(this)),
@ -85,10 +84,9 @@ LibraryWatcher::LibraryWatcher(QObject* parent)
// is only created on a stack and the removal of a directory from the watch // is only created on a stack and the removal of a directory from the watch
// list only occurs as a result of a signal and happens on the watcher's // list only occurs as a result of a signal and happens on the watcher's
// thread. So the Directory object will not be deleted out from under us. // thread. So the Directory object will not be deleted out from under us.
LibraryWatcher::ScanTransaction::ScanTransaction(LibraryWatcher* watcher, LibraryWatcher::ScanTransaction::ScanTransaction(
const Directory& dir, LibraryWatcher* watcher, const LibraryWatcher::WatchedDir& dir,
bool incremental, bool incremental, bool ignores_mtime)
bool ignores_mtime)
: progress_(0), : progress_(0),
progress_max_(0), progress_max_(0),
dir_(dir), dir_(dir),
@ -109,7 +107,7 @@ LibraryWatcher::ScanTransaction::ScanTransaction(LibraryWatcher* watcher,
LibraryWatcher::ScanTransaction::~ScanTransaction() { LibraryWatcher::ScanTransaction::~ScanTransaction() {
// If we're stopping then don't commit the transaction // If we're stopping then don't commit the transaction
if (watcher_->stop_requested_) { if (aborted()) {
watcher_->task_manager_->SetTaskFinished(task_id_); watcher_->task_manager_->SetTaskFinished(task_id_);
return; return;
} }
@ -204,12 +202,36 @@ SubdirectoryList LibraryWatcher::ScanTransaction::GetAllSubdirs() {
return known_subdirs_; return known_subdirs_;
} }
void LibraryWatcher::WatchList::StopAll() {
QMutexLocker l(&mutex_);
for (WatchedDir& wdir : list_) {
wdir.active_ = false;
}
}
void LibraryWatcher::WatchList::Stop(const Directory& dir) {
QMutexLocker l(&mutex_);
if (list_.contains(dir.id)) {
list_[dir.id].active_ = false;
}
}
void LibraryWatcher::WatchList::Remove(int id) {
QMutexLocker l(&mutex_);
list_.remove(id);
}
void LibraryWatcher::WatchList::Add(const Directory& dir) {
// Lock is not really necessary here, but it's also not going to block
// anything, so include it for sanity and completeness.
QMutexLocker l(&mutex_);
list_[dir.id] = WatchedDir(dir);
}
void LibraryWatcher::AddDirectory(const Directory& dir, void LibraryWatcher::AddDirectory(const Directory& dir,
const SubdirectoryList& subdirs) { const SubdirectoryList& subdirs) {
watched_dirs_[dir.id] = dir; watched_dirs_.Add(dir);
// Use a reference to our copy of the directory, not the reference to the const WatchedDir& new_dir = watched_dirs_.list_[dir.id];
// caller's instance.
Directory& new_dir = watched_dirs_[dir.id];
if (subdirs.isEmpty()) { if (subdirs.isEmpty()) {
// This is a new directory that we've never seen before. // This is a new directory that we've never seen before.
@ -225,7 +247,7 @@ void LibraryWatcher::AddDirectory(const Directory& dir,
transaction.SetKnownSubdirs(subdirs); transaction.SetKnownSubdirs(subdirs);
transaction.AddToProgressMax(subdirs.count()); transaction.AddToProgressMax(subdirs.count());
for (const Subdirectory& subdir : subdirs) { for (const Subdirectory& subdir : subdirs) {
if (stop_requested_) return; if (transaction.aborted()) return;
if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, &transaction); if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, &transaction);
@ -246,7 +268,7 @@ void LibraryWatcher::ScanSubdirectory(const QString& path,
// Do not scan symlinked dirs that are already in collection // Do not scan symlinked dirs that are already in collection
if (path_info.isSymLink()) { if (path_info.isSymLink()) {
QString real_path = path_info.symLinkTarget(); QString real_path = path_info.symLinkTarget();
for (const Directory& dir : watched_dirs_) { for (const Directory& dir : watched_dirs_.list_) {
if (real_path.startsWith(dir.path)) { if (real_path.startsWith(dir.path)) {
t->AddToProgress(1); t->AddToProgress(1);
return; return;
@ -290,7 +312,7 @@ void LibraryWatcher::ScanSubdirectory(const QString& path,
QDirIterator it( QDirIterator it(
path, QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot); path, QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot);
while (it.hasNext()) { while (it.hasNext()) {
if (stop_requested_) return; if (t->aborted()) return;
QString child(it.next()); QString child(it.next());
QFileInfo child_info(child); QFileInfo child_info(child);
@ -316,7 +338,7 @@ void LibraryWatcher::ScanSubdirectory(const QString& path,
} }
} }
if (stop_requested_) return; if (t->aborted()) return;
// Ask the database for a list of files in this directory // Ask the database for a list of files in this directory
SongList songs_in_db = t->FindSongsInSubdirectory(path); SongList songs_in_db = t->FindSongsInSubdirectory(path);
@ -325,7 +347,7 @@ void LibraryWatcher::ScanSubdirectory(const QString& path,
// Now compare the list from the database with the list of files on disk // Now compare the list from the database with the list of files on disk
for (const QString& file : files_on_disk) { for (const QString& file : files_on_disk) {
if (stop_requested_) return; if (t->aborted()) return;
// associated cue // associated cue
QString matching_cue = NoExtensionPart(file) + ".cue"; QString matching_cue = NoExtensionPart(file) + ".cue";
@ -360,7 +382,7 @@ void LibraryWatcher::ScanSubdirectory(const QString& path,
cue_deleted || cue_added; cue_deleted || cue_added;
// Also want to look to see whether the album art has changed // Also want to look to see whether the album art has changed
QString image = ImageForSong(file, album_art); QString image = ImageForSong(file, album_art, t);
if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) || if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) ||
(!matching_song.art_automatic().isEmpty() && (!matching_song.art_automatic().isEmpty() &&
!matching_song.has_embedded_cover() && !matching_song.has_embedded_cover() &&
@ -396,7 +418,7 @@ void LibraryWatcher::ScanSubdirectory(const QString& path,
qLog(Debug) << file << "created"; qLog(Debug) << file << "created";
// choose an image for the song(s) // choose an image for the song(s)
QString image = ImageForSong(file, album_art); QString image = ImageForSong(file, album_art, t);
for (Song song : song_list) { for (Song song : song_list) {
song.set_directory_id(t->dir_id()); song.set_directory_id(t->dir_id());
@ -438,7 +460,7 @@ void LibraryWatcher::ScanSubdirectory(const QString& path,
// Recurse into the new subdirs that we found // Recurse into the new subdirs that we found
t->AddToProgressMax(my_new_subdirs.count()); t->AddToProgressMax(my_new_subdirs.count());
for (const Subdirectory& my_new_subdir : my_new_subdirs) { for (const Subdirectory& my_new_subdir : my_new_subdirs) {
if (stop_requested_) return; if (t->aborted()) return;
ScanSubdirectory(my_new_subdir.path, my_new_subdir, t, true); ScanSubdirectory(my_new_subdir.path, my_new_subdir, t, true);
} }
} }
@ -642,7 +664,7 @@ void LibraryWatcher::RemoveWatch(const Directory& dir,
void LibraryWatcher::RemoveDirectory(const Directory& dir) { void LibraryWatcher::RemoveDirectory(const Directory& dir) {
rescan_queue_.remove(dir.id); rescan_queue_.remove(dir.id);
watched_dirs_.remove(dir.id); watched_dirs_.Remove(dir.id);
// Stop watching the directory's subdirectories // Stop watching the directory's subdirectories
for (const QString& subdir_path : subdir_mapping_.keys(dir)) { for (const QString& subdir_path : subdir_mapping_.keys(dir)) {
@ -682,21 +704,22 @@ void LibraryWatcher::DirectoryChanged(const QString& subdir) {
} }
void LibraryWatcher::RescanPathsNow() { void LibraryWatcher::RescanPathsNow() {
for (int dir : rescan_queue_.keys()) { for (int id : rescan_queue_.keys()) {
if (stop_requested_) return; if (!watched_dirs_.list_.contains(id)) {
qLog(Warning) << "Rescan id" << id << "not in watch list.";
if (!watched_dirs_.contains(dir)) {
qLog(Warning) << "Rescan id" << dir << "not in watch list.";
continue; continue;
} }
const WatchedDir& dir = watched_dirs_.list_[id];
ScanTransaction transaction(this, watched_dirs_[dir], false); if (!dir.active_) continue;
transaction.AddToProgressMax(rescan_queue_[dir].count());
for (const QString& path : rescan_queue_[dir]) { ScanTransaction transaction(this, dir, false);
if (stop_requested_) return; transaction.AddToProgressMax(rescan_queue_[id].count());
for (const QString& path : rescan_queue_[id]) {
if (transaction.aborted()) return;
Subdirectory subdir; Subdirectory subdir;
subdir.directory_id = dir; subdir.directory_id = id;
subdir.mtime = 0; subdir.mtime = 0;
subdir.path = path; subdir.path = path;
ScanSubdirectory(path, subdir, &transaction); ScanSubdirectory(path, subdir, &transaction);
@ -708,7 +731,8 @@ void LibraryWatcher::RescanPathsNow() {
emit CompilationsNeedUpdating(); emit CompilationsNeedUpdating();
} }
QString LibraryWatcher::PickBestImage(const QStringList& images) { QString LibraryWatcher::PickBestImage(const QStringList& images,
ScanTransaction* t) {
// This is used when there is more than one image in a directory. // This is used when there is more than one image in a directory.
// Pick the biggest image that matches the most important filter // Pick the biggest image that matches the most important filter
@ -748,7 +772,7 @@ QString LibraryWatcher::PickBestImage(const QStringList& images) {
QString biggest_path; QString biggest_path;
for (const QString& path : filtered) { for (const QString& path : filtered) {
if (stop_requested_) return ""; if (t->aborted()) return "";
QImage image(path); QImage image(path);
if (image.isNull()) continue; if (image.isNull()) continue;
@ -764,14 +788,15 @@ QString LibraryWatcher::PickBestImage(const QStringList& images) {
} }
QString LibraryWatcher::ImageForSong(const QString& path, QString LibraryWatcher::ImageForSong(const QString& path,
QMap<QString, QStringList>& album_art) { QMap<QString, QStringList>& album_art,
ScanTransaction* t) {
QString dir(DirectoryPart(path)); QString dir(DirectoryPart(path));
if (album_art.contains(dir)) { if (album_art.contains(dir)) {
if (album_art[dir].count() == 1) if (album_art[dir].count() == 1)
return album_art[dir][0]; return album_art[dir][0];
else { else {
QString best_image = PickBestImage(album_art[dir]); QString best_image = PickBestImage(album_art[dir], t);
album_art[dir] = QStringList() << best_image; album_art[dir] = QStringList() << best_image;
return best_image; return best_image;
} }
@ -804,7 +829,7 @@ void LibraryWatcher::ReloadSettings() {
fs_watcher_->Clear(); fs_watcher_->Clear();
} else if (monitor_ && !was_monitoring_before) { } else if (monitor_ && !was_monitoring_before) {
// Add all directories to all QFileSystemWatchers again // Add all directories to all QFileSystemWatchers again
for (const Directory& dir : watched_dirs_.values()) { for (const Directory& dir : watched_dirs_.list_.values()) {
SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id); SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
for (const Subdirectory& subdir : subdirs) { for (const Subdirectory& subdir : subdirs) {
AddWatch(dir, subdir.path); AddWatch(dir, subdir.path);
@ -836,13 +861,13 @@ void LibraryWatcher::IncrementalScanNow() { PerformScan(true, false); }
void LibraryWatcher::FullScanNow() { PerformScan(false, true); } void LibraryWatcher::FullScanNow() { PerformScan(false, true); }
void LibraryWatcher::PerformScan(bool incremental, bool ignore_mtimes) { void LibraryWatcher::PerformScan(bool incremental, bool ignore_mtimes) {
for (const Directory& dir : watched_dirs_.values()) { for (const WatchedDir& dir : watched_dirs_.list_.values()) {
ScanTransaction transaction(this, dir, incremental, ignore_mtimes); ScanTransaction transaction(this, dir, incremental, ignore_mtimes);
SubdirectoryList subdirs(transaction.GetAllSubdirs()); SubdirectoryList subdirs(transaction.GetAllSubdirs());
transaction.AddToProgressMax(subdirs.count()); transaction.AddToProgressMax(subdirs.count());
for (const Subdirectory& subdir : subdirs) { for (const Subdirectory& subdir : subdirs) {
if (stop_requested_) return; if (transaction.aborted()) return;
ScanSubdirectory(subdir.path, subdir, &transaction); ScanSubdirectory(subdir.path, subdir, &transaction);
} }

View File

@ -22,9 +22,10 @@
#include "core/song.h" #include "core/song.h"
#include <QHash> #include <QHash>
#include <QMap>
#include <QMutex>
#include <QObject> #include <QObject>
#include <QStringList> #include <QStringList>
#include <QMap>
class QFileSystemWatcher; class QFileSystemWatcher;
class QTimer; class QTimer;
@ -55,9 +56,9 @@ class LibraryWatcher : public QObject {
void SetRescanPausedAsync(bool pause); void SetRescanPausedAsync(bool pause);
void ReloadSettingsAsync(); void ReloadSettingsAsync();
void Stop() { stop_requested_ = true; } void Stop() { watched_dirs_.StopAll(); }
signals: signals:
void NewOrUpdatedSongs(const SongList& songs); void NewOrUpdatedSongs(const SongList& songs);
void SongsMTimeUpdated(const SongList& songs); void SongsMTimeUpdated(const SongList& songs);
void SongsDeleted(const SongList& songs); void SongsDeleted(const SongList& songs);
@ -77,6 +78,14 @@ signals:
void SetRescanPaused(bool pause); void SetRescanPaused(bool pause);
private: private:
class WatchedDir : public Directory {
public:
WatchedDir() : active_(true) {}
WatchedDir(const Directory& dir) : Directory(dir), active_(true) {}
bool active_;
};
// This class encapsulates a full or partial scan of a directory. // This class encapsulates a full or partial scan of a directory.
// Each directory has one or more subdirectories, and any number of // Each directory has one or more subdirectories, and any number of
// subdirectories can be scanned during one transaction. ScanSubdirectory() // subdirectories can be scanned during one transaction. ScanSubdirectory()
@ -88,8 +97,9 @@ signals:
// LibraryBackend::FindSongsInDirectory. // LibraryBackend::FindSongsInDirectory.
class ScanTransaction { class ScanTransaction {
public: public:
ScanTransaction(LibraryWatcher* watcher, const Directory& dir, ScanTransaction(LibraryWatcher* watcher,
bool incremental, bool ignores_mtime = false); const LibraryWatcher::WatchedDir& dir, bool incremental,
bool ignores_mtime = false);
~ScanTransaction(); ~ScanTransaction();
SongList FindSongsInSubdirectory(const QString& path); SongList FindSongsInSubdirectory(const QString& path);
@ -105,6 +115,8 @@ signals:
bool is_incremental() const { return incremental_; } bool is_incremental() const { return incremental_; }
bool ignores_mtime() const { return ignores_mtime_; } bool ignores_mtime() const { return ignores_mtime_; }
bool aborted() { return !dir_.active_; }
SongList deleted_songs; SongList deleted_songs;
SongList readded_songs; SongList readded_songs;
SongList new_songs; SongList new_songs;
@ -120,7 +132,7 @@ signals:
int progress_; int progress_;
int progress_max_; int progress_max_;
const Directory& dir_; const WatchedDir& dir_;
// Incremental scan enters a directory only if it has changed since the // Incremental scan enters a directory only if it has changed since the
// last scan. // last scan.
bool incremental_; bool incremental_;
@ -153,9 +165,10 @@ signals:
inline static QString NoExtensionPart(const QString& fileName); inline static QString NoExtensionPart(const QString& fileName);
inline static QString ExtensionPart(const QString& fileName); inline static QString ExtensionPart(const QString& fileName);
inline static QString DirectoryPart(const QString& fileName); inline static QString DirectoryPart(const QString& fileName);
QString PickBestImage(const QStringList& images); QString PickBestImage(const QStringList& images, ScanTransaction* t);
QString ImageForSong(const QString& path, QString ImageForSong(const QString& path,
QMap<QString, QStringList>& album_art); QMap<QString, QStringList>& album_art,
ScanTransaction* t);
void AddWatch(const Directory& dir, const QString& path); void AddWatch(const Directory& dir, const QString& path);
void RemoveWatch(const Directory& dir, const Subdirectory& subdir); void RemoveWatch(const Directory& dir, const Subdirectory& subdir);
uint GetMtimeForCue(const QString& cue_path); uint GetMtimeForCue(const QString& cue_path);
@ -200,11 +213,30 @@ signals:
*/ */
QStringList best_image_filters_; QStringList best_image_filters_;
bool stop_requested_;
bool scan_on_startup_; bool scan_on_startup_;
bool monitor_; bool monitor_;
QMap<int, Directory> watched_dirs_; // All methods of QMap are reentrant We should only need to worry about
// syncronizing methods that remove directories on the watcher thread and
// methods that modify directories called from other threads.
class WatchList {
public:
// These may be called from a different thread.
void StopAll();
void Stop(const Directory& dir);
// This should only be called on the watcher thread.
void Remove(int id);
void Add(const Directory& dir);
QMap<int, WatchedDir> list_;
private:
QMutex mutex_;
};
WatchList watched_dirs_;
QTimer* rescan_timer_; QTimer* rescan_timer_;
QMap<int, QStringList> QMap<int, QStringList>
rescan_queue_; // dir id -> list of subdirs to be scanned rescan_queue_; // dir id -> list of subdirs to be scanned