2010-03-24 00:11:46 +01:00
|
|
|
/* This file is part of Clementine.
|
2010-11-20 14:27:10 +01:00
|
|
|
Copyright 2010, David Sansome <me@davidsansome.com>
|
2010-03-24 00:11:46 +01:00
|
|
|
|
|
|
|
Clementine is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Clementine is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2009-12-24 20:16:07 +01:00
|
|
|
#include "librarywatcher.h"
|
2012-01-05 15:51:23 +01:00
|
|
|
|
2020-09-18 16:15:19 +02:00
|
|
|
#include <fileref.h>
|
|
|
|
#include <tag.h>
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2011-01-12 00:09:59 +01:00
|
|
|
#include <QDateTime>
|
2009-12-24 20:16:07 +01:00
|
|
|
#include <QDirIterator>
|
2011-01-15 19:46:23 +01:00
|
|
|
#include <QHash>
|
2020-01-21 09:32:55 +01:00
|
|
|
#include <QMutexLocker>
|
2010-12-28 16:36:01 +01:00
|
|
|
#include <QSet>
|
2011-01-15 19:46:23 +01:00
|
|
|
#include <QSettings>
|
2020-01-21 09:32:55 +01:00
|
|
|
#include <QThread>
|
2011-01-15 19:46:23 +01:00
|
|
|
#include <QTimer>
|
2020-01-21 09:32:55 +01:00
|
|
|
#include <QtDebug>
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2020-09-18 16:15:19 +02:00
|
|
|
#include "core/filesystemwatcherinterface.h"
|
|
|
|
#include "core/logging.h"
|
|
|
|
#include "core/tagreaderclient.h"
|
|
|
|
#include "core/taskmanager.h"
|
|
|
|
#include "core/utilities.h"
|
|
|
|
#include "librarybackend.h"
|
|
|
|
#include "playlistparsers/cueparser.h"
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2011-11-28 20:13:01 +01:00
|
|
|
// This is defined by one of the windows headers that is included by taglib.
|
|
|
|
#ifdef RemoveDirectory
|
|
|
|
#undef RemoveDirectory
|
|
|
|
#endif
|
|
|
|
|
2016-04-15 11:44:44 +02:00
|
|
|
namespace {
|
2020-09-18 16:15:19 +02:00
|
|
|
static const char* kNoMediaFile = ".nomedia";
|
|
|
|
static const char* kNoMusicFile = ".nomusic";
|
|
|
|
} // namespace
|
2016-04-15 11:44:44 +02:00
|
|
|
|
2020-01-19 07:23:04 +01:00
|
|
|
static const int kUnfilteredImageLimit = 10;
|
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
QStringList LibraryWatcher::sValidImages;
|
|
|
|
|
2010-05-25 22:40:45 +02:00
|
|
|
const char* LibraryWatcher::kSettingsGroup = "LibraryWatcher";
|
2009-12-24 20:16:07 +01:00
|
|
|
|
|
|
|
LibraryWatcher::LibraryWatcher(QObject* parent)
|
2014-02-07 16:34:20 +01:00
|
|
|
: QObject(parent),
|
|
|
|
backend_(nullptr),
|
|
|
|
task_manager_(nullptr),
|
|
|
|
fs_watcher_(FileSystemWatcherInterface::Create(this)),
|
|
|
|
scan_on_startup_(true),
|
|
|
|
monitor_(true),
|
|
|
|
rescan_timer_(new QTimer(this)),
|
|
|
|
rescan_paused_(false),
|
|
|
|
total_watches_(0),
|
|
|
|
cue_parser_(new CueParser(backend_, this)) {
|
2009-12-24 20:16:07 +01:00
|
|
|
rescan_timer_->setInterval(1000);
|
|
|
|
rescan_timer_->setSingleShot(true);
|
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
if (sValidImages.isEmpty()) {
|
2014-02-07 16:34:20 +01:00
|
|
|
sValidImages << "jpg"
|
|
|
|
<< "png"
|
|
|
|
<< "gif"
|
|
|
|
<< "jpeg";
|
2010-04-01 18:59:32 +02:00
|
|
|
}
|
|
|
|
|
2010-05-25 22:40:45 +02:00
|
|
|
ReloadSettings();
|
|
|
|
|
2009-12-24 20:16:07 +01:00
|
|
|
connect(rescan_timer_, SIGNAL(timeout()), SLOT(RescanPathsNow()));
|
|
|
|
}
|
|
|
|
|
2020-01-21 05:12:56 +01:00
|
|
|
// Holding a reference to a directory is safe because a ScanTransaction object
|
|
|
|
// 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
|
|
|
|
// thread. So the Directory object will not be deleted out from under us.
|
2020-01-21 09:32:55 +01:00
|
|
|
LibraryWatcher::ScanTransaction::ScanTransaction(
|
|
|
|
LibraryWatcher* watcher, const LibraryWatcher::WatchedDir& dir,
|
|
|
|
bool incremental, bool ignores_mtime)
|
2014-02-07 16:34:20 +01:00
|
|
|
: progress_(0),
|
|
|
|
progress_max_(0),
|
|
|
|
dir_(dir),
|
|
|
|
incremental_(incremental),
|
|
|
|
ignores_mtime_(ignores_mtime),
|
|
|
|
watcher_(watcher),
|
|
|
|
cached_songs_dirty_(true),
|
|
|
|
known_subdirs_dirty_(true) {
|
2010-06-26 14:41:18 +02:00
|
|
|
QString description;
|
|
|
|
if (watcher_->device_name_.isEmpty())
|
|
|
|
description = tr("Updating library");
|
|
|
|
else
|
|
|
|
description = tr("Updating %1").arg(watcher_->device_name_);
|
|
|
|
|
|
|
|
task_id_ = watcher_->task_manager_->StartTask(description);
|
2010-07-04 17:56:08 +02:00
|
|
|
emit watcher_->ScanStarted(task_id_);
|
2010-04-01 18:59:32 +02:00
|
|
|
}
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
LibraryWatcher::ScanTransaction::~ScanTransaction() {
|
2010-04-14 14:41:03 +02:00
|
|
|
// If we're stopping then don't commit the transaction
|
2020-01-21 09:32:55 +01:00
|
|
|
if (aborted()) {
|
2020-01-19 05:32:41 +01:00
|
|
|
watcher_->task_manager_->SetTaskFinished(task_id_);
|
|
|
|
return;
|
|
|
|
}
|
2010-04-14 14:41:03 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!new_songs.isEmpty()) emit watcher_->NewOrUpdatedSongs(new_songs);
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!touched_songs.isEmpty()) emit watcher_->SongsMTimeUpdated(touched_songs);
|
2010-04-01 18:59:32 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!deleted_songs.isEmpty()) emit watcher_->SongsDeleted(deleted_songs);
|
2010-04-01 18:59:32 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!readded_songs.isEmpty()) emit watcher_->SongsReadded(readded_songs);
|
2014-02-01 20:21:28 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!new_subdirs.isEmpty()) emit watcher_->SubdirsDiscovered(new_subdirs);
|
2010-04-01 18:59:32 +02:00
|
|
|
|
|
|
|
if (!touched_subdirs.isEmpty())
|
|
|
|
emit watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
|
|
|
|
2010-06-23 15:21:30 +02:00
|
|
|
watcher_->task_manager_->SetTaskFinished(task_id_);
|
2010-04-04 16:09:07 +02:00
|
|
|
|
2019-11-12 20:05:53 +01:00
|
|
|
for (const Subdirectory& subdir : deleted_subdirs) {
|
2020-01-21 05:12:56 +01:00
|
|
|
watcher_->RemoveWatch(dir_, subdir);
|
2019-11-12 19:55:48 +01:00
|
|
|
}
|
|
|
|
|
2010-07-10 19:03:28 +02:00
|
|
|
if (watcher_->monitor_) {
|
|
|
|
// Watch the new subdirectories
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Subdirectory& subdir : new_subdirs) {
|
2020-01-21 05:12:56 +01:00
|
|
|
watcher_->AddWatch(dir_, subdir.path);
|
2010-07-10 19:03:28 +02:00
|
|
|
}
|
2010-04-04 16:09:07 +02:00
|
|
|
}
|
2010-04-01 18:59:32 +02:00
|
|
|
}
|
|
|
|
|
2010-06-23 16:00:18 +02:00
|
|
|
void LibraryWatcher::ScanTransaction::AddToProgress(int n) {
|
|
|
|
progress_ += n;
|
|
|
|
watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
|
|
|
|
}
|
|
|
|
|
|
|
|
void LibraryWatcher::ScanTransaction::AddToProgressMax(int n) {
|
|
|
|
progress_max_ += n;
|
|
|
|
watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
SongList LibraryWatcher::ScanTransaction::FindSongsInSubdirectory(
|
|
|
|
const QString& path) {
|
2010-04-01 18:59:32 +02:00
|
|
|
if (cached_songs_dirty_) {
|
2020-01-21 05:12:56 +01:00
|
|
|
cached_songs_ = watcher_->backend_->FindSongsInDirectory(dir_id());
|
2010-04-01 18:59:32 +02:00
|
|
|
cached_songs_dirty_ = false;
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
2010-02-27 21:12:22 +01:00
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
// TODO: Make this faster
|
|
|
|
SongList ret;
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Song& song : cached_songs_) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (song.url().toLocalFile().section('/', 0, -2) == path) ret << song;
|
2010-04-01 18:59:32 +02:00
|
|
|
}
|
|
|
|
return ret;
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::ScanTransaction::SetKnownSubdirs(
|
|
|
|
const SubdirectoryList& subdirs) {
|
2010-04-04 16:59:55 +02:00
|
|
|
known_subdirs_ = subdirs;
|
|
|
|
known_subdirs_dirty_ = false;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
bool LibraryWatcher::ScanTransaction::HasSeenSubdir(const QString& path) {
|
2010-04-04 16:59:55 +02:00
|
|
|
if (known_subdirs_dirty_)
|
2020-01-21 05:12:56 +01:00
|
|
|
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id()));
|
2010-04-04 16:59:55 +02:00
|
|
|
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Subdirectory& subdir : known_subdirs_) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (subdir.path == path && subdir.mtime != 0) return true;
|
2010-04-04 16:59:55 +02:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
SubdirectoryList LibraryWatcher::ScanTransaction::GetImmediateSubdirs(
|
|
|
|
const QString& path) {
|
2010-04-23 15:29:11 +02:00
|
|
|
if (known_subdirs_dirty_)
|
2020-01-21 05:12:56 +01:00
|
|
|
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id()));
|
2010-04-23 15:29:11 +02:00
|
|
|
|
|
|
|
SubdirectoryList ret;
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Subdirectory& subdir : known_subdirs_) {
|
2010-04-23 15:29:11 +02:00
|
|
|
if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path &&
|
|
|
|
subdir.mtime != 0) {
|
|
|
|
ret << subdir;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2010-05-25 22:40:45 +02:00
|
|
|
SubdirectoryList LibraryWatcher::ScanTransaction::GetAllSubdirs() {
|
|
|
|
if (known_subdirs_dirty_)
|
2020-01-21 05:12:56 +01:00
|
|
|
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id()));
|
2010-05-25 22:40:45 +02:00
|
|
|
return known_subdirs_;
|
|
|
|
}
|
|
|
|
|
2020-01-21 09:32:55 +01:00
|
|
|
void LibraryWatcher::WatchList::StopAll() {
|
|
|
|
QMutexLocker l(&mutex_);
|
|
|
|
for (WatchedDir& wdir : list_) {
|
|
|
|
wdir.active_ = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-25 20:59:33 +01:00
|
|
|
void LibraryWatcher::WatchList::Stop(int dir_id) {
|
2020-01-21 09:32:55 +01:00
|
|
|
QMutexLocker l(&mutex_);
|
2020-01-25 20:59:33 +01:00
|
|
|
if (list_.contains(dir_id)) {
|
|
|
|
list_[dir_id].active_ = false;
|
2020-01-21 09:32:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::AddDirectory(const Directory& dir,
|
|
|
|
const SubdirectoryList& subdirs) {
|
2021-03-29 09:14:46 +02:00
|
|
|
qLog(Debug) << "Add directory" << dir.GetPath();
|
2020-01-21 09:32:55 +01:00
|
|
|
watched_dirs_.Add(dir);
|
|
|
|
const WatchedDir& new_dir = watched_dirs_.list_[dir.id];
|
2010-04-01 18:59:32 +02:00
|
|
|
|
|
|
|
if (subdirs.isEmpty()) {
|
|
|
|
// This is a new directory that we've never seen before.
|
|
|
|
// Scan it fully.
|
2020-01-21 05:12:56 +01:00
|
|
|
ScanTransaction transaction(this, new_dir, false);
|
2010-04-04 16:59:55 +02:00
|
|
|
transaction.SetKnownSubdirs(subdirs);
|
2010-06-23 16:00:18 +02:00
|
|
|
transaction.AddToProgressMax(1);
|
2020-01-21 05:12:56 +01:00
|
|
|
ScanSubdirectory(new_dir.path, Subdirectory(), &transaction);
|
2010-04-01 18:59:32 +02:00
|
|
|
} else {
|
|
|
|
// We can do an incremental scan - looking at the mtimes of each
|
|
|
|
// subdirectory and only rescan if the directory has changed.
|
2020-01-21 05:12:56 +01:00
|
|
|
ScanTransaction transaction(this, new_dir, true);
|
2010-04-04 16:59:55 +02:00
|
|
|
transaction.SetKnownSubdirs(subdirs);
|
2010-06-23 16:00:18 +02:00
|
|
|
transaction.AddToProgressMax(subdirs.count());
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Subdirectory& subdir : subdirs) {
|
2020-01-21 09:32:55 +01:00
|
|
|
if (transaction.aborted()) return;
|
2010-05-25 22:40:45 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, &transaction);
|
2010-05-25 22:40:45 +02:00
|
|
|
|
2020-01-21 05:12:56 +01:00
|
|
|
if (monitor_) AddWatch(new_dir, subdir.path);
|
2010-01-16 18:17:00 +01:00
|
|
|
}
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
2010-04-01 18:59:32 +02:00
|
|
|
|
2010-06-02 18:22:20 +02:00
|
|
|
emit CompilationsNeedUpdating();
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::ScanSubdirectory(const QString& path,
|
|
|
|
const Subdirectory& subdir,
|
|
|
|
ScanTransaction* t,
|
|
|
|
bool force_noincremental) {
|
2010-04-01 18:59:32 +02:00
|
|
|
QFileInfo path_info(path);
|
2020-09-18 16:15:19 +02:00
|
|
|
QDir path_dir(path);
|
2010-07-29 20:40:10 +02:00
|
|
|
|
|
|
|
// Do not scan symlinked dirs that are already in collection
|
|
|
|
if (path_info.isSymLink()) {
|
|
|
|
QString real_path = path_info.symLinkTarget();
|
2020-01-21 09:32:55 +01:00
|
|
|
for (const Directory& dir : watched_dirs_.list_) {
|
2012-01-29 18:39:28 +01:00
|
|
|
if (real_path.startsWith(dir.path)) {
|
2010-07-29 20:40:10 +02:00
|
|
|
t->AddToProgress(1);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-15 11:44:44 +02:00
|
|
|
// Do not scan directories containing a .nomedia or .nomusic file
|
2020-09-18 16:15:19 +02:00
|
|
|
if (path_dir.exists(kNoMediaFile) || path_dir.exists(kNoMusicFile)) {
|
2016-04-15 11:44:44 +02:00
|
|
|
t->AddToProgress(1);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2011-02-17 20:57:14 +01:00
|
|
|
if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() &&
|
2010-04-04 16:09:07 +02:00
|
|
|
subdir.mtime == path_info.lastModified().toTime_t()) {
|
2010-04-01 18:59:32 +02:00
|
|
|
// The directory hasn't changed since last time
|
2010-06-23 16:00:18 +02:00
|
|
|
t->AddToProgress(1);
|
2010-04-01 18:59:32 +02:00
|
|
|
return;
|
|
|
|
}
|
2010-02-28 01:35:20 +01:00
|
|
|
|
|
|
|
QMap<QString, QStringList> album_art;
|
2009-12-24 20:16:07 +01:00
|
|
|
QStringList files_on_disk;
|
2010-04-01 18:59:32 +02:00
|
|
|
SubdirectoryList my_new_subdirs;
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2010-04-23 15:29:11 +02:00
|
|
|
// If a directory is moved then only its parent gets a changed notification,
|
|
|
|
// so we need to look and see if any of our children don't exist any more.
|
|
|
|
// If one has been removed, "rescan" it to get the deleted songs
|
|
|
|
SubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Subdirectory& subdir : previous_subdirs) {
|
2010-05-05 18:56:42 +02:00
|
|
|
if (!QFile::exists(subdir.path) && subdir.path != path) {
|
2010-06-23 16:00:18 +02:00
|
|
|
t->AddToProgressMax(1);
|
2010-04-23 15:29:11 +02:00
|
|
|
ScanSubdirectory(subdir.path, subdir, t, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
// First we "quickly" get a list of the files in the directory that we
|
2014-02-07 16:34:20 +01:00
|
|
|
// think might be music. While we're here, we also look for new
|
|
|
|
// subdirectories
|
2010-04-01 18:59:32 +02:00
|
|
|
// and possible album artwork.
|
2014-02-07 16:34:20 +01:00
|
|
|
QDirIterator it(
|
|
|
|
path, QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot);
|
2010-04-01 18:59:32 +02:00
|
|
|
while (it.hasNext()) {
|
2020-01-21 09:32:55 +01:00
|
|
|
if (t->aborted()) return;
|
2010-04-14 14:41:03 +02:00
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
QString child(it.next());
|
|
|
|
QFileInfo child_info(child);
|
|
|
|
|
|
|
|
if (child_info.isDir()) {
|
2012-06-04 21:38:44 +02:00
|
|
|
if (!child_info.isHidden() && !t->HasSeenSubdir(child)) {
|
2010-04-01 18:59:32 +02:00
|
|
|
// We haven't seen this subdirectory before - add it to a list and
|
|
|
|
// later we'll tell the backend about it and scan it.
|
|
|
|
Subdirectory new_subdir;
|
2010-04-04 16:59:55 +02:00
|
|
|
new_subdir.directory_id = -1;
|
2010-04-01 18:59:32 +02:00
|
|
|
new_subdir.path = child;
|
|
|
|
new_subdir.mtime = child_info.lastModified().toTime_t();
|
|
|
|
my_new_subdirs << new_subdir;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
QString ext_part(ExtensionPart(child));
|
|
|
|
QString dir_part(DirectoryPart(child));
|
2010-03-25 15:33:09 +01:00
|
|
|
|
2021-03-14 07:45:42 +01:00
|
|
|
if (!skip_file_extensions_.contains(ext_part)) {
|
|
|
|
if (sValidImages.contains(ext_part))
|
|
|
|
album_art[dir_part] << child;
|
|
|
|
else if (!child_info.isHidden())
|
|
|
|
files_on_disk << child;
|
|
|
|
}
|
2010-04-01 18:59:32 +02:00
|
|
|
}
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
|
|
|
|
2020-01-21 09:32:55 +01:00
|
|
|
if (t->aborted()) return;
|
2010-04-01 18:59:32 +02:00
|
|
|
|
2009-12-24 20:16:07 +01:00
|
|
|
// Ask the database for a list of files in this directory
|
2010-04-01 18:59:32 +02:00
|
|
|
SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2010-12-28 16:36:01 +01:00
|
|
|
QSet<QString> cues_processed;
|
|
|
|
|
2009-12-24 20:16:07 +01:00
|
|
|
// Now compare the list from the database with the list of files on disk
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const QString& file : files_on_disk) {
|
2020-01-21 09:32:55 +01:00
|
|
|
if (t->aborted()) return;
|
2010-03-25 15:33:09 +01:00
|
|
|
|
2011-01-15 19:46:23 +01:00
|
|
|
// associated cue
|
2011-01-12 00:09:59 +01:00
|
|
|
QString matching_cue = NoExtensionPart(file) + ".cue";
|
|
|
|
|
2009-12-24 20:16:07 +01:00
|
|
|
Song matching_song;
|
|
|
|
if (FindSongByPath(songs_in_db, file, &matching_song)) {
|
2011-01-15 19:46:23 +01:00
|
|
|
uint matching_cue_mtime = GetMtimeForCue(matching_cue);
|
|
|
|
|
2009-12-24 20:16:07 +01:00
|
|
|
// The song is in the database and still on disk.
|
|
|
|
// Check the mtime to see if it's been changed since it was added.
|
2010-03-25 14:15:55 +01:00
|
|
|
QFileInfo file_info(file);
|
|
|
|
|
|
|
|
if (!file_info.exists()) {
|
|
|
|
// Partially fixes race condition - if file was removed between being
|
|
|
|
// added to the list and now.
|
|
|
|
files_on_disk.removeAll(file);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2011-01-15 19:46:23 +01:00
|
|
|
// cue sheet's path from library (if any)
|
|
|
|
QString song_cue = matching_song.cue_path();
|
|
|
|
uint song_cue_mtime = GetMtimeForCue(song_cue);
|
|
|
|
|
2011-02-17 20:57:14 +01:00
|
|
|
bool cue_deleted = song_cue_mtime == 0 && matching_song.has_cue();
|
2011-01-15 19:46:23 +01:00
|
|
|
bool cue_added = matching_cue_mtime != 0 && !matching_song.has_cue();
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
// watch out for cue songs which have their mtime equal to
|
|
|
|
// qMax(media_file_mtime, cue_sheet_mtime)
|
|
|
|
bool changed =
|
|
|
|
(matching_song.mtime() !=
|
|
|
|
qMax(file_info.lastModified().toTime_t(), song_cue_mtime)) ||
|
|
|
|
cue_deleted || cue_added;
|
2010-02-28 01:35:20 +01:00
|
|
|
|
|
|
|
// Also want to look to see whether the album art has changed
|
2020-01-23 01:02:08 +01:00
|
|
|
QString image = ImageForSong(file, &album_art, t);
|
2010-02-28 01:35:20 +01:00
|
|
|
if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) ||
|
2014-02-07 16:34:20 +01:00
|
|
|
(!matching_song.art_automatic().isEmpty() &&
|
|
|
|
!matching_song.has_embedded_cover() &&
|
|
|
|
!QFile::exists(matching_song.art_automatic()))) {
|
2010-02-28 01:35:20 +01:00
|
|
|
changed = true;
|
|
|
|
}
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2011-01-12 00:09:59 +01:00
|
|
|
// the song's changed - reread the metadata from file
|
2011-02-17 20:57:14 +01:00
|
|
|
if (t->ignores_mtime() || changed) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << file << "changed";
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2011-01-15 19:46:23 +01:00
|
|
|
// if cue associated...
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!cue_deleted && (matching_song.has_cue() || cue_added)) {
|
2011-01-15 19:46:23 +01:00
|
|
|
UpdateCueAssociatedSongs(file, path, matching_cue, image, t);
|
2014-02-07 16:34:20 +01:00
|
|
|
// if no cue or it's about to lose it...
|
2009-12-24 20:16:07 +01:00
|
|
|
} else {
|
2014-02-07 16:34:20 +01:00
|
|
|
UpdateNonCueAssociatedSong(file, matching_song, image, cue_deleted,
|
|
|
|
t);
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
|
|
|
}
|
2014-02-01 20:21:28 +01:00
|
|
|
|
|
|
|
// nothing has changed - mark the song available without re-scanning
|
2014-02-07 16:34:20 +01:00
|
|
|
if (matching_song.is_unavailable()) t->readded_songs << matching_song;
|
2014-02-01 20:21:28 +01:00
|
|
|
|
2009-12-24 20:16:07 +01:00
|
|
|
} else {
|
|
|
|
// The song is on disk but not in the DB
|
2014-02-07 16:34:20 +01:00
|
|
|
SongList song_list =
|
|
|
|
ScanNewFile(file, path, matching_cue, &cues_processed);
|
2010-12-28 16:36:01 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (song_list.isEmpty()) {
|
2009-12-24 20:16:07 +01:00
|
|
|
continue;
|
2010-12-28 16:36:01 +01:00
|
|
|
}
|
|
|
|
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << file << "created";
|
2010-12-28 16:36:01 +01:00
|
|
|
// choose an image for the song(s)
|
2020-01-23 01:02:08 +01:00
|
|
|
QString image = ImageForSong(file, &album_art, t);
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2014-02-10 14:29:07 +01:00
|
|
|
for (Song song : song_list) {
|
2020-01-21 05:12:56 +01:00
|
|
|
song.set_directory_id(t->dir_id());
|
2014-02-07 16:34:20 +01:00
|
|
|
if (song.art_automatic().isEmpty()) song.set_art_automatic(image);
|
2010-02-28 01:35:20 +01:00
|
|
|
|
2010-12-28 16:36:01 +01:00
|
|
|
t->new_songs << song;
|
|
|
|
}
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Look for deleted songs
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Song& song : songs_in_db) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!song.is_unavailable() &&
|
|
|
|
!files_on_disk.contains(song.url().toLocalFile())) {
|
2011-04-28 14:27:53 +02:00
|
|
|
qLog(Debug) << "Song deleted from disk:" << song.url().toLocalFile();
|
2010-04-01 18:59:32 +02:00
|
|
|
t->deleted_songs << song;
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
// Add this subdir to the new or touched list
|
|
|
|
Subdirectory updated_subdir;
|
2020-01-21 05:12:56 +01:00
|
|
|
updated_subdir.directory_id = t->dir_id();
|
2014-02-07 16:34:20 +01:00
|
|
|
updated_subdir.mtime =
|
|
|
|
path_info.exists() ? path_info.lastModified().toTime_t() : 0;
|
2010-04-01 18:59:32 +02:00
|
|
|
updated_subdir.path = path;
|
2010-03-25 15:33:09 +01:00
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
if (subdir.directory_id == -1)
|
|
|
|
t->new_subdirs << updated_subdir;
|
|
|
|
else
|
|
|
|
t->touched_subdirs << updated_subdir;
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2019-11-12 20:05:53 +01:00
|
|
|
if (updated_subdir.mtime ==
|
|
|
|
0) { // Subdirectory deleted, mark it for removal from the watcher.
|
2019-11-12 19:55:48 +01:00
|
|
|
t->deleted_subdirs << updated_subdir;
|
|
|
|
}
|
|
|
|
|
2010-06-23 16:00:18 +02:00
|
|
|
t->AddToProgress(1);
|
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
// Recurse into the new subdirs that we found
|
2010-06-23 16:00:18 +02:00
|
|
|
t->AddToProgressMax(my_new_subdirs.count());
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Subdirectory& my_new_subdir : my_new_subdirs) {
|
2020-01-21 09:32:55 +01:00
|
|
|
if (t->aborted()) return;
|
2010-04-04 16:09:07 +02:00
|
|
|
ScanSubdirectory(my_new_subdir.path, my_new_subdir, t, true);
|
2010-04-01 18:59:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::UpdateCueAssociatedSongs(const QString& file,
|
|
|
|
const QString& path,
|
|
|
|
const QString& matching_cue,
|
|
|
|
const QString& image,
|
2011-01-15 19:46:23 +01:00
|
|
|
ScanTransaction* t) {
|
|
|
|
QFile cue(matching_cue);
|
|
|
|
cue.open(QIODevice::ReadOnly);
|
|
|
|
|
2011-04-28 14:27:53 +02:00
|
|
|
SongList old_sections = backend_->GetSongsByUrl(QUrl::fromLocalFile(file));
|
2011-01-15 19:46:23 +01:00
|
|
|
|
2011-02-13 19:34:30 +01:00
|
|
|
QHash<quint64, Song> sections_map;
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Song& song : old_sections) {
|
2011-02-13 19:34:30 +01:00
|
|
|
sections_map[song.beginning_nanosec()] = song;
|
2011-01-15 19:46:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
QSet<int> used_ids;
|
|
|
|
|
|
|
|
// update every song that's in the cue and library
|
2014-02-10 14:29:07 +01:00
|
|
|
for (Song cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
|
2020-01-21 05:12:56 +01:00
|
|
|
cue_song.set_directory_id(t->dir_id());
|
2011-01-15 19:46:23 +01:00
|
|
|
|
2011-02-13 19:34:30 +01:00
|
|
|
Song matching = sections_map[cue_song.beginning_nanosec()];
|
2011-01-15 19:46:23 +01:00
|
|
|
// a new section
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!matching.is_valid()) {
|
2011-01-15 19:46:23 +01:00
|
|
|
t->new_songs << cue_song;
|
2014-02-07 16:34:20 +01:00
|
|
|
// changed section
|
2011-01-15 19:46:23 +01:00
|
|
|
} else {
|
|
|
|
PreserveUserSetData(file, image, matching, &cue_song, t);
|
|
|
|
used_ids.insert(matching.id());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// sections that are now missing
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Song& matching : old_sections) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!used_ids.contains(matching.id())) {
|
2011-01-15 19:46:23 +01:00
|
|
|
t->deleted_songs << matching;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::UpdateNonCueAssociatedSong(const QString& file,
|
|
|
|
const Song& matching_song,
|
|
|
|
const QString& image,
|
|
|
|
bool cue_deleted,
|
2011-01-15 19:46:23 +01:00
|
|
|
ScanTransaction* t) {
|
|
|
|
// if a cue got deleted, we turn it's first section into the new
|
|
|
|
// 'raw' (cueless) song and we just remove the rest of the sections
|
|
|
|
// from the library
|
2014-02-07 16:34:20 +01:00
|
|
|
if (cue_deleted) {
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Song& song :
|
|
|
|
backend_->GetSongsByUrl(QUrl::fromLocalFile(file))) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!song.IsMetadataEqual(matching_song)) {
|
2011-01-15 19:46:23 +01:00
|
|
|
t->deleted_songs << song;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Song song_on_disk;
|
2020-01-21 05:12:56 +01:00
|
|
|
song_on_disk.set_directory_id(t->dir_id());
|
2012-01-06 22:27:02 +01:00
|
|
|
TagReaderClient::Instance()->ReadFileBlocking(file, &song_on_disk);
|
2011-01-15 19:46:23 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (song_on_disk.is_valid()) {
|
2011-01-15 19:46:23 +01:00
|
|
|
PreserveUserSetData(file, image, matching_song, &song_on_disk, t);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
SongList LibraryWatcher::ScanNewFile(const QString& file, const QString& path,
|
2014-02-07 16:34:20 +01:00
|
|
|
const QString& matching_cue,
|
|
|
|
QSet<QString>* cues_processed) {
|
2011-01-15 19:46:23 +01:00
|
|
|
SongList song_list;
|
|
|
|
|
|
|
|
uint matching_cue_mtime = GetMtimeForCue(matching_cue);
|
|
|
|
// if it's a cue - create virtual tracks
|
2014-02-07 16:34:20 +01:00
|
|
|
if (matching_cue_mtime) {
|
2011-01-15 19:46:23 +01:00
|
|
|
// don't process the same cue many times
|
2014-02-07 16:34:20 +01:00
|
|
|
if (cues_processed->contains(matching_cue)) return song_list;
|
2011-01-15 19:46:23 +01:00
|
|
|
|
|
|
|
QFile cue(matching_cue);
|
|
|
|
cue.open(QIODevice::ReadOnly);
|
|
|
|
|
2011-02-05 14:43:04 +01:00
|
|
|
// Ignore FILEs pointing to other media files. Also, watch out for incorrect
|
|
|
|
// media files. Playlist parser for CUEs considers every entry in sheet
|
|
|
|
// valid and we don't want invalid media getting into library!
|
2016-01-31 01:31:10 +01:00
|
|
|
QString file_nfd = file.normalized(QString::NormalizationForm_D);
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Song& cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
|
2020-09-18 16:15:19 +02:00
|
|
|
if (cue_song.url().toLocalFile().normalized(
|
|
|
|
QString::NormalizationForm_D) == file_nfd) {
|
2012-01-06 22:27:02 +01:00
|
|
|
if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
|
|
|
|
song_list << cue_song;
|
|
|
|
}
|
2011-01-15 19:46:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!song_list.isEmpty()) {
|
2011-01-15 19:46:23 +01:00
|
|
|
*cues_processed << matching_cue;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
// it's a normal media file
|
2011-01-15 19:46:23 +01:00
|
|
|
} else {
|
|
|
|
Song song;
|
2012-01-06 22:27:02 +01:00
|
|
|
TagReaderClient::Instance()->ReadFileBlocking(file, &song);
|
2011-01-15 19:46:23 +01:00
|
|
|
|
|
|
|
if (song.is_valid()) {
|
|
|
|
song_list << song;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return song_list;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::PreserveUserSetData(const QString& file,
|
|
|
|
const QString& image,
|
|
|
|
const Song& matching_song, Song* out,
|
|
|
|
ScanTransaction* t) {
|
2011-01-12 00:09:59 +01:00
|
|
|
out->set_id(matching_song.id());
|
2011-01-23 16:36:09 +01:00
|
|
|
|
|
|
|
// Previous versions of Clementine incorrectly overwrote this and
|
|
|
|
// stored it in the DB, so we can't rely on matching_song to
|
|
|
|
// know if it has embedded artwork or not, but we can check here.
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!out->has_embedded_cover()) out->set_art_automatic(image);
|
2011-01-12 00:09:59 +01:00
|
|
|
|
2012-12-25 14:48:16 +01:00
|
|
|
out->MergeUserSetData(matching_song);
|
2011-01-12 00:09:59 +01:00
|
|
|
|
2014-02-01 20:21:28 +01:00
|
|
|
// The song was deleted from the database (e.g. due to an unmounted
|
2011-05-14 15:43:57 +02:00
|
|
|
// filesystem), but has been restored.
|
|
|
|
if (matching_song.is_unavailable()) {
|
|
|
|
qLog(Debug) << file << " unavailable song restored";
|
2014-02-01 20:21:28 +01:00
|
|
|
|
2011-05-14 15:43:57 +02:00
|
|
|
t->new_songs << *out;
|
|
|
|
} else if (!matching_song.IsMetadataEqual(*out)) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << file << "metadata changed";
|
2011-01-12 00:09:59 +01:00
|
|
|
|
|
|
|
// Update the song in the DB
|
|
|
|
t->new_songs << *out;
|
|
|
|
} else {
|
|
|
|
// Only the mtime's changed
|
|
|
|
t->touched_songs << *out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-01-15 19:46:23 +01:00
|
|
|
uint LibraryWatcher::GetMtimeForCue(const QString& cue_path) {
|
|
|
|
// slight optimisation
|
2012-06-19 21:27:28 +02:00
|
|
|
if (cue_path.isEmpty()) {
|
2011-01-15 19:46:23 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2012-06-19 21:27:28 +02:00
|
|
|
const QFileInfo file_info(cue_path);
|
|
|
|
if (!file_info.exists()) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QDateTime cue_last_modified = file_info.lastModified();
|
2011-01-15 19:46:23 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
return cue_last_modified.isValid() ? cue_last_modified.toTime_t() : 0;
|
2011-01-15 19:46:23 +01:00
|
|
|
}
|
|
|
|
|
2012-01-29 18:39:28 +01:00
|
|
|
void LibraryWatcher::AddWatch(const Directory& dir, const QString& path) {
|
2020-01-13 10:02:40 +01:00
|
|
|
QFileInfo info(path);
|
|
|
|
if (!info.exists() || !info.isReadable()) return;
|
2010-04-04 16:59:55 +02:00
|
|
|
|
2012-01-05 15:51:23 +01:00
|
|
|
connect(fs_watcher_, SIGNAL(PathChanged(const QString&)), this,
|
2014-02-07 16:34:20 +01:00
|
|
|
SLOT(DirectoryChanged(const QString&)), Qt::UniqueConnection);
|
2020-01-13 10:02:40 +01:00
|
|
|
if (!fs_watcher_->AddPath(path)) {
|
|
|
|
// Since this may be a system error, don't spam the user.
|
|
|
|
static int errCount = 0;
|
|
|
|
if (errCount++ == 0) {
|
|
|
|
#ifdef Q_OS_LINUX
|
|
|
|
// The Linux implementation of QFileSystemWatcher utilizes inotify, so
|
|
|
|
// the limit in /proc/sys/fs/inotify/max_user_watches may be a problem
|
|
|
|
// in large libraries.
|
|
|
|
const char* fmt =
|
|
|
|
"Failed to watch %1\n"
|
|
|
|
"On a Linux system, this may be due to the inotify max_user_watches "
|
|
|
|
"limit.\n\n"
|
|
|
|
"This error will not be shown again during this session.";
|
|
|
|
#else
|
|
|
|
const char* fmt =
|
|
|
|
"Failed to watch %1 for unknown reasons.\n\n"
|
|
|
|
"This error will not be shown again during this session.";
|
|
|
|
#endif
|
|
|
|
emit Error(tr(fmt).arg(path));
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-01-05 16:56:46 +01:00
|
|
|
subdir_mapping_[path] = dir;
|
2010-04-01 18:59:32 +02:00
|
|
|
}
|
|
|
|
|
2019-11-12 20:05:53 +01:00
|
|
|
void LibraryWatcher::RemoveWatch(const Directory& dir,
|
|
|
|
const Subdirectory& subdir) {
|
|
|
|
for (const QString& subdir_path : subdir_mapping_.keys(dir)) {
|
2019-11-12 19:55:48 +01:00
|
|
|
if (subdir_path != subdir.path) continue;
|
|
|
|
fs_watcher_->RemovePath(subdir_path);
|
|
|
|
subdir_mapping_.remove(subdir_path);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-25 20:59:33 +01:00
|
|
|
void LibraryWatcher::RemoveDirectory(int dir_id) {
|
|
|
|
watched_dirs_.Stop(dir_id);
|
2020-01-21 09:32:55 +01:00
|
|
|
// Invoke the DoRemoveDirectory slot on the watcher's thread.
|
2020-01-25 20:59:33 +01:00
|
|
|
QMetaObject::invokeMethod(this, "DoRemoveDirectory", Q_ARG(int, dir_id));
|
2020-01-21 09:32:55 +01:00
|
|
|
}
|
|
|
|
|
2020-01-25 20:59:33 +01:00
|
|
|
void LibraryWatcher::DoRemoveDirectory(int dir_id) {
|
|
|
|
rescan_queue_.remove(dir_id);
|
2012-01-29 18:39:28 +01:00
|
|
|
|
2020-01-25 20:59:33 +01:00
|
|
|
const WatchedDir& dir = watched_dirs_.list_[dir_id];
|
2012-01-29 18:39:28 +01:00
|
|
|
// Stop watching the directory's subdirectories
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const QString& subdir_path : subdir_mapping_.keys(dir)) {
|
2012-01-29 18:39:28 +01:00
|
|
|
fs_watcher_->RemovePath(subdir_path);
|
|
|
|
subdir_mapping_.remove(subdir_path);
|
|
|
|
}
|
2020-01-25 20:59:33 +01:00
|
|
|
watched_dirs_.Remove(dir_id);
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
bool LibraryWatcher::FindSongByPath(const SongList& list, const QString& path,
|
|
|
|
Song* out) {
|
2010-04-01 18:59:32 +02:00
|
|
|
// TODO: Make this faster
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Song& song : list) {
|
2011-04-28 14:27:53 +02:00
|
|
|
if (song.url().toLocalFile() == path) {
|
2009-12-24 20:16:07 +01:00
|
|
|
*out = song;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::DirectoryChanged(const QString& subdir) {
|
2010-04-01 18:59:32 +02:00
|
|
|
// Find what dir it was in
|
2014-02-07 16:34:20 +01:00
|
|
|
QHash<QString, Directory>::const_iterator it =
|
|
|
|
subdir_mapping_.constFind(subdir);
|
2012-01-05 16:56:46 +01:00
|
|
|
if (it == subdir_mapping_.constEnd()) {
|
|
|
|
return;
|
2010-04-01 18:59:32 +02:00
|
|
|
}
|
2012-01-29 18:39:28 +01:00
|
|
|
Directory dir = *it;
|
2010-04-01 18:59:32 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Debug) << "Subdir" << subdir << "changed under directory" << dir.path
|
|
|
|
<< "id" << dir.id;
|
2010-04-01 18:59:32 +02:00
|
|
|
|
|
|
|
// Queue the subdir for rescanning
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!rescan_queue_[dir.id].contains(subdir)) rescan_queue_[dir.id] << subdir;
|
2009-12-24 20:16:07 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!rescan_paused_) rescan_timer_->start();
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void LibraryWatcher::RescanPathsNow() {
|
2020-01-21 09:32:55 +01:00
|
|
|
for (int id : rescan_queue_.keys()) {
|
|
|
|
if (!watched_dirs_.list_.contains(id)) {
|
|
|
|
qLog(Warning) << "Rescan id" << id << "not in watch list.";
|
2020-01-21 05:12:56 +01:00
|
|
|
continue;
|
|
|
|
}
|
2020-01-21 09:32:55 +01:00
|
|
|
const WatchedDir& dir = watched_dirs_.list_[id];
|
|
|
|
|
|
|
|
if (!dir.active_) continue;
|
2020-01-21 05:12:56 +01:00
|
|
|
|
2020-01-21 09:32:55 +01:00
|
|
|
ScanTransaction transaction(this, dir, false);
|
|
|
|
transaction.AddToProgressMax(rescan_queue_[id].count());
|
2010-04-01 18:59:32 +02:00
|
|
|
|
2020-01-21 09:32:55 +01:00
|
|
|
for (const QString& path : rescan_queue_[id]) {
|
|
|
|
if (transaction.aborted()) return;
|
2010-04-01 18:59:32 +02:00
|
|
|
Subdirectory subdir;
|
2020-01-21 09:32:55 +01:00
|
|
|
subdir.directory_id = id;
|
2010-04-01 18:59:32 +02:00
|
|
|
subdir.mtime = 0;
|
|
|
|
subdir.path = path;
|
|
|
|
ScanSubdirectory(path, subdir, &transaction);
|
|
|
|
}
|
2010-03-25 15:33:09 +01:00
|
|
|
}
|
|
|
|
|
2010-04-01 18:59:32 +02:00
|
|
|
rescan_queue_.clear();
|
2010-02-27 21:12:22 +01:00
|
|
|
|
2010-06-02 18:22:20 +02:00
|
|
|
emit CompilationsNeedUpdating();
|
2009-12-24 20:16:07 +01:00
|
|
|
}
|
2010-02-28 01:35:20 +01:00
|
|
|
|
2020-01-21 09:32:55 +01:00
|
|
|
QString LibraryWatcher::PickBestImage(const QStringList& images,
|
|
|
|
ScanTransaction* t) {
|
2010-02-28 01:35:20 +01:00
|
|
|
// This is used when there is more than one image in a directory.
|
2010-12-28 13:52:58 +01:00
|
|
|
// Pick the biggest image that matches the most important filter
|
2014-02-01 20:21:28 +01:00
|
|
|
|
2010-12-28 13:52:58 +01:00
|
|
|
QStringList filtered;
|
2014-02-01 20:21:28 +01:00
|
|
|
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const QString& filter_text : best_image_filters_) {
|
2014-02-01 20:21:28 +01:00
|
|
|
// the images in the images list are represented by a full path,
|
2010-12-28 13:52:58 +01:00
|
|
|
// so we need to isolate just the filename
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const QString& image : images) {
|
2010-12-28 13:52:58 +01:00
|
|
|
QFileInfo file_info(image);
|
|
|
|
QString filename(file_info.fileName());
|
|
|
|
if (filename.contains(filter_text, Qt::CaseInsensitive))
|
|
|
|
filtered << image;
|
|
|
|
}
|
|
|
|
|
2014-02-01 20:21:28 +01:00
|
|
|
/* We assume the filters are give in the order best to worst, so
|
2010-12-28 13:52:58 +01:00
|
|
|
if we've got a result, we go with it. Otherwise we might
|
|
|
|
start capturing more generic rules */
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!filtered.isEmpty()) break;
|
2010-12-28 13:52:58 +01:00
|
|
|
}
|
2014-02-01 20:21:28 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (filtered.isEmpty()) {
|
2020-01-19 07:23:04 +01:00
|
|
|
// If we're scanning a device, we may hit a directory that contains
|
|
|
|
// multiple types of media. An example is a camera directory on a smart
|
|
|
|
// phone that contains JPG and MP4. We don't want to cycle through hundreds
|
|
|
|
// of images for each audio file found, so we've set a threshold to try to
|
|
|
|
// detect this case.
|
|
|
|
if (images.count() > kUnfilteredImageLimit) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2010-12-28 13:52:58 +01:00
|
|
|
// the filter was too restrictive, just use the original list
|
|
|
|
filtered = images;
|
|
|
|
}
|
2010-02-28 01:35:20 +01:00
|
|
|
|
|
|
|
int biggest_size = 0;
|
|
|
|
QString biggest_path;
|
|
|
|
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const QString& path : filtered) {
|
2020-01-21 09:32:55 +01:00
|
|
|
if (t->aborted()) return "";
|
2019-02-20 09:03:44 +01:00
|
|
|
|
2010-02-28 01:35:20 +01:00
|
|
|
QImage image(path);
|
2014-02-07 16:34:20 +01:00
|
|
|
if (image.isNull()) continue;
|
2010-02-28 01:35:20 +01:00
|
|
|
|
|
|
|
int size = image.width() * image.height();
|
|
|
|
if (size > biggest_size) {
|
|
|
|
biggest_size = size;
|
|
|
|
biggest_path = path;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return biggest_path;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
QString LibraryWatcher::ImageForSong(const QString& path,
|
2020-01-23 01:02:08 +01:00
|
|
|
QMap<QString, QStringList>* album_art,
|
2020-01-21 09:32:55 +01:00
|
|
|
ScanTransaction* t) {
|
2010-02-28 01:35:20 +01:00
|
|
|
QString dir(DirectoryPart(path));
|
|
|
|
|
2020-01-23 01:02:08 +01:00
|
|
|
if (album_art->contains(dir)) {
|
|
|
|
if (album_art->value(dir).count() == 1)
|
|
|
|
return album_art->value(dir)[0];
|
2010-02-28 01:35:20 +01:00
|
|
|
else {
|
2020-01-23 01:02:08 +01:00
|
|
|
QString best_image = PickBestImage(album_art->value(dir), t);
|
|
|
|
album_art->insert(dir, QStringList() << best_image);
|
2010-02-28 01:35:20 +01:00
|
|
|
return best_image;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return QString();
|
|
|
|
}
|
2010-05-25 22:40:45 +02:00
|
|
|
|
2010-07-10 19:03:28 +02:00
|
|
|
void LibraryWatcher::ReloadSettingsAsync() {
|
|
|
|
QMetaObject::invokeMethod(this, "ReloadSettings", Qt::QueuedConnection);
|
|
|
|
}
|
|
|
|
|
2010-05-25 22:40:45 +02:00
|
|
|
void LibraryWatcher::ReloadSettings() {
|
2010-07-10 19:03:28 +02:00
|
|
|
const bool was_monitoring_before = monitor_;
|
|
|
|
|
2010-05-25 22:40:45 +02:00
|
|
|
QSettings s;
|
|
|
|
s.beginGroup(kSettingsGroup);
|
|
|
|
scan_on_startup_ = s.value("startup_scan", true).toBool();
|
2010-07-10 19:03:28 +02:00
|
|
|
monitor_ = s.value("monitor", true).toBool();
|
2012-01-05 15:51:23 +01:00
|
|
|
|
2010-12-28 13:52:58 +01:00
|
|
|
best_image_filters_.clear();
|
2020-09-18 16:15:19 +02:00
|
|
|
QStringList filters = s.value("cover_art_patterns", QStringList() << "front"
|
|
|
|
<< "cover")
|
|
|
|
.toStringList();
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const QString& filter : filters) {
|
2010-12-28 13:52:58 +01:00
|
|
|
QString s = filter.trimmed();
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!s.isEmpty()) best_image_filters_ << s;
|
2010-12-28 13:52:58 +01:00
|
|
|
}
|
2012-01-05 15:51:23 +01:00
|
|
|
|
2021-03-14 07:45:42 +01:00
|
|
|
skip_file_extensions_.clear();
|
|
|
|
QStringList extensions = s.value("skip_file_extensions").toStringList();
|
|
|
|
for (const QString& extension : extensions) {
|
|
|
|
QString s = extension.trimmed().toLower();
|
|
|
|
if (!s.isEmpty()) skip_file_extensions_ << s;
|
|
|
|
}
|
|
|
|
|
2010-07-10 19:03:28 +02:00
|
|
|
if (!monitor_ && was_monitoring_before) {
|
2012-01-08 19:37:46 +01:00
|
|
|
fs_watcher_->Clear();
|
2010-07-10 19:03:28 +02:00
|
|
|
} else if (monitor_ && !was_monitoring_before) {
|
|
|
|
// Add all directories to all QFileSystemWatchers again
|
2020-01-21 09:32:55 +01:00
|
|
|
for (const Directory& dir : watched_dirs_.list_.values()) {
|
2012-01-29 18:39:28 +01:00
|
|
|
SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Subdirectory& subdir : subdirs) {
|
2012-01-29 18:39:28 +01:00
|
|
|
AddWatch(dir, subdir.path);
|
2010-07-10 19:03:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2010-05-25 22:40:45 +02:00
|
|
|
}
|
|
|
|
|
2010-06-24 23:46:18 +02:00
|
|
|
void LibraryWatcher::SetRescanPausedAsync(bool pause) {
|
|
|
|
QMetaObject::invokeMethod(this, "SetRescanPaused", Qt::QueuedConnection,
|
|
|
|
Q_ARG(bool, pause));
|
|
|
|
}
|
|
|
|
|
|
|
|
void LibraryWatcher::SetRescanPaused(bool pause) {
|
|
|
|
rescan_paused_ = pause;
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!rescan_paused_ && !rescan_queue_.isEmpty()) RescanPathsNow();
|
2010-06-24 23:46:18 +02:00
|
|
|
}
|
|
|
|
|
2010-05-25 22:40:45 +02:00
|
|
|
void LibraryWatcher::IncrementalScanAsync() {
|
|
|
|
QMetaObject::invokeMethod(this, "IncrementalScanNow", Qt::QueuedConnection);
|
|
|
|
}
|
|
|
|
|
2011-02-17 20:57:14 +01:00
|
|
|
void LibraryWatcher::FullScanAsync() {
|
|
|
|
QMetaObject::invokeMethod(this, "FullScanNow", Qt::QueuedConnection);
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::IncrementalScanNow() { PerformScan(true, false); }
|
2011-02-17 20:57:14 +01:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void LibraryWatcher::FullScanNow() { PerformScan(false, true); }
|
2011-02-17 20:57:14 +01:00
|
|
|
|
|
|
|
void LibraryWatcher::PerformScan(bool incremental, bool ignore_mtimes) {
|
2020-01-21 09:32:55 +01:00
|
|
|
for (const WatchedDir& dir : watched_dirs_.list_.values()) {
|
2021-03-29 09:14:46 +02:00
|
|
|
qLog(Debug) << "Scanning library directory" << dir.GetPath();
|
2020-01-21 05:12:56 +01:00
|
|
|
ScanTransaction transaction(this, dir, incremental, ignore_mtimes);
|
2010-05-25 22:40:45 +02:00
|
|
|
SubdirectoryList subdirs(transaction.GetAllSubdirs());
|
2021-03-29 09:14:46 +02:00
|
|
|
|
|
|
|
// On Linux systems, if the library directory is deleted then re-created,
|
|
|
|
// inotify won't find it. This could be corrected by watching parent
|
|
|
|
// directories, but not worth the complexity for an edge case.
|
|
|
|
if (subdirs.isEmpty()) {
|
|
|
|
qLog(Debug) << "Library directory wasn't in subdir list.";
|
|
|
|
Subdirectory subdir;
|
|
|
|
subdir.path = dir.GetPath();
|
|
|
|
subdir.directory_id = dir.GetId();
|
|
|
|
subdirs << subdir;
|
|
|
|
}
|
|
|
|
|
2010-06-23 16:00:18 +02:00
|
|
|
transaction.AddToProgressMax(subdirs.count());
|
|
|
|
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Subdirectory& subdir : subdirs) {
|
2020-01-21 09:32:55 +01:00
|
|
|
if (transaction.aborted()) return;
|
2010-05-25 22:40:45 +02:00
|
|
|
|
|
|
|
ScanSubdirectory(subdir.path, subdir, &transaction);
|
|
|
|
}
|
|
|
|
}
|
2011-02-17 20:57:14 +01:00
|
|
|
|
2010-06-02 18:22:20 +02:00
|
|
|
emit CompilationsNeedUpdating();
|
2010-05-25 22:40:45 +02:00
|
|
|
}
|