Simplified logger configuration, it no longer depends on Wt

This commit is contained in:
emeric
2023-11-24 16:23:31 +01:00
parent 6f47c32ac2
commit 9060c0d925
104 changed files with 2603 additions and 2598 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ ffmpeg-file = "/usr/bin/ffmpeg";
# Log files, empty means stdout
log-file = "";
access-log-file = "";
# Logger configuration, see log-config in https://webtoolkit.eu/wt/doc/reference/html/overview.html#config_general
log-config = "* -debug -info:WebRequest";
# Minimum severity, can be "debug", "info", "warning", "error" or "fatal"
log-min-severity = "info";
# Output db queries on stdout
db-show-queries = false;
+6 -6
View File
@@ -31,7 +31,7 @@ extern "C"
#include <map>
#include <unordered_map>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
namespace Av
@@ -105,14 +105,14 @@ namespace Av
int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) };
if (error < 0)
{
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
LMS_LOG(AV, ERROR, "Cannot open " << _p.string() << ": " << averror_to_string(error));
throw AudioFileException{ error };
}
error = avformat_find_stream_info(_context, nullptr);
if (error < 0)
{
LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error));
avformat_close_input(&_context);
throw AudioFileException{ error };
}
@@ -233,7 +233,7 @@ namespace Av
if (avstream->codecpar == nullptr)
{
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
LMS_LOG(AV, ERROR, "Skipping stream " << i << " since no codecpar is set");
continue;
}
@@ -247,7 +247,7 @@ namespace Av
else
{
picture.mimeType = "application/octet-stream";
LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion";
LMS_LOG(AV, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
}
const AVPacket& pkt{ avstream->attached_pic };
@@ -271,7 +271,7 @@ namespace Av
if (!avstream->codecpar)
{
LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
LMS_LOG(AV, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
return res;
}
+5 -5
View File
@@ -25,13 +25,13 @@
#include "utils/IChildProcessManager.hpp"
#include "utils/IConfig.hpp"
#include "utils/Path.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
namespace Av::Transcoding
{
#define LOG(sev) LMS_LOG(TRANSCODING, sev) << "[" << _debugId << "] - "
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
static std::atomic<size_t> globalId{};
static std::filesystem::path ffmpegPath;
@@ -84,7 +84,7 @@ namespace Av::Transcoding
throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
}
LOG(INFO) << "Transcoding file '" << _inputParameters.trackPath.string() << "'";
LOG(INFO, "Transcoding file '" << _inputParameters.trackPath.string() << "'");
std::vector<std::string> args;
@@ -176,9 +176,9 @@ namespace Av::Transcoding
args.emplace_back("pipe:1");
LOG(DEBUG) << "Dumping args (" << args.size() << ")";
LOG(DEBUG, "Dumping args (" << args.size() << ")");
for (const std::string& arg : args)
LOG(DEBUG) << "Arg = '" << arg << "'";
LOG(DEBUG, "Arg = '" << arg << "'");
// Caution: stdin must have been closed before
try
@@ -18,7 +18,7 @@
*/
#include "TranscodingResourceHandler.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Av::Transcoding
{
@@ -43,9 +43,9 @@ namespace Av::Transcoding
, _transcoder{ inputParameters, outputParameters }
{
if (_estimatedContentLength)
LMS_LOG(TRANSCODING, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength);
else
LMS_LOG(TRANSCODING, DEBUG) << "Not using estimated content length";
LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length");
}
Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
@@ -53,11 +53,11 @@ namespace Av::Transcoding
if (_estimatedContentLength)
response.setContentLength(*_estimatedContentLength);
response.setMimeType(_transcoder.getOutputMimeType());
LMS_LOG(TRANSCODING, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
LMS_LOG(TRANSCODING, DEBUG, "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType());
if (_bytesReadyCount > 0)
{
LMS_LOG(TRANSCODING, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
LMS_LOG(TRANSCODING, DEBUG, "Writing " << _bytesReadyCount << " bytes back to client");
response.out().write(reinterpret_cast<const char*>(&_buffer[0]), _bytesReadyCount);
_totalServedByteCount += _bytesReadyCount;
@@ -70,7 +70,7 @@ namespace Av::Transcoding
continuation->waitForMoreData();
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
{
LMS_LOG(TRANSCODING, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
LMS_LOG(TRANSCODING, DEBUG, "Have " << nbBytesRead << " more bytes to send back");
assert(_bytesReadyCount == 0);
_bytesReadyCount = nbBytesRead;
@@ -86,7 +86,7 @@ namespace Av::Transcoding
{
const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
LMS_LOG(TRANSCODING, DEBUG) << "Adding " << padSize << " padding bytes";
LMS_LOG(TRANSCODING, DEBUG, "Adding " << padSize << " padding bytes");
for (std::size_t i{}; i < padSize; ++i)
response.out().put(0);
@@ -94,7 +94,7 @@ namespace Av::Transcoding
_totalServedByteCount += padSize;
}
LMS_LOG(TRANSCODING, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
LMS_LOG(TRANSCODING, DEBUG, "Transcoding finished. Total served byte count = " << _totalServedByteCount);
}
return {};
@@ -21,7 +21,7 @@
#include "RawImage.hpp"
#include "image/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Image::GraphicsMagick
{
@@ -36,7 +36,7 @@ namespace Image::GraphicsMagick
}
catch (Magick::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
throw ImageException {std::string {"Magick read error: "} + e.what()};
}
}
+13 -13
View File
@@ -23,7 +23,7 @@
#include "JPEGImage.hpp"
#include "image/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Image
{
@@ -43,16 +43,16 @@ namespace Image
Magick::InitializeMagick(path.string().c_str());
if (auto nbThreads {MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource)}; nbThreads != 1)
LMS_LOG(COVER, WARNING) << "Consider setting env var OMP_NUM_THREADS=1 to save resources";
LMS_LOG(COVER, WARNING, "Consider setting env var OMP_NUM_THREADS=1 to save resources");
if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1))
LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 1!";
LMS_LOG(COVER, ERROR, "Cannot set Magick thread resource limit to 1!");
if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0))
LMS_LOG(COVER, ERROR) << "Cannot set Magick disk resource limit to 0!";
LMS_LOG(COVER, ERROR, "Cannot set Magick disk resource limit to 0!");
LMS_LOG(COVER, INFO) << "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource);
LMS_LOG(COVER, INFO) << "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource);
LMS_LOG(COVER, INFO, "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource));
LMS_LOG(COVER, INFO, "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource));
}
}
@@ -68,16 +68,16 @@ RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
}
catch (Magick::WarningCoder& e)
{
LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder: " << e.what();
LMS_LOG(COVER, WARNING, "Caught Magick WarningCoder: " << e.what());
}
catch (Magick::Warning& e)
{
LMS_LOG(COVER, WARNING) << "Caught Magick warning: " << e.what();
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
throw ImageException {std::string {"Magick read warning: "} + e.what()};
}
catch (Magick::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
throw ImageException {std::string {"Magick read error: "} + e.what()};
}
}
@@ -90,16 +90,16 @@ RawImage::RawImage(const std::filesystem::path& p)
}
catch (Magick::WarningCoder& e)
{
LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder: " << e.what();
LMS_LOG(COVER, WARNING, "Caught Magick WarningCoder: " << e.what());
}
catch (Magick::Warning& e)
{
LMS_LOG(COVER, WARNING) << "Caught Magick warning: " << e.what();
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
throw ImageException {std::string {"Magick read warning: "} + e.what()};
}
catch (Magick::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
throw ImageException {std::string {"Magick read error: "} + e.what()};
}
}
@@ -113,7 +113,7 @@ RawImage::resize(ImageSize width)
}
catch (Magick::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Caught Magick exception while resizing: " << e.what();
LMS_LOG(COVER, ERROR, "Caught Magick exception while resizing: " << e.what());
throw ImageException {std::string {"Magick resize error: "} + e.what()};
}
}
+1 -1
View File
@@ -23,7 +23,7 @@
#include <iostream>
#include "av/IAudioFile.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "Utils.hpp"
+3 -3
View File
@@ -20,7 +20,7 @@
#include "metadata/IParser.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "AvFormatParser.hpp"
#include "TagLibParser.hpp"
@@ -34,10 +34,10 @@ namespace MetaData
switch (parserType)
{
case ParserType::TagLib:
LMS_LOG(METADATA, INFO) << "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle);
LMS_LOG(METADATA, INFO, "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle));
return std::make_unique<TagLibParser>(parserReadStyle);
case ParserType::AvFormat:
LMS_LOG(METADATA, INFO) << "Creating AvFormat parser";
LMS_LOG(METADATA, INFO, "Creating AvFormat parser");
return std::make_unique<AvFormatParser>();
}
+3 -3
View File
@@ -37,7 +37,7 @@
#include "utils/IConfig.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
#include "Utils.hpp"
@@ -391,7 +391,7 @@ namespace MetaData
if (f.isNull())
{
LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed";
LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed");
return std::nullopt;
}
@@ -404,7 +404,7 @@ namespace MetaData
}
else
{
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
LMS_LOG(METADATA, INFO, "File '" << p.string() << "': no audio properties");
return std::nullopt;
}
@@ -22,7 +22,7 @@
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
@@ -43,7 +43,7 @@ namespace Auth
{
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN);
LMS_LOG(AUTH, DEBUG, "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN));
user = session.create<User>(loginName);
user.modify()->setType(type);
@@ -28,7 +28,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
@@ -62,7 +62,7 @@ namespace Auth
Database::AuthToken::pointer authToken {session.create<Database::AuthToken>(secretHash, expiry, user)};
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString());
if (user->getAuthTokensCount() >= 50)
Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
@@ -88,7 +88,7 @@ namespace Auth
return std::nullopt;
}
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!");
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser()->getId(), authToken->getExpiry()};
authToken.remove();
@@ -21,7 +21,7 @@
#include "LoginThrottler.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
namespace Auth {
@@ -81,10 +81,10 @@ LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
attemptInfo.badConsecutiveAttemptCount += 1;
LMS_LOG(AUTH, DEBUG) << "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount;
LMS_LOG(AUTH, DEBUG, "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount);
if (attemptInfo.badConsecutiveAttemptCount >= _maxBadConsecutiveAttemptCount)
{
LMS_LOG(AUTH, DEBUG) << "Throttling '" << clientAddress.to_string() << "'";
LMS_LOG(AUTH, DEBUG, "Throttling '" << clientAddress.to_string() << "'");
attemptInfo.nextAttempt = now.addMSecs(std::chrono::duration_cast<std::chrono::milliseconds>(_throttlingDuration).count());
}
else
@@ -31,7 +31,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
@@ -61,7 +61,7 @@ namespace Auth
PasswordServiceBase::CheckResult
PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
{
LMS_LOG(AUTH, DEBUG) << "Checking password for user '" << loginName << "'";
LMS_LOG(AUTH, DEBUG, "Checking password for user '" << loginName << "'");
// Do not waste too much resource on brute force attacks (optim)
{
@@ -22,46 +22,41 @@
#include <Wt/WEnvironment.h>
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
namespace Auth
{
HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db)
: AuthServiceBase{ db }
, _fieldName{ Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User") }
{
LMS_LOG(AUTH, INFO, "Using http header field = '" << _fieldName << "'");
}
HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db)
: AuthServiceBase {db}
, _fieldName {Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User")}
{
LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'";
}
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
{
const std::string loginName{ env.headerValue(_fieldName) };
if (loginName.empty())
return { CheckResult::State::Denied };
HttpHeadersEnvService::CheckResult
HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
{
const std::string loginName {env.headerValue(_fieldName)};
if (loginName.empty())
return {CheckResult::State::Denied};
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
const Database::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId };
}
const Database::UserId userId {getOrCreateUser(loginName)};
onUserAuthenticated(userId);
return {CheckResult::State::Granted, userId};
}
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
{
const std::string loginName{ request.headerValue(_fieldName) };
if (loginName.empty())
return { CheckResult::State::Denied };
HttpHeadersEnvService::CheckResult
HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
{
const std::string loginName {request.headerValue(_fieldName)};
if (loginName.empty())
return {CheckResult::State::Denied};
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
const Database::UserId userId {getOrCreateUser(loginName)};
onUserAuthenticated(userId);
return {CheckResult::State::Granted, userId};
}
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
const Database::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId };
}
} // namespace Auth
@@ -25,7 +25,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
@@ -44,7 +44,7 @@ namespace Auth
bool
InternalPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
LMS_LOG(AUTH, DEBUG, "Checking internal password for user '" << loginName << "'");
Database::User::PasswordHash passwordHash;
{
@@ -54,7 +54,7 @@ namespace Auth
const Database::User::pointer user {Database::User::find(session, loginName)};
if (!user)
{
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
LMS_LOG(AUTH, DEBUG, "hashing random stuff");
// hash random stuff here to waste some time
hashRandomPassword();
return false;
@@ -28,175 +28,173 @@
#include "services/auth/Types.hpp"
#include "services/database/Session.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
class PAMError
{
public:
PAMError(std::string_view msg, pam_handle_t *pamh, int err)
{
_errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err);
}
namespace
{
class PAMError
{
public:
PAMError(std::string_view msg, pam_handle_t* pamh, int err)
{
_errorMsg = std::string{ msg } + ": " + pam_strerror(pamh, err);
}
std::string_view message() const { return _errorMsg; }
std::string_view message() const { return _errorMsg; }
private:
std::string _errorMsg;
};
private:
std::string _errorMsg;
};
class PAMContext
{
public:
PAMContext(std::string_view loginName)
{
int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)};
if (err != PAM_SUCCESS)
throw PAMError {"start failed", _pamh, err};
}
class PAMContext
{
public:
PAMContext(std::string_view loginName)
{
int err{ pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh) };
if (err != PAM_SUCCESS)
throw PAMError{ "start failed", _pamh, err };
}
~PAMContext()
{
int err {pam_end(_pamh, 0)};
if (err != PAM_SUCCESS)
LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err);
}
~PAMContext()
{
int err{ pam_end(_pamh, 0) };
if (err != PAM_SUCCESS)
LMS_LOG(AUTH, ERROR, "end failed: " << pam_strerror(_pamh, err));
}
void authenticate(std::string_view password)
{
AuthenticateConvContext authContext {password};
ScopedConvContextSetter scopedContext {*this, authContext};
void authenticate(std::string_view password)
{
AuthenticateConvContext authContext{ password };
ScopedConvContextSetter scopedContext{ *this, authContext };
int err {pam_authenticate(_pamh, 0)};
if (err != PAM_SUCCESS)
throw PAMError {"authenticate failed", _pamh, err};
}
int err{ pam_authenticate(_pamh, 0) };
if (err != PAM_SUCCESS)
throw PAMError{ "authenticate failed", _pamh, err };
}
void validateAccount()
{
int err {pam_acct_mgmt(_pamh, PAM_SILENT)};
if (err != PAM_SUCCESS)
throw PAMError {"acct_mgmt failed", _pamh, err};
}
void validateAccount()
{
int err{ pam_acct_mgmt(_pamh, PAM_SILENT) };
if (err != PAM_SUCCESS)
throw PAMError{ "acct_mgmt failed", _pamh, err };
}
private:
class ConvContext
{
public:
virtual ~ConvContext() = default;
};
private:
class ConvContext
{
public:
virtual ~ConvContext() = default;
};
class AuthenticateConvContext final : public ConvContext
{
public:
AuthenticateConvContext(std::string_view password) : _password {password} {}
class AuthenticateConvContext final : public ConvContext
{
public:
AuthenticateConvContext(std::string_view password) : _password{ password } {}
std::string_view getPassword() const { return _password; }
std::string_view getPassword() const { return _password; }
private:
std::string_view _password;
};
private:
std::string_view _password;
};
class ScopedConvContextSetter
{
public:
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
: _pamContext {pamContext}
{
_pamContext._convContext = &convContext;
}
class ScopedConvContextSetter
{
public:
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
: _pamContext{ pamContext }
{
_pamContext._convContext = &convContext;
}
~ScopedConvContextSetter()
{
_pamContext._convContext = nullptr;
}
~ScopedConvContextSetter()
{
_pamContext._convContext = nullptr;
}
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
private:
PAMContext& _pamContext;
};
private:
PAMContext& _pamContext;
};
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
{
if (msgCount < 1)
return PAM_CONV_ERR;
if (!resps || !msgs || !userData)
return PAM_CONV_ERR;
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
{
if (msgCount < 1)
return PAM_CONV_ERR;
if (!resps || !msgs || !userData)
return PAM_CONV_ERR;
PAMContext& context{ *static_cast<PAMContext*>(userData) };
PAMContext& context {*static_cast<PAMContext*>(userData)};
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
if (!authenticateContext)
{
LMS_LOG(AUTH, ERROR, "Unexpected conv!");
return PAM_CONV_ERR;
}
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
if (!authenticateContext)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv!";
return PAM_CONV_ERR;
}
// Only expect a PAM_PROMPT_ECHO_OFF msg
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
{
LMS_LOG(AUTH, ERROR, "Unexpected conv message. Count = " << msgCount);
return PAM_CONV_ERR;
}
// Only expect a PAM_PROMPT_ECHO_OFF msg
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount;
return PAM_CONV_ERR;
}
pam_response* response{ static_cast<pam_response*>(malloc(sizeof(pam_response))) };
if (!response)
return PAM_CONV_ERR;
pam_response* response {static_cast<pam_response*>(malloc(sizeof(pam_response)))};
if (!response)
return PAM_CONV_ERR;
response->resp = strdup(std::string{ authenticateContext->getPassword() }.c_str());
response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str());
*resps = response;
return PAM_SUCCESS;
}
*resps = response;
return PAM_SUCCESS;
}
ConvContext* _convContext{};
pam_conv _conv{ &PAMContext::conv, this };
pam_handle_t* _pamh{};
};
}
ConvContext* _convContext {};
pam_conv _conv {&PAMContext::conv, this};
pam_handle_t *_pamh {};
};
bool PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
try
{
LMS_LOG(AUTH, DEBUG, "Checking PAM password for user '" << loginName << "'");
PAMContext pamContext{ loginName };
bool
PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
try
{
LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'";
PAMContext pamContext {loginName};
pamContext.authenticate(password);
pamContext.validateAccount();
pamContext.authenticate(password);
pamContext.validateAccount();
return true;
}
catch (const PAMError& error)
{
LMS_LOG(AUTH, ERROR, "PAM error: " << error.message());
return false;
}
}
return true;
}
catch (const PAMError& error)
{
LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message();
return false;
}
}
bool PAMPasswordService::canSetPasswords() const
{
return false;
}
bool
PAMPasswordService::canSetPasswords() const
{
return false;
}
IPasswordService::PasswordAcceptabilityResult PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
{
throw NotImplementedException{};
}
IPasswordService::PasswordAcceptabilityResult
PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
{
throw NotImplementedException {};
}
void
PAMPasswordService::setPassword(Database::UserId, std::string_view)
{
throw NotImplementedException {};
}
void PAMPasswordService::setPassword(Database::UserId, std::string_view)
{
throw NotImplementedException{};
}
} // namespace Auth
+12 -12
View File
@@ -29,7 +29,7 @@
#include "image/Exception.hpp"
#include "image/IRawImage.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
#include "utils/String.hpp"
#include "utils/Utils.hpp"
@@ -110,10 +110,10 @@ namespace Cover
{
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
LMS_LOG(COVER, INFO, "Default cover path = '" << _defaultCoverPath.string() << "'");
LMS_LOG(COVER, INFO, "Max cache size = " << _maxCacheSize);
LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize);
LMS_LOG(COVER, INFO, "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ","));
#if LMS_SUPPORT_IMAGE_GM
GraphicsMagick::init(execPath);
@@ -148,7 +148,7 @@ namespace Cover
}
catch (const Image::ImageException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
}
});
@@ -167,7 +167,7 @@ namespace Cover
}
catch (const ImageException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
LMS_LOG(COVER, ERROR, "Cannot read cover in file '" << p.string() << "': " << e.what());
}
return image;
@@ -190,7 +190,7 @@ namespace Cover
std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
_defaultCoverCache[width] = image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
LMS_LOG(COVER, DEBUG, "Default cache entries = " << _defaultCoverCache.size());
return image;
}
@@ -269,7 +269,7 @@ namespace Cover
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
{
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
LMS_LOG(COVER, INFO, "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize);
return false;
}
@@ -306,7 +306,7 @@ namespace Cover
}
catch (Av::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p.string() << ": " << e.what());
}
return image;
@@ -404,7 +404,7 @@ namespace Cover
{
std::unique_lock lock{ _cacheMutex };
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
LMS_LOG(COVER, DEBUG, "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize);
_cacheHits = 0;
_cacheMisses = 0;
_cacheSize = 0;
@@ -415,7 +415,7 @@ namespace Cover
{
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
}
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
+1 -1
View File
@@ -25,7 +25,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
#include "EnumSetTraits.hpp"
+1 -1
View File
@@ -157,7 +157,7 @@ namespace Database
{
}
ClusterType::pointer ClusterType::create(Session& session, const std::string& name)
ClusterType::pointer ClusterType::create(Session& session, std::string_view name)
{
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
}
+6 -6
View File
@@ -26,7 +26,7 @@
#include "services/database/User.hpp"
#include "utils/IConfig.hpp"
#include "utils/Service.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Database
{
@@ -65,18 +65,18 @@ namespace Database
void prepare()
{
LMS_LOG(DB, DEBUG) << "Setting per-connection settings...";
LMS_LOG(DB, DEBUG, "Setting per-connection settings...");
executeSql("pragma journal_mode=WAL");
executeSql("pragma synchronous=normal");
executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries
LMS_LOG(DB, DEBUG) << "Setting per-connection settings done!";
LMS_LOG(DB, DEBUG, "Setting per-connection settings done!");
}
void optimize()
{
LMS_LOG(DB, DEBUG) << "connection close: Running pragma optimize...";
LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize...");
executeSql("pragma optimize");
LMS_LOG(DB, DEBUG) << "connection close: pragma optimize complete";
LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete");
}
std::filesystem::path _dbPath;
@@ -86,7 +86,7 @@ namespace Database
// Session living class handling the database and the login
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
{
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath.string());
auto connection{ std::make_unique<Connection>(dbPath.string()) };
if (IConfig * config{ Service<IConfig>::get() })// may not be here on testU
@@ -26,7 +26,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Database
{
@@ -282,11 +282,11 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
try
{
version = VersionInfo::getOrCreate(session)->getVersion();
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
LMS_LOG(DB, INFO, "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION);
}
catch (std::exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
LMS_LOG(DB, ERROR, "Cannot get database version info: " << e.what());
throw LmsException{ outdatedMsg };
}
@@ -298,7 +298,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
while (version < LMS_DATABASE_VERSION)
{
LMS_LOG(DB, INFO) << "Migrating database from version " << version << " to " << version + 1 << "...";
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
auto itMigrationFunc{ migrationFunctions.find(version) };
assert(itMigrationFunc != std::cend(migrationFunctions));
@@ -306,7 +306,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
VersionInfo::get(session).modify()->setVersion(++version);
LMS_LOG(DB, INFO) << "Migration complete to version " << version;
LMS_LOG(DB, INFO, "Migration complete to version " << version);
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "SqlQuery.hpp"
#include "EnumSetTraits.hpp"
#include "IdTypeTraits.hpp"
@@ -22,131 +22,123 @@
#include <Wt/Dbo/WtSqlTraits.h>
#include "utils/Path.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "services/database/Cluster.hpp"
#include "services/database/Session.hpp"
namespace {
const std::set<std::string> defaultClusterTypeNames =
namespace Database
{
"GENRE",
"ALBUMGROUPING",
"MOOD",
"ALBUMMOOD",
};
namespace
{
}
const std::set<std::string_view> defaultClusterTypeNames =
{
"GENRE",
"ALBUMGROUPING",
"MOOD",
"ALBUMMOOD",
};
namespace Database {
}
void ScanSettings::init(Session& session)
{
session.checkWriteTransaction();
void
ScanSettings::init(Session& session)
{
session.checkWriteTransaction();
pointer settings{ get(session) };
if (settings)
return;
pointer settings {get(session)};
if (settings)
return;
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
settings.modify()->setClusterTypes(session, defaultClusterTypeNames);
}
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
settings.modify()->setClusterTypes(session, defaultClusterTypeNames );
}
ScanSettings::pointer ScanSettings::get(Session& session)
{
session.checkReadTransaction();
ScanSettings::pointer
ScanSettings::get(Session& session)
{
session.checkReadTransaction();
return session.getDboSession().find<ScanSettings>().resultValue();
}
return session.getDboSession().find<ScanSettings>().resultValue();
}
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
{
const auto extensions{ StringUtils::splitString(_audioFileExtensions, " ") };
std::vector<std::filesystem::path>
ScanSettings::getAudioFileExtensions() const
{
const auto extensions {StringUtils::splitString(_audioFileExtensions, " ")};
std::vector<std::filesystem::path> res(std::cbegin(extensions), std::cend(extensions));
std::sort(std::begin(res), std::end(res));
res.erase(std::unique(std::begin(res), std::end(res)), std::end(res));
std::vector<std::filesystem::path> res (std::cbegin(extensions), std::cend(extensions));
std::sort(std::begin(res), std::end(res));
res.erase(std::unique( std::begin(res), std::end(res)), std::end(res));
return res;
}
return res;
}
void ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
{
_audioFileExtensions += " " + ext.string();
}
void
ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
{
_audioFileExtensions += " " + ext.string();
}
std::vector<ClusterType::pointer> ScanSettings::getClusterTypes() const
{
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
}
std::vector<ClusterType::pointer>
ScanSettings::getClusterTypes() const
{
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
}
void ScanSettings::setMediaDirectory(const std::filesystem::path& p)
{
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
}
void
ScanSettings::setMediaDirectory(const std::filesystem::path& p)
{
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
}
template <typename It>
std::set<std::string> getNames(It begin, It end)
{
std::set<std::string> names;
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
[](const ClusterType::pointer& clusterType)
{
return clusterType->getName();
});
template <typename It>
std::set<std::string> getNames(It begin, It end)
{
std::set<std::string> names;
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
[](const ClusterType::pointer& clusterType)
{
return clusterType->getName();
});
return names;
}
return names;
}
void ScanSettings::setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames)
{
session.checkWriteTransaction();
void
ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames)
{
session.checkWriteTransaction();
bool needRescan{};
bool needRescan {};
// Create any missing cluster type
for (const std::string_view clusterTypeName : clusterTypeNames)
{
ClusterType::pointer clusterType{ ClusterType::find(session, clusterTypeName) };
if (!clusterType)
{
LMS_LOG(DB, INFO, "Creating cluster type " << clusterTypeName);
clusterType = session.create<ClusterType>(clusterTypeName);
_clusterTypes.insert(getDboPtr(clusterType));
// Create any missing cluster type
for (const std::string& clusterTypeName : clusterTypeNames)
{
ClusterType::pointer clusterType {ClusterType::find(session, clusterTypeName)};
if (!clusterType)
{
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
clusterType = session.create<ClusterType>(clusterTypeName);
_clusterTypes.insert(getDboPtr(clusterType));
needRescan = true;
}
}
needRescan = true;
}
}
// Delete no longer existing cluster types
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
{
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
[clusterType](std::string_view name) { return name == clusterType->getName(); }))
{
LMS_LOG(DB, INFO, "Deleting cluster type " << clusterType->getName());
clusterType.remove();
}
}
// Delete no longer existing cluster types
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
{
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
[clusterType](const std::string& name) { return name == clusterType->getName(); }))
{
LMS_LOG(DB, INFO) << "Deleting cluster type " << clusterType->getName();
clusterType.remove();
}
}
if (needRescan)
_scanVersion += 1;
}
if (needRescan)
_scanVersion += 1;
}
void
ScanSettings::incScanVersion()
{
_scanVersion += 1;
}
void
ScanSettings::incScanVersion()
{
_scanVersion += 1;
}
} // namespace Database
+9 -9
View File
@@ -22,7 +22,7 @@
#include <cassert>
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "services/database/Artist.hpp"
#include "services/database/AuthToken.hpp"
@@ -107,20 +107,20 @@ namespace Database
void Session::prepareTables()
{
LMS_LOG(DB, INFO) << "Preparing tables...";
LMS_LOG(DB, INFO, "Preparing tables...");
// Initial creation case
try
{
_session.createTables();
LMS_LOG(DB, INFO) << "Tables created";
LMS_LOG(DB, INFO, "Tables created");
}
catch (Wt::Dbo::Exception& e)
{
LMS_LOG(DB, DEBUG) << "Cannot create tables: " << e.what();
LMS_LOG(DB, DEBUG, "Cannot create tables: " << e.what());
if (std::string_view{ e.what() }.find("already exists") == std::string_view::npos)
{
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
LMS_LOG(DB, ERROR, "Cannot create tables: " << e.what());
throw e;
}
}
@@ -182,22 +182,22 @@ namespace Database
void Session::analyze()
{
LMS_LOG(DB, INFO) << "Analyzing database...";
LMS_LOG(DB, INFO, "Analyzing database...");
{
auto transaction{ createWriteTransaction() };
_session.execute("ANALYZE");
}
LMS_LOG(DB, INFO) << "Database Analyze complete";
LMS_LOG(DB, INFO, "Database Analyze complete");
}
void Session::optimize()
{
LMS_LOG(DB, INFO) << "Optimizing database...";
LMS_LOG(DB, INFO, "Optimizing database...");
{
auto transaction{ createWriteTransaction() };
_session.execute("PRAGMA optimize");
}
LMS_LOG(DB, INFO) << "Database optimizing complete";
LMS_LOG(DB, INFO, "Database optimizing complete");
}
} // namespace Database
+2 -2
View File
@@ -28,7 +28,7 @@
#include "services/database/TrackFeatures.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
@@ -536,7 +536,7 @@ namespace Database
for (auto artist : track->getArtists({ TrackArtistLinkType::Artist }))
os << " - " << artist->getName();
for (auto cluster : track->getClusters())
os << " {" + cluster->getType()->getName() << "-" << cluster->getName() << "}";
os << " {" << cluster->getType()->getName() << "-" << cluster->getName() << "}";
}
else
{
@@ -24,7 +24,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
@@ -111,7 +111,7 @@ namespace Database {
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
LMS_LOG(DB, ERROR, "Track " << _track.id() << ": ptree exception: " << error.what());
res.clear();
}
@@ -20,7 +20,7 @@
#include <cassert>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Cluster.hpp"
+1 -1
View File
@@ -23,7 +23,7 @@
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
@@ -126,7 +126,7 @@ namespace Database {
static void remove(Session& session, const std::string& name);
// Accessors
const std::string& getName() const { return _name; }
std::string_view getName() const { return _name; }
std::vector<Cluster::pointer> getClusters() const;
Cluster::pointer getCluster(const std::string& name) const;
@@ -141,7 +141,7 @@ namespace Database {
private:
friend class Session;
ClusterType(std::string_view name);
static pointer create(Session& session, const std::string& name);
static pointer create(Session& session, std::string_view name);
static const std::size_t _maxNameLength = 128;
@@ -20,6 +20,8 @@
#pragma once
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include <Wt/Dbo/Dbo.h>
@@ -74,7 +76,7 @@ namespace Database {
void setMediaDirectory(const std::filesystem::path& p);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
void setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames);
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
void incScanVersion();
@@ -29,7 +29,7 @@
#include "services/database/StarredTrack.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
@@ -44,15 +44,15 @@ namespace Feedback
FeedbackService::FeedbackService(boost::asio::io_context& ioContext, Db& db)
: _db{ db }
{
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
LMS_LOG(SCROBBLING, INFO, "Starting service...");
_backends.emplace(Database::FeedbackBackend::Internal, std::make_unique<InternalBackend>(_db));
_backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
LMS_LOG(SCROBBLING, INFO) << "Service started!";
LMS_LOG(SCROBBLING, INFO, "Service started!");
}
FeedbackService::~FeedbackService()
{
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
LMS_LOG(SCROBBLING, INFO, "Service stopped!");
}
std::optional<Database::FeedbackBackend> FeedbackService::getUserFeedbackBackend(UserId userId)
@@ -57,7 +57,7 @@ namespace Feedback::ListenBrainz
const Wt::Json::Array& feedbacks = root.get("feedback");
LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks";
LOG(DEBUG, "Got " << feedbacks.size() << " feedbacks");
if (feedbacks.empty())
return res;
@@ -72,17 +72,17 @@ namespace Feedback::ListenBrainz
}
catch (const Exception& e)
{
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
LOG(DEBUG, "Cannot parse feedback: " << e.what() << ", skipping");
}
catch (const Wt::WException& e)
{
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
LOG(DEBUG, "Cannot parse feedback: " << e.what() << ", skipping");
}
}
}
catch (const Wt::WException& error)
{
LOG(ERROR) << "Cannot parse 'feedback' result: " << error.what();
LOG(ERROR, "Cannot parse 'feedback' result: " << error.what());
}
return res;
@@ -53,7 +53,7 @@ namespace Feedback::ListenBrainz
}
catch (const Wt::WException& e)
{
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
LOG(ERROR, "Cannot parse listen count response: " << e.what());
return std::nullopt;
}
}
@@ -66,7 +66,7 @@ namespace Feedback::ListenBrainz
, _maxSyncFeedbackCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000) }
, _syncFeedbacksPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1) }
{
LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours";
LOG(INFO, "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours");
scheduleSync(std::chrono::seconds{ 30 });
}
@@ -95,7 +95,7 @@ namespace Feedback::ListenBrainz
case FeedbackType::Erase:
if (!recordingMBID)
{
LOG(DEBUG) << "Track has no recording MBID: erasing star";
LOG(DEBUG, "Track has no recording MBID: erasing star");
starredTrack.remove();
}
else
@@ -112,7 +112,7 @@ namespace Feedback::ListenBrainz
if (!recordingMBID)
{
LOG(DEBUG) << "Track has no recording MBID: skipping";
LOG(DEBUG, "Track has no recording MBID: skipping");
return;
}
@@ -142,7 +142,7 @@ namespace Feedback::ListenBrainz
}
catch (Exception& e)
{
LOG(DEBUG) << "Cannot send feedback: " << e.what();
LOG(DEBUG, "Cannot send feedback: " << e.what());
}
}
@@ -156,7 +156,7 @@ namespace Feedback::ListenBrainz
Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) };
if (!starredTrack)
{
LOG(DEBUG) << "Starred track not found. deleted?";
LOG(DEBUG, "Starred track not found. deleted?");
return;
}
@@ -166,23 +166,23 @@ namespace Feedback::ListenBrainz
{
case FeedbackType::Love:
starredTrack.modify()->setSyncState(Database::SyncState::Synchronized);
LOG(DEBUG) << "State set to synchronized";
LOG(DEBUG, "State set to synchronized");
if (userContext.feedbackCount)
{
(*userContext.feedbackCount)++;
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
LOG(DEBUG, "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'");
}
break;
case FeedbackType::Erase:
starredTrack.remove();
LOG(DEBUG) << "Removed starred track";
LOG(DEBUG, "Removed starred track");
if (userContext.feedbackCount && *userContext.feedbackCount > 0)
{
(*userContext.feedbackCount)--;
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
LOG(DEBUG, "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'");
}
break;
@@ -211,7 +211,7 @@ namespace Feedback::ListenBrainz
pendingFeedbacks = StarredTrack::find(session, params);
}
LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks";
LOG(DEBUG, "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks");
for (const StarredTrackId starredTrackId : pendingFeedbacks.results)
enqueFeedback(feedbackType, starredTrackId);
@@ -247,13 +247,13 @@ namespace Feedback::ListenBrainz
if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0)
return;
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
LOG(DEBUG, "Scheduled sync in " << fromNow.count() << " seconds...");
_syncTimer.expires_after(fromNow);
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG) << "getFeedbacks aborted";
LOG(DEBUG, "getFeedbacks aborted");
return;
}
else if (ec)
@@ -267,7 +267,7 @@ namespace Feedback::ListenBrainz
void FeedbacksSynchronizer::startSync()
{
LOG(DEBUG) << "Starting sync!";
LOG(DEBUG, "Starting sync!");
assert(!isSyncing());
assert(_strand.running_in_this_thread());
@@ -303,7 +303,7 @@ namespace Feedback::ListenBrainz
{
_strand.dispatch([this, &context]
{
LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount;
LOG(INFO, "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount);
context.syncing = false;
if (!isSyncing())
@@ -356,11 +356,11 @@ namespace Feedback::ListenBrainz
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context]
{
LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'";
LOG(DEBUG, "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'");
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
if (totalFeedbackCount)
LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount;
LOG(DEBUG, "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount);
bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) };
context.feedbackCount = totalFeedbackCount;
@@ -416,7 +416,7 @@ namespace Feedback::ListenBrainz
{
const FeedbacksParser::Result parseResult{ FeedbacksParser::parse(msgBody) };
LOG(DEBUG) << "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries";
LOG(DEBUG, "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries");
context.fetchedFeedbackCount += parseResult.feedbackCount;
for (const Feedback& feedback : parseResult.feedbacks)
@@ -441,12 +441,12 @@ namespace Feedback::ListenBrainz
const std::vector<Track::pointer> tracks{ Track::findByRecordingMBID(session, feedback.recordingMBID) };
if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found";
LOG(DEBUG, "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found");
return;
}
else if (tracks.empty())
{
LOG(DEBUG) << "Cannot match feedback '" << feedback << "': no track found for this recording MBID";
LOG(DEBUG, "Cannot match feedback '" << feedback << "': no track found for this recording MBID");
return;
}
@@ -461,7 +461,7 @@ namespace Feedback::ListenBrainz
if (needImport)
{
LOG(DEBUG) << "Importing feedback '" << feedback << "'";
LOG(DEBUG, "Importing feedback '" << feedback << "'");
auto transaction{ session.createWriteTransaction() };
@@ -481,7 +481,7 @@ namespace Feedback::ListenBrainz
}
else
{
LOG(DEBUG) << "No need to import feedback '" << feedback << "', already imported";
LOG(DEBUG, "No need to import feedback '" << feedback << "', already imported");
context.matchedFeedbackCount++;
}
}
@@ -26,7 +26,7 @@
#include "services/database/Track.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "Utils.hpp"
@@ -63,12 +63,12 @@ namespace Feedback::ListenBrainz
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
, _feedbacksSynchronizer{ _ioContext, db, *_client }
{
LOG(INFO) << "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'";
LOG(INFO, "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'");
}
ListenBrainzBackend::~ListenBrainzBackend()
{
LOG(INFO) << "Stopped ListenBrainz feedback backend!";
LOG(INFO, "Stopped ListenBrainz feedback backend!");
}
void ListenBrainzBackend::onStarred(Database::StarredArtistId starredArtistId)
@@ -46,13 +46,13 @@ namespace Feedback::ListenBrainz::Utils
Wt::Json::Object root;
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
{
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
LOG(ERROR, "Cannot parse 'validate-token' result: " << error.what());
return listenBrainzUserName;
}
if (!root.get("valid").orIfNull(false))
{
LOG(INFO) << "Invalid listenbrainz user";
LOG(INFO, "Invalid listenbrainz user");
return listenBrainzUserName;
}
@@ -20,10 +20,10 @@
#pragma once
#include "services/database/UserId.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/UUID.hpp"
#define LOG(sev) LMS_LOG(FEEDBACK, sev) << "[listenbrainz] "
#define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message)
namespace Database
{
@@ -26,7 +26,7 @@
#include "playlist-constraints/ConsecutiveArtists.hpp"
#include "playlist-constraints/ConsecutiveReleases.hpp"
#include "playlist-constraints/DuplicateTracks.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Recommendation
{
@@ -48,7 +48,7 @@ namespace Recommendation
std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
LMS_LOG(RECOMMENDATION, DEBUG, "Requested to extend playlist by " << maxCount << " similar tracks");
// supposed to be ordered from most similar to least similar
std::vector<TrackId> similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints
@@ -29,7 +29,7 @@
#include "services/database/Session.hpp"
#include "services/database/ScanSettings.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Recommendation
{
@@ -30,397 +30,384 @@
#include "services/database/TrackFeatures.hpp"
#include "services/database/TrackList.hpp"
#include "som/DataNormalizer.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
namespace Recommendation {
using namespace Database;
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
namespace Recommendation
{
return std::make_unique<FeaturesEngine>(db);
}
const FeatureSettingsMap&
FeaturesEngine::getDefaultTrainFeatureSettings()
{
static const FeatureSettingsMap defaultTrainFeatureSettings
{
{ "lowlevel.spectral_energyband_high.mean", {1}},
{ "lowlevel.spectral_rolloff.median", {1}},
{ "lowlevel.spectral_contrast_valleys.var", {1}},
{ "lowlevel.erbbands.mean", {1}},
{ "lowlevel.gfcc.mean", {1}},
};
return defaultTrainFeatureSettings;
}
static
std::optional<SOM::InputVector>
convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
{
std::size_t i {};
std::optional<SOM::InputVector> res {SOM::InputVector {nbDimensions}};
for (const auto& [featureName, values] : featureValuesMap)
{
if (values.size() != getFeatureDef(featureName).nbDimensions)
{
LMS_LOG(RECOMMENDATION, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size();
res.reset();
break;
}
for (double val : values)
(*res)[i++] = val;
}
return res;
}
static
SOM::InputVector
getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
{
SOM::InputVector weights {nbDimensions};
std::size_t index {};
for (const auto& [featureName, featureSettings] : featureSettingsMap)
{
const std::size_t featureNbDimensions {getFeatureDef(featureName).nbDimensions};
for (std::size_t i {}; i < featureNbDimensions; ++i)
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
}
assert(index == nbDimensions);
return weights;
}
void
FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
{
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
std::unordered_set<FeatureName> featureNames;
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
const std::size_t nbDimensions {std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; })};
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
Session& session {_db.getTLSSession()};
RangeResults<TrackFeaturesId> trackFeaturesIds;
{
auto transaction {session.createReadTransaction()};
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features...";
trackFeaturesIds = TrackFeatures::find(session);
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)";
}
std::vector<SOM::InputVector> samples;
std::vector<TrackId> samplesTrackIds;
samples.reserve(trackFeaturesIds.results.size());
samplesTrackIds.reserve(trackFeaturesIds.results.size());
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
// TODO handle errors using exceptions
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
{
if (_loadCancelled)
return;
auto transaction {session.createReadTransaction()};
TrackFeatures::pointer trackFeatures {TrackFeatures::find(session, trackFeaturesId)};
if (!trackFeatures)
continue;
FeatureValuesMap featureValuesMap {trackFeatures->getFeatureValuesMap(featureNames)};
if (featureValuesMap.empty())
continue;
std::optional<SOM::InputVector> inputVector {convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions)};
if (!inputVector)
continue;
samples.emplace_back(std::move(*inputVector));
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
}
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE";
if (samples.empty())
{
LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
return;
}
LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
SOM::DataNormalizer dataNormalizer {nbDimensions};
dataNormalizer.computeNormalizationFactors(samples);
for (auto& sample : samples)
dataNormalizer.normalizeData(sample);
using namespace Database;
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
{
return std::make_unique<FeaturesEngine>(db);
}
namespace
{
std::optional<SOM::InputVector> convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
{
std::size_t i{};
std::optional<SOM::InputVector> res{ SOM::InputVector {nbDimensions} };
for (const auto& [featureName, values] : featureValuesMap)
{
if (values.size() != getFeatureDef(featureName).nbDimensions)
{
LMS_LOG(RECOMMENDATION, WARNING, "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size());
res.reset();
break;
}
for (double val : values)
(*res)[i++] = val;
}
return res;
}
SOM::InputVector getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
{
SOM::InputVector weights{ nbDimensions };
std::size_t index{};
for (const auto& [featureName, featureSettings] : featureSettingsMap)
{
const std::size_t featureNbDimensions{ getFeatureDef(featureName).nbDimensions };
for (std::size_t i{}; i < featureNbDimensions; ++i)
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
}
assert(index == nbDimensions);
return weights;
}
}
const FeatureSettingsMap& FeaturesEngine::getDefaultTrainFeatureSettings()
{
static const FeatureSettingsMap defaultTrainFeatureSettings
{
{ "lowlevel.spectral_energyband_high.mean", {1}},
{ "lowlevel.spectral_rolloff.median", {1}},
{ "lowlevel.spectral_contrast_valleys.var", {1}},
{ "lowlevel.erbbands.mean", {1}},
{ "lowlevel.gfcc.mean", {1}},
};
return defaultTrainFeatureSettings;
}
void FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
{
LMS_LOG(RECOMMENDATION, INFO, "Constructing features classifier...");
std::unordered_set<FeatureName> featureNames;
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
const std::size_t nbDimensions{ std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; }) };
LMS_LOG(RECOMMENDATION, DEBUG, "Features dimension = " << nbDimensions);
Session & session{ _db.getTLSSession() };
RangeResults<TrackFeaturesId> trackFeaturesIds;
{
auto transaction{ session.createReadTransaction() };
LMS_LOG(RECOMMENDATION, DEBUG, "Getting Track features...");
trackFeaturesIds = TrackFeatures::find(session);
LMS_LOG(RECOMMENDATION, DEBUG, "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)");
}
std::vector<SOM::InputVector> samples;
std::vector<TrackId> samplesTrackIds;
samples.reserve(trackFeaturesIds.results.size());
samplesTrackIds.reserve(trackFeaturesIds.results.size());
LMS_LOG(RECOMMENDATION, DEBUG, "Extracting features...");
// TODO handle errors using exceptions
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
{
if (_loadCancelled)
return;
auto transaction{ session.createReadTransaction() };
TrackFeatures::pointer trackFeatures{ TrackFeatures::find(session, trackFeaturesId) };
if (!trackFeatures)
continue;
FeatureValuesMap featureValuesMap{ trackFeatures->getFeatureValuesMap(featureNames) };
if (featureValuesMap.empty())
continue;
std::optional<SOM::InputVector> inputVector{ convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions) };
if (!inputVector)
continue;
samples.emplace_back(std::move(*inputVector));
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
}
LMS_LOG(RECOMMENDATION, DEBUG, "Extracting features DONE");
if (samples.empty())
{
LMS_LOG(RECOMMENDATION, INFO, "Nothing to classify!");
return;
}
SOM::Coordinate size {static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))};
if (size < 2)
{
LMS_LOG(RECOMMENDATION, WARNING) << "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors";
size = 2;
}
LMS_LOG(RECOMMENDATION, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network";
LMS_LOG(RECOMMENDATION, DEBUG, "Normalizing data...");
SOM::DataNormalizer dataNormalizer{ nbDimensions };
SOM::Network network {size, size, nbDimensions};
SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)};
network.setDataWeights(weights);
auto somProgressCallback{[&](const SOM::Network::CurrentIteration& iter)
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount;
progressCallback(Progress {iter.idIteration, iter.iterationCount});
}};
dataNormalizer.computeNormalizationFactors(samples);
for (auto& sample : samples)
dataNormalizer.normalizeData(sample);
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network...";
network.train(samples, trainSettings.iterationCount,
progressCallback ? somProgressCallback : SOM::Network::ProgressCallback {},
[this] { return _loadCancelled; });
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
SOM::Coordinate size{ static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron)) };
if (size < 2)
{
LMS_LOG(RECOMMENDATION, WARNING, "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors");
size = 2;
}
LMS_LOG(RECOMMENDATION, INFO, "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network");
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
TrackPositions trackPositions;
for (std::size_t i {}; i < samples.size(); ++i)
{
if (_loadCancelled)
return;
SOM::Network network{ size, size, nbDimensions };
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
SOM::InputVector weights{ getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions) };
network.setDataWeights(weights);
trackPositions[samplesTrackIds[i]].push_back(position);
}
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
auto somProgressCallback{ [&](const SOM::Network::CurrentIteration& iter)
{
LMS_LOG(RECOMMENDATION, DEBUG, "Current pass = " << iter.idIteration << " / " << iter.iterationCount);
progressCallback(Progress {iter.idIteration, iter.iterationCount});
} };
LMS_LOG(RECOMMENDATION, DEBUG, "Training network...");
network.train(samples, trainSettings.iterationCount,
progressCallback ? somProgressCallback : SOM::Network::ProgressCallback{},
[this] { return _loadCancelled; });
LMS_LOG(RECOMMENDATION, DEBUG, "Training network DONE");
load(std::move(network), std::move(trackPositions));
}
LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks...");
TrackPositions trackPositions;
for (std::size_t i{}; i < samples.size(); ++i)
{
if (_loadCancelled)
return;
void
FeaturesEngine::loadFromCache(FeaturesEngineCache&& cache)
{
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache...";
load(std::move(cache._network), cache._trackPositions);
}
TrackContainer
FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
{
const TrackContainer trackIds {[&]
{
TrackContainer res;
Session& session {_db.getTLSSession()};
auto transaction {session.createReadTransaction()};
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
if (trackList)
res = trackList->getTrackIds();
return res;
}()};
return findSimilarTracks(trackIds, maxCount);
}
TrackContainer
FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
{
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
Session& session {_db.getTLSSession()};
{
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
auto transaction {session.createReadTransaction()};
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
[&](TrackId trackId)
{
return !Track::exists(session, trackId);
}), std::end(similarTrackIds));
}
return similarTrackIds;
}
ReleaseContainer
FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
{
auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
Session& session {_db.getTLSSession()};
if (!similarReleaseIds.empty())
{
// Report only existing ids
auto transaction {session.createReadTransaction()};
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
[&](ReleaseId releaseId)
{
return !Release::exists(session, releaseId);
}), std::end(similarReleaseIds));
}
return similarReleaseIds;
}
ArtistContainer
FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
{
auto getSimilarArtistIdsForLinkType {[&] (TrackArtistLinkType linkType)
{
ArtistContainer similarArtistIds;
const auto itArtists {_artistMatrix.find(linkType)};
if (itArtists == std::cend(_artistMatrix))
{
return similarArtistIds;
}
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
}};
std::unordered_set<ArtistId> similarArtistIds;
for (TrackArtistLinkType linkType : linkTypes)
{
const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)};
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
}
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
Session& session {_db.getTLSSession()};
{
// Report only existing ids
auto transaction {session.createReadTransaction()};
res.erase(std::remove_if(std::begin(res), std::end(res),
[&](ArtistId artistId)
{
return !Artist::exists(session, artistId);
}), std::end(res));
}
while (res.size() > maxCount)
res.erase(Random::pickRandom(res));
return res;
}
FeaturesEngineCache
FeaturesEngine::toCache() const
{
return FeaturesEngineCache {*_network, _trackPositions};
}
void
FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
{
if (forceReload)
{
FeaturesEngineCache::invalidate();
}
else if (std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()})
{
loadFromCache(std::move(*cache));
return;
}
TrainSettings trainSettings;
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
loadFromTraining(trainSettings, progressCallback);
if (!_loadCancelled && _network)
toCache().write();
}
void
FeaturesEngine::requestCancelLoad()
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation";
_loadCancelled = true;
}
void
FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
{
using namespace Database;
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
LMS_LOG(RECOMMENDATION, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian;
const SOM::Coordinate width {network.getWidth()};
const SOM::Coordinate height {network.getHeight()};
_releaseMatrix = ReleaseMatrix {width, height};
_trackMatrix = TrackMatrix {width, height};
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
Session& session {_db.getTLSSession()};
for (const auto& [trackId, positions] : trackPositions)
{
if (_loadCancelled)
return;
auto transaction {session.createReadTransaction()};
const Track::pointer track {Track::find(session, trackId)};
if (!track)
continue;
for (const SOM::Position& position : positions)
{
Utils::push_back_if_not_present(_trackPositions[trackId], position);
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
if (Release::pointer release {track->getRelease()})
{
const ReleaseId releaseId {release->getId()};
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
}
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
{
const ArtistId artistId {artistLink->getArtist()->getId()};
Utils::push_back_if_not_present(_artistPositions[artistId], position);
auto itArtists {_artistMatrix.find(artistLink->getType())};
if (itArtists == std::cend(_artistMatrix))
{
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix {width, height});
assert(inserted);
itArtists = it;
}
Utils::push_back_if_not_present(itArtists->second[position], artistId);
}
}
}
_network = std::make_unique<SOM::Network>(network);
LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully loaded!";
}
const SOM::Position position{ network.getClosestRefVectorPosition(samples[i]) };
trackPositions[samplesTrackIds[i]].push_back(position);
}
LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks DONE");
load(std::move(network), std::move(trackPositions));
}
void FeaturesEngine::loadFromCache(FeaturesEngineCache&& cache)
{
LMS_LOG(RECOMMENDATION, INFO, "Constructing features classifier from cache...");
load(std::move(cache._network), cache._trackPositions);
}
TrackContainer FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
{
const TrackContainer trackIds{ [&]
{
TrackContainer res;
Session& session {_db.getTLSSession()};
auto transaction {session.createReadTransaction()};
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
if (trackList)
res = trackList->getTrackIds();
return res;
}() };
return findSimilarTracks(trackIds, maxCount);
}
TrackContainer FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
{
auto similarTrackIds{ getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount) };
Session& session{ _db.getTLSSession() };
{
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
auto transaction{ session.createReadTransaction() };
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
[&](TrackId trackId)
{
return !Track::exists(session, trackId);
}), std::end(similarTrackIds));
}
return similarTrackIds;
}
ReleaseContainer FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
{
auto similarReleaseIds{ getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount) };
Session& session{ _db.getTLSSession() };
if (!similarReleaseIds.empty())
{
// Report only existing ids
auto transaction{ session.createReadTransaction() };
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
[&](ReleaseId releaseId)
{
return !Release::exists(session, releaseId);
}), std::end(similarReleaseIds));
}
return similarReleaseIds;
}
ArtistContainer FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
{
auto getSimilarArtistIdsForLinkType{ [&](TrackArtistLinkType linkType)
{
ArtistContainer similarArtistIds;
const auto itArtists {_artistMatrix.find(linkType)};
if (itArtists == std::cend(_artistMatrix))
{
return similarArtistIds;
}
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
} };
std::unordered_set<ArtistId> similarArtistIds;
for (TrackArtistLinkType linkType : linkTypes)
{
const auto similarArtistIdsForLinkType{ getSimilarArtistIdsForLinkType(linkType) };
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
}
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
Session& session{ _db.getTLSSession() };
{
// Report only existing ids
auto transaction{ session.createReadTransaction() };
res.erase(std::remove_if(std::begin(res), std::end(res),
[&](ArtistId artistId)
{
return !Artist::exists(session, artistId);
}), std::end(res));
}
while (res.size() > maxCount)
res.erase(Random::pickRandom(res));
return res;
}
FeaturesEngineCache FeaturesEngine::toCache() const
{
return FeaturesEngineCache{ *_network, _trackPositions };
}
void FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
{
if (forceReload)
{
FeaturesEngineCache::invalidate();
}
else if (std::optional<FeaturesEngineCache> cache{ FeaturesEngineCache::read() })
{
loadFromCache(std::move(*cache));
return;
}
TrainSettings trainSettings;
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
loadFromTraining(trainSettings, progressCallback);
if (!_loadCancelled && _network)
toCache().write();
}
void FeaturesEngine::requestCancelLoad()
{
LMS_LOG(RECOMMENDATION, DEBUG, "Requesting init cancellation");
_loadCancelled = true;
}
void FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
{
using namespace Database;
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
LMS_LOG(RECOMMENDATION, DEBUG, "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian);
const SOM::Coordinate width{ network.getWidth() };
const SOM::Coordinate height{ network.getHeight() };
_releaseMatrix = ReleaseMatrix{ width, height };
_trackMatrix = TrackMatrix{ width, height };
LMS_LOG(RECOMMENDATION, DEBUG, "Constructing maps...");
Session & session{ _db.getTLSSession() };
for (const auto& [trackId, positions] : trackPositions)
{
if (_loadCancelled)
return;
auto transaction{ session.createReadTransaction() };
const Track::pointer track{ Track::find(session, trackId) };
if (!track)
continue;
for (const SOM::Position& position : positions)
{
Utils::push_back_if_not_present(_trackPositions[trackId], position);
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
if (Release::pointer release{ track->getRelease() })
{
const ReleaseId releaseId{ release->getId() };
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
}
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
{
const ArtistId artistId{ artistLink->getArtist()->getId() };
Utils::push_back_if_not_present(_artistPositions[artistId], position);
auto itArtists{ _artistMatrix.find(artistLink->getType()) };
if (itArtists == std::cend(_artistMatrix))
{
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix{ width, height });
assert(inserted);
itArtists = it;
}
Utils::push_back_if_not_present(itArtists->second[position], artistId);
}
}
}
_network = std::make_unique<SOM::Network>(network);
LMS_LOG(RECOMMENDATION, INFO, "Classifier successfully loaded!");
}
} // ns Recommendation
@@ -23,233 +23,226 @@
#include <boost/property_tree/xml_parser.hpp>
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
namespace Recommendation {
static
std::filesystem::path getCacheDirectory()
namespace Recommendation
{
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
}
namespace
{
std::filesystem::path getCacheDirectory()
{
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
}
static std::filesystem::path getCacheNetworkFilePath()
{
return getCacheDirectory() / "network";
}
std::filesystem::path getCacheNetworkFilePath()
{
return getCacheDirectory() / "network";
}
static std::filesystem::path getCacheTrackPositionsFilePath()
{
return getCacheDirectory() / "track_positions";
}
std::filesystem::path getCacheTrackPositionsFilePath()
{
return getCacheDirectory() / "track_positions";
}
static
bool
networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
{
try
{
boost::property_tree::ptree root;
bool networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
{
try
{
boost::property_tree::ptree root;
root.put("width", network.getWidth());
root.put("height", network.getHeight());
root.put("dim_count", network.getInputDimCount());
root.put("width", network.getWidth());
root.put("height", network.getHeight());
root.put("dim_count", network.getInputDimCount());
for (SOM::InputVector::value_type weight : network.getDataWeights())
root.add("weights.weight", weight);
for (SOM::InputVector::value_type weight : network.getDataWeights())
root.add("weights.weight", weight);
for (SOM::Coordinate x = 0; x < network.getWidth(); ++x)
{
for (SOM::Coordinate y = 0; y < network.getWidth(); ++y)
{
const auto& refVector = network.getRefVector({x, y});
for (SOM::Coordinate x = 0; x < network.getWidth(); ++x)
{
for (SOM::Coordinate y = 0; y < network.getWidth(); ++y)
{
const auto& refVector = network.getRefVector({ x, y });
boost::property_tree::ptree node;
for (auto value : refVector)
node.add("values.value", value);
boost::property_tree::ptree node;
for (auto value : refVector)
node.add("values.value", value);
node.put("coord_x", x);
node.put("coord_y", y);
node.put("coord_x", x);
node.put("coord_y", y);
root.add_child("ref_vectors.ref_vector", node);
}
}
root.add_child("ref_vectors.ref_vector", node);
}
}
boost::property_tree::write_xml(path.string(), root);
boost::property_tree::write_xml(path.string(), root);
LMS_LOG(RECOMMENDATION, DEBUG) << "Created network cache";
return true;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create network cache: " << error.what();
return false;
}
}
LMS_LOG(RECOMMENDATION, DEBUG, "Created network cache");
return true;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot create network cache: " << error.what());
return false;
}
}
}
std::optional<SOM::Network>
FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
{
if (!std::filesystem::exists(path))
return std::nullopt;
std::optional<SOM::Network> FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
{
if (!std::filesystem::exists(path))
return std::nullopt;
try
{
LMS_LOG(RECOMMENDATION, INFO) << "Reading network from cache...";
try
{
LMS_LOG(RECOMMENDATION, INFO, "Reading network from cache...");
boost::property_tree::ptree root;
boost::property_tree::ptree root;
boost::property_tree::read_xml(path.string(), root);
boost::property_tree::read_xml(path.string(), root);
SOM::Coordinate width {root.get<SOM::Coordinate>("width")};
SOM::Coordinate height {root.get<SOM::Coordinate>("height")};
std::size_t dimCount {root.get<std::size_t>("dim_count")};
SOM::Coordinate width{ root.get<SOM::Coordinate>("width") };
SOM::Coordinate height{ root.get<SOM::Coordinate>("height") };
std::size_t dimCount{ root.get<std::size_t>("dim_count") };
SOM::Network res {width, height, dimCount};
SOM::Network res{ width, height, dimCount };
{
SOM::InputVector weights {dimCount};
std::size_t i {};
for (const auto& val : root.get_child("weights"))
weights[i++] = val.second.get_value<double>();
{
SOM::InputVector weights{ dimCount };
std::size_t i{};
for (const auto& val : root.get_child("weights"))
weights[i++] = val.second.get_value<double>();
res.setDataWeights(weights);
}
res.setDataWeights(weights);
}
for (const auto& node : root.get_child("ref_vectors"))
{
SOM::Coordinate x {node.second.get<SOM::Coordinate>("coord_x")};
SOM::Coordinate y {node.second.get<SOM::Coordinate>("coord_y")};
for (const auto& node : root.get_child("ref_vectors"))
{
SOM::Coordinate x{ node.second.get<SOM::Coordinate>("coord_x") };
SOM::Coordinate y{ node.second.get<SOM::Coordinate>("coord_y") };
SOM::InputVector refVector {dimCount};
std::size_t i {};
for (const auto& val : node.second.get_child("values"))
refVector[i++] = val.second.get_value<SOM::InputVector::value_type>();
SOM::InputVector refVector{ dimCount };
std::size_t i{};
for (const auto& val : node.second.get_child("values"))
refVector[i++] = val.second.get_value<SOM::InputVector::value_type>();
res.setRefVector({x, y}, refVector);
}
res.setRefVector({ x, y }, refVector);
}
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read network from cache";
LMS_LOG(RECOMMENDATION, INFO, "Successfully read network from cache");
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot read network cache: " << error.what();
return std::nullopt;
}
}
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot read network cache: " << error.what());
return std::nullopt;
}
}
bool
FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
{
try
{
boost::property_tree::ptree root;
bool FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
{
try
{
boost::property_tree::ptree root;
for (const auto& [id, positions] : trackPositions)
{
boost::property_tree::ptree node;
for (const auto& [id, positions] : trackPositions)
{
boost::property_tree::ptree node;
node.put("id", id.getValue());
node.put("id", id.getValue());
for (const SOM::Position& position : positions)
{
boost::property_tree::ptree positionNode;
positionNode.put("x", position.x);
positionNode.put("y", position.y);
for (const SOM::Position& position : positions)
{
boost::property_tree::ptree positionNode;
positionNode.put("x", position.x);
positionNode.put("y", position.y);
node.add_child("position.position", positionNode);
}
node.add_child("position.position", positionNode);
}
root.add_child("objects.object", node);
}
root.add_child("objects.object", node);
}
boost::property_tree::write_xml(path.string(), root);
return true;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot cache object position: " << error.what();
return false;
}
}
boost::property_tree::write_xml(path.string(), root);
return true;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot cache object position: " << error.what());
return false;
}
}
std::optional<FeaturesEngineCache::TrackPositions>
FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
{
try
{
LMS_LOG(RECOMMENDATION, INFO) << "Reading object position from cache...";
std::optional<FeaturesEngineCache::TrackPositions> FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
{
try
{
LMS_LOG(RECOMMENDATION, INFO, "Reading object position from cache...");
boost::property_tree::ptree root;
boost::property_tree::ptree root;
boost::property_tree::read_xml(path.string(), root);
boost::property_tree::read_xml(path.string(), root);
TrackPositions res;
TrackPositions res;
for (const auto& object : root.get_child("objects"))
{
const Database::TrackId id {object.second.get<Database::IdType::ValueType>("id")};
for (const auto& position : object.second.get_child("position"))
{
auto x = position.second.get<SOM::Coordinate>("x");
auto y = position.second.get<SOM::Coordinate>("y");
for (const auto& object : root.get_child("objects"))
{
const Database::TrackId id{ object.second.get<Database::IdType::ValueType>("id") };
for (const auto& position : object.second.get_child("position"))
{
auto x = position.second.get<SOM::Coordinate>("x");
auto y = position.second.get<SOM::Coordinate>("y");
res[id].push_back({x, y});
}
}
res[id].push_back({ x, y });
}
}
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read object position from cache";
LMS_LOG(RECOMMENDATION, INFO, "Successfully read object position from cache");
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create object position from cache file: " << error.what();
return std::nullopt;
}
}
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot create object position from cache file: " << error.what());
return std::nullopt;
}
}
void
FeaturesEngineCache::invalidate()
{
std::filesystem::remove(getCacheNetworkFilePath());
std::filesystem::remove(getCacheTrackPositionsFilePath());
}
void FeaturesEngineCache::invalidate()
{
std::filesystem::remove(getCacheNetworkFilePath());
std::filesystem::remove(getCacheTrackPositionsFilePath());
}
std::optional<FeaturesEngineCache>
FeaturesEngineCache::read()
{
auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())};
if (!network)
return std::nullopt;
std::optional<FeaturesEngineCache> FeaturesEngineCache::read()
{
auto network{ createNetworkFromCacheFile(getCacheNetworkFilePath()) };
if (!network)
return std::nullopt;
auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())};
if (!trackPositions)
return std::nullopt;
auto trackPositions{ createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath()) };
if (!trackPositions)
return std::nullopt;
return FeaturesEngineCache {std::move(*network), std::move(*trackPositions)};
}
return FeaturesEngineCache{ std::move(*network), std::move(*trackPositions) };
}
void
FeaturesEngineCache::write() const
{
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
void FeaturesEngineCache::write() const
{
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
{
invalidate();
}
}
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
{
invalidate();
}
}
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
: _network {std::move(network)},
_trackPositions {std::move(trackPositions)}
{
}
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
: _network{ std::move(network) },
_trackPositions{ std::move(trackPositions) }
{
}
} // namespace Recommendation
@@ -25,7 +25,7 @@
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Recommendation::PlaylistGeneratorConstraint
{
@@ -23,7 +23,7 @@
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Recommendation::PlaylistGeneratorConstraint
{
@@ -22,7 +22,7 @@
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Scanner
{
@@ -45,13 +45,13 @@ namespace Scanner
const Track::pointer track{ Track::find(session, trackId) };
if (auto trackMBID{ track->getTrackMBID() })
{
LMS_LOG(DBUPDATER, INFO) << "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName();
LMS_LOG(DBUPDATER, INFO, "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName());
context.stats.duplicates.emplace_back(ScanDuplicate{ track->getId(), DuplicateReason::SameTrackMBID });
context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats);
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Found " << context.currentStepStats.processedElems << " duplicated audio files";
LMS_LOG(DBUPDATER, DEBUG, "Found " << context.currentStepStats.processedElems << " duplicated audio files");
}
}
@@ -21,7 +21,7 @@
#include "services/database/Db.hpp"
#include "services/database/Cluster.hpp"
#include "services/database/Session.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
namespace Scanner
@@ -84,6 +84,6 @@ namespace Scanner
return true;
});
LMS_LOG(DBUPDATER, DEBUG) << "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!";
LMS_LOG(DBUPDATER, DEBUG, "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!");
}
}
@@ -18,7 +18,7 @@
*/
#include "ScanStepDiscoverFiles.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
namespace Scanner
@@ -42,6 +42,6 @@ namespace Scanner
context.stats.filesScanned = context.currentStepStats.processedElems;
LMS_LOG(DBUPDATER, DEBUG) << "Discovered " << context.stats.filesScanned << " files in '" << context.directory << "'";
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << context.stats.filesScanned << " files in '" << context.directory << "'");
}
}
@@ -25,7 +25,7 @@
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
namespace Scanner
@@ -87,14 +87,14 @@ namespace Scanner
Session& session{ _db.getTLSSession() };
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
LMS_LOG(DBUPDATER, DEBUG, "Checking tracks to be removed...");
std::size_t trackCount{};
{
auto transaction{ session.createReadTransaction() };
trackCount = Track::getCount(session);
}
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
LMS_LOG(DBUPDATER, DEBUG, trackCount << " tracks to be checked...");
context.currentStepStats.totalElems = trackCount;
@@ -143,24 +143,24 @@ namespace Scanner
break;
}
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
LMS_LOG(DBUPDATER, DEBUG, trackCount << " tracks checked!");
}
void ScanStepRemoveOrphanDbFiles::removeOrphanClusters()
{
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters...";
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan clusters...");
removeOrphanEntries<Database::Cluster>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
{
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists...";
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists...");
removeOrphanEntries<Database::Artist>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
{
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan releases...");
removeOrphanEntries<Database::Release>(_db.getTLSSession(), _abortScan);
}
@@ -172,19 +172,19 @@ namespace Scanner
// and still belongs to a media directory
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing";
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': missing");
return false;
}
if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName))
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory";
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': out of media directory");
return false;
}
if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions))
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled";
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': file format no longer handled");
return false;
}
@@ -192,7 +192,7 @@ namespace Scanner
}
catch (std::filesystem::filesystem_error& e)
{
LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what();
LMS_LOG(DBUPDATER, ERROR, "Caught exception while checking file '" << p.string() << "': " << e.what());
return false;
}
}
@@ -30,7 +30,7 @@
#include "services/database/TrackArtistLink.hpp"
#include "utils/Exception.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
using namespace Database;
@@ -258,7 +258,7 @@ namespace Scanner
MetaData::ParserReadStyle getParserReadStyle()
{
std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "accurate") };
std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "average") };
if (readStyle == "fast")
return MetaData::ParserReadStyle::Fast;
@@ -290,7 +290,7 @@ namespace Scanner
if (ec)
{
LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message();
LMS_LOG(DBUPDATER, ERROR, "Cannot process entry '" << path.string() << "': " << ec.message());
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
}
else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
@@ -319,7 +319,7 @@ namespace Scanner
}
catch (LmsException& e)
{
LMS_LOG(DBUPDATER, ERROR) << e.what();
LMS_LOG(DBUPDATER, ERROR, e.what());
stats.skips++;
return;
}
@@ -365,7 +365,7 @@ namespace Scanner
std::error_code ec;
if (!std::filesystem::exists(otherTrack->getPath(), ec))
{
LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'");
track = otherTrack;
track.modify()->setPath(file);
}
@@ -384,7 +384,7 @@ namespace Scanner
if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName))
continue;
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')";
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')");
// As this MBID already exists, just remove what we just scanned
if (track)
{
@@ -399,7 +399,7 @@ namespace Scanner
// We estimate this is an audio file if the duration is not null
if (trackInfo->duration == std::chrono::milliseconds::zero())
{
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
// If Track exists here, delete it!
if (track)
@@ -427,12 +427,12 @@ namespace Scanner
if (!track)
{
track = dbSession.create<Track>(file);
LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Adding '" << file.string() << "'");
stats.additions++;
}
else
{
LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Updating '" << file.string() << "'");
stats.updates++;
}
@@ -27,7 +27,7 @@
#include "services/database/ScanSettings.hpp"
#include "utils/Exception.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
#include "utils/Tuple.hpp"
@@ -82,9 +82,9 @@ namespace Scanner
ScannerService::~ScannerService()
{
LMS_LOG(DBUPDATER, INFO) << "Stopping service...";
LMS_LOG(DBUPDATER, INFO, "Stopping service...");
stop();
LMS_LOG(DBUPDATER, INFO) << "Service stopped!";
LMS_LOG(DBUPDATER, INFO, "Service stopped!");
}
void ScannerService::start()
@@ -113,15 +113,15 @@ namespace Scanner
void ScannerService::abortScan()
{
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
LMS_LOG(DBUPDATER, DEBUG, "Aborting scan...");
std::scoped_lock lock{ _controlMutex };
LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort...";
LMS_LOG(DBUPDATER, DEBUG, "Waiting for the scan to abort...");
_abortScan = true;
_scheduleTimer.cancel();
_ioService.stop();
LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!";
LMS_LOG(DBUPDATER, DEBUG, "Scan abort done!");
_abortScan = false;
_ioService.start();
@@ -167,7 +167,7 @@ namespace Scanner
void ScannerService::scheduleNextScan()
{
LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan";
LMS_LOG(DBUPDATER, DEBUG, "Scheduling next scan");
refreshScanSettings();
@@ -202,7 +202,7 @@ namespace Scanner
break;
case ScanSettings::UpdatePeriod::Never:
LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!";
LMS_LOG(DBUPDATER, INFO, "Auto scan disabled!");
break;
}
@@ -230,7 +230,7 @@ namespace Scanner
if (dateTime.isNull())
{
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan right now");
_scheduleTimer.expires_from_now(std::chrono::seconds{ 0 });
_scheduleTimer.async_wait(cb);
}
@@ -240,7 +240,7 @@ namespace Scanner
std::time_t t{ std::chrono::system_clock::to_time_t(timePoint) };
char ctimeStr[26];
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr));
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr)));
_scheduleTimer.expires_at(timePoint);
_scheduleTimer.async_wait(cb);
}
@@ -257,7 +257,7 @@ namespace Scanner
}
LMS_LOG(UI, INFO) << "New scan started!";
LMS_LOG(UI, INFO, "New scan started!");
refreshScanSettings();
@@ -267,16 +267,16 @@ namespace Scanner
for (auto& scanStep : _scanSteps)
{
LMS_LOG(DBUPDATER, DEBUG) << "Starting scan step '" << scanStep->getStepName() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'");
scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() };
notifyInProgress(scanContext.currentStepStats);
scanStep->process(scanContext);
notifyInProgress(scanContext.currentStepStats);
LMS_LOG(DBUPDATER, DEBUG) << "Completed scan step '" << scanStep->getStepName() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Completed scan step '" << scanStep->getStepName() << "'");
}
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 << ", 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 << ", duplicates = " << stats.duplicates.size());
_dbSession.analyze();
@@ -290,14 +290,14 @@ namespace Scanner
_currentScanStepStats.reset();
}
LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!";
LMS_LOG(DBUPDATER, DEBUG, "Scan not aborted, scheduling next scan!");
scheduleNextScan();
_events.scanComplete.emit(stats);
}
else
{
LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!";
LMS_LOG(DBUPDATER, DEBUG, "Scan aborted, not scheduling next scan!");
std::unique_lock lock{ _statusMutex };
@@ -312,9 +312,9 @@ namespace Scanner
if (_settings == newSettings)
return;
LMS_LOG(DBUPDATER, DEBUG) << "Scanner settings updated";
LMS_LOG(DBUPDATER, DEBUG) << "skipDuplicateMBID = " << newSettings.skipDuplicateMBID;
LMS_LOG(DBUPDATER, DEBUG) << "Using scan settings version " << newSettings.scanVersion;
LMS_LOG(DBUPDATER, DEBUG, "Scanner settings updated");
LMS_LOG(DBUPDATER, DEBUG, "skipDuplicateMBID = " << newSettings.skipDuplicateMBID);
LMS_LOG(DBUPDATER, DEBUG, "Using scan settings version " << newSettings.scanVersion);
_settings = std::move(newSettings);
@@ -366,7 +366,7 @@ namespace Scanner
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
[](ClusterType::pointer clusterType) { return clusterType->getName(); });
[](const ClusterType::pointer& clusterType) { return std::string{ clusterType->getName() }; });
newSettings.clusterTypeNames = std::move(clusterTypeNames);
}
@@ -26,7 +26,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
@@ -43,15 +43,15 @@ namespace Scrobbling
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
: _db{ db }
{
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
LMS_LOG(SCROBBLING, INFO, "Starting service...");
_scrobblingBackends.emplace(ScrobblingBackend::Internal, std::make_unique<InternalBackend>(_db));
_scrobblingBackends.emplace(ScrobblingBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
LMS_LOG(SCROBBLING, INFO) << "Service started!";
LMS_LOG(SCROBBLING, INFO, "Service started!");
}
ScrobblingService::~ScrobblingService()
{
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
LMS_LOG(SCROBBLING, INFO, "Service stopped!");
}
void ScrobblingService::listenStarted(const Listen& listen)
@@ -24,7 +24,7 @@
#include "services/database/Track.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "Utils.hpp"
@@ -44,7 +44,7 @@ namespace Scrobbling::ListenBrainz
const bool res{ duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2) };
if (!res)
LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s";
LOG(DEBUG, "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s");
return res;
}
@@ -57,12 +57,12 @@ namespace Scrobbling::ListenBrainz
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
, _listensSynchronizer{ _ioContext, db, *_client }
{
LOG(INFO) << "Starting ListenBrainz backend... API endpoint = '" << _baseAPIUrl << "'";
LOG(INFO, "Starting ListenBrainz backend... API endpoint = '" << _baseAPIUrl << "'");
}
ListenBrainzBackend::~ListenBrainzBackend()
{
LOG(INFO) << "Stopped ListenBrainz backend!";
LOG(INFO, "Stopped ListenBrainz backend!");
}
void ListenBrainzBackend::listenStarted(const Listen& listen)
@@ -24,86 +24,82 @@
#include <Wt/Json/Value.h>
#include <Wt/Json/Parser.h>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "Utils.hpp"
namespace
{
using namespace Scrobbling::ListenBrainz;
Listen
parseListen(const Wt::Json::Object& listenObject)
{
Listen listen;
// Mandatory fields
const Wt::Json::Object& metadata = listenObject.get("track_metadata");
listen.trackName = static_cast<std::string>(metadata.get("track_name"));
listen.artistName = static_cast<std::string>(metadata.get("artist_name"));
// Optional fields
listen.releaseName = static_cast<std::string>(metadata.get("release_name").orIfNull(""));
if (listenObject.type("listened_at") == Wt::Json::Type::Number)
listen.listenedAt = Wt::WDateTime::fromTime_t(static_cast<int>(listenObject.get("listened_at")));
if (!listen.listenedAt.isValid())
LOG(ERROR) << "Invalid or missing 'listened_at' field!";
if (metadata.type("additional_info") == Wt::Json::Type::Object)
{
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
listen.trackMBID = UUID::fromString(additionalInfo.get("track_mbid").orIfNull(""));
listen.recordingMBID = UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""));
listen.releaseMBID = UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""));
// tracknumber should be an integer but some players encode as strings
int trackNumber {additionalInfo.get("tracknumber").toNumber().orIfNull(-1)};
if (trackNumber > 0)
listen.trackNumber = trackNumber;
}
return listen;
}
} // namespace
namespace Scrobbling::ListenBrainz
{
ListensParser::Result
ListensParser::parse(std::string_view msgBody)
{
Result result;
namespace
{
Listen parseListen(const Wt::Json::Object& listenObject)
{
Listen listen;
try
{
Wt::Json::Object root;
Wt::Json::parse(std::string {msgBody}, root);
// Mandatory fields
const Wt::Json::Object& metadata = listenObject.get("track_metadata");
listen.trackName = static_cast<std::string>(metadata.get("track_name"));
listen.artistName = static_cast<std::string>(metadata.get("artist_name"));
const Wt::Json::Object& payload = root.get("payload");
const Wt::Json::Array& listens = payload.get("listens");
// Optional fields
listen.releaseName = static_cast<std::string>(metadata.get("release_name").orIfNull(""));
if (listenObject.type("listened_at") == Wt::Json::Type::Number)
listen.listenedAt = Wt::WDateTime::fromTime_t(static_cast<int>(listenObject.get("listened_at")));
if (!listen.listenedAt.isValid())
LOG(ERROR, "Invalid or missing 'listened_at' field!");
LOG(DEBUG) << "Parsing " << listens.size() << " listens...";
result.listenCount = listens.size();
if (metadata.type("additional_info") == Wt::Json::Type::Object)
{
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
listen.trackMBID = UUID::fromString(additionalInfo.get("track_mbid").orIfNull(""));
listen.recordingMBID = UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""));
listen.releaseMBID = UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""));
if (listens.empty())
return result;
// tracknumber should be an integer but some players encode as strings
int trackNumber{ additionalInfo.get("tracknumber").toNumber().orIfNull(-1) };
if (trackNumber > 0)
listen.trackNumber = trackNumber;
}
for (const Wt::Json::Value& value : listens)
{
try
{
const Wt::Json::Object& listen = value;
result.listens.push_back(parseListen(listen));
}
catch (const Wt::WException& error)
{
LOG(ERROR) << "Cannot parse 'listen': " << error.what();
}
}
}
catch (const Wt::WException& error)
{
LOG(ERROR) << "Cannot parse 'listens': " << error.what();
}
return listen;
}
} // namespace
return result;
}
ListensParser::Result ListensParser::parse(std::string_view msgBody)
{
Result result;
try
{
Wt::Json::Object root;
Wt::Json::parse(std::string{ msgBody }, root);
const Wt::Json::Object& payload = root.get("payload");
const Wt::Json::Array& listens = payload.get("listens");
LOG(DEBUG, "Parsing " << listens.size() << " listens...");
result.listenCount = listens.size();
if (listens.empty())
return result;
for (const Wt::Json::Value& value : listens)
{
try
{
const Wt::Json::Object& listen = value;
result.listens.push_back(parseListen(listen));
}
catch (const Wt::WException& error)
{
LOG(ERROR, "Cannot parse 'listen': " << error.what());
}
}
}
catch (const Wt::WException& error)
{
LOG(ERROR, "Cannot parse 'listens': " << error.what());
}
return result;
}
} // Scrobbling::ListenBrainz
@@ -58,7 +58,7 @@ namespace
if (artists.empty())
{
LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist";
LOG(DEBUG, "Track cannot be scrobbled since it does not have any artist");
return std::nullopt;
}
@@ -134,7 +134,7 @@ namespace
}
catch (const Wt::WException& e)
{
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
LOG(ERROR, "Cannot parse listen count response: " << e.what());
return std::nullopt;
}
}
@@ -152,12 +152,12 @@ namespace
// if duplicated files, do not record it (let the user correct its database)
if (tracks.size() == 1)
{
LOG(DEBUG) << "Matched listen '" << listen << "' using track MBID";
LOG(DEBUG, "Matched listen '" << listen << "' using track MBID");
return tracks.front()->getId();
}
else if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using track MBID!";
LOG(DEBUG, "Too many matches for listen '" << listen << "' using track MBID!");
return {};
}
}
@@ -168,12 +168,12 @@ namespace
// if duplicated files, do not record it (let the user correct its database)
if (tracks.size() == 1)
{
LOG(DEBUG) << "Matched listen '" << listen << "' using recording MBID";
LOG(DEBUG, "Matched listen '" << listen << "' using recording MBID");
return tracks.front()->getId();
}
else if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using recording MBID!";
LOG(DEBUG, "Too many matches for listen '" << listen << "' using recording MBID!");
return {};
}
}
@@ -192,16 +192,16 @@ namespace
// conservative behavior: in case of multiple matches: reject
if (tracks.results.size() == 1)
{
LOG(DEBUG) << "Matched listen '" << listen << "' using metadata";
LOG(DEBUG, "Matched listen '" << listen << "' using metadata");
return tracks.results.front();
}
else if (tracks.results.size() > 1)
{
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using metadata";
LOG(DEBUG, "Too many matches for listen '" << listen << "' using metadata");
return {};
}
LOG(DEBUG) << "No match for listen '" << listen << "'";
LOG(DEBUG, "No match for listen '" << listen << "'");
return {};
}
}
@@ -215,7 +215,7 @@ namespace Scrobbling::ListenBrainz
, _maxSyncListenCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-listen-count", 1000) }
, _syncListensPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-listens-period-hours", 1) }
{
LOG(INFO) << "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours";
LOG(INFO, "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours");
scheduleSync(std::chrono::seconds{ 30 });
}
@@ -267,14 +267,14 @@ namespace Scrobbling::ListenBrainz
std::string bodyText{ listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now") };
if (bodyText.empty())
{
LOG(DEBUG) << "Cannot convert listen to json: skipping";
LOG(DEBUG, "Cannot convert listen to json: skipping");
return;
}
const std::optional<UUID> listenBrainzToken{ Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) };
if (!listenBrainzToken)
{
LOG(DEBUG) << "No listenbrainz token found: skipping";
LOG(DEBUG, "No listenbrainz token found: skipping");
return;
}
@@ -305,7 +305,7 @@ namespace Scrobbling::ListenBrainz
dbListen = session.create<Database::Listen>(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt);
dbListen.modify()->setSyncState(scrobblingState);
LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString();
LOG(DEBUG, "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString());
return true;
}
@@ -347,7 +347,7 @@ namespace Scrobbling::ListenBrainz
}
}
LOG(DEBUG) << "Queing " << pendingListens.size() << " pending listen";
LOG(DEBUG, "Queing " << pendingListens.size() << " pending listen");
for (const TimedListen& pendingListen : pendingListens)
enqueListen(pendingListen);
@@ -379,13 +379,13 @@ namespace Scrobbling::ListenBrainz
if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0)
return;
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
LOG(DEBUG, "Scheduled sync in " << fromNow.count() << " seconds...");
_syncTimer.expires_after(fromNow);
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG) << "getListens aborted";
LOG(DEBUG, "getListens aborted");
return;
}
else if (ec)
@@ -399,7 +399,7 @@ namespace Scrobbling::ListenBrainz
void ListensSynchronizer::startSync()
{
LOG(DEBUG) << "Starting sync!";
LOG(DEBUG, "Starting sync!");
assert(!isSyncing());
@@ -435,7 +435,7 @@ namespace Scrobbling::ListenBrainz
{
_strand.dispatch([this, &context]
{
LOG(INFO) << "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount;
LOG(INFO, "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount);
context.syncing = false;
if (!isSyncing())
@@ -489,7 +489,7 @@ namespace Scrobbling::ListenBrainz
{
const auto listenCount = parseListenCount(msgBody);
if (listenCount)
LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount;
LOG(DEBUG, "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount);
bool needSync{ listenCount && (!context.listenCount || *context.listenCount != *listenCount) };
context.listenCount = listenCount;
@@ -551,7 +551,7 @@ namespace Scrobbling::ListenBrainz
// update oldest listen for the next query
if (!parsedListen.listenedAt.isValid())
{
LOG(DEBUG) << "Skipping entry due to invalid listenedAt";
LOG(DEBUG, "Skipping entry due to invalid listenedAt");
continue;
}
@@ -27,38 +27,36 @@
namespace Scrobbling::ListenBrainz::Utils
{
std::optional<UUID>
getListenBrainzToken(Database::Session& session, Database::UserId userId)
{
auto transaction {session.createReadTransaction()};
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId)
{
auto transaction{ session.createReadTransaction() };
const Database::User::pointer user {Database::User::find(session, userId)};
if (!user)
return std::nullopt;
const Database::User::pointer user{ Database::User::find(session, userId) };
if (!user)
return std::nullopt;
return user->getListenBrainzToken();
}
return user->getListenBrainzToken();
}
std::string
parseValidateToken(std::string_view msgBody)
{
std::string listenBrainzUserName;
std::string parseValidateToken(std::string_view msgBody)
{
std::string listenBrainzUserName;
Wt::Json::ParseError error;
Wt::Json::Object root;
if (!Wt::Json::parse(std::string {msgBody}, root, error))
{
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
return listenBrainzUserName;
}
Wt::Json::ParseError error;
Wt::Json::Object root;
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
{
LOG(ERROR, "Cannot parse 'validate-token' result: " << error.what());
return listenBrainzUserName;
}
if (!root.get("valid").orIfNull(false))
{
LOG(INFO) << "Invalid listenbrainz user";
return listenBrainzUserName;
}
if (!root.get("valid").orIfNull(false))
{
LOG(INFO, "Invalid listenbrainz user");
return listenBrainzUserName;
}
listenBrainzUserName = root.get("user_name").orIfNull("");
return listenBrainzUserName;
}
listenBrainzUserName = root.get("user_name").orIfNull("");
return listenBrainzUserName;
}
}
@@ -20,10 +20,10 @@
#pragma once
#include "services/database/UserId.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/UUID.hpp"
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] "
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, message << "[listenbrainz] ")
namespace Database
{
@@ -19,16 +19,16 @@
#include <gtest/gtest.h>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/StreamLogger.hpp"
int main(int argc, char **argv)
int main(int argc, char** argv)
{
// log to stdout
Service<Logger> logger {std::make_unique<StreamLogger>(std::cout, EnumSet<Severity> {Severity::FATAL, Severity::ERROR})};
// log to stdout
Service<ILogger> logger{ std::make_unique<StreamLogger>(std::cout, EnumSet<Severity> {Severity::FATAL, Severity::ERROR}) };
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+1 -1
View File
@@ -26,7 +26,7 @@
#include <sstream>
#include <unordered_set>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
namespace SOM
+1 -1
View File
@@ -21,7 +21,7 @@
#include "SubsonicResponse.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
namespace API::Subsonic
+7 -7
View File
@@ -29,7 +29,7 @@
#include "services/database/User.hpp"
#include "utils/EnumSet.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
#include "utils/Utils.hpp"
@@ -290,7 +290,7 @@ namespace API::Subsonic
const std::size_t requestId{ curRequestId++ };
LMS_LOG(API_SUBSONIC, DEBUG) << "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap());
LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap()));
std::string requestPath{ request.pathInfo() };
if (StringUtils::stringEndsWith(requestPath, ".view"))
@@ -319,7 +319,7 @@ namespace API::Subsonic
resp.write(response.out(), format);
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
return;
}
@@ -328,18 +328,18 @@ namespace API::Subsonic
if (itStreamHandler != mediaRetrievalHandlers.end())
{
itStreamHandler->second(requestContext, request, response);
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
return;
}
LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << requestPath << "'";
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
throw UnknownEntryPointGenericError{};
}
catch (const Error& e)
{
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'"
LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestPath << "'"
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'");
Response resp{ Response::createFailedResponse(protocolVersion, e) };
resp.write(response.out(), format);
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
@@ -26,7 +26,7 @@
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
#include "utils/Service.hpp"
#include "responses/Album.hpp"
@@ -122,7 +122,7 @@ namespace API::Subsonic
// This endpoint does not scale: make sort lived transactions in order not to block the whole application
// first pass: dispatch the artists by first letter
LMS_LOG(API_SUBSONIC, DEBUG) << "GetArtists: fetching all artists...";
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists...");
std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar;
std::size_t currentArtistOffset{ 0 };
constexpr std::size_t batchSize{ 100 };
@@ -151,7 +151,7 @@ namespace API::Subsonic
}
// second pass: add each artist
LMS_LOG(API_SUBSONIC, DEBUG) << "GetArtists: constructing response...";
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: constructing response...");
for (const auto& [sortChar, artistIds] : artistsSortedByFirstChar)
{
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
@@ -29,7 +29,7 @@
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/IResourceHandler.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/FileResourceHandlerCreator.hpp"
#include "utils/Utils.hpp"
#include "utils/String.hpp"
@@ -153,7 +153,7 @@ namespace API::Subsonic
if (!requestedFormat && (maxBitRate == 0 || track->getBitrate() <= maxBitRate ))
{
LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate is compatible with parameters => no transcoding";
LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate is compatible with parameters => no transcoding");
return parameters; // no transcoding needed
}
@@ -165,7 +165,7 @@ namespace API::Subsonic
{
if (maxBitRate == 0 || track->getBitrate() <= maxBitRate)
{
LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate and format are compatible with parameters => no transcoding";
LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate and format are compatible with parameters => no transcoding");
return parameters; // no transcoding needed
}
bitrate = maxBitRate;
@@ -246,7 +246,7 @@ namespace API::Subsonic
}
catch (const Av::Exception& e)
{
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
LMS_LOG(API_SUBSONIC, ERROR, "Caught Av exception: " << e.what());
}
}
+3 -3
View File
@@ -26,7 +26,7 @@
#include <archive.h>
#include <archive_entry.h>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Zip
{
@@ -73,7 +73,7 @@ namespace Zip
{
const int res {::archive_write_free(arch)};
if (res != ARCHIVE_OK)
LMS_LOG(UTILS, ERROR) << "Failure while freeing archive control struct: " << std::string {::strerror(res)};
LMS_LOG(UTILS, ERROR, "Failure while freeing archive control struct: " << std::string {::strerror(res)});
}
void
@@ -176,7 +176,7 @@ namespace Zip
void
ArchiveZipper::abort()
{
LMS_LOG(UTILS, DEBUG) << "Aborting zip creation";
LMS_LOG(UTILS, DEBUG, "Aborting zip creation");
if (_archive)
{
::archive_write_fail(_archive.get());
+116 -121
View File
@@ -36,177 +36,172 @@
#include <boost/asio/buffer.hpp>
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace
{
class SystemException : public ChildProcessException
{
public:
SystemException(int err, const std::string& errMsg)
: ChildProcessException {errMsg + ": " + ::strerror(err)}
{}
class SystemException : public ChildProcessException
{
public:
SystemException(int err, const std::string& errMsg)
: ChildProcessException{ errMsg + ": " + ::strerror(err) }
{}
SystemException(boost::system::error_code ec, const std::string& errMsg)
: ChildProcessException {errMsg + ": " + ec.message()}
{}
};
SystemException(boost::system::error_code ec, const std::string& errMsg)
: ChildProcessException{ errMsg + ": " + ec.message() }
{}
};
}
ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args)
: _ioContext {ioContext}
, _childStdout {_ioContext}
: _ioContext{ ioContext }
, _childStdout{ _ioContext }
{
// make sure only one thread is executing this part of code
static std::mutex mutex;
std::unique_lock<std::mutex> lock {mutex};
// make sure only one thread is executing this part of code
static std::mutex mutex;
std::unique_lock<std::mutex> lock{ mutex };
int pipe[2];
int pipe[2];
int res {pipe2(pipe, O_NONBLOCK | O_CLOEXEC)};
if (res < 0)
throw SystemException {errno, "pipe2 failed!"};
int res{ pipe2(pipe, O_NONBLOCK | O_CLOEXEC) };
if (res < 0)
throw SystemException{ errno, "pipe2 failed!" };
{
{
#if defined(__linux__) && defined(F_SETPIPE_SZ)
// Just a hint here to prevent the writer from writing too many bytes ahead of the reader
constexpr std::size_t pipeSize {65536*4};
// Just a hint here to prevent the writer from writing too many bytes ahead of the reader
constexpr std::size_t pipeSize{ 65536 * 4 };
if (fcntl(pipe[0], F_SETPIPE_SZ, pipeSize) == -1)
throw SystemException {errno, "fcntl failed!"};
if (fcntl(pipe[1], F_SETPIPE_SZ, pipeSize) == -1)
throw SystemException {errno, "fcntl failed!"};
if (fcntl(pipe[0], F_SETPIPE_SZ, pipeSize) == -1)
throw SystemException{ errno, "fcntl failed!" };
if (fcntl(pipe[1], F_SETPIPE_SZ, pipeSize) == -1)
throw SystemException{ errno, "fcntl failed!" };
#endif
}
}
res = fork();
if (res == -1)
throw SystemException {errno, "fork failed!"};
res = fork();
if (res == -1)
throw SystemException{ errno, "fork failed!" };
if (res == 0) // CHILD
{
close(pipe[0]);
close(STDIN_FILENO);
close(STDERR_FILENO);
if (res == 0) // CHILD
{
close(pipe[0]);
close(STDIN_FILENO);
close(STDERR_FILENO);
// Replace stdout with pipe write
if (dup2(pipe[1], STDOUT_FILENO) == -1)
exit(-1);
// Replace stdout with pipe write
if (dup2(pipe[1], STDOUT_FILENO) == -1)
exit(-1);
std::vector<const char*> execArgs;
std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); });
execArgs.push_back(nullptr);
std::vector<const char*> execArgs;
std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); });
execArgs.push_back(nullptr);
res = execv(path.string().c_str(), (char *const*)&execArgs[0]);
if (res == -1)
exit(-1);
}
else // PARENT
{
close(pipe[1]);
{
boost::system::error_code assignError;
_childStdout.assign(pipe[0], assignError);
if (assignError)
throw SystemException {assignError, "fork failed!"};
}
_childPID = res;
}
res = execv(path.string().c_str(), (char* const*)&execArgs[0]);
if (res == -1)
exit(-1);
}
else // PARENT
{
close(pipe[1]);
{
boost::system::error_code assignError;
_childStdout.assign(pipe[0], assignError);
if (assignError)
throw SystemException{ assignError, "fork failed!" };
}
_childPID = res;
}
}
ChildProcess::~ChildProcess()
{
LMS_LOG(CHILDPROCESS, DEBUG) << "Closing child process...";
{
boost::system::error_code closeError;
_childStdout.close(closeError);
if (closeError)
LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message();
}
LMS_LOG(CHILDPROCESS, DEBUG, "Closing child process...");
{
boost::system::error_code closeError;
_childStdout.close(closeError);
if (closeError)
LMS_LOG(CHILDPROCESS, ERROR, "Closed failed: " << closeError.message());
}
if (!_finished)
kill();
if (!_finished)
kill();
wait(true);
wait(true);
}
void
ChildProcess::kill()
void ChildProcess::kill()
{
// process may already have finished
LMS_LOG(CHILDPROCESS, DEBUG) << "Killing child process...";
if (::kill(_childPID, SIGKILL) == -1)
LMS_LOG(CHILDPROCESS, DEBUG) << "Kill failed: " << ::strerror(errno);
// process may already have finished
LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process...");
if (::kill(_childPID, SIGKILL) == -1)
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << ::strerror(errno));
}
bool
ChildProcess::wait(bool block)
bool ChildProcess::wait(bool block)
{
assert(!_waited);
assert(!_waited);
int wstatus {};
const pid_t pid {waitpid(_childPID, &wstatus, block ? 0 : WNOHANG)};
int wstatus{};
const pid_t pid{ waitpid(_childPID, &wstatus, block ? 0 : WNOHANG) };
if (pid == -1)
throw SystemException {errno, "waitpid failed!"};
else if (pid == 0)
return false;
if (pid == -1)
throw SystemException{ errno, "waitpid failed!" };
else if (pid == 0)
return false;
if (WIFEXITED(wstatus))
{
_exitCode = WEXITSTATUS(wstatus);
LMS_LOG(CHILDPROCESS, DEBUG) << "Exit code = " << *_exitCode;
}
if (WIFEXITED(wstatus))
{
_exitCode = WEXITSTATUS(wstatus);
LMS_LOG(CHILDPROCESS, DEBUG, "Exit code = " << *_exitCode);
}
_waited = true;
return true;
_waited = true;
return true;
}
void
ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
void ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
{
assert(!finished());
assert(!finished());
LMS_LOG(CHILDPROCESS, DEBUG) << "Async read, bufferSize = " << bufferSize;
LMS_LOG(CHILDPROCESS, DEBUG, "Async read, bufferSize = " << bufferSize);
boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize),
[this, callback {std::move(callback)}](const boost::system::error_code& error, std::size_t bytesTransferred)
{
LMS_LOG(CHILDPROCESS, DEBUG) << "Async read cb - ec = '" << error.message() << "' (" << error.value() << "), bytesTransferred = " << bytesTransferred;
boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize),
[this, callback{ std::move(callback) }](const boost::system::error_code& error, std::size_t bytesTransferred)
{
LMS_LOG(CHILDPROCESS, DEBUG, "Async read cb - ec = '" << error.message() << "' (" << error.value() << "), bytesTransferred = " << bytesTransferred);
ReadResult readResult {ReadResult::Success};
if (error)
{
if (error != boost::asio::error::eof)
{
// forbidden to read any captured param here as the ChildProcess instance may already have been killed
return;
}
ReadResult readResult{ ReadResult::Success };
if (error)
{
if (error != boost::asio::error::eof)
{
// forbidden to read any captured param here as the ChildProcess instance may already have been killed
return;
}
readResult = ReadResult::EndOfFile;
_finished = true;
}
readResult = ReadResult::EndOfFile;
_finished = true;
}
callback(readResult, bytesTransferred);
});
callback(readResult, bytesTransferred);
});
}
std::size_t
ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
std::size_t ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
{
boost::system::error_code ec;
const std::size_t res {_childStdout.read_some(boost::asio::buffer(data, bufferSize), ec)};
LMS_LOG(CHILDPROCESS, DEBUG) << "read some " << res << " bytes, ec = " << ec.message();
if (ec)
_childStdout.close(ec);
boost::system::error_code ec;
const std::size_t res{ _childStdout.read_some(boost::asio::buffer(data, bufferSize), ec) };
LMS_LOG(CHILDPROCESS, DEBUG, "read some " << res << " bytes, ec = " << ec.message());
if (ec)
_childStdout.close(ec);
return res;
return res;
}
bool
ChildProcess::finished() const
bool ChildProcess::finished() const
{
return _finished;
return _finished;
}
+1 -1
View File
@@ -19,7 +19,7 @@
#include "ChildProcessManager.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "ChildProcess.hpp"
+1 -1
View File
@@ -20,7 +20,7 @@
#include "Config.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
std::unique_ptr<IConfig> createConfig(const std::filesystem::path& p)
{
+19 -14
View File
@@ -21,7 +21,7 @@
#include <fstream>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
std::unique_ptr<IResourceHandler>
createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
@@ -45,7 +45,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
{
if (!ifs)
{
LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'";
LMS_LOG(UTILS, ERROR, "Cannot open file stream for '" << _path.string() << "'");
response.setStatus(404);
return {};
}
@@ -54,7 +54,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) };
ifs.seekg(0, std::ios::beg);
LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize;
LMS_LOG(UTILS, DEBUG, "File '" << _path.string() << "', fileSize = " << fileSize);
response.addHeader("Accept-Ranges", "bytes");
@@ -66,13 +66,13 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
response.setStatus(416); // Requested range not satisfiable
response.addHeader("Content-Range", contentRange.str());
LMS_LOG(UTILS, DEBUG) << "Range not satisfiable";
LMS_LOG(UTILS, DEBUG, "Range not satisfiable");
return {};
}
if (ranges.size() == 1)
{
LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte();
LMS_LOG(UTILS, DEBUG, "Range requested = " << ranges[0].firstByte() << "-" << ranges[0].lastByte());
response.setStatus(206);
startByte = ranges[0].firstByte();
@@ -87,19 +87,19 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
}
else
{
LMS_LOG(UTILS, DEBUG) << "No range requested";
LMS_LOG(UTILS, DEBUG, "No range requested");
response.setStatus(200);
_beyondLastByte = fileSize;
response.setContentLength(_beyondLastByte);
}
LMS_LOG(UTILS, DEBUG) << "Mimetype set to '" << _mimeType << "'";
LMS_LOG(UTILS, DEBUG, "Mimetype set to '" << _mimeType << "'");
response.setMimeType(_mimeType);
}
else if (!ifs)
{
LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'";
LMS_LOG(UTILS, ERROR, "Cannot reopen file stream for '" << _path.string() << "'");
return {};
}
@@ -113,19 +113,24 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
ifs.read(&buf[0], pieceSize);
const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) };
response.out().write(&buf[0], actualPieceSize);
if (actualPieceSize > 0)
{
response.out().write(&buf[0], actualPieceSize);
LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << startByte << "-" << startByte + actualPieceSize - 1 << "");
}
else
{
LMS_LOG(UTILS, DEBUG, "Written 0 byte");
}
LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes";
LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize;
if (ifs.good() && actualPieceSize < restSize)
{
_offset = startByte + actualPieceSize;
LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset;
LMS_LOG(UTILS, DEBUG, "Job not complete! Remaining range: " << _offset << "-" << _beyondLastByte - 1);
return response.createContinuation();
}
LMS_LOG(UTILS, DEBUG) << "Job complete!";
LMS_LOG(UTILS, DEBUG, "Job complete!");
return nullptr;
}
+5 -5
View File
@@ -21,13 +21,13 @@
#include <cstdlib>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount)
: _ioService {ioService}
, _work {ioService}
{
LMS_LOG(UTILS, INFO) << "Starting IO context with " << threadCount << " threads...";
LMS_LOG(UTILS, INFO, "Starting IO context with " << threadCount << " threads...");
for (std::size_t i {}; i < threadCount; ++i)
{
_threads.emplace_back([&]
@@ -38,7 +38,7 @@ IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t
}
catch (const std::exception& e)
{
LMS_LOG(UTILS, FATAL) << "Exception caught in IO context: " << e.what();
LMS_LOG(UTILS, FATAL, "Exception caught in IO context: " << e.what());
std::abort();
}
});
@@ -48,10 +48,10 @@ IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t
void
IOContextRunner::stop()
{
LMS_LOG(UTILS, DEBUG) << "Stopping IO context...";
LMS_LOG(UTILS, DEBUG, "Stopping IO context...");
_work.reset();
_ioService.stop();
LMS_LOG(UTILS, DEBUG) << "IO context stopped!";
LMS_LOG(UTILS, DEBUG, "IO context stopped!");
}
IOContextRunner::~IOContextRunner()
+8 -9
View File
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
const char* getModuleName(Module mod)
{
@@ -59,20 +59,19 @@ const char* getSeverityName(Severity sev)
return "";
}
Log::Log(Logger* logger, Module module, Severity severity)
: _module{ module },
_severity{ severity },
_logger{ logger }
Log::Log(ILogger& logger, Module module, Severity severity)
: _logger{ logger }
, _module{ module }
, _severity{ severity }
{}
Log::~Log()
{
if (_logger)
_logger->processLog(*this);
_logger.processLog(*this);
}
std::string
Log::getMessage() const
std::string Log::getMessage() const
{
return _oss.str();
}
+104 -114
View File
@@ -30,146 +30,136 @@
#include "utils/Crc32Calculator.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
namespace PathUtils
{
std::uint32_t computeCrc32(const std::filesystem::path& p)
{
Utils::Crc32Calculator crc32;
std::uint32_t
computeCrc32(const std::filesystem::path& p)
{
Utils::Crc32Calculator crc32;
std::ifstream ifs{ p.string().c_str(), std::ios_base::binary };
if (ifs)
{
do
{
std::array<char, 1024> buffer;
std::ifstream ifs {p.string().c_str(), std::ios_base::binary};
if (ifs)
{
do
{
std::array<char,1024> buffer;
ifs.read(buffer.data(), buffer.size());
crc32.processBytes(reinterpret_cast<const std::byte*>(buffer.data()), ifs.gcount());
} while (ifs);
}
else
{
LMS_LOG(DBUPDATER, ERROR, "Failed to open file '" << p.string() << "'");
throw LmsException("Failed to open file '" + p.string() + "'");
}
ifs.read( buffer.data(), buffer.size() );
crc32.processBytes( reinterpret_cast<const std::byte*>(buffer.data()), ifs.gcount() );
}
while (ifs);
}
else
{
LMS_LOG(DBUPDATER, ERROR) << "Failed to open file '" << p.string() << "'";
throw LmsException("Failed to open file '" + p.string() + "'" );
}
return crc32.getResult();
}
return crc32.getResult();
}
bool ensureDirectory(const std::filesystem::path& dir)
{
if (std::filesystem::exists(dir))
return std::filesystem::is_directory(dir);
else
return std::filesystem::create_directory(dir);
}
bool
ensureDirectory(const std::filesystem::path& dir)
{
if (std::filesystem::exists(dir))
return std::filesystem::is_directory(dir);
else
return std::filesystem::create_directory(dir);
}
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file)
{
struct stat sb {};
Wt::WDateTime
getLastWriteTime(const std::filesystem::path& file)
{
struct stat sb {};
if (stat(file.string().c_str(), &sb) == -1)
throw LmsException("Failed to get stats on file '" + file.string() + "'");
if (stat(file.string().c_str(), &sb) == -1)
throw LmsException("Failed to get stats on file '" + file.string() + "'" );
return Wt::WDateTime::fromTime_t(sb.st_mtime);
}
return Wt::WDateTime::fromTime_t(sb.st_mtime);
}
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName)
{
std::error_code ec;
std::filesystem::directory_iterator itPath{ directory, std::filesystem::directory_options::follow_directory_symlink, ec };
bool
exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName)
{
std::error_code ec;
std::filesystem::directory_iterator itPath {directory, std::filesystem::directory_options::follow_directory_symlink, ec};
if (ec)
{
cb(ec, directory);
return true; // try to continue exploring anyway
}
if (ec)
{
cb(ec, directory);
return true; // try to continue exploring anyway
}
if (excludeDirFileName && !excludeDirFileName->empty())
{
const std::filesystem::path excludePath{ directory / *excludeDirFileName };
if (excludeDirFileName && !excludeDirFileName->empty())
{
const std::filesystem::path excludePath {directory / *excludeDirFileName};
if (std::filesystem::exists(excludePath, ec))
{
LMS_LOG(DBUPDATER, DEBUG, "Found '" << excludePath.string() << "': skipping directory");
return true;
}
}
if (std::filesystem::exists(excludePath, ec))
{
LMS_LOG(DBUPDATER, DEBUG) << "Found '" << excludePath.string() << "': skipping directory";
return true;
}
}
std::filesystem::directory_iterator itEnd;
while (itPath != itEnd)
{
bool continueExploring{ true };
std::filesystem::directory_iterator itEnd;
while (itPath != itEnd)
{
bool continueExploring {true};
if (ec)
{
continueExploring = cb(ec, *itPath);
}
else
{
if (std::filesystem::is_regular_file(*itPath, ec))
{
continueExploring = cb(ec, *itPath);
}
else if (std::filesystem::is_directory(*itPath, ec))
{
if (!ec)
continueExploring = exploreFilesRecursive(*itPath, cb, excludeDirFileName);
else
continueExploring = cb(ec, *itPath);
}
}
if (ec)
{
continueExploring = cb(ec, *itPath);
}
else
{
if (std::filesystem::is_regular_file(*itPath, ec))
{
continueExploring = cb(ec, *itPath);
}
else if (std::filesystem::is_directory(*itPath, ec))
{
if (!ec)
continueExploring = exploreFilesRecursive(*itPath, cb, excludeDirFileName);
else
continueExploring = cb(ec, *itPath);
}
}
if (!continueExploring)
return false;
if (!continueExploring)
return false;
itPath.increment(ec);
}
itPath.increment(ec);
}
return true;
}
return true;
}
bool hasFileAnyExtension(const std::filesystem::path& file, const std::vector<std::filesystem::path>& supportedExtensions)
{
const std::filesystem::path extension{ StringUtils::stringToLower(file.extension().string()) };
bool
hasFileAnyExtension(const std::filesystem::path& file, const std::vector<std::filesystem::path>& supportedExtensions)
{
const std::filesystem::path extension {StringUtils::stringToLower(file.extension().string())};
return (std::find(std::cbegin(supportedExtensions), std::cend(supportedExtensions), extension) != std::cend(supportedExtensions));
}
return (std::find(std::cbegin(supportedExtensions), std::cend(supportedExtensions), extension) != std::cend(supportedExtensions));
}
bool isPathInRootPath(const std::filesystem::path& path, const std::filesystem::path& rootPath, const std::filesystem::path* excludeDirFileName)
{
std::filesystem::path curPath = path;
bool
isPathInRootPath(const std::filesystem::path& path, const std::filesystem::path& rootPath, const std::filesystem::path* excludeDirFileName)
{
std::filesystem::path curPath = path;
while (curPath.parent_path() != curPath)
{
curPath = curPath.parent_path();
while (curPath.parent_path() != curPath)
{
curPath = curPath.parent_path();
if (excludeDirFileName && !excludeDirFileName->empty())
{
assert(!excludeDirFileName->has_parent_path());
if (excludeDirFileName && !excludeDirFileName->empty())
{
assert(!excludeDirFileName->has_parent_path());
std::error_code ec;
if (std::filesystem::exists(curPath / *excludeDirFileName, ec))
return false;
}
if (curPath == rootPath)
return true;
}
return false;
}
std::error_code ec;
if (std::filesystem::exists(curPath / *excludeDirFileName, ec))
return false;
}
if (curPath == rootPath)
return true;
}
return false;
}
} // ns PathUtils
+16
View File
@@ -170,6 +170,22 @@ namespace StringUtils
return res;
}
std::string joinStrings(const std::vector<std::string_view>& strings, std::string_view delimiter)
{
std::string res;
bool first{ true };
for (std::string_view str : strings)
{
if (!first)
res += delimiter;
res += str;
first = false;
}
return res;
}
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
{
return boost::algorithm::join(strings, delimiter);
+26 -2
View File
@@ -21,10 +21,10 @@
#include <thread>
#include <sstream>
#include <Wt/WApplication.h>
#include <Wt/WServer.h>
#include <Wt/WLogger.h>
#include "utils/Logger.hpp"
#include "utils/Exception.hpp"
namespace
{
@@ -36,6 +36,30 @@ namespace
}
}
WtLogger::WtLogger(Severity minSeverity)
: _minSeverity{ minSeverity }
{
}
std::string WtLogger::computeLogConfig(Severity minSeverity)
{
switch (minSeverity)
{
case Severity::DEBUG: return "*";
case Severity::INFO: return "* -debug";
case Severity::WARNING: return "* -debug -info";
case Severity::ERROR: return "* -debug -info -warning";
case Severity::FATAL: return "* -debug -info -warning -error";
}
throw LmsException{ "Unhandled severity" };
}
bool WtLogger::isSeverityActive(Severity severity) const
{
return static_cast<int>(severity) <= static_cast<int>(_minSeverity);
}
void WtLogger::processLog(const Log& log)
{
Wt::log(getSeverityName(log.getSeverity())) << Wt::WLogger::sep << to_string(std::this_thread::get_id()) << Wt::WLogger::sep << "[" << getModuleName(log.getModule()) << "]" << Wt::WLogger::sep << log.getMessage();
+13 -13
View File
@@ -23,10 +23,10 @@
#include <boost/asio/bind_executor.hpp>
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[Http SendQueue] - "
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, "[Http SendQueue] - " << message)
namespace StringUtils
{
@@ -98,7 +98,7 @@ namespace Http
for (auto& [prio, requests] : _sendQueue)
{
LOG(DEBUG) << "Processing prio " << static_cast<int>(prio) << ", request count = " << requests.size();
LOG(DEBUG, "Processing prio " << static_cast<int>(prio) << ", request count = " << requests.size());
while (!requests.empty())
{
std::unique_ptr<ClientRequest> request {std::move(requests.front())};
@@ -118,7 +118,7 @@ namespace Http
SendQueue::sendRequest(const ClientRequest& request)
{
std::string url {_baseUrl + request.getParameters().relativeUrl};
LOG(DEBUG) << "Sending request to url '" << url << "'";
LOG(DEBUG, "Sending request to url '" << url << "'");
bool res {};
switch (request.getType())
@@ -133,7 +133,7 @@ namespace Http
}
if (!res)
LOG(ERROR) << "Send failed, bad url or unsupported scheme?";
LOG(ERROR, "Send failed, bad url or unsupported scheme?");
return res;
}
@@ -143,14 +143,14 @@ namespace Http
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG) << "Client aborted";
LOG(DEBUG, "Client aborted");
return;
}
assert(_currentRequest);
_state = State::Idle;
LOG(DEBUG) << "Client done. status = " << msg.status();
LOG(DEBUG, "Client done. status = " << msg.status());
if (ec)
onClientDoneError(std::move(_currentRequest), ec);
else
@@ -160,7 +160,7 @@ namespace Http
void
SendQueue::onClientDoneError(std::unique_ptr<ClientRequest> request, Wt::AsioWrapper::error_code ec)
{
LOG(ERROR) << "Retry " << request->retryCount << ", client error: '" << ec.message() << "'";
LOG(ERROR, "Retry " << request->retryCount << ", client error: '" << ec.message() << "'");
// may be a network error, try again later
throttle(_defaultRetryWaitDuration);
@@ -171,7 +171,7 @@ namespace Http
}
else
{
LOG(ERROR) << "Too many retries, giving up operation and throttle";
LOG(ERROR, "Too many retries, giving up operation and throttle");
if (request->getParameters().onFailureFunc)
request->getParameters().onFailureFunc();
}
@@ -189,7 +189,7 @@ namespace Http
}
const auto remainingCount {headerReadAs<std::size_t>(msg, "X-RateLimit-Remaining")};
LOG(DEBUG) << "Remaining messages = " << (remainingCount ? *remainingCount : 0);
LOG(DEBUG, "Remaining messages = " << (remainingCount ? *remainingCount : 0));
if (mustThrottle || (remainingCount && *remainingCount == 0))
{
const auto waitDuration {headerReadAs<std::chrono::seconds>(msg, "X-RateLimit-Reset-In")};
@@ -205,7 +205,7 @@ namespace Http
}
else
{
LOG(ERROR) << "Send error: '" << msg.body() << "'";
LOG(ERROR, "Send error: '" << msg.body() << "'");
if (requestParameters.onFailureFunc)
requestParameters.onFailureFunc();
}
@@ -221,14 +221,14 @@ namespace Http
assert(_state == State::Idle);
const std::chrono::seconds duration {clamp(requestedDuration, _minRetryWaitDuration, _maxRetryWaitDuration)};
LOG(DEBUG) << "Throttling for " << duration.count() << " seconds";
LOG(DEBUG, "Throttling for " << duration.count() << " seconds");
_throttleTimer.expires_after(duration);
_throttleTimer.async_wait([this](const boost::system::error_code& ec)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG) << "Throttle aborted";
LOG(DEBUG, "Throttle aborted");
return;
}
else if (ec)
@@ -22,6 +22,7 @@
#include <string>
#include <sstream>
#include "utils/String.hpp"
#include "Service.hpp"
enum class Severity
@@ -59,11 +60,11 @@ enum class Module
const char* getModuleName(Module mod);
const char* getSeverityName(Severity sev);
class Logger;
class ILogger;
class Log
{
public:
Log(Logger* logger, Module module, Severity severity);
Log(ILogger& logger, Module module, Severity severity);
~Log();
Module getModule() const { return _module; }
@@ -76,18 +77,24 @@ private:
Log(const Log&) = delete;
Log& operator=(const Log&) = delete;
ILogger& _logger;
Module _module;
Severity _severity;
std::ostringstream _oss;
Logger* _logger{};
};
class Logger
class ILogger
{
public:
virtual ~Logger() = default;
virtual ~ILogger() = default;
virtual bool isSeverityActive(Severity severity) const = 0;
virtual void processLog(const Log& log) = 0;
};
#define LMS_LOG(module, severity) Log{Service<Logger>::get(), Module::module, Severity::severity}.getOstream()
#define LMS_LOG_EX(module, severity) Log{Service<Logger>::get(), module, severity}.getOstream()
#define LMS_LOG(module, severity, message) \
do \
{ \
if (auto* logger {::Service<::ILogger>::get()}; logger && logger->isSeverityActive(::Severity::severity)) \
::Log{ *logger, ::Module::module, ::Severity::severity }.getOstream() << message; \
} while(0)
+10 -9
View File
@@ -20,19 +20,20 @@
#pragma once
#include "utils/EnumSet.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
class StreamLogger final : public Logger
class StreamLogger final : public ILogger
{
public:
static constexpr EnumSet<Severity> defaultSeverities {Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO};
public:
static constexpr EnumSet<Severity> defaultSeverities{ Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO };
StreamLogger(std::ostream& oss, EnumSet<Severity> severities = defaultSeverities);
StreamLogger(std::ostream& oss, EnumSet<Severity> severities = defaultSeverities);
void processLog(const Log& log);
bool isSeverityActive(Severity) const override { return true; }
void processLog(const Log& log) override;
private:
std::ostream& _os;
const EnumSet<Severity> _severities;
private:
std::ostream& _os;
const EnumSet<Severity> _severities;
};
+1 -4
View File
@@ -38,19 +38,16 @@ namespace Wt
namespace StringUtils {
[[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
[[nodiscard]] std::string joinStrings(const std::vector<std::string_view>& strings, std::string_view delimiter);
[[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
[[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]] std::string stringToLower(std::string_view str);
void stringToLower(std::string& str);
[[nodiscard]] std::string stringToUpper(const std::string& str);
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
+13 -5
View File
@@ -19,11 +19,19 @@
#pragma once
#include "Logger.hpp"
#include <string>
class WtLogger final : public Logger
#include "utils/ILogger.hpp"
class WtLogger final : public ILogger
{
public:
void processLog(const Log& log) override;
};
public:
WtLogger(Severity minSeverity);
static std::string computeLogConfig(Severity minSeverity);
private:
bool isSeverityActive(Severity severity) const override;
void processLog(const Log& log) override;
const Severity _minSeverity;
};
+129 -102
View File
@@ -26,155 +26,182 @@
TEST(StringUtils, splitString)
{
{
const std::string test{ "a" };
{
const std::string test{ "a" };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
{
const std::string test{ "a b" };
{
const std::string test{ "a b" };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "|") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a b");
}
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "|") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a b");
}
{
const std::string test{ " a" };
{
const std::string test{ " a" };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
{
const std::string test{ "a " };
{
const std::string test{ "a " };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 1);
EXPECT_EQ(strings.front(), "a");
}
{
const std::string test{ "a b" };
{
const std::string test{ "a b" };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings.front(), "a");
EXPECT_EQ(strings.back(), "b");
}
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings.front(), "a");
EXPECT_EQ(strings.back(), "b");
}
{
const std::string test{ "a b,c|defgh " };
{
const std::string test{ "a b,c|defgh " };
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ,|") };
ASSERT_EQ(strings.size(), 4);
EXPECT_EQ(strings[0], "a");
EXPECT_EQ(strings[1], "b");
EXPECT_EQ(strings[2], "c");
EXPECT_EQ(strings[3], "defgh");
}
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ,|") };
ASSERT_EQ(strings.size(), 4);
EXPECT_EQ(strings[0], "a");
EXPECT_EQ(strings[1], "b");
EXPECT_EQ(strings[2], "c");
EXPECT_EQ(strings[3], "defgh");
}
}
TEST(StringUtils, splitStringCopy)
{
{
const std::string test{ "test=foo" };
{
const std::string test{ "test=foo" };
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings[0], "test");
EXPECT_EQ(strings[1], "foo");
}
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings[0], "test");
EXPECT_EQ(strings[1], "foo");
}
{
const std::string test{ "test=foo bar" };
{
const std::string test{ "test=foo bar" };
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings[0], "test");
EXPECT_EQ(strings[1], "foo bar");
}
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
ASSERT_EQ(strings.size(), 2);
EXPECT_EQ(strings[0], "test");
EXPECT_EQ(strings[1], "foo bar");
}
}
TEST(StringUtils, joinStrings)
{
struct TestCase
{
std::vector<std::string_view> input;
std::string delimiter;
std::string expectedOutput;
};
TestCase tests[]
{
{{"a", "b", "c"}, "-", "a-b-c"},
{{"a", "b", "c"}, ",", "a,b,c"},
{{"a", "b", "c"}, "***", "a***b***c"},
{{"a", "", "c"}, "-", "a--c"},
{{"", "b", "c"}, "-", "-b-c"},
{{"a"}, "-", "a"},
{{"a"}, ",", "a"},
};
for (const TestCase& test : tests)
{
const std::string str{ StringUtils::joinStrings(test.input, test.delimiter) };
EXPECT_EQ(str, test.expectedOutput);
}
}
TEST(StringUtils, escapeJSString)
{
EXPECT_EQ(StringUtils::jsEscape(""), "");
EXPECT_EQ(StringUtils::jsEscape(R"(Test'.mp3)"), R"(Test\'.mp3)");
EXPECT_EQ(StringUtils::jsEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)");
EXPECT_EQ(StringUtils::jsEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)");
EXPECT_EQ(StringUtils::jsEscape(""), "");
EXPECT_EQ(StringUtils::jsEscape(R"(Test'.mp3)"), R"(Test\'.mp3)");
EXPECT_EQ(StringUtils::jsEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)");
EXPECT_EQ(StringUtils::jsEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)");
}
TEST(StringUtils, escapeJsonString)
{
EXPECT_EQ(StringUtils::jsonEscape(""), "");
EXPECT_EQ(StringUtils::jsonEscape(R"(Test'.mp3)"), R"(Test'.mp3)");
EXPECT_EQ(StringUtils::jsonEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)");
EXPECT_EQ(StringUtils::jsonEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)");
EXPECT_EQ(StringUtils::jsonEscape(""), "");
EXPECT_EQ(StringUtils::jsonEscape(R"(Test'.mp3)"), R"(Test'.mp3)");
EXPECT_EQ(StringUtils::jsonEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)");
EXPECT_EQ(StringUtils::jsonEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)");
}
TEST(StringUtils, escapeString)
{
EXPECT_EQ(StringUtils::escapeString("", "*", ' '), "");
EXPECT_EQ(StringUtils::escapeString("", "", ' '), "");
EXPECT_EQ(StringUtils::escapeString("a", "", ' '), "a");
EXPECT_EQ(StringUtils::escapeString("*", "*", '_'), "_*");
EXPECT_EQ(StringUtils::escapeString("*a*", "*", '_'), "_*a_*");
EXPECT_EQ(StringUtils::escapeString("*a|", "*|", '_'), "_*a_|");
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
EXPECT_EQ(StringUtils::escapeString("", "*", ' '), "");
EXPECT_EQ(StringUtils::escapeString("", "", ' '), "");
EXPECT_EQ(StringUtils::escapeString("a", "", ' '), "a");
EXPECT_EQ(StringUtils::escapeString("*", "*", '_'), "_*");
EXPECT_EQ(StringUtils::escapeString("*a*", "*", '_'), "_*a_*");
EXPECT_EQ(StringUtils::escapeString("*a|", "*|", '_'), "_*a_|");
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
}
TEST(StringUtils, readAs)
{
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
EXPECT_EQ(StringUtils::readAs<bool>("false"), false);
EXPECT_EQ(StringUtils::readAs<bool>("0"), false);
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
EXPECT_EQ(StringUtils::readAs<bool>("false"), false);
EXPECT_EQ(StringUtils::readAs<bool>("0"), false);
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
}
TEST(StringUtils, capitalize)
{
struct TestCase
{
std::string input;
std::string expectedOutput;
};
struct TestCase
{
std::string input;
std::string expectedOutput;
};
TestCase tests[]
{
{"", ""},
{"C", "C"},
{"c", "C"},
{" c", " C"},
{" cc", " Cc"},
{"(c", "(c"},
{"1c", "1c"},
{"&c", "&c"},
{"c c", "C c"}
};
TestCase tests[]
{
{"", ""},
{"C", "C"},
{"c", "C"},
{" c", " C"},
{" cc", " Cc"},
{"(c", "(c"},
{"1c", "1c"},
{"&c", "&c"},
{"c c", "C c"}
};
for (const TestCase& test : tests)
{
std::string str{ test.input };
StringUtils::capitalize(str);
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
}
for (const TestCase& test : tests)
{
std::string str{ test.input };
StringUtils::capitalize(str);
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
}
}
TEST(Stringutils, date)
{
const Wt::WDate date{ 2020, 01, 03 };
EXPECT_EQ(StringUtils::toISO8601String(date), "2020-01-03");
const Wt::WDate date{ 2020, 01, 03 };
EXPECT_EQ(StringUtils::toISO8601String(date), "2020-01-03");
}
TEST(Stringutils, dateTime)
{
const Wt::WDateTime dateTime{ Wt::WDate {2020, 01, 03 }, Wt::WTime{9, 8, 11, 75} };
EXPECT_EQ(StringUtils::toISO8601String(dateTime), "2020-01-03T09:08:11.075");
const Wt::WDateTime dateTime{ Wt::WDate {2020, 01, 03 }, Wt::WTime{9, 8, 11, 75} };
EXPECT_EQ(StringUtils::toISO8601String(dateTime), "2020-01-03T09:08:11.075");
}
+145 -128
View File
@@ -47,140 +47,156 @@
#include "utils/String.hpp"
#include "utils/WtLogger.hpp"
static
std::size_t
getThreadCount()
namespace
{
const unsigned long configHttpServerThreadCount{ Service<IConfig>::get()->getULong("http-server-thread-count", 0) };
// Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg)
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
}
static
std::vector<std::string>
generateWtConfig(std::string execPath)
{
std::vector<std::string> args;
const std::filesystem::path wtConfigPath{ Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml" };
const std::filesystem::path wtLogFilePath{ Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log") };
const std::filesystem::path wtAccessLogFilePath{ Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log") };
const std::filesystem::path wtResourcesPath{ Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources") };
args.push_back(execPath);
args.push_back("--config=" + wtConfigPath.string());
args.push_back("--docroot=" + std::string{ Service<IConfig>::get()->getString("docroot") });
args.push_back("--approot=" + std::string{ Service<IConfig>::get()->getString("approot") });
args.push_back("--deploy-path=" + std::string{ Service<IConfig>::get()->getString("deploy-path", "/") });
if (!wtResourcesPath.empty())
args.push_back("--resources-dir=" + wtResourcesPath.string());
if (Service<IConfig>::get()->getBool("tls-enable", false))
std::size_t getThreadCount()
{
args.push_back("--https-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
args.push_back("--https-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
args.push_back("--ssl-certificate=" + std::string{ Service<IConfig>::get()->getString("tls-cert") });
args.push_back("--ssl-private-key=" + std::string{ Service<IConfig>::get()->getString("tls-key") });
args.push_back("--ssl-tmp-dh=" + std::string{ Service<IConfig>::get()->getString("tls-dh") });
}
else
{
args.push_back("--http-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
args.push_back("--http-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
const unsigned long configHttpServerThreadCount{ Service<IConfig>::get()->getULong("http-server-thread-count", 0) };
// Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg)
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
}
if (!wtAccessLogFilePath.empty())
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
args.push_back("--threads=" + std::to_string(getThreadCount()));
// Generate the wt_config.xml file
boost::property_tree::ptree pt;
pt.put("server.application-settings.<xmlattr>.location", "*");
pt.put("server.application-settings.log-file", wtLogFilePath.string());
pt.put("server.application-settings.log-config", Service<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
pt.put("server.application-settings.behind-reverse-proxy", Service<IConfig>::get()->getBool("behind-reverse-proxy", false));
Severity getLogMinSeverity()
{
boost::property_tree::ptree viewport;
viewport.put("<xmlattr>.name", "viewport");
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
pt.add_child("server.application-settings.head-matter.meta", viewport);
}
{
boost::property_tree::ptree themeColor;
themeColor.put("<xmlattr>.name", "theme-color");
themeColor.put("<xmlattr>.content", "#303030");
pt.add_child("server.application-settings.head-matter.meta", themeColor);
std::string_view minSeverity{ Service<IConfig>::get()->getString("log-min-severity", "info") };
if (minSeverity == "debug")
return Severity::DEBUG;
else if (minSeverity == "info")
return Severity::INFO;
else if (minSeverity == "warning")
return Severity::WARNING;
else if (minSeverity == "error")
return Severity::ERROR;
else if (minSeverity == "fatal")
return Severity::FATAL;
throw LmsException{ "Invalid config value for 'log-min-severity'" };
}
std::vector<std::string> generateWtConfig(std::string execPath, Severity minSeverity)
{
std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out };
if (!oss)
throw LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" };
std::vector<std::string> args;
boost::property_tree::xml_parser::write_xml(oss, pt);
const std::filesystem::path wtConfigPath{ Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml" };
const std::filesystem::path wtLogFilePath{ Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log") };
const std::filesystem::path wtAccessLogFilePath{ Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log") };
const std::filesystem::path wtResourcesPath{ Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources") };
if (!oss)
throw LmsException{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" };
}
args.push_back(execPath);
args.push_back("--config=" + wtConfigPath.string());
args.push_back("--docroot=" + std::string{ Service<IConfig>::get()->getString("docroot") });
args.push_back("--approot=" + std::string{ Service<IConfig>::get()->getString("approot") });
args.push_back("--deploy-path=" + std::string{ Service<IConfig>::get()->getString("deploy-path", "/") });
if (!wtResourcesPath.empty())
args.push_back("--resources-dir=" + wtResourcesPath.string());
return args;
}
static
void
proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server)
{
auto postAll{ [](Wt::WServer& server, std::function<void()> cb)
{
server.postAll([cb = std::move(cb)]
if (Service<IConfig>::get()->getBool("tls-enable", false))
{
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
if (LmsApp)
cb();
args.push_back("--https-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
args.push_back("--https-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
args.push_back("--ssl-certificate=" + std::string{ Service<IConfig>::get()->getString("tls-cert") });
args.push_back("--ssl-private-key=" + std::string{ Service<IConfig>::get()->getString("tls-key") });
args.push_back("--ssl-tmp-dh=" + std::string{ Service<IConfig>::get()->getString("tls-dh") });
}
else
{
args.push_back("--http-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
args.push_back("--http-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
}
if (!wtAccessLogFilePath.empty())
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
args.push_back("--threads=" + std::to_string(getThreadCount()));
// Generate the wt_config.xml file
boost::property_tree::ptree pt;
pt.put("server.application-settings.<xmlattr>.location", "*");
pt.put("server.application-settings.log-file", wtLogFilePath.string());
// log-config
pt.put("server.application-settings.log-config", WtLogger::computeLogConfig(minSeverity));
pt.put("server.application-settings.behind-reverse-proxy", Service<IConfig>::get()->getBool("behind-reverse-proxy", false));
{
boost::property_tree::ptree viewport;
viewport.put("<xmlattr>.name", "viewport");
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
pt.add_child("server.application-settings.head-matter.meta", viewport);
}
{
boost::property_tree::ptree themeColor;
themeColor.put("<xmlattr>.name", "theme-color");
themeColor.put("<xmlattr>.content", "#303030");
pt.add_child("server.application-settings.head-matter.meta", themeColor);
}
{
std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out };
if (!oss)
throw LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" };
boost::property_tree::xml_parser::write_xml(oss, pt);
if (!oss)
throw LmsException{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" };
}
return args;
}
void proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server)
{
auto postAll{ [](Wt::WServer& server, std::function<void()> cb)
{
server.postAll([cb = std::move(cb)]
{
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
if (LmsApp)
cb();
});
} };
scanner.getEvents().scanStarted.connect([&]
{
postAll(server, []
{
LmsApp->getScannerEvents().scanStarted.emit();
LmsApp->triggerUpdate();
});
});
} };
scanner.getEvents().scanStarted.connect([&]
{
postAll(server, []
{
LmsApp->getScannerEvents().scanStarted.emit();
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanComplete.connect([&](const Scanner::ScanStats& stats)
{
postAll(server, [=]
{
LmsApp->getScannerEvents().scanComplete.emit(stats);
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanComplete.connect([&](const Scanner::ScanStats& stats)
{
postAll(server, [=]
{
LmsApp->getScannerEvents().scanComplete.emit(stats);
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanInProgress.connect([&](const Scanner::ScanStepStats& stats)
{
postAll(server, [=]
{
LmsApp->getScannerEvents().scanInProgress.emit(stats);
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanInProgress.connect([&](const Scanner::ScanStepStats& stats)
{
postAll(server, [=]
{
LmsApp->getScannerEvents().scanInProgress.emit(stats);
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime)
{
postAll(server, [=]
{
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
LmsApp->triggerUpdate();
});
});
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime)
{
postAll(server, [=]
{
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
LmsApp->triggerUpdate();
});
});
}
}
int main(int argc, char* argv[])
@@ -206,20 +222,21 @@ int main(int argc, char* argv[])
close(STDIN_FILENO);
Service<IConfig> config{ createConfig(configFilePath) };
Service<Logger> logger{ std::make_unique<WtLogger>() };
const Severity minLogSeverity{getLogMinSeverity()};
Service<ILogger> logger{ std::make_unique<WtLogger>(minLogSeverity) };
// use system locale. libarchive relies on this to write filenames
if (char* locale{ ::setlocale(LC_ALL, "") })
LMS_LOG(MAIN, INFO) << "locale set to '" << locale << "'";
LMS_LOG(MAIN, INFO, "locale set to '" << locale << "'");
else
LMS_LOG(MAIN, WARNING) << "Cannot set locale from system";
LMS_LOG(MAIN, WARNING, "Cannot set locale from system");
// Make sure the working directory exists
std::filesystem::create_directories(config->getPath("working-dir"));
std::filesystem::create_directories(config->getPath("working-dir") / "cache");
// Construct WT configuration and get the argc/argv back
const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0]) };
const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0], minLogSeverity) };
std::vector<const char*> wtArgv(wtServerArgs.size());
for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
@@ -300,27 +317,27 @@ int main(int argc, char* argv[])
proxyScannerEventsToApplication(*scannerService, server);
LMS_LOG(MAIN, INFO) << "Starting server...";
LMS_LOG(MAIN, INFO, "Starting server...");
server.start();
LMS_LOG(MAIN, INFO) << "Now running...";
LMS_LOG(MAIN, INFO, "Now running...");
Wt::WServer::waitForShutdown();
LMS_LOG(MAIN, INFO) << "Stopping server...";
LMS_LOG(MAIN, INFO, "Stopping server...");
server.stop();
LMS_LOG(MAIN, INFO) << "Quitting...";
LMS_LOG(MAIN, INFO, "Quitting...");
res = EXIT_SUCCESS;
}
catch (const Wt::WServer::Exception& e)
{
LMS_LOG(MAIN, FATAL) << "Caught WServer::Exception: " << e.what();
LMS_LOG(MAIN, FATAL, "Caught WServer::Exception: " << e.what());
std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl;
res = EXIT_FAILURE;
}
catch (const std::exception& e)
{
LMS_LOG(MAIN, FATAL) << "Caught std::exception: " << e.what();
LMS_LOG(MAIN, FATAL, "Caught std::exception: " << e.what());
std::cerr << "Caught std::exception: " << e.what() << std::endl;
res = EXIT_FAILURE;
}
+1 -1
View File
@@ -32,7 +32,7 @@
#include "services/auth/IPasswordService.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "common/LoginNameValidator.hpp"
+419 -446
View File
@@ -37,7 +37,7 @@
#include "services/database/TrackList.hpp"
#include "services/database/User.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
@@ -62,534 +62,507 @@
#include "PlayQueue.hpp"
#include "SettingsView.hpp"
namespace UserInterface {
static
std::shared_ptr<Wt::WMessageResourceBundle>
createMessageResourceBundle()
namespace UserInterface
{
const std::string appRoot {Wt::WApplication::appRoot()};
namespace
{
constexpr const char* defaultPath{ "/releases" };
auto res {std::make_shared<Wt::WMessageResourceBundle>()};
res->use(appRoot + "admin-database");
res->use(appRoot + "admin-initwizard");
res->use(appRoot + "admin-scannercontroller");
res->use(appRoot + "admin-user");
res->use(appRoot + "admin-users");
res->use(appRoot + "artist");
res->use(appRoot + "artists");
res->use(appRoot + "error");
res->use(appRoot + "explore");
res->use(appRoot + "login");
res->use(appRoot + "main");
res->use(appRoot + "mediaplayer");
res->use(appRoot + "messages");
res->use(appRoot + "misc");
res->use(appRoot + "notifications");
res->use(appRoot + "playqueue");
res->use(appRoot + "release");
res->use(appRoot + "releases");
res->use(appRoot + "search");
res->use(appRoot + "settings");
res->use(appRoot + "tracklist");
res->use(appRoot + "tracklists");
res->use(appRoot + "tracks");
std::shared_ptr<Wt::WMessageResourceBundle> createMessageResourceBundle()
{
const std::string appRoot{ Wt::WApplication::appRoot() };
return res;
}
auto res{ std::make_shared<Wt::WMessageResourceBundle>() };
res->use(appRoot + "admin-database");
res->use(appRoot + "admin-initwizard");
res->use(appRoot + "admin-scannercontroller");
res->use(appRoot + "admin-user");
res->use(appRoot + "admin-users");
res->use(appRoot + "artist");
res->use(appRoot + "artists");
res->use(appRoot + "error");
res->use(appRoot + "explore");
res->use(appRoot + "login");
res->use(appRoot + "main");
res->use(appRoot + "mediaplayer");
res->use(appRoot + "messages");
res->use(appRoot + "misc");
res->use(appRoot + "notifications");
res->use(appRoot + "playqueue");
res->use(appRoot + "release");
res->use(appRoot + "releases");
res->use(appRoot + "search");
res->use(appRoot + "settings");
res->use(appRoot + "tracklist");
res->use(appRoot + "tracklists");
res->use(appRoot + "tracks");
static
std::shared_ptr<Wt::WMessageResourceBundle>
getOrCreateMessageBundle()
{
static std::shared_ptr<Wt::WMessageResourceBundle> res {createMessageResourceBundle()};
return res;
}
return res;
}
static constexpr const char* defaultPath {"/releases"};
std::shared_ptr<Wt::WMessageResourceBundle> getOrCreateMessageBundle()
{
static std::shared_ptr<Wt::WMessageResourceBundle> res{ createMessageResourceBundle() };
return res;
}
std::unique_ptr<Wt::WApplication>
LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager)
{
if (auto *authEnvService {Service<::Auth::IEnvService>::get()})
{
const auto checkResult {authEnvService->processEnv(env)};
if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
{
LMS_LOG(UI, ERROR) << "Cannot authenticate user from environment!";
// return a blank page
return std::make_unique<Wt::WApplication>(env);
}
enum IdxRoot
{
IdxExplore = 0,
IdxPlayQueue,
IdxSettings,
IdxAdminDatabase,
IdxAdminUsers,
IdxAdminUser,
};
return std::make_unique<LmsApplication>(env, db, appManager, checkResult.userId);
}
void handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
{
static const struct
{
std::string path;
int index;
bool admin;
std::optional<Wt::WString> title;
} views[] =
{
{ "/artists", IdxExplore, false, Wt::WString::tr("Lms.Explore.artists") },
{ "/artist", IdxExplore, false, std::nullopt },
{ "/releases", IdxExplore, false, Wt::WString::tr("Lms.Explore.releases") },
{ "/release", IdxExplore, false, std::nullopt },
{ "/search", IdxExplore, false, Wt::WString::tr("Lms.Explore.search") },
{ "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") },
{ "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") },
{ "/tracklist", IdxExplore, false, std::nullopt },
{ "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") },
{ "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") },
{ "/admin/database", IdxAdminDatabase, true, Wt::WString::tr("Lms.Admin.Database.database") },
{ "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") },
{ "/admin/user", IdxAdminUser, true, std::nullopt },
};
return std::make_unique<LmsApplication>(env, db, appManager);
}
LMS_LOG(UI, DEBUG, "Internal path changed to '" << wApp->internalPath() << "'");
LmsApplication*
LmsApplication::instance()
{
return reinterpret_cast<LmsApplication*>(Wt::WApplication::instance());
}
for (const auto& view : views)
{
if (wApp->internalPathMatches(view.path))
{
if (view.admin && !isAdmin)
break;
Database::Db&
LmsApplication::getDb()
{
return _db;
}
stack.setCurrentIndex(view.index);
if (view.title)
LmsApp->setTitle(*view.title);
Database::Session&
LmsApplication::getDbSession()
{
return _db.getTLSSession();
}
LmsApp->doJavaScript(LmsApp->javaScriptClass() + ".updateActiveNav('" + view.path + "')");
return;
}
}
Database::User::pointer
LmsApplication::getUser()
{
if (!_authenticatedUser)
return {};
wApp->setInternalPath(defaultPath, true);
}
}
return Database::User::find(getDbSession(), _authenticatedUser->userId);
}
std::unique_ptr<Wt::WApplication> LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager)
{
if (auto * authEnvService{ Service<::Auth::IEnvService>::get() })
{
const auto checkResult{ authEnvService->processEnv(env) };
if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
{
LMS_LOG(UI, ERROR, "Cannot authenticate user from environment!");
// return a blank page
return std::make_unique<Wt::WApplication>(env);
}
Database::UserId
LmsApplication::getUserId()
{
return _authenticatedUser->userId;
}
return std::make_unique<LmsApplication>(env, db, appManager, checkResult.userId);
}
bool
LmsApplication::isUserAuthStrong() const
{
return _authenticatedUser->strongAuth;
}
return std::make_unique<LmsApplication>(env, db, appManager);
}
Database::UserType
LmsApplication::getUserType()
{
auto transaction {getDbSession().createReadTransaction()};
LmsApplication* LmsApplication::instance()
{
return static_cast<LmsApplication*>(Wt::WApplication::instance());
}
return getUser()->getType();
}
Database::Db& LmsApplication::getDb()
{
return _db;
}
std::string
LmsApplication::getUserLoginName()
{
auto transaction {getDbSession().createReadTransaction()};
Database::Session& LmsApplication::getDbSession()
{
return _db.getTLSSession();
}
return getUser()->getLoginName();
}
Database::User::pointer LmsApplication::getUser()
{
if (!_authenticatedUser)
return {};
LmsApplication::LmsApplication(const Wt::WEnvironment& env,
Database::Db& db,
LmsApplicationManager& appManager,
std::optional<Database::UserId> userId)
: Wt::WApplication {env}
, _db {db}
, _appManager {appManager}
, _authenticatedUser {userId ? std::make_optional<UserAuthInfo>(UserAuthInfo {*userId, false}) : std::nullopt}
{
try
{
init();
}
catch (LmsApplicationException& e)
{
LMS_LOG(UI, WARNING) << "Caught a LmsApplication exception: " << e.what();
handleException(e);
}
catch (std::exception& e)
{
LMS_LOG(UI, ERROR) << "Caught exception: " << e.what();
throw LmsException {"Internal error"}; // Do not put details here at it may appear on the user rendered html
}
}
return Database::User::find(getDbSession(), _authenticatedUser->userId);
}
LmsApplication::~LmsApplication() = default;
Database::UserId LmsApplication::getUserId()
{
return _authenticatedUser->userId;
}
void
LmsApplication::init()
{
setTheme(std::make_shared<LmsTheme>());
bool LmsApplication::isUserAuthStrong() const
{
return _authenticatedUser->strongAuth;
}
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
require("js/mediaplayer.js");
Database::UserType LmsApplication::getUserType()
{
auto transaction{ getDbSession().createReadTransaction() };
setTitle();
setLocalizedStrings(getOrCreateMessageBundle());
return getUser()->getType();
}
// Handle Media Scanner events and other session events
enableUpdates(true);
std::string LmsApplication::getUserLoginName()
{
auto transaction{ getDbSession().createReadTransaction() };
if (_authenticatedUser)
onUserLoggedIn();
else if (Service<::Auth::IPasswordService>::exists())
processPasswordAuth();
}
return getUser()->getLoginName();
}
void
LmsApplication::processPasswordAuth()
{
{
std::optional<Database::UserId> userId {processAuthToken(environment())};
if (userId)
{
LMS_LOG(UI, DEBUG) << "User authenticated using Auth token!";
_authenticatedUser = {*userId, false};
onUserLoggedIn();
return;
}
}
LmsApplication::LmsApplication(const Wt::WEnvironment& env,
Database::Db& db,
LmsApplicationManager& appManager,
std::optional<Database::UserId> userId)
: Wt::WApplication{ env }
, _db{ db }
, _appManager{ appManager }
, _authenticatedUser{ userId ? std::make_optional<UserAuthInfo>(UserAuthInfo {*userId, false}) : std::nullopt }
{
try
{
init();
}
catch (LmsApplicationException& e)
{
LMS_LOG(UI, WARNING, "Caught a LmsApplication exception: " << e.what());
handleException(e);
}
catch (std::exception& e)
{
LMS_LOG(UI, ERROR, "Caught exception: " << e.what());
throw LmsException{ "Internal error" }; // Do not put details here at it may appear on the user rendered html
}
}
// If here is no account in the database, launch the first connection wizard
bool firstConnection {};
{
auto transaction {getDbSession().createReadTransaction()};
firstConnection = Database::User::getCount(getDbSession()) == 0;
}
LmsApplication::~LmsApplication() = default;
LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection;
void LmsApplication::init()
{
setTheme(std::make_shared<LmsTheme>());
if (firstConnection && Service<::Auth::IPasswordService>::get()->canSetPasswords())
{
root()->addWidget(std::make_unique<InitWizardView>());
}
else
{
Auth* auth {root()->addNew<Auth>()};
auth->userLoggedIn.connect(this, [this](Database::UserId userId)
{
_authenticatedUser = {userId, true};
onUserLoggedIn();
});
}
}
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
require("js/mediaplayer.js");
void
LmsApplication::finalize()
{
if (_authenticatedUser)
_appManager.unregisterApplication(*this);
setTitle();
setLocalizedStrings(getOrCreateMessageBundle());
preQuit().emit();
}
// Handle Media Scanner events and other session events
enableUpdates(true);
void
LmsApplication::handleException(LmsApplicationException& e)
{
root()->clear();
Wt::WTemplate* t {root()->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Error.template"))};
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
if (_authenticatedUser)
onUserLoggedIn();
else if (Service<::Auth::IPasswordService>::exists())
processPasswordAuth();
}
t->bindString("error", e.what(), Wt::TextFormat::Plain);
Wt::WPushButton* btn {t->bindNew<Wt::WPushButton>("btn-go-home", Wt::WString::tr("Lms.Error.go-home"))};
btn->clicked().connect([this]()
{
redirect(defaultPath);
});
}
void LmsApplication::processPasswordAuth()
{
{
std::optional<Database::UserId> userId{ processAuthToken(environment()) };
if (userId)
{
LMS_LOG(UI, DEBUG, "User authenticated using Auth token!");
_authenticatedUser = { *userId, false };
onUserLoggedIn();
return;
}
}
void
LmsApplication::goHomeAndQuit()
{
WApplication::quit("");
redirect(".");
}
// If here is no account in the database, launch the first connection wizard
bool firstConnection{};
{
auto transaction{ getDbSession().createReadTransaction() };
firstConnection = Database::User::getCount(getDbSession()) == 0;
}
enum IdxRoot
{
IdxExplore = 0,
IdxPlayQueue,
IdxSettings,
IdxAdminDatabase,
IdxAdminUsers,
IdxAdminUser,
};
LMS_LOG(UI, DEBUG, "Creating root widget. First connection = " << firstConnection);
static
void
handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
{
static const struct
{
std::string path;
int index;
bool admin;
std::optional<Wt::WString> title;
} views[] =
{
{ "/artists", IdxExplore, false, Wt::WString::tr("Lms.Explore.artists") },
{ "/artist", IdxExplore, false, std::nullopt },
{ "/releases", IdxExplore, false, Wt::WString::tr("Lms.Explore.releases") },
{ "/release", IdxExplore, false, std::nullopt },
{ "/search", IdxExplore, false, Wt::WString::tr("Lms.Explore.search") },
{ "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") },
{ "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") },
{ "/tracklist", IdxExplore, false, std::nullopt },
{ "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") },
{ "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") },
{ "/admin/database", IdxAdminDatabase, true, Wt::WString::tr("Lms.Admin.Database.database") },
{ "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") },
{ "/admin/user", IdxAdminUser, true, std::nullopt },
};
if (firstConnection && Service<::Auth::IPasswordService>::get()->canSetPasswords())
{
root()->addWidget(std::make_unique<InitWizardView>());
}
else
{
Auth* auth{ root()->addNew<Auth>() };
auth->userLoggedIn.connect(this, [this](Database::UserId userId)
{
_authenticatedUser = { userId, true };
onUserLoggedIn();
});
}
}
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
void LmsApplication::finalize()
{
if (_authenticatedUser)
_appManager.unregisterApplication(*this);
for (const auto& view : views)
{
if (wApp->internalPathMatches(view.path))
{
if (view.admin && !isAdmin)
break;
preQuit().emit();
}
stack.setCurrentIndex(view.index);
if (view.title)
LmsApp->setTitle(*view.title);
void LmsApplication::handleException(LmsApplicationException& e)
{
root()->clear();
Wt::WTemplate* t{ root()->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Error.template")) };
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
LmsApp->doJavaScript(LmsApp->javaScriptClass() + ".updateActiveNav('" + view.path +"')");
return;
}
}
t->bindString("error", e.what(), Wt::TextFormat::Plain);
Wt::WPushButton* btn{ t->bindNew<Wt::WPushButton>("btn-go-home", Wt::WString::tr("Lms.Error.go-home")) };
btn->clicked().connect([this]()
{
redirect(defaultPath);
});
}
wApp->setInternalPath(defaultPath, true);
}
void LmsApplication::goHomeAndQuit()
{
WApplication::quit("");
redirect(".");
}
void
LmsApplication::logoutUser()
{
{
auto transaction {getDbSession().createWriteTransaction()};
getUser().modify()->clearAuthTokens();
}
void LmsApplication::logoutUser()
{
{
auto transaction{ getDbSession().createWriteTransaction() };
getUser().modify()->clearAuthTokens();
}
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out";
goHomeAndQuit();
}
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << " 'logged out");
goHomeAndQuit();
}
void
LmsApplication::onUserLoggedIn()
{
root()->clear();
void LmsApplication::onUserLoggedIn()
{
root()->clear();
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent();
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent());
_appManager.registerApplication(*this);
_appManager.applicationRegistered.connect(this, [this] (LmsApplication& otherApplication)
{
// Only one active session by user
if (otherApplication.getUserId() == getUserId())
{
if (LmsApp->getUserType() != Database::UserType::DEMO)
{
quit(Wt::WString::tr("Lms.quit-other-session"));
}
}
});
_appManager.registerApplication(*this);
_appManager.applicationRegistered.connect(this, [this](LmsApplication& otherApplication)
{
// Only one active session by user
if (otherApplication.getUserId() == getUserId())
{
if (LmsApp->getUserType() != Database::UserType::DEMO)
{
quit(Wt::WString::tr("Lms.quit-other-session"));
}
}
});
createHome();
}
createHome();
}
void
LmsApplication::createHome()
{
_coverResource = std::make_shared<CoverResource>();
void LmsApplication::createHome()
{
_coverResource = std::make_shared<CoverResource>();
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
declareJavaScriptFunction("updateActiveNav",
R"(function(current) {
const menuItems = document.querySelectorAll('.nav-item a[href]:not([href=""])');
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
declareJavaScriptFunction("updateActiveNav",
R"(function(current) {
const menuItems = document.querySelectorAll('.nav-item a[href]:not([href=""])');
for (const menuItem of menuItems) {
if (menuItem.getAttribute("href") === current) {
menuItem.classList.add('active');
}
else {
else {
menuItem.classList.remove('active');
}
}
}
})");
Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.main.template")))};
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WTemplate* main{ root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.main.template"))) };
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
Template* navbar {main->bindNew<Template>("navbar", Wt::WString::tr("Lms.main.template.navbar"))};
navbar->addFunction("tr", &Wt::WTemplate::Functions::tr);
Template* navbar{ main->bindNew<Template>("navbar", Wt::WString::tr("Lms.main.template.navbar")) };
navbar->addFunction("tr", &Wt::WTemplate::Functions::tr);
_notificationContainer = main->bindNew<NotificationContainer>("notifications");
_modalManager = main->bindNew<ModalManager>("modal");
_notificationContainer = main->bindNew<NotificationContainer>("notifications");
_modalManager = main->bindNew<ModalManager>("modal");
// MediaPlayer
_mediaPlayer = main->bindNew<MediaPlayer>("player");
// MediaPlayer
_mediaPlayer = main->bindNew<MediaPlayer>("player");
navbar->bindNew<Wt::WAnchor>("title", Wt::WLink {Wt::LinkType::InternalPath, defaultPath}, "LMS");
navbar->bindNew<Wt::WAnchor>("artists", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}, Wt::WString::tr("Lms.Explore.artists"));
navbar->bindNew<Wt::WAnchor>("releases", Wt::WLink {Wt::LinkType::InternalPath, "/releases"}, Wt::WString::tr("Lms.Explore.releases"));
navbar->bindNew<Wt::WAnchor>("tracks", Wt::WLink {Wt::LinkType::InternalPath, "/tracks"}, Wt::WString::tr("Lms.Explore.tracks"));
navbar->bindNew<Wt::WAnchor>("tracklists", Wt::WLink {Wt::LinkType::InternalPath, "/tracklists"}, Wt::WString::tr("Lms.Explore.tracklists"));
navbar->bindNew<Wt::WAnchor>("title", Wt::WLink{ Wt::LinkType::InternalPath, defaultPath }, "LMS");
navbar->bindNew<Wt::WAnchor>("artists", Wt::WLink{ Wt::LinkType::InternalPath, "/artists" }, Wt::WString::tr("Lms.Explore.artists"));
navbar->bindNew<Wt::WAnchor>("releases", Wt::WLink{ Wt::LinkType::InternalPath, "/releases" }, Wt::WString::tr("Lms.Explore.releases"));
navbar->bindNew<Wt::WAnchor>("tracks", Wt::WLink{ Wt::LinkType::InternalPath, "/tracks" }, Wt::WString::tr("Lms.Explore.tracks"));
navbar->bindNew<Wt::WAnchor>("tracklists", Wt::WLink{ Wt::LinkType::InternalPath, "/tracklists" }, Wt::WString::tr("Lms.Explore.tracklists"));
Filters* filters {navbar->bindNew<Filters>("filters")};
navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
Filters* filters{ navbar->bindNew<Filters>("filters") };
navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink{ Wt::LinkType::InternalPath, "/settings" }, Wt::WString::tr("Lms.Settings.menu-settings"));
{
Wt::WAnchor* logout {navbar->bindNew<Wt::WAnchor>("logout")};
logout->setText(Wt::WString::tr("Lms.logout"));
logout->clicked().connect(this, &LmsApplication::logoutUser);
}
{
Wt::WAnchor* logout{ navbar->bindNew<Wt::WAnchor>("logout") };
logout->setText(Wt::WString::tr("Lms.logout"));
logout->clicked().connect(this, &LmsApplication::logoutUser);
}
Wt::WLineEdit* searchEdit {navbar->bindNew<Wt::WLineEdit>("search")};
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
Wt::WLineEdit* searchEdit{ navbar->bindNew<Wt::WLineEdit>("search") };
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
if (LmsApp->getUserType() == Database::UserType::ADMIN)
{
navbar->setCondition("if-is-admin", true);
navbar->bindNew<Wt::WAnchor>("database", Wt::WLink {Wt::LinkType::InternalPath, "/admin/database"}, Wt::WString::tr("Lms.Admin.Database.menu-database"));
navbar->bindNew<Wt::WAnchor>("users", Wt::WLink {Wt::LinkType::InternalPath, "/admin/users"}, Wt::WString::tr("Lms.Admin.Users.menu-users"));
}
if (LmsApp->getUserType() == Database::UserType::ADMIN)
{
navbar->setCondition("if-is-admin", true);
navbar->bindNew<Wt::WAnchor>("database", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/database" }, Wt::WString::tr("Lms.Admin.Database.menu-database"));
navbar->bindNew<Wt::WAnchor>("users", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/users" }, Wt::WString::tr("Lms.Admin.Users.menu-users"));
}
// Contents
// Order is important in mainStack, see IdxRoot!
Wt::WStackedWidget* mainStack {main->bindNew<Wt::WStackedWidget>("contents")};
mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
// Contents
// Order is important in mainStack, see IdxRoot!
Wt::WStackedWidget* mainStack{ main->bindNew<Wt::WStackedWidget>("contents") };
mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
std::unique_ptr<PlayQueue> playQueue {std::make_unique<PlayQueue>()};
Explore* explore {mainStack->addNew<Explore>(*filters, *playQueue)};
_playQueue = mainStack->addWidget(std::move(playQueue));
mainStack->addNew<SettingsView>();
std::unique_ptr<PlayQueue> playQueue{ std::make_unique<PlayQueue>() };
Explore* explore{ mainStack->addNew<Explore>(*filters, *playQueue) };
_playQueue = mainStack->addWidget(std::move(playQueue));
mainStack->addNew<SettingsView>();
searchEdit->enterPressed().connect([=]
{
setInternalPath("/search", true);
});
searchEdit->enterPressed().connect([=]
{
setInternalPath("/search", true);
});
searchEdit->textInput().connect([=]
{
setInternalPath("/search", true);
explore->search(searchEdit->text());
});
searchEdit->textInput().connect([=]
{
setInternalPath("/search", true);
explore->search(searchEdit->text());
});
// Admin stuff
if (getUserType() == Database::UserType::ADMIN)
{
mainStack->addNew<DatabaseSettingsView>();
mainStack->addNew<UsersView>();
mainStack->addNew<UserView>();
}
// Admin stuff
if (getUserType() == Database::UserType::ADMIN)
{
mainStack->addNew<DatabaseSettingsView>();
mainStack->addNew<UsersView>();
mainStack->addNew<UserView>();
}
explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity());
explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity());
// Events from MediaPlayer
_mediaPlayer->playNext.connect([this]
{
_playQueue->playNext();
});
_mediaPlayer->playPrevious.connect([this]
{
_playQueue->playPrevious();
});
// Events from MediaPlayer
_mediaPlayer->playNext.connect([this]
{
_playQueue->playNext();
});
_mediaPlayer->playPrevious.connect([this]
{
_playQueue->playPrevious();
});
_mediaPlayer->scrobbleListenNow.connect([this](Database::TrackId trackId)
{
LMS_LOG(UI, DEBUG) << "Received ScrobbleListenNow from player for trackId = " << trackId.toString();
const Scrobbling::Listen listen {getUserId(), trackId};
Service<Scrobbling::IScrobblingService>::get()->listenStarted(listen);
});
_mediaPlayer->scrobbleListenFinished.connect([this](Database::TrackId trackId, unsigned durationMs)
{
LMS_LOG(UI, DEBUG) << "Received ScrobbleListenFinished from player for trackId = " << trackId.toString() << ", duration = " << (durationMs / 1000) << "s";
const std::chrono::milliseconds duration {durationMs};
const Scrobbling::Listen listen {getUserId(), trackId};
Service<Scrobbling::IScrobblingService>::get()->listenFinished(listen, std::chrono::duration_cast<std::chrono::seconds>(duration));
});
_mediaPlayer->scrobbleListenNow.connect([this](Database::TrackId trackId)
{
LMS_LOG(UI, DEBUG, "Received ScrobbleListenNow from player for trackId = " << trackId.toString());
const Scrobbling::Listen listen{ getUserId(), trackId };
Service<Scrobbling::IScrobblingService>::get()->listenStarted(listen);
});
_mediaPlayer->scrobbleListenFinished.connect([this](Database::TrackId trackId, unsigned durationMs)
{
LMS_LOG(UI, DEBUG, "Received ScrobbleListenFinished from player for trackId = " << trackId.toString() << ", duration = " << (durationMs / 1000) << "s");
const std::chrono::milliseconds duration{ durationMs };
const Scrobbling::Listen listen{ getUserId(), trackId };
Service<Scrobbling::IScrobblingService>::get()->listenFinished(listen, std::chrono::duration_cast<std::chrono::seconds>(duration));
});
_mediaPlayer->playbackEnded.connect([this]
{
_playQueue->onPlaybackEnded();
});
_mediaPlayer->playbackEnded.connect([this]
{
_playQueue->onPlaybackEnded();
});
_playQueue->trackSelected.connect([this] (Database::TrackId trackId, bool play, float replayGain)
{
_mediaPlayer->loadTrack(trackId, play, replayGain);
});
_playQueue->trackSelected.connect([this](Database::TrackId trackId, bool play, float replayGain)
{
_mediaPlayer->loadTrack(trackId, play, replayGain);
});
_playQueue->trackUnselected.connect([this]
{
_mediaPlayer->stop();
});
_playQueue->trackCountChanged.connect([this] (std::size_t trackCount)
{
_mediaPlayer->onPlayQueueUpdated(trackCount);
});
_mediaPlayer->onPlayQueueUpdated(_playQueue->getCount());
_playQueue->trackUnselected.connect([this]
{
_mediaPlayer->stop();
});
_playQueue->trackCountChanged.connect([this](std::size_t trackCount)
{
_mediaPlayer->onPlayQueueUpdated(trackCount);
});
_mediaPlayer->onPlayQueueUpdated(_playQueue->getCount());
const bool isAdmin {getUserType() == Database::UserType::ADMIN};
if (isAdmin)
{
_scannerEvents.scanComplete.connect([=] (const Scanner::ScanStats& stats)
{
notifyMsg(Notification::Type::Info,
Wt::WString::tr("Lms.Admin.Database.database"),
Wt::WString::tr("Lms.Admin.Database.scan-complete")
.arg(static_cast<unsigned>(stats.nbFiles()))
.arg(static_cast<unsigned>(stats.additions))
.arg(static_cast<unsigned>(stats.updates))
.arg(static_cast<unsigned>(stats.deletions))
.arg(static_cast<unsigned>(stats.duplicates.size()))
.arg(static_cast<unsigned>(stats.errors.size())));
});
}
const bool isAdmin{ getUserType() == Database::UserType::ADMIN };
if (isAdmin)
{
_scannerEvents.scanComplete.connect([=](const Scanner::ScanStats& stats)
{
notifyMsg(Notification::Type::Info,
Wt::WString::tr("Lms.Admin.Database.database"),
Wt::WString::tr("Lms.Admin.Database.scan-complete")
.arg(static_cast<unsigned>(stats.nbFiles()))
.arg(static_cast<unsigned>(stats.additions))
.arg(static_cast<unsigned>(stats.updates))
.arg(static_cast<unsigned>(stats.deletions))
.arg(static_cast<unsigned>(stats.duplicates.size()))
.arg(static_cast<unsigned>(stats.errors.size())));
});
}
internalPathChanged().connect(mainStack, [=]
{
handlePathChange(*mainStack, isAdmin);
});
internalPathChanged().connect(mainStack, [=]
{
handlePathChange(*mainStack, isAdmin);
});
handlePathChange(*mainStack, isAdmin);
}
handlePathChange(*mainStack, isAdmin);
}
void
LmsApplication::notify(const Wt::WEvent& event)
{
try
{
WApplication::notify(event);
}
catch (LmsApplicationException& e)
{
LMS_LOG(UI, WARNING) << "Caught a LmsApplication exception: " << e.what();
handleException(e);
}
catch (std::exception& e)
{
LMS_LOG(UI, ERROR) << "Caught exception: " << e.what();
throw LmsException {"Internal error"}; // Do not put details here at it may appear on the user rendered html
}
}
void LmsApplication::notify(const Wt::WEvent& event)
{
try
{
WApplication::notify(event);
}
catch (LmsApplicationException& e)
{
LMS_LOG(UI, WARNING, "Caught a LmsApplication exception: " << e.what());
handleException(e);
}
catch (std::exception& e)
{
LMS_LOG(UI, ERROR, "Caught exception: " << e.what());
throw LmsException{ "Internal error" }; // Do not put details here at it may appear on the user rendered html
}
}
void
LmsApplication::post(std::function<void()> func)
{
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
}
void LmsApplication::post(std::function<void()> func)
{
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
}
void
LmsApplication::setTitle(const Wt::WString& title)
{
if (title.empty())
WApplication::setTitle("LMS");
else
WApplication::setTitle(title);
}
void
LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
{
LMS_LOG(UI, INFO) << "Notifying message '" << message.toUTF8() << "' for category '" << category.toUTF8() << "'";
_notificationContainer->add(type, category, message, duration);
}
void LmsApplication::setTitle(const Wt::WString& title)
{
if (title.empty())
WApplication::setTitle("LMS");
else
WApplication::setTitle(title);
}
void LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
{
LMS_LOG(UI, INFO, "Notifying message '" << message.toUTF8() << "' for category '" << category.toUTF8() << "'");
_notificationContainer->add(type, category, message, duration);
}
} // namespace UserInterface
+16 -16
View File
@@ -24,7 +24,7 @@
#include <Wt/Json/Serializer.h>
#include <Wt/WPushButton.h>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Release.hpp"
@@ -210,30 +210,30 @@ namespace UserInterface
_settingsLoaded.connect([this](const std::string& settings)
{
LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'";
LMS_LOG(UI, DEBUG, "Settings loaded! '" << settings << "'");
_settings = settingsfromJSString(settings);
settingsLoaded.emit();
});
});
{
Settings defaultSettings;
{
Settings defaultSettings;
std::ostringstream oss;
oss << "LMS.mediaplayer.init("
<< jsRef()
<< ", defaultSettings = " << settingsToJSString(defaultSettings)
<< ")";
std::ostringstream oss;
oss << "LMS.mediaplayer.init("
<< jsRef()
<< ", defaultSettings = " << settingsToJSString(defaultSettings)
<< ")";
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
doJavaScript(oss.str());
}
LMS_LOG(UI, DEBUG, "Running js = '" << oss.str() << "'");
doJavaScript(oss.str());
}
}
void MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
{
LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId.toString();
LMS_LOG(UI, DEBUG, "Playing track ID = " << trackId.toString());
std::ostringstream oss;
{
@@ -305,7 +305,7 @@ namespace UserInterface
_separator->setText("");
}
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
LMS_LOG(UI, DEBUG, "Running js = '" << oss.str() << "'");
doJavaScript(oss.str());
_trackIdLoaded = trackId;
@@ -325,7 +325,7 @@ namespace UserInterface
std::ostringstream oss;
oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
LMS_LOG(UI, DEBUG, "Running js = '" << oss.str() << "'");
doJavaScript(oss.str());
}
}
+47 -49
View File
@@ -18,64 +18,62 @@
*/
#include "ModalManager.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace UserInterface
{
ModalManager::ModalManager()
: _closed {this, "closed"}
{
_closed.connect([=](const std::string& id)
{
LMS_LOG(UI, DEBUG) << "Received closed for id '" << id << "'";
for (int i {}; i < count(); ++i)
{
Wt::WWidget* widget {this->widget(i)};
LMS_LOG(UI, DEBUG) << "Widget " << i << ", id = '" << widget->id();
if (widget->id() == id)
{
removeWidget(widget);
break;
}
}
});
}
ModalManager::ModalManager()
: _closed{ this, "closed" }
{
_closed.connect([=](const std::string& id)
{
LMS_LOG(UI, DEBUG, "Received closed for id '" << id << "'");
for (int i{}; i < count(); ++i)
{
Wt::WWidget* widget{ this->widget(i) };
LMS_LOG(UI, DEBUG, "Widget " << i << ", id = '" << widget->id());
if (widget->id() == id)
{
removeWidget(widget);
break;
}
}
});
}
void
ModalManager::show(std::unique_ptr<Wt::WWidget> modalWidget)
{
LMS_LOG(UI, DEBUG) << "Want to show, id = " << modalWidget->id();
void ModalManager::show(std::unique_ptr<Wt::WWidget> modalWidget)
{
LMS_LOG(UI, DEBUG, "Want to show, id = " << modalWidget->id());
std::ostringstream oss;
oss
<< R"({const modalElement = )" << jsRef() << R"(.getElementsByClassName('modal')[0];)"
<< R"(const modal = bootstrap.Modal.getOrCreateInstance(modalElement);)"
<< R"(modal.show();)"
<< R"(modalElement.addEventListener('hidden.bs.modal', function () {)"
<< _closed.createCall({"'" + modalWidget->id() + "'"})
<< R"(modal.dispose();)"
<< R"(});})";
std::ostringstream oss;
oss
<< R"({const modalElement = )" << jsRef() << R"(.getElementsByClassName('modal')[0];)"
<< R"(const modal = bootstrap.Modal.getOrCreateInstance(modalElement);)"
<< R"(modal.show();)"
<< R"(modalElement.addEventListener('hidden.bs.modal', function () {)"
<< _closed.createCall({ "'" + modalWidget->id() + "'" })
<< R"(modal.dispose();)"
<< R"(});})";
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
doJavaScript(oss.str());
doJavaScript(oss.str());
addWidget(std::move(modalWidget));
}
addWidget(std::move(modalWidget));
}
void
ModalManager::dispose(Wt::WWidget* modalWidget)
{
std::ostringstream oss;
oss
<< R"({const modalElementParent = document.getElementById(')" << modalWidget->id() << R"(');)"
<< R"(const modalElement = modalElementParent.getElementsByClassName('modal')[0];)"
<< R"(const modal = bootstrap.Modal.getInstance(modalElement);)"
<< R"(modal.hide();)"
<< R"(})";
void ModalManager::dispose(Wt::WWidget* modalWidget)
{
std::ostringstream oss;
oss
<< R"({const modalElementParent = document.getElementById(')" << modalWidget->id() << R"(');)"
<< R"(const modalElement = modalElementParent.getElementsByClassName('modal')[0];)"
<< R"(const modal = bootstrap.Modal.getInstance(modalElement);)"
<< R"(modal.hide();)"
<< R"(})";
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
doJavaScript(oss.str());
}
doJavaScript(oss.str());
}
}
+51 -49
View File
@@ -22,65 +22,67 @@
#include <sstream>
#include <Wt/WTemplate.h>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "LmsApplication.hpp"
namespace UserInterface
{
class NotificationWidget : public Wt::WTemplate
{
public:
NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration);
Wt::JSignal<> closed {this, "closed"};
};
namespace
{
class NotificationWidget : public Wt::WTemplate
{
public:
NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration);
Wt::JSignal<> closed{ this, "closed" };
};
}
NotificationWidget::NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
: Wt::WTemplate {Wt::WString::tr("Lms.notifications.template.entry")}
{
switch (type)
{
case Notification::Type::Info:
bindString("bg-color", "bg-primary");
bindString("text-color", "white");
break;
case Notification::Type::Warning:
bindString("bg-color", "bg-warning");
bindString("text-color", "dark");
break;
case Notification::Type::Danger:
bindString("bg-color", "bg-danger");
bindString("text-color", "white");
break;
}
NotificationWidget::NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
: Wt::WTemplate{ Wt::WString::tr("Lms.notifications.template.entry") }
{
switch (type)
{
case Notification::Type::Info:
bindString("bg-color", "bg-primary");
bindString("text-color", "white");
break;
case Notification::Type::Warning:
bindString("bg-color", "bg-warning");
bindString("text-color", "dark");
break;
case Notification::Type::Danger:
bindString("bg-color", "bg-danger");
bindString("text-color", "white");
break;
}
bindString("category", category);
bindString("message", message);
bindInt("duration", duration.count());
bindString("category", category);
bindString("message", message);
bindInt("duration", duration.count());
std::ostringstream oss;
std::ostringstream oss;
oss
<< R"({const toastElement = )" << jsRef() << R"(.getElementsByClassName('toast')[0];)"
<< R"(const toast = bootstrap.Toast.getOrCreateInstance(toastElement);)"
<< R"(toast.show();)"
<< R"(toastElement.addEventListener('hidden.bs.toast', function () {)"
<< closed.createCall({})
<< R"(toast.dispose();)"
<< R"(});})";
oss
<< R"({const toastElement = )" << jsRef() << R"(.getElementsByClassName('toast')[0];)"
<< R"(const toast = bootstrap.Toast.getOrCreateInstance(toastElement);)"
<< R"(toast.show();)"
<< R"(toastElement.addEventListener('hidden.bs.toast', function () {)"
<< closed.createCall({})
<< R"(toast.dispose();)"
<< R"(});})";
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
doJavaScript(oss.str());
}
doJavaScript(oss.str());
}
void
NotificationContainer::add(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
{
NotificationWidget* notification {addNew<NotificationWidget>(type, category, message, duration)};
void NotificationContainer::add(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
{
NotificationWidget* notification{ addNew<NotificationWidget>(type, category, message, duration) };
notification->closed.connect([=]
{
removeWidget(notification);
});
}
notification->closed.connect([=]
{
removeWidget(notification);
});
}
}
+2 -2
View File
@@ -37,7 +37,7 @@
#include "services/feedback/IFeedbackService.hpp"
#include "services/recommendation/IPlaylistGeneratorService.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
@@ -226,7 +226,7 @@ namespace UserInterface
if (LmsApp->getUser()->isDemo())
{
LMS_LOG(UI, DEBUG) << "Removing queue (tracklist id " << _queueId.toString() << ")";
LMS_LOG(UI, DEBUG, "Removing queue (tracklist id " << _queueId.toString() << ")");
if (Database::TrackList::pointer queue{ getQueue() })
queue.remove();
}
+1 -4
View File
@@ -38,7 +38,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "LmsApplication.hpp"
@@ -259,10 +259,7 @@ namespace UserInterface
setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow));
if (auto listenBrainzToken{ user->getListenBrainzToken() })
{
LMS_LOG(UI, DEBUG) << "Read listenBrainzToken! value = " << listenBrainzToken->getAsString();
setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() }));
}
{
const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz };
+1 -1
View File
@@ -110,7 +110,7 @@ namespace UserInterface::Utils
auto res{ std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::TextFormat::UnsafeXHTML) };
res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
res->setToolTip(std::string{ cluster->getType()->getName() }, Wt::TextFormat::Plain);
res->setInline(true);
return res;
+5 -5
View File
@@ -31,7 +31,7 @@
#include "services/database/Session.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "services/scanner/IScannerService.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
@@ -112,8 +112,8 @@ class DatabaseSettingsModel : public Wt::WFormModel
auto clusterTypes {scanSettings->getClusterTypes()};
if (!clusterTypes.empty())
{
std::vector<std::string> names;
std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](auto clusterType) { return clusterType->getName(); });
std::vector<std::string_view> names;
std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](const auto& clusterType) { return clusterType->getName(); });
setValue(ClustersField, StringUtils::joinStrings(names, " "));
}
}
@@ -138,8 +138,8 @@ class DatabaseSettingsModel : public Wt::WFormModel
if (similarityEngineTypeRow)
scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow));
auto clusterTypes {StringUtils::splitStringCopy(valueText(ClustersField).toUTF8(), " ")};
scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string>(clusterTypes.begin(), clusterTypes.end()));
const std::vector<std::string_view> clusterTypes {StringUtils::splitString(valueText(ClustersField).toUTF8(), " ")};
scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string_view>(clusterTypes.begin(), clusterTypes.end()));
}
private:
+1 -1
View File
@@ -28,7 +28,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "common/LoginNameValidator.hpp"
+1 -1
View File
@@ -32,7 +32,7 @@
#include "services/database/Session.hpp"
#include "utils/IConfig.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
+1 -1
View File
@@ -26,7 +26,7 @@
#include "services/auth/IPasswordService.hpp"
#include "services/database/User.hpp"
#include "services/database/Session.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "LmsApplication.hpp"
+1 -1
View File
@@ -30,7 +30,7 @@
#include "services/database/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "common/InfiniteScrollingContainer.hpp"
+1 -1
View File
@@ -24,7 +24,7 @@
#include "services/database/Artist.hpp"
#include "services/database/Session.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "ArtistListHelpers.hpp"
+1 -1
View File
@@ -88,7 +88,7 @@ Filters::showDialog()
for (const ClusterTypeId clusterTypeId : clusterTypesIds.results)
{
const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypeId)};
typeCombo->addItem(Wt::WString::fromUTF8(clusterType->getName()));
typeCombo->addItem(Wt::WString::fromUTF8(std::string{ clusterType->getName() }));
}
if (!clusterTypesIds.results.empty())
+1 -1
View File
@@ -34,7 +34,7 @@
#include "services/database/TrackArtistLink.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "common/Template.hpp"
#include "resource/DownloadResource.hpp"
+1 -1
View File
@@ -31,7 +31,7 @@
#include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "common/Template.hpp"
+1 -1
View File
@@ -26,7 +26,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackList.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "common/InfiniteScrollingContainer.hpp"
+1 -1
View File
@@ -23,7 +23,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "explore/Filters.hpp"
+59 -66
View File
@@ -26,87 +26,80 @@
#include "av/RawResourceHandlerCreator.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
#define LOG(level) LMS_LOG(UI, level) << "Audio file resource: "
AudioFileResource:: ~AudioFileResource()
namespace UserInterface
{
beingDeleted();
}
#define LOG(severity, message) LMS_LOG(UI, severity, "Audio file resource: " << message)
std::string
AudioFileResource::getUrl(Database::TrackId trackId) const
{
return url()+ "&trackid=" + trackId.toString();
}
namespace
{
std::optional<std::filesystem::path> getTrackPathFromTrackId(Database::TrackId trackId)
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
static
std::optional<std::filesystem::path>
getTrackPathFromTrackId(Database::TrackId trackId)
{
auto transaction {LmsApp->getDbSession().createReadTransaction()};
const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), trackId) };
if (!track)
{
LOG(ERROR, "Missing track");
return std::nullopt;
}
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)};
if (!track)
{
LOG(ERROR) << "Missing track";
return std::nullopt;
}
return track->getPath();
}
return track->getPath();
}
std::optional<std::filesystem::path> getTrackPathFromURLArgs(const Wt::Http::Request& request)
{
const std::string* trackIdParameter{ request.getParameter("trackid") };
if (!trackIdParameter)
{
LOG(ERROR, "Missing trackid URL parameter!");
return std::nullopt;
}
static
std::optional<std::filesystem::path>
getTrackPathFromURLArgs(const Wt::Http::Request& request)
{
const std::string* trackIdParameter {request.getParameter("trackid")};
if (!trackIdParameter)
{
LOG(ERROR) << "Missing trackid URL parameter!";
return std::nullopt;
}
const std::optional<Database::TrackId> trackId{ StringUtils::readAs<Database::TrackId::ValueType>(*trackIdParameter) };
if (!trackId)
{
LOG(ERROR, "Bad trackid URL parameter!");
return std::nullopt;
}
const std::optional<Database::TrackId> trackId {StringUtils::readAs<Database::TrackId::ValueType>(*trackIdParameter)};
if (!trackId)
{
LOG(ERROR) << "Bad trackid URL parameter!";
return std::nullopt;
}
return getTrackPathFromTrackId(*trackId);
}
return getTrackPathFromTrackId(*trackId);
}
}
void
AudioFileResource::handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response)
{
std::shared_ptr<IResourceHandler> fileResourceHandler;
AudioFileResource:: ~AudioFileResource()
{
beingDeleted();
}
if (!request.continuation())
{
auto trackPath {getTrackPathFromURLArgs(request)};
if (!trackPath)
return;
std::string AudioFileResource::getUrl(Database::TrackId trackId) const
{
return url() + "&trackid=" + trackId.toString();
}
fileResourceHandler = Av::createRawResourceHandler(*trackPath);
}
else
{
fileResourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(request.continuation()->data());
}
void AudioFileResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
{
std::shared_ptr<IResourceHandler> fileResourceHandler;
auto* continuation {fileResourceHandler->processRequest(request, response)};
if (continuation)
continuation->setData(fileResourceHandler);
}
if (!request.continuation())
{
auto trackPath{ getTrackPathFromURLArgs(request) };
if (!trackPath)
return;
fileResourceHandler = Av::createRawResourceHandler(*trackPath);
}
else
{
fileResourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(request.continuation()->data());
}
auto* continuation{ fileResourceHandler->processRequest(request, response) };
if (continuation)
continuation->setData(fileResourceHandler);
}
} // namespace UserInterface
@@ -28,12 +28,12 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "LmsApplication.hpp"
#define LOG(level) LMS_LOG(UI, level) << "Audio transcode resource: "
#define LOG(severity, message) LMS_LOG(UI, severity, "Audio transcode resource: " << message)
namespace StringUtils
{
@@ -61,7 +61,7 @@ namespace StringUtils
return format;
}
LOG(ERROR) << "Cannot determine audio format from value '" << str << "'";
LOG(ERROR, "Cannot determine audio format from value '" << str << "'");
return std::nullopt;
}
@@ -82,7 +82,7 @@ namespace UserInterface
case Database::TranscodingOutputFormat::WEBM_VORBIS: return Av::Transcoding::OutputFormat::WEBM_VORBIS;
}
LOG(ERROR) << "Cannot convert from audio format to AV format";
LOG(ERROR, "Cannot convert from audio format to AV format");
return std::nullopt;
}
@@ -93,13 +93,13 @@ namespace UserInterface
auto paramStr{ request.getParameter(parameterName) };
if (!paramStr)
{
LOG(DEBUG) << "Missing parameter '" << parameterName << "'";
LOG(DEBUG, "Missing parameter '" << parameterName << "'");
return std::nullopt;
}
auto res{ StringUtils::readAs<T>(*paramStr) };
if (!res)
LOG(ERROR) << "Cannot parse parameter '" << parameterName << "' from value '" << *paramStr << "'";
LOG(ERROR, "Cannot parse parameter '" << parameterName << "' from value '" << *paramStr << "'");
return res;
}
@@ -124,7 +124,7 @@ namespace UserInterface
if (!Database::isAudioBitrateAllowed(*bitrate))
{
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
LOG(ERROR, "Bitrate '" << *bitrate << "' is not allowed");
return std::nullopt;
}
@@ -142,7 +142,7 @@ namespace UserInterface
const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), *trackId) };
if (!track)
{
LOG(ERROR) << "Missing track";
LOG(ERROR, "Missing track");
return std::nullopt;
}
@@ -195,7 +195,7 @@ namespace UserInterface
}
catch (const Av::Exception& e)
{
LOG(ERROR) << "Caught Av exception: " << e.what();
LOG(ERROR, "Caught Av exception: " << e.what());
}
}
+10 -11
View File
@@ -25,16 +25,16 @@
#include "services/cover/ICoverService.hpp"
#include "services/database/Track.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
#include "LmsApplication.hpp"
#define LOG(level) LMS_LOG(UI, level) << "Image resource: "
namespace UserInterface {
#define LOG(severity, message) LMS_LOG(UI, severity, "Image resource: " << message)
namespace UserInterface
{
CoverResource::CoverResource()
{
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
@@ -69,14 +69,14 @@ namespace UserInterface {
// Mandatory parameter size
if (!sizeStr)
{
LOG(DEBUG) << "no size provided!";
LOG(DEBUG, "no size provided!");
return;
}
const auto size{ StringUtils::readAs<std::size_t>(*sizeStr) };
if (!size || *size > maxSize)
{
LOG(DEBUG) << "invalid size provided!";
LOG(DEBUG, "invalid size provided!");
return;
}
@@ -84,12 +84,12 @@ namespace UserInterface {
if (trackIdStr)
{
LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size;
LOG(DEBUG, "Requested cover for track " << *trackIdStr << ", size = " << *size);
const std::optional<Database::TrackId> trackId{ StringUtils::readAs<Database::TrackId::ValueType>(*trackIdStr) };
if (!trackId)
{
LOG(DEBUG) << "track not found";
LOG(DEBUG, "track not found");
return;
}
@@ -99,7 +99,7 @@ namespace UserInterface {
}
else if (releaseIdStr)
{
LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size;
LOG(DEBUG, "Requested cover for release " << *releaseIdStr << ", size = " << *size);
const std::optional<Database::ReleaseId> releaseId{ StringUtils::readAs<Database::ReleaseId::ValueType>(*releaseIdStr) };
if (!releaseId)
@@ -111,7 +111,7 @@ namespace UserInterface {
}
else
{
LOG(DEBUG) << "No track or release provided";
LOG(DEBUG, "No track or release provided");
return;
}
@@ -119,5 +119,4 @@ namespace UserInterface {
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
}
} // namespace UserInterface
+4 -6
View File
@@ -28,15 +28,14 @@
#include "services/database/Track.hpp"
#include "services/database/TrackList.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "LmsApplication.hpp"
#define LOG(level) LMS_LOG(UI, level) << "Download resource: "
#define LOG(severity, message) LMS_LOG(UI, severity, "Download resource: " << message)
namespace UserInterface
{
DownloadResource::~DownloadResource()
{
beingDeleted();
@@ -66,7 +65,7 @@ namespace UserInterface
}
catch (Zip::Exception& exception)
{
LOG(ERROR) << "Zipper exception: " << exception.what();
LOG(ERROR, "Zipper exception: " << exception.what());
}
}
@@ -216,7 +215,7 @@ namespace UserInterface
const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), _trackId) };
if (!track)
{
LOG(DEBUG) << "Cannot find track";
LOG(DEBUG, "Cannot find track");
return {};
}
@@ -243,5 +242,4 @@ namespace UserInterface
const auto tracks{ Track::find(LmsApp->getDbSession(), params) };
return details::createZipper(tracks.results);
}
} // namespace UserInterface

Some files were not shown because too many files have changed in this diff Show More