From 322f90dc9306429b3e4d44892602390d06a9eba0 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 10 Oct 2019 13:05:05 +0200 Subject: [PATCH] Added a scan report button (Fixes #4), also fixed the regression about files being always rescanned even if no change has been made --- .gitignore | 1 + approot/admin-database.xml | 58 +++++--- approot/messages.xml | 13 +- approot/messages_fr.xml | 13 +- src/Makefile.am | 4 + src/scanner/MediaScanner.cpp | 83 ++++++------ src/scanner/MediaScanner.hpp | 80 ++++------- src/scanner/MediaScannerStats.cpp | 56 ++++++++ src/scanner/MediaScannerStats.hpp | 94 +++++++++++++ src/ui/LmsApplication.cpp | 14 +- src/ui/LmsApplication.hpp | 4 +- src/ui/admin/DatabaseSettingsView.cpp | 84 +----------- src/ui/admin/DatabaseStatus.cpp | 184 ++++++++++++++++++++++++++ src/ui/admin/DatabaseStatus.hpp | 36 +++++ 14 files changed, 517 insertions(+), 207 deletions(-) create mode 100644 src/scanner/MediaScannerStats.cpp create mode 100644 src/scanner/MediaScannerStats.hpp create mode 100644 src/ui/admin/DatabaseStatus.cpp create mode 100644 src/ui/admin/DatabaseStatus.hpp diff --git a/.gitignore b/.gitignore index 8a23214a..58c93d16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.sw? +*.*~ *.in build/ Makefile.in diff --git a/approot/admin-database.xml b/approot/admin-database.xml index 7674fab7..e5c8ec6f 100644 --- a/approot/admin-database.xml +++ b/approot/admin-database.xml @@ -76,28 +76,12 @@ - ${status} - - - - ${tr:Lms.Admin.Database.Status.status} -
-
- -
-
- ${last-scan} -
-
-
-
- -
-
+
+
+
+
+
${tr:Lms.Admin.Database.Status.status}
+
${status}
@@ -105,4 +89,34 @@
+ +
+
+ +
+
+ ${status} +
+
+
+
+ +
+
+ ${last-scan} +
+
+
+
+
+ ${btn-report class="btn"} +
+
+
+
+ diff --git a/approot/messages.xml b/approot/messages.xml index 3e33bf4e..3b9ef2f2 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -51,10 +51,19 @@ Update start time Weekly -Status +Cannot get track duration +Cannot parse file +Cannot read file +{1} duplicate files: +{1} errors: +Get report Last scan Not available -Scanned {1} files in {2} on {3} ({4} errors) +Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates) +No audio track +Duplicated file hash +Duplicated MBID +Status Not scheduled Scheduled on {1} Scanning {1} of {2} files ({3} %) diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 50503410..9f8ceb58 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -51,10 +51,19 @@ Heure de départ de la mise à jour Toutes les semaines -Statut +Impossible de récupérer la durée de la piste +Impossible d'analyser le fichier +Impossible de lire le fichier +{1} fichiers dupliqués : +{1} erreurs : +Rapport Dernier scan Non disponible -{1} fichiers scannés en {2} le {3} ({4} erreurs) +{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas) +Pas de piste audio +Hash dupliqué +MBID dupliqué +Statut Non planifié Planifié le {1} Scan de {1} fichiers sur {2} ({3} %) diff --git a/src/Makefile.am b/src/Makefile.am index 8121e124..d124c7e9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -59,6 +59,8 @@ lms_SOURCES = \ $(srcdir)/metadata/TagLibParser.hpp \ $(srcdir)/scanner/MediaScanner.cpp \ $(srcdir)/scanner/MediaScanner.hpp \ + $(srcdir)/scanner/MediaScannerStats.cpp \ + $(srcdir)/scanner/MediaScannerStats.hpp \ $(srcdir)/scanner/MediaScannerAddon.hpp \ $(srcdir)/similarity/SimilaritySearcher.cpp \ $(srcdir)/similarity/SimilaritySearcher.hpp \ @@ -95,6 +97,8 @@ lms_SOURCES = \ $(srcdir)/ui/SettingsView.hpp \ $(srcdir)/ui/admin/DatabaseSettingsView.cpp \ $(srcdir)/ui/admin/DatabaseSettingsView.hpp \ + $(srcdir)/ui/admin/DatabaseStatus.cpp \ + $(srcdir)/ui/admin/DatabaseStatus.hpp \ $(srcdir)/ui/admin/InitWizardView.cpp \ $(srcdir)/ui/admin/InitWizardView.hpp \ $(srcdir)/ui/admin/UserView.cpp \ diff --git a/src/scanner/MediaScanner.cpp b/src/scanner/MediaScanner.cpp index 739ac201..85c79652 100644 --- a/src/scanner/MediaScanner.cpp +++ b/src/scanner/MediaScanner.cpp @@ -19,8 +19,6 @@ #include "MediaScanner.hpp" -#include - #include #include @@ -72,7 +70,7 @@ isPathInParentPath(const std::filesystem::path& path, const std::filesystem::pat { std::filesystem::path curPath = path; - while (curPath.has_parent_path()) + while (curPath.parent_path() != curPath) { curPath = curPath.parent_path(); @@ -260,10 +258,11 @@ MediaScanner::getStatus() Status res; std::unique_lock lock {_statusMutex}; + res.currentState = _curState; res.nextScheduledScan = _nextScheduledScan; - res.lastScanStats = _lastScanStats; - res.inProgressStats = _inProgressStats; + res.lastCompleteScanStats = _lastCompleteScanStats; + res.inProgressScanStats = _inProgressScanStats; return res; } @@ -275,7 +274,7 @@ MediaScanner::scheduleNextScan() refreshScanSettings(); - Wt::WDateTime now = Wt::WLocalDateTime::currentServerDateTime().toUTC(); + Wt::WDateTime now {Wt::WLocalDateTime::currentServerDateTime().toUTC()}; Wt::WDate nextScanDate; switch (_updatePeriod) @@ -324,11 +323,11 @@ MediaScanner::scheduleNextScan() } void -MediaScanner::countAllFiles(Stats& stats) +MediaScanner::countAllFiles(ScanStats& stats) { std::error_code ec; - stats.totalFiles = 0; + stats.filesToScan = 0; std::filesystem::recursive_directory_iterator itPath {_mediaDirectory, ec}; if (ec) @@ -345,9 +344,9 @@ MediaScanner::countAllFiles(Stats& stats) if (!ec) { if (std::filesystem::is_regular_file(path) && isFileSupported(path, _fileExtensions)) - stats.totalFiles++; + stats.filesToScan ++; - if (stats.totalFiles % 250 == 0) + if (stats.filesToScan % 250 == 0) notifyInProgressIfNeeded(stats); } @@ -385,11 +384,10 @@ MediaScanner::scan(boost::system::error_code err) std::unique_lock lock {_statusMutex}; _curState = State::InProgress; _nextScheduledScan = {}; - _inProgressStats = Stats{}; - _inProgressStats->startTime = Wt::WLocalDateTime::currentDateTime().toUTC(); } - Stats& stats {*_inProgressStats}; + ScanStats stats; + stats.startTime = Wt::WLocalDateTime::currentDateTime().toUTC(); LMS_LOG(UI, INFO) << "New scan started!"; @@ -399,7 +397,7 @@ MediaScanner::scan(boost::system::error_code err) LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'..."; countAllFiles(stats); - LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.totalFiles; + LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.filesToScan; removeMissingTracks(stats); @@ -414,7 +412,7 @@ MediaScanner::scan(boost::system::error_code err) if (_running) checkDuplicatedAudioFiles(stats); - LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.scanErrors << ", not imported = " << stats.incompleteScans << "), duplicates = " << stats.nbDuplicates() << " (hash = " << stats.duplicateHashes << ", mbid = " << stats.duplicateMBID << ")"; + LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), duplicates = " << stats.duplicates.size(); if (_running) { @@ -427,19 +425,21 @@ MediaScanner::scan(boost::system::error_code err) stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC(); { std::unique_lock lock {_statusMutex}; - _lastScanStats = std::move(_inProgressStats); - _inProgressStats.reset(); + + _lastCompleteScanStats = std::move(stats); + _inProgressScanStats.reset(); } scheduleNextScan(); - scanComplete().emit(stats); + scanComplete().emit(); } else { std::unique_lock lock {_statusMutex}; + _curState = State::NotScheduled; - _inProgressStats.reset(); + _inProgressScanStats.reset(); } LMS_LOG(DBUPDATER, INFO) << "Optimizing db..."; @@ -478,39 +478,44 @@ MediaScanner::refreshScanSettings() addon->refreshSettings(); } -void MediaScanner::notifyInProgress(Stats& stats) +void +MediaScanner::notifyInProgress(const ScanStats& stats) { + { + std::unique_lock lock {_statusMutex}; + _inProgressScanStats = stats.toProgressStats(); + } + std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; - _sigScanInProgress(stats); + _sigScanInProgress(*_inProgressScanStats); _lastScanInProgressEmit = now; } -void MediaScanner::notifyInProgressIfNeeded(Stats& stats) +void +MediaScanner::notifyInProgressIfNeeded(const ScanStats& 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; - } + notifyInProgress(stats); } void -MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, Stats& stats) +MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats) { notifyInProgressIfNeeded(stats); - const auto lastWriteTime {Wt::WDateTime::fromTimePoint(std::filesystem::last_write_time(file))}; + const Wt::WDateTime lastWriteTime {std::filesystem::last_write_time(file)}; if (!forceScan) { // Skip file if last write is the same auto transaction {_dbSession->createSharedTransaction()}; - Track::pointer track {Track::getByPath(*_dbSession, file)}; + const Track::pointer track {Track::getByPath(*_dbSession, file)}; - if (track && track->getLastWriteTime() == lastWriteTime && track->getScanVersion() == _scanVersion) + if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() + && track->getScanVersion() == _scanVersion) { stats.skips++; return; @@ -520,7 +525,7 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S std::optional trackInfo {_metadataParser.parse(file)}; if (!trackInfo) { - stats.scanErrors++; + stats.errors.emplace_back(file, ScanErrorType::CannotParseFile); return; } @@ -543,7 +548,7 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S track.remove(); stats.deletions++; } - stats.incompleteScans++; + stats.errors.emplace_back(ScanError {file, ScanErrorType::NoAudioTrack}); return; } if (trackInfo->duration == std::chrono::milliseconds::zero()) @@ -556,7 +561,7 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S track.remove(); stats.deletions++; } - stats.incompleteScans++; + stats.errors.emplace_back(ScanError {file, ScanErrorType::BadDuration}); return; } @@ -641,7 +646,7 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S } void -MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, Stats& stats) +MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats) { std::error_code ec; @@ -649,7 +654,7 @@ MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bo if (ec) { LMS_LOG(DBUPDATER, ERROR) << "Cannot iterate over '" << mediaDirectory.string() << "': " << ec.message(); - stats.scanErrors++; + stats.errors.emplace_back(ScanError {mediaDirectory, ScanErrorType::CannotReadFile, ec.message()}); return; } @@ -661,7 +666,7 @@ MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bo if (ec) { LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message(); - stats.scanErrors++; + stats.errors.emplace_back(ScanError {path, ScanErrorType::CannotReadFile, ec.message()}); } else if (std::filesystem::is_regular_file(path)) { @@ -713,7 +718,7 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire } void -MediaScanner::removeMissingTracks(Stats& stats) +MediaScanner::removeMissingTracks(ScanStats& stats) { std::vector trackPaths; { @@ -785,7 +790,7 @@ MediaScanner::removeOrphanEntries() } void -MediaScanner::checkDuplicatedAudioFiles(Stats& stats) +MediaScanner::checkDuplicatedAudioFiles(ScanStats& stats) { LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files"; @@ -795,7 +800,7 @@ MediaScanner::checkDuplicatedAudioFiles(Stats& stats) for (const Track::pointer& track : tracks) { LMS_LOG(DBUPDATER, INFO) << "Found duplicated MBID [" << track->getMBID() << "], file: " << track->getPath().string() << " - " << track->getName(); - stats.duplicateMBID++; + stats.duplicates.emplace_back(ScanDuplicate {track->getPath(), DuplicateReason::SameMBID}); } LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files done!"; diff --git a/src/scanner/MediaScanner.hpp b/src/scanner/MediaScanner.hpp index fe2fa0c2..9d4c2f0c 100644 --- a/src/scanner/MediaScanner.hpp +++ b/src/scanner/MediaScanner.hpp @@ -34,6 +34,7 @@ #include "metadata/TagLibParser.hpp" #include "MediaScannerAddon.hpp" +#include "MediaScannerStats.hpp" namespace Scanner { @@ -52,29 +53,6 @@ class MediaScanner 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 - std::size_t incompleteScans = 0; // Scanned, but not imported (criteria not filled) - std::size_t additions = 0; // Added in DB - std::size_t deletions = 0; // removed from DB - std::size_t updates = 0; // updated file in DB - - 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 { @@ -85,19 +63,19 @@ class MediaScanner struct Status { - State currentState {State::NotScheduled}; - Wt::WDateTime nextScheduledScan; - std::optional lastScanStats; - std::optional inProgressStats; + State currentState {State::NotScheduled}; + Wt::WDateTime nextScheduledScan; + std::optional lastCompleteScanStats; + std::optional inProgressScanStats; }; Status getStatus(); // Called just after scan complete - Wt::Signal& scanComplete() { return _sigScanComplete; } + Wt::Signal<>& scanComplete() { return _sigScanComplete; } // Called during scan in progress - Wt::Signal& scanInProgress() { return _sigScanInProgress; } + Wt::Signal& scanInProgress() { return _sigScanInProgress; } // Called after a schedule Wt::Signal& scheduled() { return _sigScheduled; } @@ -111,36 +89,36 @@ class MediaScanner // Update database (scheduled callback) void scan(boost::system::error_code ec); - void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, Stats& stats); + void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); // Helpers void refreshScanSettings(); - void countAllFiles(Stats& stats); - void removeMissingTracks(Stats& stats); + void countAllFiles(ScanStats& stats); + void removeMissingTracks(ScanStats& stats); void removeOrphanEntries(); - void checkDuplicatedAudioFiles(Stats& stats); - void scanAudioFile(const std::filesystem::path& file, bool forceScan, Stats& stats); - Database::IdType doScanAudioFile(const std::filesystem::path& file, Stats& stats); - void notifyInProgressIfNeeded(Stats& stats); - void notifyInProgress(Stats& stats); + void checkDuplicatedAudioFiles(ScanStats& stats); + void scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats); + Database::IdType doScanAudioFile(const std::filesystem::path& file, ScanStats& stats); + void notifyInProgressIfNeeded(const ScanStats& stats); + void notifyInProgress(const ScanStats& stats); - bool _running {false}; - Wt::WIOService _ioService; - boost::asio::system_timer _scheduleTimer {_ioService}; - Wt::Signal _sigScanComplete; - Wt::Signal _sigScanInProgress; - std::chrono::system_clock::time_point _lastScanInProgressEmit {}; - Wt::Signal _sigScheduled; + bool _running {false}; + Wt::WIOService _ioService; + boost::asio::system_timer _scheduleTimer {_ioService}; + Wt::Signal<> _sigScanComplete; + Wt::Signal _sigScanInProgress; + std::chrono::system_clock::time_point _lastScanInProgressEmit {}; + Wt::Signal _sigScheduled; std::unique_ptr _dbSession; - MetaData::TagLibParser _metadataParser; - std::vector _addons; + MetaData::TagLibParser _metadataParser; + std::vector _addons; - std::mutex _statusMutex; - State _curState {State::NotScheduled}; - std::optional _inProgressStats; - std::optional _lastScanStats; - Wt::WDateTime _nextScheduledScan; + std::mutex _statusMutex; + State _curState {State::NotScheduled}; + std::optional _lastCompleteScanStats; + std::optional _inProgressScanStats; + Wt::WDateTime _nextScheduledScan; // Current scan settings std::size_t _scanVersion {}; diff --git a/src/scanner/MediaScannerStats.cpp b/src/scanner/MediaScannerStats.cpp new file mode 100644 index 00000000..f2f991f5 --- /dev/null +++ b/src/scanner/MediaScannerStats.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#include "MediaScannerStats.hpp" + +namespace Scanner { + +ScanError::ScanError(const std::filesystem::path& _file, ScanErrorType _error, const std::string& _systemError) +: file {_file}, +error {_error}, +systemError {_systemError} +{ +} + +std::size_t +ScanStats::nbFiles() const +{ + return skips + additions + updates; +} + +std::size_t +ScanStats::nbChanges() const +{ + return additions + deletions + updates; +} + +ScanProgressStats +ScanStats::toProgressStats() const +{ + return ScanProgressStats {startTime, filesToScan, nbFiles()}; +} + +unsigned +ScanProgressStats::progress() const +{ + return (processedFiles / static_cast(filesToScan ? filesToScan : 1)) * 100; +} + +} // namespace Scanner + diff --git a/src/scanner/MediaScannerStats.hpp b/src/scanner/MediaScannerStats.hpp new file mode 100644 index 00000000..6f4e514d --- /dev/null +++ b/src/scanner/MediaScannerStats.hpp @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#pragma once + +#include + +#include +#include + +namespace Scanner { + + enum class ScanErrorType + { + CannotReadFile, // cannot read file + CannotParseFile, // cannot parse file + NoAudioTrack, // no audio track found + BadDuration, // bad duration + }; + + enum class DuplicateReason + { + SameHash, + SameMBID, + }; + + struct ScanError + { + std::filesystem::path file; + ScanErrorType error; + std::string systemError; + + ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = ""); + }; + + struct ScanDuplicate + { + std::filesystem::path file; + DuplicateReason reason; + + }; + + // reduced scan stats + struct ScanProgressStats + { + Wt::WDateTime startTime; + + std::size_t filesToScan {}; + std::size_t processedFiles {}; + + unsigned progress() const; + }; + + struct ScanStats + { + Wt::WDateTime startTime; + Wt::WDateTime stopTime; + + std::size_t filesToScan {}; // Total number of files to be scanned (estimated) + + std::size_t skips {}; // no change since last scan + std::size_t scans {}; // actually scanned filed + + std::size_t additions {}; // Added in DB + std::size_t deletions {}; // removed from DB + std::size_t updates {}; // updated file in DB + + std::vector errors; + std::vector duplicates; + + std::size_t nbFiles() const; + std::size_t nbChanges() const; + + ScanProgressStats toProgressStats() const; + }; + +} + diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index a535d33e..7e6fcb13 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -529,16 +529,16 @@ LmsApplication::createHome() // Events from MediaScanner { const std::string sessionId {LmsApp->sessionId()}; - getService()->scanComplete().connect(this, [=] (Scanner::MediaScanner::Stats stats) + getService()->scanComplete().connect(this, [=] () { Wt::WServer::instance()->post(sessionId, [=] { - _events.dbScanned.emit(stats); + _events.dbScanned.emit(); triggerUpdate(); }); }); - getService()->scanInProgress().connect(this, [=] (Scanner::MediaScanner::Stats stats) + getService()->scanInProgress().connect(this, [=] (Scanner::ScanProgressStats stats) { Wt::WServer::instance()->post(sessionId, [=] { @@ -558,17 +558,19 @@ LmsApplication::createHome() } - _events.dbScanned.connect([=] (Scanner::MediaScanner::Stats stats) + _events.dbScanned.connect([=] () { if (isUserAdmin()) { + const auto& stats {*getService()->getStatus().lastCompleteScanStats}; + notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-complete") .arg(static_cast(stats.nbFiles())) .arg(static_cast(stats.additions)) .arg(static_cast(stats.updates)) .arg(static_cast(stats.deletions)) - .arg(static_cast(stats.nbDuplicates())) - .arg(static_cast(stats.nbErrors()))); + .arg(static_cast(stats.duplicates.size())) + .arg(static_cast(stats.errors.size()))); } }); diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp index a677ff50..39c4744c 100644 --- a/src/ui/LmsApplication.hpp +++ b/src/ui/LmsApplication.hpp @@ -56,8 +56,8 @@ struct Events Wt::Signal<> trackUnloaded; // Database events - Wt::Signal dbScanned; - Wt::Signal dbScanInProgress; + Wt::Signal<> dbScanned; + Wt::Signal dbScanInProgress; Wt::Signal dbScanScheduled; }; diff --git a/src/ui/admin/DatabaseSettingsView.cpp b/src/ui/admin/DatabaseSettingsView.cpp index 441c6ead..6086eec7 100644 --- a/src/ui/admin/DatabaseSettingsView.cpp +++ b/src/ui/admin/DatabaseSettingsView.cpp @@ -19,14 +19,10 @@ #include "DatabaseSettingsView.hpp" -#include -#include - #include #include #include #include -#include #include #include @@ -38,6 +34,7 @@ #include "common/Validators.hpp" #include "common/ValueStringModel.hpp" +#include "DatabaseStatus.hpp" #include "LmsApplication.hpp" namespace UserInterface { @@ -178,85 +175,6 @@ 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([=] diff --git a/src/ui/admin/DatabaseStatus.cpp b/src/ui/admin/DatabaseStatus.cpp new file mode 100644 index 00000000..d67aab6f --- /dev/null +++ b/src/ui/admin/DatabaseStatus.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#include "DatabaseStatus.hpp" + +#include + +#include +#include +#include +#include + +#include "main/Service.hpp" +#include "scanner/MediaScanner.hpp" +#include "LmsApplication.hpp" + +namespace UserInterface { + +static +std::string +durationToString(const Wt::WDateTime& begin, const Wt::WDateTime& end) +{ + const 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 ReportResource : public Wt::WResource +{ + public: + + ReportResource(const Scanner::ScanStats& stats) + : _stats {stats} + { + suggestFileName("report.txt"); + } + + ~ReportResource() + { + beingDeleted(); + } + + void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response) + { + response.out() << Wt::WString::tr("Lms.Admin.Database.Status.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl; + + for (const auto& error : _stats.errors) + { + response.out() << error.file.string() << " - " << errorTypeToWString(error.error).toUTF8(); + if (!error.systemError.empty()) + response.out() << ": " << error.systemError; + response.out() << std::endl; + } + + response.out() << std::endl; + + response.out() << Wt::WString::tr("Lms.Admin.Database.Status.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl; + + for (const auto& duplicate : _stats.duplicates) + response.out() << duplicate.file.string() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << std::endl; + } + + private: + + static Wt::WString errorTypeToWString(Scanner::ScanErrorType error) + { + switch (error) + { + case Scanner::ScanErrorType::CannotReadFile: return Wt::WString::tr("Lms.Admin.Database.Status.cannot-read-file"); + case Scanner::ScanErrorType::CannotParseFile: return Wt::WString::tr("Lms.Admin.Database.Status.cannot-parse-file"); + case Scanner::ScanErrorType::NoAudioTrack: return Wt::WString::tr("Lms.Admin.Database.Status.no-audio-track"); + case Scanner::ScanErrorType::BadDuration: return Wt::WString::tr("Lms.Admin.Database.Status.bad-duration"); + } + return "?"; + } + + static Wt::WString duplicateReasonToWString(Scanner::DuplicateReason reason) + { + switch (reason) + { + case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.Database.Status.same-hash"); + case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.Database.Status.same-mbid"); + } + return "?"; + } + + Scanner::ScanStats _stats; +}; + + +DatabaseStatus::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(); +} + +void +DatabaseStatus::refreshContents() +{ + using namespace Scanner; + + Wt::WPushButton* reportBtn {bindNew("btn-report", Wt::WString::tr("Lms.Admin.Database.Status.get-report"))}; + + const MediaScanner::Status status {getService()->getStatus()}; + if (status.lastCompleteScanStats) + { + bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-status") + .arg(status.lastCompleteScanStats->nbFiles()) + .arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime)) + .arg(status.lastCompleteScanStats->stopTime.toString()) + .arg(status.lastCompleteScanStats->errors.size()) + .arg(status.lastCompleteScanStats->duplicates.size()) + ); + + Wt::WLink link {std::make_shared(*status.lastCompleteScanStats)}; + link.setTarget(Wt::LinkTarget::NewWindow); + reportBtn->setLink(link); + } + else + { + bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-not-available")); + reportBtn->setEnabled(false); + } + + + 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.inProgressScanStats->processedFiles) + .arg(status.inProgressScanStats->filesToScan) + .arg(status.inProgressScanStats->progress())); + } + break; + } + +} + +} // namespace UserInterface + diff --git a/src/ui/admin/DatabaseStatus.hpp b/src/ui/admin/DatabaseStatus.hpp new file mode 100644 index 00000000..d970ca36 --- /dev/null +++ b/src/ui/admin/DatabaseStatus.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#pragma once + +#include + +namespace UserInterface { + +class DatabaseStatus : public Wt::WTemplate +{ + public: + DatabaseStatus(); + + private: + void refreshContents(); +}; + +} // namespace DatabaseStatus +