Use local time, if available, in the web interface, fixes #427

This commit is contained in:
emeric
2026-06-07 17:37:38 +02:00
parent 17ca039087
commit 709a1509b6
10 changed files with 111 additions and 29 deletions
+2 -2
View File
@@ -129,7 +129,7 @@
<message id="Lms.Admin.ScannerController.get-report">Get report</message>
<message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} files were scanned in {2} at {4} on {3} (UTC) - {5} errors, {6} duplicates</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} files were scanned in {2} at {4} on {3} - {5} errors, {6} duplicates</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Missing artist name</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Unable to find track paths</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">Unable to find the path "{1}"</message>
@@ -141,7 +141,7 @@
<message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Status</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Not scheduled</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Scheduled at {2} on {1} (UTC)</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Scheduled at {2} on {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associating artist images: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associating external lyrics: {1}%...</message>
+2 -2
View File
@@ -129,7 +129,7 @@
<message id="Lms.Admin.ScannerController.get-report">Informe</message>
<message id="Lms.Admin.ScannerController.last-scan">Último escaneo</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">No disponible</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} ficheros analizados en {2} el {3} a las {4} (UTC) - {5} errores, {6} duplicados</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} ficheros analizados en {2} el {3} a las {4} - {5} errores, {6} duplicados</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Falta el nombre del artista</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">No se pudieron encontrar las rutas de las pistas</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">No se pudo encontrar la ruta "{1}"</message>
@@ -141,7 +141,7 @@
<message id="Lms.Admin.ScannerController.scanner">Escanear</message>
<message id="Lms.Admin.ScannerController.status">Estado</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">No progamado</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Programado el {1} a las {2} (UTC)</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Programado el {1} a las {2}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Escaneando: etapa {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
+2 -2
View File
@@ -129,7 +129,7 @@
<message id="Lms.Admin.ScannerController.get-report">Rapport</message>
<message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers analysés en {2} à {4} le {3} (UTC) - {5} erreurs, {6} duplicatas</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers analysés en {2} à {4} le {3} - {5} erreurs, {6} duplicatas</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Nom de l'artiste manquant</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossible de trouver les chemins des pistes</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">Impossible de trouver le chemin "{1}"</message>
@@ -141,7 +141,7 @@
<message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Statut</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non planifié</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Planifié à {2} le {1} (UTC)</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Planifié à {2} le {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
+2 -2
View File
@@ -129,7 +129,7 @@
<message id="Lms.Admin.ScannerController.get-report">Scarica un resoconto</message>
<message id="Lms.Admin.ScannerController.last-scan">Ultima scansione</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponibile</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} file scansionati in {2} alle {3} il {4} (UTC) - {5} errori, {6} duplicati</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} file scansionati in {2} alle {3} il {4} - {5} errori, {6} duplicati</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Nome dell'artista mancante</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossibile trovare i percorsi delle tracce</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">Impossibile trovare il percorso "{1}"</message>
@@ -141,7 +141,7 @@
<message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Stato</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non pianificato</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Programmato alle {2} del {1} (UTC)</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Programmato alle {2} del {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associando immagini degli artisti: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associazione dei testi esterni: {1}%...</message>
+2 -2
View File
@@ -138,7 +138,7 @@
<message id="Lms.Admin.ScannerController.get-report">Pokaż raport</message>
<message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message>
<message id="Lms.Admin.ScannerController.last-scan-status">Przeskanowano {1} plików w {2} o {4} dnia {3} (UTC) - {5} błędów, {6} duplikatów</message>
<message id="Lms.Admin.ScannerController.last-scan-status">Przeskanowano {1} plików w {2} o {4} dnia {3} - {5} błędów, {6} duplikatów</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Brakuje nazwy artysty</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Nie można znaleźć ścieżek utworów</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">Nie można znaleźć ścieżki „{1}”</message>
@@ -150,7 +150,7 @@
<message id="Lms.Admin.ScannerController.scanner">Skaner</message>
<message id="Lms.Admin.ScannerController.status">Status</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Nie zaplanowano</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Zaplanowano o {2} dnia {1} (UTC)</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Zaplanowano o {2} dnia {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Kojarzenie obrazów artystów: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Kojarzenie zewnętrznych tekstów: {1}%...</message>
+2 -2
View File
@@ -127,7 +127,7 @@
<message id="Lms.Admin.ScannerController.get-report">生成报告</message>
<message id="Lms.Admin.ScannerController.last-scan">最后扫描</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message>
<message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项</message>
<message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} - {5} 个错误,{6} 个重复项</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">缺少艺术家名称</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">无法找到曲目路径</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">无法找到路径 "{1}"</message>
@@ -139,7 +139,7 @@
<message id="Lms.Admin.ScannerController.scanner">扫描器</message>
<message id="Lms.Admin.ScannerController.status">状态</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">无计划</message>
<message id="Lms.Admin.ScannerController.status-scheduled">计划于 {2} 在 {1} (UTC)</message>
<message id="Lms.Admin.ScannerController.status-scheduled">计划于 {2} 在 {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">关联艺术家图片:{1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">关联外部歌词:{1}%...</message>
+36 -5
View File
@@ -19,15 +19,47 @@
#include "Logger.hpp"
#include <Wt/WDateTime.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <ctime>
#include <fstream>
#include <iostream>
#include <string_view>
#include <thread>
#include "core/Exception.hpp"
#include "core/String.hpp"
namespace
{
std::string localISO8601TimeString()
{
using Clock = std::chrono::system_clock;
const auto now{ Clock::now() };
const int ms{ static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() % 1000) };
const std::time_t tt{ Clock::to_time_t(now) };
std::tm tm{};
::localtime_r(&tt, &tm);
std::array<char, 24> datetime{};
std::array<char, 8> offset{};
std::strftime(datetime.data(), datetime.size(), "%Y-%m-%dT%H:%M:%S", &tm);
std::strftime(offset.data(), offset.size(), "%z", &tm); // "+HHMM" or "-HHMM"
// Append msecs and offset
const std::string_view offsetView{ offset.data() };
std::string result{ datetime.data() };
result += '.';
result += static_cast<char>('0' + ms / 100);
result += static_cast<char>('0' + (ms / 10) % 10);
result += static_cast<char>('0' + ms % 10);
result += offsetView.substr(0, 3); // sign + HH
result += ':';
result += offsetView.substr(3); // MM
return result;
}
} // namespace
namespace lms::core::logging
{
@@ -205,9 +237,8 @@ namespace lms::core::logging
{
assert(isSeverityActive(severity)); // should have been filtered out by a isSeverityActive call
OutputStream* outputStream{ _severityToOutputStreamMap.at(severity) };
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
std::unique_lock lock{ outputStream->mutex };
outputStream->stream << stringUtils::toISO8601String(now) << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl;
outputStream->stream << localISO8601TimeString() << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl;
}
} // namespace lms::core::logging
+18 -3
View File
@@ -19,6 +19,8 @@
#include "LmsApplication.hpp"
#include <Wt/WConfig.h>
#include <Wt/WAnchor.h>
#include <Wt/WEnvironment.h>
#include <Wt/WLineEdit.h>
@@ -117,7 +119,7 @@ namespace lms::ui
return res;
}
Wt::WLocale createLocale(const std::string& name)
Wt::WLocale createLocale(const std::string& name, [[maybe_unused]] const std::string& timeZoneName)
{
Wt::WLocale locale{ name };
locale.setDecimalPoint(Wt::WString::tr("Lms.locale.decimal-point").toUTF8());
@@ -126,6 +128,19 @@ namespace lms::ui
locale.setTimeFormat(Wt::WString::tr("Lms.locale.time-format").toUTF8());
locale.setDateTimeFormat(Wt::WString::tr("Lms.locale.date-time-format").toUTF8());
#ifdef WT_DATE_TZ_USE_STD
if (!timeZoneName.empty())
{
try
{
locale.setTimeZone(std::chrono::locate_zone(timeZoneName));
}
catch (const std::runtime_error&)
{
// unknown zone, display stays UTC
}
}
#endif
return locale;
}
@@ -224,7 +239,7 @@ namespace lms::ui
setTitle();
setLocalizedStrings(getOrCreateMessageBundle());
setLocale(createLocale(Wt::WLocale::currentLocale().name()));
setLocale(createLocale(Wt::WLocale::currentLocale().name(), environment().timeZoneName()));
// Handle Media Scanner events and other session events
enableUpdates(true);
@@ -325,7 +340,7 @@ namespace lms::ui
setUserInfo(userId, strongAuth);
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent() << ", locale = '" << locale().name() << "'");
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent() << ", locale = '" << locale().name() << "', timezone = '" << environment().timeZoneName() << "'");
_appManager.registerApplication(*this);
_appManager.applicationRegistered.connect(this, [this](LmsApplication& otherApplication) {
+39 -7
View File
@@ -21,6 +21,7 @@
#include <Wt/WCheckBox.h>
#include <Wt/WDateTime.h>
#include <Wt/WLocalDateTime.h>
#include <Wt/WLocale.h>
#include <Wt/WPushButton.h>
#include <Wt/WResource.h>
@@ -114,11 +115,26 @@ namespace lms::ui
{
if (status.lastCompleteScanStats)
{
const auto& locale{ Wt::WLocale::currentLocale() };
const Wt::WDateTime& stopTime{ status.lastCompleteScanStats->stopTime };
Wt::WString stopDateStr;
Wt::WString stopTimeStr;
if (locale.timeZone())
{
const Wt::WLocalDateTime local{ stopTime.toLocalTime() };
stopDateStr = local.date().toString(locale.dateFormat());
stopTimeStr = local.time().toString(locale.timeFormat());
}
else
{
stopDateStr = stopTime.date().toString(locale.dateFormat());
stopTimeStr = stopTime.time().toString(locale.timeFormat()) + " (UTC)";
}
_lastScanStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status")
.arg(status.lastCompleteScanStats->getTotalFileCount())
.arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime))
.arg(status.lastCompleteScanStats->stopTime.date().toString(Wt::WLocale::currentLocale().dateFormat()))
.arg(status.lastCompleteScanStats->stopTime.time().toString(Wt::WLocale::currentLocale().timeFormat()))
.arg(stopDateStr)
.arg(stopTimeStr)
.arg(status.lastCompleteScanStats->errorsCount)
.arg(status.lastCompleteScanStats->duplicates.size()));
@@ -144,11 +160,27 @@ namespace lms::ui
break;
case IScannerService::State::Scheduled:
_status->setText(Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled")
.arg(status.nextScheduledScan.date().toString(Wt::WLocale::currentLocale().dateFormat()))
.arg(status.nextScheduledScan.time().toString(Wt::WLocale::currentLocale().timeFormat())));
_stepStatus->setText("");
break;
{
const auto& locale{ Wt::WLocale::currentLocale() };
Wt::WString nextDateStr;
Wt::WString nextTimeStr;
if (locale.timeZone())
{
const Wt::WLocalDateTime local{ status.nextScheduledScan.toLocalTime() };
nextDateStr = local.date().toString(locale.dateFormat());
nextTimeStr = local.time().toString(locale.timeFormat());
}
else
{
nextDateStr = status.nextScheduledScan.date().toString(locale.dateFormat());
nextTimeStr = status.nextScheduledScan.time().toString(locale.timeFormat()) + " (UTC)";
}
_status->setText(Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled")
.arg(nextDateStr)
.arg(nextTimeStr));
_stepStatus->setText("");
break;
}
case IScannerService::State::InProgress:
assert(status.currentScanStepStats);
+5 -1
View File
@@ -22,6 +22,7 @@
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WLineEdit.h>
#include <Wt/WLocalDateTime.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplateFormView.h>
@@ -229,7 +230,10 @@ namespace lms::ui
t->bindString("title", title, Wt::TextFormat::Plain);
t->setCondition("if-has-last-login", true);
t->bindString("last-login", user->getLastLogin().toString(), Wt::TextFormat::Plain);
const auto& locale{ Wt::WLocale::currentLocale() };
const Wt::WDateTime lastLogin{ user->getLastLogin() };
const Wt::WString lastLoginStr{ locale.timeZone() ? lastLogin.toLocalTime().toString() : lastLogin.toString(locale.dateTimeFormat()) + " (UTC)" };
t->bindString("last-login", lastLoginStr, Wt::TextFormat::Plain);
}
else
{