diff --git a/approot/admin-database.xml b/approot/admin-database.xml
index 435428e8..51b9eeff 100644
--- a/approot/admin-database.xml
+++ b/approot/admin-database.xml
@@ -76,6 +76,29 @@
+ ${status}
+
+
+
+
+
diff --git a/approot/messages.xml b/approot/messages.xml
index 0c5446d9..a1962507 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -38,6 +38,14 @@
Update start time
Weekly
+Status
+Last scan
+Not available
+Scanned {1} files in {2} on {3} ({4} errors)
+Not scheduled
+Scheduled on {1}
+Scanning {1} of {2} files ({3} %)
+
New user
Admin
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index a084ac8b..a3b22cc4 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -38,6 +38,14 @@
Heure de départ de la mise à jour
Toutes les semaines
+Statut
+Dernier scan
+Non disponible
+{1} fichiers scannés en {2} le {3} ({4} erreurs)
+Non planifié
+Planifié le {1}
+Scan de {1} fichiers sur {2} ({3} %)
+
Ajouter
Admin
diff --git a/src/scanner/MediaScanner.cpp b/src/scanner/MediaScanner.cpp
index bc1744ed..61323d46 100644
--- a/src/scanner/MediaScanner.cpp
+++ b/src/scanner/MediaScanner.cpp
@@ -194,9 +194,7 @@ getOrCreateClusters(Wt::Dbo::Session& session, const MetaData::Clusters& cluster
namespace Scanner {
MediaScanner::MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool)
- : _running(false),
-_scheduleTimer(_ioService),
-_db(connectionPool)
+: _db {connectionPool}
{
_ioService.setThreadCount(1);
@@ -221,8 +219,7 @@ MediaScanner::start(void)
{
_running = true;
- // post some jobs in the io_service
- scheduleScan();
+ scheduleNextScan();
_ioService.start();
}
@@ -241,27 +238,41 @@ MediaScanner::stop(void)
}
void
-MediaScanner::scheduleImmediateScan()
+MediaScanner::requestImmediateScan()
{
_ioService.post([=]()
{
LMS_LOG(DBUPDATER, INFO) << "Schedule immediate scan";
- scheduleScan(std::chrono::seconds(0));
- });
-}
-
-void
-MediaScanner::reschedule()
-{
- _ioService.post([=]()
- {
- LMS_LOG(DBUPDATER, INFO) << "Rescheduling scan";
scheduleScan();
});
}
void
-MediaScanner::scheduleScan()
+MediaScanner::requestReschedule()
+{
+ _ioService.post([=]()
+ {
+ LMS_LOG(DBUPDATER, INFO) << "Rescheduling scan";
+ scheduleNextScan();
+ });
+}
+
+MediaScanner::Status
+MediaScanner::getStatus()
+{
+ Status res;
+
+ std::unique_lock lock {_statusMutex};
+ res.currentState = _curState;
+ res.nextScheduledScan = _nextScheduledScan;
+ res.lastScanStats = _lastScanStats;
+ res.inProgressStats = _inProgressStats;
+
+ return res;
+}
+
+void
+MediaScanner::scheduleNextScan()
{
refreshScanSettings();
@@ -296,25 +307,71 @@ MediaScanner::scheduleScan()
break;
}
+ Wt::WDateTime nextScanDateTime;
+
if (nextScanDate.isValid())
- scheduleScan( Wt::WDateTime(nextScanDate, _startTime).toTimePoint() );
+ {
+ nextScanDateTime = Wt::WDateTime {nextScanDate, _startTime};
+ scheduleScan(nextScanDateTime);
+ }
+
+ {
+ std::unique_lock lock {_statusMutex};
+ _curState = nextScanDateTime.isValid() ? State::Scheduled : State::NotScheduled;
+ _nextScheduledScan = nextScanDateTime;
+ }
+
+ _sigScheduled.emit(_nextScheduledScan);
}
void
-MediaScanner::scheduleScan(std::chrono::seconds duration)
+MediaScanner::countAllFiles(Stats& stats)
{
- LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan in " << duration.count() << " seconds";
- _scheduleTimer.expires_from_now(duration);
- _scheduleTimer.async_wait( std::bind( &MediaScanner::scan, this, std::placeholders::_1) );
+ boost::system::error_code ec;
+
+ stats.totalFiles = 0;
+
+ boost::filesystem::recursive_directory_iterator itPath(_mediaDirectory, ec);
+ if (ec)
+ {
+ LMS_LOG(DBUPDATER, ERROR) << "Cannot iterate over '" << _mediaDirectory.string() << "': " << ec.message();
+ return;
+ }
+
+ boost::filesystem::recursive_directory_iterator itEnd;
+ while (itPath != itEnd && _running)
+ {
+ if (stats.totalFiles % 250 == 0)
+ notifyInProgressIfNeeded(stats);
+
+ const boost::filesystem::path& path {*itPath++};
+
+ if (!_running)
+ break;
+
+ if (boost::filesystem::is_regular(path) && isFileSupported(path, _fileExtensions))
+ stats.totalFiles++;
+ }
}
void
-MediaScanner::scheduleScan(std::chrono::system_clock::time_point timePoint)
+MediaScanner::scheduleScan(const Wt::WDateTime& dateTime)
{
- std::time_t t = std::chrono::system_clock::to_time_t(timePoint);
- LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t));
- _scheduleTimer.expires_at(timePoint);
- _scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
+ if (dateTime.isNull())
+ {
+ LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
+ _scheduleTimer.expires_from_now(std::chrono::seconds(0));
+ _scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
+ }
+ else
+ {
+ std::chrono::system_clock::time_point timePoint {dateTime.toTimePoint()};
+ std::time_t t {std::chrono::system_clock::to_time_t(timePoint)};
+
+ LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t));
+ _scheduleTimer.expires_at(timePoint);
+ _scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
+ }
}
void
@@ -323,12 +380,25 @@ MediaScanner::scan(boost::system::error_code err)
if (err)
return;
+ {
+ std::unique_lock lock {_statusMutex};
+ _curState = State::InProgress;
+ _nextScheduledScan = {};
+ _inProgressStats = Stats{};
+ _inProgressStats->startTime = Wt::WLocalDateTime::currentDateTime().toUTC();
+ }
+
+ Stats& stats {*_inProgressStats};
+
LMS_LOG(UI, INFO) << "New scan started!";
refreshScanSettings();
- bool forceScan = false;
- Stats stats;
+ bool forceScan {false};
+
+ LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'...";
+ countAllFiles(stats);
+ LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.totalFiles;
removeMissingTracks(stats);
@@ -351,10 +421,23 @@ MediaScanner::scan(boost::system::error_code err)
for (auto& addon : _addons)
addon->preScanComplete();
- scheduleScan();
+ stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC();
+ {
+ std::unique_lock lock {_statusMutex};
+ _lastScanStats = std::move(_inProgressStats);
+ _inProgressStats.reset();
+ }
+
+ scheduleNextScan();
scanComplete().emit(stats);
}
+ else
+ {
+ std::unique_lock lock {_statusMutex};
+ _curState = State::NotScheduled;
+ _inProgressStats.reset();
+ }
}
void
@@ -388,9 +471,22 @@ MediaScanner::refreshScanSettings()
addon->refreshSettings();
}
+void MediaScanner::notifyInProgressIfNeeded(Stats& stats)
+{
+ std::chrono::system_clock::time_point now {std::chrono::system_clock::now()};
+
+ if (std::chrono::duration_cast(now - _lastScanInProgressEmit).count() > 2)
+ {
+ _sigScanInProgress(stats);
+ _lastScanInProgressEmit = now;
+ }
+}
+
void
MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, Stats& stats)
{
+ notifyInProgressIfNeeded(stats);
+
auto lastWriteTime = Wt::WDateTime::fromTime_t(boost::filesystem::last_write_time(file));
if (!forceScan)
@@ -561,7 +657,7 @@ MediaScanner::scanMediaDirectory(boost::filesystem::path mediaDirectory, bool fo
boost::filesystem::recursive_directory_iterator itEnd;
while (itPath != itEnd)
{
- boost::filesystem::path path = *itPath;
+ const boost::filesystem::path& path {*itPath};
if (!_running)
return;
diff --git a/src/scanner/MediaScanner.hpp b/src/scanner/MediaScanner.hpp
index 5474a6d1..904fca4c 100644
--- a/src/scanner/MediaScanner.hpp
+++ b/src/scanner/MediaScanner.hpp
@@ -20,7 +20,9 @@
#pragma once
#include
+#include
+#include
#include
#include
@@ -37,7 +39,6 @@ namespace Scanner {
class MediaScanner
{
public:
-
MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool);
void setAddon(MediaScannerAddon& addon);
@@ -46,11 +47,14 @@ class MediaScanner
void stop();
void restart();
- void scheduleImmediateScan();
- void reschedule();
+ // Async requests
+ void requestImmediateScan();
+ void requestReschedule();
struct Stats
{
+ Wt::WDateTime startTime;
+ Wt::WDateTime stopTime;
std::size_t skips = 0; // no change since last scan
std::size_t scans = 0; // actually scanned filed
std::size_t scanErrors = 0; // cannot scan file
@@ -62,21 +66,46 @@ class MediaScanner
std::size_t duplicateHashes = 0; // Same file hashes
std::size_t duplicateMBID = 0; // Same MBID
+ std::size_t totalFiles = 0; // Total number of files to be scanned
+
std::size_t nbFiles() const { return skips + additions + updates; }
std::size_t nbChanges() const { return additions + deletions + updates; }
std::size_t nbErrors() const { return scanErrors + incompleteScans; }
std::size_t nbDuplicates() const { return duplicateHashes + duplicateMBID; }
+ float progress() const { return (nbFiles() - totalFiles) / static_cast(totalFiles); }
};
+ enum class State
+ {
+ NotScheduled,
+ Scheduled,
+ InProgress,
+ };
+
+ struct Status
+ {
+ State currentState {State::NotScheduled};
+ Wt::WDateTime nextScheduledScan;
+ boost::optional lastScanStats;
+ boost::optional inProgressStats;
+ };
+
+ Status getStatus();
+
// Called just after scan complete
Wt::Signal& scanComplete() { return _sigScanComplete; }
+ // Called during scan in progress
+ Wt::Signal& scanInProgress() { return _sigScanInProgress; }
+
+ // Called after a schedule
+ Wt::Signal& scheduled() { return _sigScheduled; }
+
private:
// Job handling
- void scheduleScan();
- void scheduleScan(std::chrono::seconds duration);
- void scheduleScan(std::chrono::system_clock::time_point time);
+ void scheduleNextScan();
+ void scheduleScan(const Wt::WDateTime& dateTime = {});
// Update database (scheduled callback)
void scan(boost::system::error_code ec);
@@ -86,30 +115,37 @@ class MediaScanner
// Helpers
void refreshScanSettings();
+ void countAllFiles(Stats& stats);
void removeMissingTracks(Stats& stats);
void removeOrphanEntries();
void checkDuplicatedAudioFiles(Stats& stats);
void scanAudioFile(const boost::filesystem::path& file, bool forceScan, Stats& stats);
+ void notifyInProgressIfNeeded(Stats& stats);
- bool _running;
+ bool _running {false};
Wt::WIOService _ioService;
-
+ boost::asio::system_timer _scheduleTimer {_ioService};
Wt::Signal _sigScanComplete;
-
- boost::asio::system_timer _scheduleTimer;
-
+ Wt::Signal _sigScanInProgress;
+ std::chrono::system_clock::time_point _lastScanInProgressEmit {};
+ Wt::Signal _sigScheduled;
Database::Handler _db;
+ MetaData::TagLibParser _metadataParser;
+ std::vector _addons;
+
+ std::mutex _statusMutex;
+ State _curState {State::NotScheduled};
+ boost::optional _inProgressStats;
+ boost::optional _lastScanStats;
+ Wt::WDateTime _nextScheduledScan;
// Current scan settings
- std::size_t _scanVersion;
+ std::size_t _scanVersion {};
Wt::WTime _startTime;
- Database::ScanSettings::UpdatePeriod _updatePeriod;
+ Database::ScanSettings::UpdatePeriod _updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
std::set _fileExtensions;
boost::filesystem::path _mediaDirectory;
- MetaData::TagLibParser _metadataParser;
-
- std::vector _addons;
}; // class MediaScanner
diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp
index 0937abaa..98563177 100644
--- a/src/ui/LmsApplication.cpp
+++ b/src/ui/LmsApplication.cpp
@@ -458,16 +458,36 @@ LmsApplication::createHome()
});
// Events from MediaScanner
- std::string sessionId = LmsApp->sessionId();
- getService()->scanComplete().connect([=] (Scanner::MediaScanner::Stats stats)
{
- // Runs from media scanner context
- Wt::WServer::instance()->post(sessionId, [=]
+ std::string sessionId = LmsApp->sessionId();
+ getService()->scanComplete().connect(this, [=] (Scanner::MediaScanner::Stats stats)
{
- _events.dbScanned.emit(stats);
- triggerUpdate();
+ Wt::WServer::instance()->post(sessionId, [=]
+ {
+ _events.dbScanned.emit(stats);
+ triggerUpdate();
+ });
});
- });
+
+ getService()->scanInProgress().connect(this, [=] (Scanner::MediaScanner::Stats stats)
+ {
+ Wt::WServer::instance()->post(sessionId, [=]
+ {
+ _events.dbScanInProgress.emit(stats);
+ triggerUpdate();
+ });
+ });
+
+ getService()->scheduled().connect(this, [=] (Wt::WDateTime dateTime)
+ {
+ Wt::WServer::instance()->post(sessionId, [=]
+ {
+ _events.dbScanScheduled.emit(dateTime);
+ triggerUpdate();
+ });
+ });
+
+ }
_events.dbScanned.connect([=] (Scanner::MediaScanner::Stats stats)
{
diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp
index a8d296e1..6d75158e 100644
--- a/src/ui/LmsApplication.hpp
+++ b/src/ui/LmsApplication.hpp
@@ -55,8 +55,10 @@ struct Events
// Unload current track
Wt::Signal<> trackUnloaded;
- // A database scan is complete
+ // Database events
Wt::Signal dbScanned;
+ Wt::Signal dbScanInProgress;
+ Wt::Signal dbScanScheduled;
};
// Used to classify the message sent to the user
diff --git a/src/ui/admin/DatabaseSettingsView.cpp b/src/ui/admin/DatabaseSettingsView.cpp
index 63f704a3..4794c714 100644
--- a/src/ui/admin/DatabaseSettingsView.cpp
+++ b/src/ui/admin/DatabaseSettingsView.cpp
@@ -19,10 +19,14 @@
#include "DatabaseSettingsView.hpp"
+#include
+#include
+
#include
#include
#include
#include
+#include
#include
#include
@@ -62,7 +66,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
addField(SimilarityEngineTypeField);
addField(TagsField);
- auto dirValidator = std::make_shared();
+ auto dirValidator {std::make_shared()};
dirValidator->setMandatory(true);
setValidator(MediaDirectoryField, dirValidator);
@@ -174,6 +178,85 @@ const Wt::WFormModel::Field DatabaseSettingsModel::UpdateStartTimeField = "upda
const Wt::WFormModel::Field DatabaseSettingsModel::SimilarityEngineTypeField = "similarity-engine-type";
const Wt::WFormModel::Field DatabaseSettingsModel::TagsField = "tags";
+static
+std::string durationToString(const Wt::WDateTime& begin, const Wt::WDateTime& end)
+{
+ auto secs {std::chrono::duration_cast(end.toTimePoint() - begin.toTimePoint()).count()};
+
+ std::ostringstream oss;
+
+ if (secs >= 3600)
+ oss << secs/3600 << "h";
+ if (secs >= 60)
+ oss << std::setw(2) << std::setfill('0') << (secs % 3600) / 60 << "m";
+ oss << std::setw(2) << std::setfill('0') << (secs % 60) << "s";
+
+ return oss.str();
+}
+
+class DatabaseStatus : public Wt::WTemplate
+{
+ public:
+
+ DatabaseStatus(): WTemplate {Wt::WString::tr("Lms.Admin.Database.Status.template")}
+ {
+ addFunction("tr", &Wt::WTemplate::Functions::tr);
+
+ using namespace Scanner;
+
+ auto onDbEvent = [&]() { refreshContents(); };
+
+ LmsApp->getEvents().dbScanned.connect(this, onDbEvent);
+ LmsApp->getEvents().dbScanInProgress.connect(this, onDbEvent);
+ LmsApp->getEvents().dbScanScheduled.connect(this, onDbEvent);
+
+ refreshContents();
+ }
+
+ private:
+ void refreshContents()
+ {
+ using namespace Scanner;
+
+ MediaScanner::Status status {getService()->getStatus()};
+
+ if (status.lastScanStats)
+ {
+ bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-status")
+ .arg(status.lastScanStats->totalFiles)
+ .arg(durationToString(status.lastScanStats->startTime, status.lastScanStats->stopTime))
+ .arg(status.lastScanStats->stopTime.toString())
+ .arg(status.lastScanStats->nbErrors())
+ );
+ }
+ else
+ {
+ bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-not-available"));
+ }
+
+ switch (status.currentState)
+ {
+ case MediaScanner::State::NotScheduled:
+ bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-not-scheduled"));
+ break;
+ case MediaScanner::State::Scheduled:
+ bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-scheduled")
+ .arg(status.nextScheduledScan.toString()));
+ break;
+ case MediaScanner::State::InProgress:
+ {
+ std::ostringstream oss;
+ bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-in-progress")
+ .arg(status.inProgressStats->nbFiles())
+ .arg(status.inProgressStats->totalFiles)
+ .arg(static_cast(status.inProgressStats->nbFiles() / static_cast(status.inProgressStats->totalFiles ? status.inProgressStats->totalFiles : 1) * 100)));
+ }
+ break;
+ }
+ }
+ private:
+};
+
DatabaseSettingsView::DatabaseSettingsView()
{
wApp->internalPathChanged().connect(std::bind([=]
@@ -221,6 +304,8 @@ DatabaseSettingsView::refreshView()
Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard")));
Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique(Wt::WString::tr("Lms.Admin.Database.immediate-scan")));
+ t->bindNew("status");
+
saveBtn->clicked().connect([=] ()
{
t->updateModel(model.get());
@@ -229,7 +314,7 @@ DatabaseSettingsView::refreshView()
{
model->saveData();
- getService()->reschedule();
+ getService()->requestReschedule();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
}
@@ -246,7 +331,7 @@ DatabaseSettingsView::refreshView()
immScanBtn->clicked().connect([=] ()
{
- getService()->scheduleImmediateScan();
+ getService()->requestImmediateScan();
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched"));
});