Added some feedback about the last/current/next media directory scans
This commit is contained in:
@@ -76,6 +76,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${status}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.Admin.Database.Status.template">
|
||||||
|
<legend>${tr:Lms.Admin.Database.Status.status}</legend>
|
||||||
|
<div class="form-horizontal">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2" for="${id:last-scan}">
|
||||||
|
${tr:Lms.Admin.Database.Status.last-scan}
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-5 well well-sm">
|
||||||
|
${last-scan}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-2" for="${id:status}">
|
||||||
|
${tr:Lms.Admin.Database.Status.status}
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-5 well well-sm">
|
||||||
|
${status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
</messages>
|
</messages>
|
||||||
|
|||||||
@@ -38,6 +38,14 @@
|
|||||||
<message id="Lms.Admin.Database.update-start-time">Update start time</message>
|
<message id="Lms.Admin.Database.update-start-time">Update start time</message>
|
||||||
<message id="Lms.Admin.Database.weekly">Weekly</message>
|
<message id="Lms.Admin.Database.weekly">Weekly</message>
|
||||||
|
|
||||||
|
<message id="Lms.Admin.Database.Status.status">Status</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.last-scan">Last scan</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.last-scan-not-available">Not available</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.last-scan-status">Scanned {1} files in {2} on {3} ({4} errors)</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.status-not-scheduled">Not scheduled</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.status-scheduled">Scheduled on {1}</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.status-in-progress">Scanning {1} of {2} files ({3} %)</message>
|
||||||
|
|
||||||
<!--Users-->
|
<!--Users-->
|
||||||
<message id="Lms.Admin.Users.add">New user</message>
|
<message id="Lms.Admin.Users.add">New user</message>
|
||||||
<message id="Lms.Admin.Users.admin">Admin</message>
|
<message id="Lms.Admin.Users.admin">Admin</message>
|
||||||
|
|||||||
@@ -38,6 +38,14 @@
|
|||||||
<message id="Lms.Admin.Database.update-start-time">Heure de départ de la mise à jour</message>
|
<message id="Lms.Admin.Database.update-start-time">Heure de départ de la mise à jour</message>
|
||||||
<message id="Lms.Admin.Database.weekly">Toutes les semaines</message>
|
<message id="Lms.Admin.Database.weekly">Toutes les semaines</message>
|
||||||
|
|
||||||
|
<message id="Lms.Admin.Database.Status.status">Statut</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.last-scan">Dernier scan</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.last-scan-not-available">Non disponible</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.last-scan-status">{1} fichiers scannés en {2} le {3} ({4} erreurs)</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.status-not-scheduled">Non planifié</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.status-scheduled">Planifié le {1}</message>
|
||||||
|
<message id="Lms.Admin.Database.Status.status-in-progress">Scan de {1} fichiers sur {2} ({3} %)</message>
|
||||||
|
|
||||||
<!--Users-->
|
<!--Users-->
|
||||||
<message id="Lms.Admin.Users.add">Ajouter</message>
|
<message id="Lms.Admin.Users.add">Ajouter</message>
|
||||||
<message id="Lms.Admin.Users.admin">Admin</message>
|
<message id="Lms.Admin.Users.admin">Admin</message>
|
||||||
|
|||||||
+125
-29
@@ -194,9 +194,7 @@ getOrCreateClusters(Wt::Dbo::Session& session, const MetaData::Clusters& cluster
|
|||||||
namespace Scanner {
|
namespace Scanner {
|
||||||
|
|
||||||
MediaScanner::MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool)
|
MediaScanner::MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool)
|
||||||
: _running(false),
|
: _db {connectionPool}
|
||||||
_scheduleTimer(_ioService),
|
|
||||||
_db(connectionPool)
|
|
||||||
{
|
{
|
||||||
_ioService.setThreadCount(1);
|
_ioService.setThreadCount(1);
|
||||||
|
|
||||||
@@ -221,8 +219,7 @@ MediaScanner::start(void)
|
|||||||
{
|
{
|
||||||
_running = true;
|
_running = true;
|
||||||
|
|
||||||
// post some jobs in the io_service
|
scheduleNextScan();
|
||||||
scheduleScan();
|
|
||||||
|
|
||||||
_ioService.start();
|
_ioService.start();
|
||||||
}
|
}
|
||||||
@@ -241,27 +238,41 @@ MediaScanner::stop(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MediaScanner::scheduleImmediateScan()
|
MediaScanner::requestImmediateScan()
|
||||||
{
|
{
|
||||||
_ioService.post([=]()
|
_ioService.post([=]()
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Schedule immediate scan";
|
LMS_LOG(DBUPDATER, INFO) << "Schedule immediate scan";
|
||||||
scheduleScan(std::chrono::seconds(0));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
MediaScanner::reschedule()
|
|
||||||
{
|
|
||||||
_ioService.post([=]()
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Rescheduling scan";
|
|
||||||
scheduleScan();
|
scheduleScan();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MediaScanner::scheduleScan()
|
MediaScanner::requestReschedule()
|
||||||
|
{
|
||||||
|
_ioService.post([=]()
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, INFO) << "Rescheduling scan";
|
||||||
|
scheduleNextScan();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaScanner::Status
|
||||||
|
MediaScanner::getStatus()
|
||||||
|
{
|
||||||
|
Status res;
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock {_statusMutex};
|
||||||
|
res.currentState = _curState;
|
||||||
|
res.nextScheduledScan = _nextScheduledScan;
|
||||||
|
res.lastScanStats = _lastScanStats;
|
||||||
|
res.inProgressStats = _inProgressStats;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MediaScanner::scheduleNextScan()
|
||||||
{
|
{
|
||||||
refreshScanSettings();
|
refreshScanSettings();
|
||||||
|
|
||||||
@@ -296,26 +307,72 @@ MediaScanner::scheduleScan()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Wt::WDateTime nextScanDateTime;
|
||||||
|
|
||||||
if (nextScanDate.isValid())
|
if (nextScanDate.isValid())
|
||||||
scheduleScan( Wt::WDateTime(nextScanDate, _startTime).toTimePoint() );
|
{
|
||||||
|
nextScanDateTime = Wt::WDateTime {nextScanDate, _startTime};
|
||||||
|
scheduleScan(nextScanDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock {_statusMutex};
|
||||||
|
_curState = nextScanDateTime.isValid() ? State::Scheduled : State::NotScheduled;
|
||||||
|
_nextScheduledScan = nextScanDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sigScheduled.emit(_nextScheduledScan);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MediaScanner::scheduleScan(std::chrono::seconds duration)
|
MediaScanner::countAllFiles(Stats& stats)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan in " << duration.count() << " seconds";
|
boost::system::error_code ec;
|
||||||
_scheduleTimer.expires_from_now(duration);
|
|
||||||
|
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(const Wt::WDateTime& dateTime)
|
||||||
|
{
|
||||||
|
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));
|
_scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
|
||||||
}
|
}
|
||||||
|
else
|
||||||
void
|
|
||||||
MediaScanner::scheduleScan(std::chrono::system_clock::time_point timePoint)
|
|
||||||
{
|
{
|
||||||
std::time_t t = std::chrono::system_clock::to_time_t(timePoint);
|
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));
|
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t));
|
||||||
_scheduleTimer.expires_at(timePoint);
|
_scheduleTimer.expires_at(timePoint);
|
||||||
_scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
|
_scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MediaScanner::scan(boost::system::error_code err)
|
MediaScanner::scan(boost::system::error_code err)
|
||||||
@@ -323,12 +380,25 @@ MediaScanner::scan(boost::system::error_code err)
|
|||||||
if (err)
|
if (err)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock {_statusMutex};
|
||||||
|
_curState = State::InProgress;
|
||||||
|
_nextScheduledScan = {};
|
||||||
|
_inProgressStats = Stats{};
|
||||||
|
_inProgressStats->startTime = Wt::WLocalDateTime::currentDateTime().toUTC();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stats& stats {*_inProgressStats};
|
||||||
|
|
||||||
LMS_LOG(UI, INFO) << "New scan started!";
|
LMS_LOG(UI, INFO) << "New scan started!";
|
||||||
|
|
||||||
refreshScanSettings();
|
refreshScanSettings();
|
||||||
|
|
||||||
bool forceScan = false;
|
bool forceScan {false};
|
||||||
Stats stats;
|
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'...";
|
||||||
|
countAllFiles(stats);
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.totalFiles;
|
||||||
|
|
||||||
removeMissingTracks(stats);
|
removeMissingTracks(stats);
|
||||||
|
|
||||||
@@ -351,10 +421,23 @@ MediaScanner::scan(boost::system::error_code err)
|
|||||||
for (auto& addon : _addons)
|
for (auto& addon : _addons)
|
||||||
addon->preScanComplete();
|
addon->preScanComplete();
|
||||||
|
|
||||||
scheduleScan();
|
stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC();
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock {_statusMutex};
|
||||||
|
_lastScanStats = std::move(_inProgressStats);
|
||||||
|
_inProgressStats.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNextScan();
|
||||||
|
|
||||||
scanComplete().emit(stats);
|
scanComplete().emit(stats);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock {_statusMutex};
|
||||||
|
_curState = State::NotScheduled;
|
||||||
|
_inProgressStats.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -388,9 +471,22 @@ MediaScanner::refreshScanSettings()
|
|||||||
addon->refreshSettings();
|
addon->refreshSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MediaScanner::notifyInProgressIfNeeded(Stats& stats)
|
||||||
|
{
|
||||||
|
std::chrono::system_clock::time_point now {std::chrono::system_clock::now()};
|
||||||
|
|
||||||
|
if (std::chrono::duration_cast<std::chrono::seconds>(now - _lastScanInProgressEmit).count() > 2)
|
||||||
|
{
|
||||||
|
_sigScanInProgress(stats);
|
||||||
|
_lastScanInProgressEmit = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, Stats& stats)
|
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));
|
auto lastWriteTime = Wt::WDateTime::fromTime_t(boost::filesystem::last_write_time(file));
|
||||||
|
|
||||||
if (!forceScan)
|
if (!forceScan)
|
||||||
@@ -561,7 +657,7 @@ MediaScanner::scanMediaDirectory(boost::filesystem::path mediaDirectory, bool fo
|
|||||||
boost::filesystem::recursive_directory_iterator itEnd;
|
boost::filesystem::recursive_directory_iterator itEnd;
|
||||||
while (itPath != itEnd)
|
while (itPath != itEnd)
|
||||||
{
|
{
|
||||||
boost::filesystem::path path = *itPath;
|
const boost::filesystem::path& path {*itPath};
|
||||||
|
|
||||||
if (!_running)
|
if (!_running)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
#include <Wt/WIOService.h>
|
#include <Wt/WIOService.h>
|
||||||
#include <Wt/WSignal.h>
|
#include <Wt/WSignal.h>
|
||||||
|
|
||||||
@@ -37,7 +39,6 @@ namespace Scanner {
|
|||||||
class MediaScanner
|
class MediaScanner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
||||||
MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool);
|
MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool);
|
||||||
|
|
||||||
void setAddon(MediaScannerAddon& addon);
|
void setAddon(MediaScannerAddon& addon);
|
||||||
@@ -46,11 +47,14 @@ class MediaScanner
|
|||||||
void stop();
|
void stop();
|
||||||
void restart();
|
void restart();
|
||||||
|
|
||||||
void scheduleImmediateScan();
|
// Async requests
|
||||||
void reschedule();
|
void requestImmediateScan();
|
||||||
|
void requestReschedule();
|
||||||
|
|
||||||
struct Stats
|
struct Stats
|
||||||
{
|
{
|
||||||
|
Wt::WDateTime startTime;
|
||||||
|
Wt::WDateTime stopTime;
|
||||||
std::size_t skips = 0; // no change since last scan
|
std::size_t skips = 0; // no change since last scan
|
||||||
std::size_t scans = 0; // actually scanned filed
|
std::size_t scans = 0; // actually scanned filed
|
||||||
std::size_t scanErrors = 0; // cannot scan file
|
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 duplicateHashes = 0; // Same file hashes
|
||||||
std::size_t duplicateMBID = 0; // Same MBID
|
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 nbFiles() const { return skips + additions + updates; }
|
||||||
std::size_t nbChanges() const { return additions + deletions + updates; }
|
std::size_t nbChanges() const { return additions + deletions + updates; }
|
||||||
std::size_t nbErrors() const { return scanErrors + incompleteScans; }
|
std::size_t nbErrors() const { return scanErrors + incompleteScans; }
|
||||||
std::size_t nbDuplicates() const { return duplicateHashes + duplicateMBID; }
|
std::size_t nbDuplicates() const { return duplicateHashes + duplicateMBID; }
|
||||||
|
float progress() const { return (nbFiles() - totalFiles) / static_cast<float>(totalFiles); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class State
|
||||||
|
{
|
||||||
|
NotScheduled,
|
||||||
|
Scheduled,
|
||||||
|
InProgress,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Status
|
||||||
|
{
|
||||||
|
State currentState {State::NotScheduled};
|
||||||
|
Wt::WDateTime nextScheduledScan;
|
||||||
|
boost::optional<Stats> lastScanStats;
|
||||||
|
boost::optional<Stats> inProgressStats;
|
||||||
|
};
|
||||||
|
|
||||||
|
Status getStatus();
|
||||||
|
|
||||||
// Called just after scan complete
|
// Called just after scan complete
|
||||||
Wt::Signal<Stats>& scanComplete() { return _sigScanComplete; }
|
Wt::Signal<Stats>& scanComplete() { return _sigScanComplete; }
|
||||||
|
|
||||||
|
// Called during scan in progress
|
||||||
|
Wt::Signal<Stats>& scanInProgress() { return _sigScanInProgress; }
|
||||||
|
|
||||||
|
// Called after a schedule
|
||||||
|
Wt::Signal<Wt::WDateTime>& scheduled() { return _sigScheduled; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
// Job handling
|
// Job handling
|
||||||
void scheduleScan();
|
void scheduleNextScan();
|
||||||
void scheduleScan(std::chrono::seconds duration);
|
void scheduleScan(const Wt::WDateTime& dateTime = {});
|
||||||
void scheduleScan(std::chrono::system_clock::time_point time);
|
|
||||||
|
|
||||||
// Update database (scheduled callback)
|
// Update database (scheduled callback)
|
||||||
void scan(boost::system::error_code ec);
|
void scan(boost::system::error_code ec);
|
||||||
@@ -86,30 +115,37 @@ class MediaScanner
|
|||||||
// Helpers
|
// Helpers
|
||||||
void refreshScanSettings();
|
void refreshScanSettings();
|
||||||
|
|
||||||
|
void countAllFiles(Stats& stats);
|
||||||
void removeMissingTracks(Stats& stats);
|
void removeMissingTracks(Stats& stats);
|
||||||
void removeOrphanEntries();
|
void removeOrphanEntries();
|
||||||
void checkDuplicatedAudioFiles(Stats& stats);
|
void checkDuplicatedAudioFiles(Stats& stats);
|
||||||
void scanAudioFile(const boost::filesystem::path& file, bool forceScan, 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;
|
Wt::WIOService _ioService;
|
||||||
|
boost::asio::system_timer _scheduleTimer {_ioService};
|
||||||
Wt::Signal<Stats> _sigScanComplete;
|
Wt::Signal<Stats> _sigScanComplete;
|
||||||
|
Wt::Signal<Stats> _sigScanInProgress;
|
||||||
boost::asio::system_timer _scheduleTimer;
|
std::chrono::system_clock::time_point _lastScanInProgressEmit {};
|
||||||
|
Wt::Signal<Wt::WDateTime> _sigScheduled;
|
||||||
Database::Handler _db;
|
Database::Handler _db;
|
||||||
|
MetaData::TagLibParser _metadataParser;
|
||||||
|
std::vector<MediaScannerAddon*> _addons;
|
||||||
|
|
||||||
|
std::mutex _statusMutex;
|
||||||
|
State _curState {State::NotScheduled};
|
||||||
|
boost::optional<Stats> _inProgressStats;
|
||||||
|
boost::optional<Stats> _lastScanStats;
|
||||||
|
Wt::WDateTime _nextScheduledScan;
|
||||||
|
|
||||||
// Current scan settings
|
// Current scan settings
|
||||||
std::size_t _scanVersion;
|
std::size_t _scanVersion {};
|
||||||
Wt::WTime _startTime;
|
Wt::WTime _startTime;
|
||||||
Database::ScanSettings::UpdatePeriod _updatePeriod;
|
Database::ScanSettings::UpdatePeriod _updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
|
||||||
std::set<boost::filesystem::path> _fileExtensions;
|
std::set<boost::filesystem::path> _fileExtensions;
|
||||||
boost::filesystem::path _mediaDirectory;
|
boost::filesystem::path _mediaDirectory;
|
||||||
|
|
||||||
MetaData::TagLibParser _metadataParser;
|
|
||||||
|
|
||||||
std::vector<MediaScannerAddon*> _addons;
|
|
||||||
|
|
||||||
}; // class MediaScanner
|
}; // class MediaScanner
|
||||||
|
|
||||||
|
|||||||
@@ -458,10 +458,10 @@ LmsApplication::createHome()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Events from MediaScanner
|
// Events from MediaScanner
|
||||||
std::string sessionId = LmsApp->sessionId();
|
|
||||||
getService<Scanner::MediaScanner>()->scanComplete().connect([=] (Scanner::MediaScanner::Stats stats)
|
|
||||||
{
|
{
|
||||||
// Runs from media scanner context
|
std::string sessionId = LmsApp->sessionId();
|
||||||
|
getService<Scanner::MediaScanner>()->scanComplete().connect(this, [=] (Scanner::MediaScanner::Stats stats)
|
||||||
|
{
|
||||||
Wt::WServer::instance()->post(sessionId, [=]
|
Wt::WServer::instance()->post(sessionId, [=]
|
||||||
{
|
{
|
||||||
_events.dbScanned.emit(stats);
|
_events.dbScanned.emit(stats);
|
||||||
@@ -469,6 +469,26 @@ LmsApplication::createHome()
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getService<Scanner::MediaScanner>()->scanInProgress().connect(this, [=] (Scanner::MediaScanner::Stats stats)
|
||||||
|
{
|
||||||
|
Wt::WServer::instance()->post(sessionId, [=]
|
||||||
|
{
|
||||||
|
_events.dbScanInProgress.emit(stats);
|
||||||
|
triggerUpdate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
getService<Scanner::MediaScanner>()->scheduled().connect(this, [=] (Wt::WDateTime dateTime)
|
||||||
|
{
|
||||||
|
Wt::WServer::instance()->post(sessionId, [=]
|
||||||
|
{
|
||||||
|
_events.dbScanScheduled.emit(dateTime);
|
||||||
|
triggerUpdate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
_events.dbScanned.connect([=] (Scanner::MediaScanner::Stats stats)
|
_events.dbScanned.connect([=] (Scanner::MediaScanner::Stats stats)
|
||||||
{
|
{
|
||||||
if (_isAdmin)
|
if (_isAdmin)
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ struct Events
|
|||||||
// Unload current track
|
// Unload current track
|
||||||
Wt::Signal<> trackUnloaded;
|
Wt::Signal<> trackUnloaded;
|
||||||
|
|
||||||
// A database scan is complete
|
// Database events
|
||||||
Wt::Signal<Scanner::MediaScanner::Stats> dbScanned;
|
Wt::Signal<Scanner::MediaScanner::Stats> dbScanned;
|
||||||
|
Wt::Signal<Scanner::MediaScanner::Stats> dbScanInProgress;
|
||||||
|
Wt::Signal<Wt::WDateTime> dbScanScheduled;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Used to classify the message sent to the user
|
// Used to classify the message sent to the user
|
||||||
|
|||||||
@@ -19,10 +19,14 @@
|
|||||||
|
|
||||||
#include "DatabaseSettingsView.hpp"
|
#include "DatabaseSettingsView.hpp"
|
||||||
|
|
||||||
|
#include <ostream>
|
||||||
|
#include <iomanip>
|
||||||
|
|
||||||
#include <Wt/WComboBox.h>
|
#include <Wt/WComboBox.h>
|
||||||
#include <Wt/WFormModel.h>
|
#include <Wt/WFormModel.h>
|
||||||
#include <Wt/WLineEdit.h>
|
#include <Wt/WLineEdit.h>
|
||||||
#include <Wt/WPushButton.h>
|
#include <Wt/WPushButton.h>
|
||||||
|
#include <Wt/WServer.h>
|
||||||
#include <Wt/WString.h>
|
#include <Wt/WString.h>
|
||||||
#include <Wt/WTemplateFormView.h>
|
#include <Wt/WTemplateFormView.h>
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
|||||||
addField(SimilarityEngineTypeField);
|
addField(SimilarityEngineTypeField);
|
||||||
addField(TagsField);
|
addField(TagsField);
|
||||||
|
|
||||||
auto dirValidator = std::make_shared<DirectoryValidator>();
|
auto dirValidator {std::make_shared<DirectoryValidator>()};
|
||||||
dirValidator->setMandatory(true);
|
dirValidator->setMandatory(true);
|
||||||
setValidator(MediaDirectoryField, dirValidator);
|
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::SimilarityEngineTypeField = "similarity-engine-type";
|
||||||
const Wt::WFormModel::Field DatabaseSettingsModel::TagsField = "tags";
|
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<std::chrono::seconds>(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<MediaScanner>()->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<int>(status.inProgressStats->nbFiles() / static_cast<float>(status.inProgressStats->totalFiles ? status.inProgressStats->totalFiles : 1) * 100)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
|
||||||
DatabaseSettingsView::DatabaseSettingsView()
|
DatabaseSettingsView::DatabaseSettingsView()
|
||||||
{
|
{
|
||||||
wApp->internalPathChanged().connect(std::bind([=]
|
wApp->internalPathChanged().connect(std::bind([=]
|
||||||
@@ -221,6 +304,8 @@ DatabaseSettingsView::refreshView()
|
|||||||
Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")));
|
Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")));
|
||||||
Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Admin.Database.immediate-scan")));
|
Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Admin.Database.immediate-scan")));
|
||||||
|
|
||||||
|
t->bindNew<DatabaseStatus>("status");
|
||||||
|
|
||||||
saveBtn->clicked().connect([=] ()
|
saveBtn->clicked().connect([=] ()
|
||||||
{
|
{
|
||||||
t->updateModel(model.get());
|
t->updateModel(model.get());
|
||||||
@@ -229,7 +314,7 @@ DatabaseSettingsView::refreshView()
|
|||||||
{
|
{
|
||||||
model->saveData();
|
model->saveData();
|
||||||
|
|
||||||
getService<Scanner::MediaScanner>()->reschedule();
|
getService<Scanner::MediaScanner>()->requestReschedule();
|
||||||
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
|
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +331,7 @@ DatabaseSettingsView::refreshView()
|
|||||||
|
|
||||||
immScanBtn->clicked().connect([=] ()
|
immScanBtn->clicked().connect([=] ()
|
||||||
{
|
{
|
||||||
getService<Scanner::MediaScanner>()->scheduleImmediateScan();
|
getService<Scanner::MediaScanner>()->requestImmediateScan();
|
||||||
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched"));
|
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user