diff --git a/approot/admin-database.xml b/approot/admin-database.xml index 4e01c81b..91e9347b 100644 --- a/approot/admin-database.xml +++ b/approot/admin-database.xml @@ -60,7 +60,7 @@
- ${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"} + ${apply-btn class="btn-primary"} ${discard-btn}
@@ -69,42 +69,7 @@
-
-
${tr:Lms.Admin.Database.Status.status}
-
- ${status} -
-
-
-
- - - -
-
- -
-
- ${status} -
-
-
-
- -
-
- ${last-scan} -
-
-
-
-
- ${btn-report class="btn"} -
+ ${scanner-controller}
diff --git a/approot/admin-scannercontroller.xml b/approot/admin-scannercontroller.xml new file mode 100644 index 00000000..234e298c --- /dev/null +++ b/approot/admin-scannercontroller.xml @@ -0,0 +1,44 @@ + + + + +
+
${tr:Lms.Admin.ScannerController.scanner}
+
+
+
+ +
+
+ ${status} +
+
+
+
+ +
+
+ ${last-scan} +
+
+
+
+
+ ${btn-report class="btn btn-xs"} +
+
+
+
+ ${btn-action} +
+
+
+
+
+
+ +
diff --git a/approot/messages.xml b/approot/messages.xml index ac96275f..eb64a2f4 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -54,22 +54,25 @@ Update start time Weekly -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, {5} duplicates) -No audio track -Duplicated file hash -Duplicated MBID -Status -Not scheduled -Scheduled on {1} -Scanning {1} of {2} files ({3} %) +Cannot get track duration +Cannot parse file +Cannot read file +{1} duplicate files: +{1} errors: +Force scan now +Get report +Last scan +Not available +Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates) +No audio track +Duplicated file hash +Duplicated MBID +Scan now +Scanner +Status +Not scheduled +Scheduled on {1} +Scanning {1} of {2} files ({3} %) New user diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index d28c0b35..fa3ffb37 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -54,22 +54,25 @@ Heure de départ de la mise à jour Toutes les semaines -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, {5} duplicatas) -Pas de piste audio -Hash dupliqué -MBID dupliqué -Statut -Non planifié -Planifié le {1} -Scan de {1} fichiers sur {2} ({3} %) +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 : +Lancer un scan forcé +Rapport +Dernier scan +Non disponible +{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas) +Pas de piste audio +Hash dupliqué +MBID dupliqué +Lancer un scan +Scanner +Statut +Non planifié +Planifié le {1} +Scan de {1} fichiers sur {2} ({3} %) Ajouter @@ -132,7 +135,7 @@ Artistes d'album -Albums similaires +Albums similaires Copyright Disque {1} diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index 0529ff4a..ccc83e34 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -257,49 +257,51 @@ MediaScanner::MediaScanner(Database::Db& db) } MediaScanner::~MediaScanner() -{ - if (_running) - stop(); -} - -void -MediaScanner::restart(void) { stop(); - start(); } void -MediaScanner::start(void) +MediaScanner::start() { - _running = true; - scheduleNextScan(); _ioService.start(); } void -MediaScanner::stop(void) +MediaScanner::stop() { - _running = false; + _abortScan = true; _scheduleTimer.cancel(); - _ioService.stop(); } void -MediaScanner::requestImmediateScan() +MediaScanner::abortScan() { + _abortScan = true; + + _scheduleTimer.cancel(); + _ioService.stop(); + + _abortScan = false; + _ioService.start(); +} + +void +MediaScanner::requestImmediateScan(bool force) +{ + abortScan(); _ioService.post([=]() { - scheduleScan(); + scheduleScan(force); }); } void -MediaScanner::requestReschedule() +MediaScanner::requestReload() { _ioService.post([=]() { @@ -308,7 +310,7 @@ MediaScanner::requestReschedule() } MediaScanner::Status -MediaScanner::getStatus() +MediaScanner::getStatus() const { Status res; @@ -365,7 +367,7 @@ MediaScanner::scheduleNextScan() if (nextScanDate.isValid()) { nextScanDateTime = Wt::WDateTime {nextScanDate, _startTime}; - scheduleScan(nextScanDateTime); + scheduleScan(false, nextScanDateTime); } { @@ -384,7 +386,7 @@ MediaScanner::countAllFiles(ScanStats& stats) exploreFilesRecursive(_mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) { - if (!_running) + if (_abortScan) return false; if (!ec && isFileSupported(path, _fileExtensions)) @@ -398,13 +400,21 @@ MediaScanner::countAllFiles(ScanStats& stats) } void -MediaScanner::scheduleScan(const Wt::WDateTime& dateTime) +MediaScanner::scheduleScan(bool force, const Wt::WDateTime& dateTime) { + auto cb {[=](boost::system::error_code ec) + { + if (ec) + return; + + scan(force); + }}; + 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.expires_from_now(std::chrono::seconds {0}); + _scheduleTimer.async_wait(cb); } else { @@ -413,16 +423,13 @@ MediaScanner::scheduleScan(const Wt::WDateTime& dateTime) 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)); + _scheduleTimer.async_wait(cb); } } void -MediaScanner::scan(boost::system::error_code err) +MediaScanner::scan(bool forceScan) { - if (err) - return; - scanStarted().emit(); { @@ -438,8 +445,6 @@ MediaScanner::scan(boost::system::error_code err) refreshScanSettings(); - bool forceScan {false}; - LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'..."; countAllFiles(stats); LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.filesToScan; @@ -454,17 +459,17 @@ MediaScanner::scan(boost::system::error_code err) removeOrphanEntries(); - if (_running) + if (!_abortScan) checkDuplicatedAudioFiles(stats); // Now update all the track features if needed fetchTrackFeatures(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.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size(); + LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size(); _dbSession.optimize(); - if (_running) + if (!_abortScan) { stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC(); { @@ -546,7 +551,7 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats) for (const TrackInfo& trackToFetch : tracksToFetch) { - if (!_running) + if (_abortScan) return; if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid)) @@ -774,7 +779,7 @@ MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bo { exploreFilesRecursive(mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) { - if (!_running) + if (_abortScan) return false; if (ec) @@ -861,7 +866,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats) for (const auto& [trackId, trackPath] : trackPaths) { - if (!_running) + if (_abortScan) return; if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) diff --git a/src/libs/scanner/impl/MediaScanner.hpp b/src/libs/scanner/impl/MediaScanner.hpp index 8c19defb..3a2c737e 100644 --- a/src/libs/scanner/impl/MediaScanner.hpp +++ b/src/libs/scanner/impl/MediaScanner.hpp @@ -52,12 +52,10 @@ class MediaScanner : public IMediaScanner void start() override; void stop() override; - void restart() override; + void requestReload() override; + void requestImmediateScan(bool force) override; - void requestImmediateScan() override; - void requestReschedule() override ; - - Status getStatus() override; + Status getStatus() const override; Wt::Signal<>& scanStarted() override { return _sigScanStarted; } Wt::Signal<>& scanComplete() override { return _sigScanComplete; } @@ -68,10 +66,12 @@ class MediaScanner : public IMediaScanner // Job handling void scheduleNextScan(); - void scheduleScan(const Wt::WDateTime& dateTime = {}); + void scheduleScan(bool force, const Wt::WDateTime& dateTime = {}); + + void abortScan(); // Update database (scheduled callback) - void scan(boost::system::error_code ec); + void scan(bool force); void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); bool fetchTrackFeatures(Database::IdType trackId, const UUID& MBID); @@ -89,7 +89,9 @@ class MediaScanner : public IMediaScanner void notifyInProgressIfNeeded(const ScanStats& stats); void notifyInProgress(const ScanStats& stats); - bool _running {}; + std::mutex _scanInProgress {}; + std::atomic _abortScan {}; + Wt::WIOService _ioService; boost::asio::system_timer _scheduleTimer {_ioService}; Wt::Signal<> _sigScanStarted; @@ -100,7 +102,7 @@ class MediaScanner : public IMediaScanner Database::Session _dbSession; std::unique_ptr _metadataParser; - std::shared_mutex _statusMutex; + mutable std::shared_mutex _statusMutex; State _curState {State::NotScheduled}; std::optional _lastCompleteScanStats; std::optional _inProgressScanStats; diff --git a/src/libs/scanner/include/scanner/IMediaScanner.hpp b/src/libs/scanner/include/scanner/IMediaScanner.hpp index 1c0d3fdc..c70358c4 100644 --- a/src/libs/scanner/include/scanner/IMediaScanner.hpp +++ b/src/libs/scanner/include/scanner/IMediaScanner.hpp @@ -40,11 +40,10 @@ class IMediaScanner virtual void start() = 0; virtual void stop() = 0; - virtual void restart() = 0; // Async requests - virtual void requestImmediateScan() = 0; - virtual void requestReschedule() = 0; + virtual void requestReload() = 0; + virtual void requestImmediateScan(bool force) = 0; enum class State @@ -56,13 +55,13 @@ class IMediaScanner struct Status { - State currentState {State::NotScheduled}; - Wt::WDateTime nextScheduledScan; - std::optional lastCompleteScanStats; + State currentState {State::NotScheduled}; + Wt::WDateTime nextScheduledScan; + std::optional lastCompleteScanStats; std::optional inProgressScanStats; }; - virtual Status getStatus() = 0; + virtual Status getStatus() const = 0; // Called just after scan start virtual Wt::Signal<>& scanStarted() = 0; diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 70b2deb0..46231758 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -54,7 +54,7 @@ namespace API::Subsonic::Scan Response handleStartScan(RequestContext& context) { - ServiceProvider::get()->requestImmediateScan(); + ServiceProvider::get()->requestImmediateScan(false); Response response {Response::createOkResponse(context)}; response.addNode("scanStatus", createStatusResponseNode()); diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index beb8ac4a..269986c3 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -10,7 +10,7 @@ add_executable(lms ui/SettingsView.cpp ui/TrackStringUtils.cpp ui/admin/DatabaseSettingsView.cpp - ui/admin/DatabaseStatus.cpp + ui/admin/ScannerController.cpp ui/admin/InitWizardView.cpp ui/admin/UserView.cpp ui/admin/UsersView.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 250ceaab..c9efc903 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -123,6 +123,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, // Add a resource bundle messageResourceBundle().use(appRoot() + "admin-database"); messageResourceBundle().use(appRoot() + "admin-initwizard"); + messageResourceBundle().use(appRoot() + "admin-scannercontroller"); messageResourceBundle().use(appRoot() + "admin-user"); messageResourceBundle().use(appRoot() + "admin-users"); messageResourceBundle().use(appRoot() + "artist"); diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index 5b612ff6..3b734728 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -34,7 +34,7 @@ #include "common/Validators.hpp" #include "common/ValueStringModel.hpp" -#include "DatabaseStatus.hpp" +#include "ScannerController.hpp" #include "LmsApplication.hpp" namespace UserInterface { @@ -220,7 +220,7 @@ 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"); + t->bindNew("scanner-controller"); saveBtn->clicked().connect([=] () { @@ -230,7 +230,7 @@ DatabaseSettingsView::refreshView() { model->saveData(); - ServiceProvider::get()->requestReschedule(); + ServiceProvider::get()->requestReload(); LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved")); } @@ -247,7 +247,7 @@ DatabaseSettingsView::refreshView() immScanBtn->clicked().connect([=] () { - ServiceProvider::get()->requestImmediateScan(); + ServiceProvider::get()->requestImmediateScan(false); }); t->updateView(model.get()); diff --git a/src/lms/ui/admin/DatabaseStatus.cpp b/src/lms/ui/admin/ScannerController.cpp similarity index 68% rename from src/lms/ui/admin/DatabaseStatus.cpp rename to src/lms/ui/admin/ScannerController.cpp index 1030c452..5bc6cb35 100644 --- a/src/lms/ui/admin/DatabaseStatus.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -17,14 +17,16 @@ * along with LMS. If not, see . */ -#include "DatabaseStatus.hpp" +#include "ScannerController.hpp" #include #include #include +#include #include #include +#include #include "utils/Service.hpp" #include "LmsApplication.hpp" @@ -66,7 +68,7 @@ class ReportResource : public Wt::WResource 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; + response.out() << Wt::WString::tr("Lms.Admin.ScannerController.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl; for (const auto& error : _stats.errors) { @@ -78,7 +80,7 @@ class ReportResource : public Wt::WResource response.out() << std::endl; - response.out() << Wt::WString::tr("Lms.Admin.Database.Status.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl; + response.out() << Wt::WString::tr("Lms.Admin.ScannerController.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; @@ -90,10 +92,10 @@ class ReportResource : public Wt::WResource { 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"); + case Scanner::ScanErrorType::CannotReadFile: return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file"); + case Scanner::ScanErrorType::CannotParseFile: return Wt::WString::tr("Lms.Admin.ScannerController.cannot-parse-file"); + case Scanner::ScanErrorType::NoAudioTrack: return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track"); + case Scanner::ScanErrorType::BadDuration: return Wt::WString::tr("Lms.Admin.ScannerController.bad-duration"); } return "?"; } @@ -102,8 +104,8 @@ class ReportResource : public Wt::WResource { 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"); + case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.ScannerController.same-hash"); + case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.ScannerController.same-mbid"); } return "?"; } @@ -112,8 +114,8 @@ class ReportResource : public Wt::WResource }; -DatabaseStatus::DatabaseStatus() -: WTemplate {Wt::WString::tr("Lms.Admin.Database.Status.template")} +ScannerController::ScannerController() +: WTemplate {Wt::WString::tr("Lms.Admin.ScannerController.template")} { addFunction("tr", &Wt::WTemplate::Functions::tr); @@ -133,16 +135,34 @@ DatabaseStatus::DatabaseStatus() } void -DatabaseStatus::refreshContents() +ScannerController::refreshContents() { using namespace Scanner; - Wt::WPushButton* reportBtn {bindNew("btn-report", Wt::WString::tr("Lms.Admin.Database.Status.get-report"))}; + Wt::WPushButton* reportBtn {bindNew("btn-report", Wt::WString::tr("Lms.Admin.ScannerController.get-report"))}; + + Wt::WSplitButton* actionBtn {bindNew("btn-action")}; + actionBtn->actionButton()->addStyleClass("btn-primary"); + actionBtn->actionButton()->setText(Wt::WString::tr("Lms.Admin.ScannerController.scan-now")); + actionBtn->actionButton()->clicked().connect([] + { + ServiceProvider::get()->requestImmediateScan(false); + }); + + auto popup = std::make_unique(); + popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now")); + popup->itemSelected().connect([] + { + ServiceProvider::get()->requestImmediateScan(true); + }); + actionBtn->dropDownButton()->setMenu(std::move(popup)); + actionBtn->dropDownButton()->addStyleClass("btn-primary"); + const IMediaScanner::Status status {ServiceProvider::get()->getStatus()}; if (status.lastCompleteScanStats) { - bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-status") + bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status") .arg(status.lastCompleteScanStats->nbFiles()) .arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime)) .arg(status.lastCompleteScanStats->stopTime.toString()) @@ -156,7 +176,7 @@ DatabaseStatus::refreshContents() } else { - bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-not-available")); + bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-not-available")); reportBtn->setEnabled(false); } @@ -164,16 +184,16 @@ DatabaseStatus::refreshContents() switch (status.currentState) { case IMediaScanner::State::NotScheduled: - bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-not-scheduled")); + bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled")); break; case IMediaScanner::State::Scheduled: - bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-scheduled") + bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled") .arg(status.nextScheduledScan.toString())); break; case IMediaScanner::State::InProgress: { std::ostringstream oss; - bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-in-progress") + bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress") .arg(status.inProgressScanStats->processedFiles) .arg(status.inProgressScanStats->filesToScan) .arg(status.inProgressScanStats->progress())); diff --git a/src/lms/ui/admin/DatabaseStatus.hpp b/src/lms/ui/admin/ScannerController.hpp similarity index 83% rename from src/lms/ui/admin/DatabaseStatus.hpp rename to src/lms/ui/admin/ScannerController.hpp index d970ca36..498d28fe 100644 --- a/src/lms/ui/admin/DatabaseStatus.hpp +++ b/src/lms/ui/admin/ScannerController.hpp @@ -21,16 +21,17 @@ #include -namespace UserInterface { - -class DatabaseStatus : public Wt::WTemplate +namespace UserInterface { - public: - DatabaseStatus(); - private: - void refreshContents(); -}; + class ScannerController : public Wt::WTemplate + { + public: + ScannerController(); + + private: + void refreshContents(); + }; } // namespace DatabaseStatus