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

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