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
@@ -22,7 +22,7 @@
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
@@ -43,7 +43,7 @@ namespace Auth
{
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN);
LMS_LOG(AUTH, DEBUG, "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN));
user = session.create<User>(loginName);
user.modify()->setType(type);
@@ -28,7 +28,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
@@ -62,7 +62,7 @@ namespace Auth
Database::AuthToken::pointer authToken {session.create<Database::AuthToken>(secretHash, expiry, user)};
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString());
if (user->getAuthTokensCount() >= 50)
Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
@@ -88,7 +88,7 @@ namespace Auth
return std::nullopt;
}
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!");
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser()->getId(), authToken->getExpiry()};
authToken.remove();
@@ -21,7 +21,7 @@
#include "LoginThrottler.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
namespace Auth {
@@ -81,10 +81,10 @@ LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
attemptInfo.badConsecutiveAttemptCount += 1;
LMS_LOG(AUTH, DEBUG) << "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount;
LMS_LOG(AUTH, DEBUG, "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount);
if (attemptInfo.badConsecutiveAttemptCount >= _maxBadConsecutiveAttemptCount)
{
LMS_LOG(AUTH, DEBUG) << "Throttling '" << clientAddress.to_string() << "'";
LMS_LOG(AUTH, DEBUG, "Throttling '" << clientAddress.to_string() << "'");
attemptInfo.nextAttempt = now.addMSecs(std::chrono::duration_cast<std::chrono::milliseconds>(_throttlingDuration).count());
}
else
@@ -31,7 +31,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
@@ -61,7 +61,7 @@ namespace Auth
PasswordServiceBase::CheckResult
PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
{
LMS_LOG(AUTH, DEBUG) << "Checking password for user '" << loginName << "'";
LMS_LOG(AUTH, DEBUG, "Checking password for user '" << loginName << "'");
// Do not waste too much resource on brute force attacks (optim)
{
@@ -22,46 +22,41 @@
#include <Wt/WEnvironment.h>
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
namespace Auth
{
HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db)
: AuthServiceBase{ db }
, _fieldName{ Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User") }
{
LMS_LOG(AUTH, INFO, "Using http header field = '" << _fieldName << "'");
}
HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db)
: AuthServiceBase {db}
, _fieldName {Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User")}
{
LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'";
}
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
{
const std::string loginName{ env.headerValue(_fieldName) };
if (loginName.empty())
return { CheckResult::State::Denied };
HttpHeadersEnvService::CheckResult
HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
{
const std::string loginName {env.headerValue(_fieldName)};
if (loginName.empty())
return {CheckResult::State::Denied};
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
const Database::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId };
}
const Database::UserId userId {getOrCreateUser(loginName)};
onUserAuthenticated(userId);
return {CheckResult::State::Granted, userId};
}
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
{
const std::string loginName{ request.headerValue(_fieldName) };
if (loginName.empty())
return { CheckResult::State::Denied };
HttpHeadersEnvService::CheckResult
HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
{
const std::string loginName {request.headerValue(_fieldName)};
if (loginName.empty())
return {CheckResult::State::Denied};
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
const Database::UserId userId {getOrCreateUser(loginName)};
onUserAuthenticated(userId);
return {CheckResult::State::Granted, userId};
}
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
const Database::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId };
}
} // namespace Auth
@@ -25,7 +25,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
@@ -44,7 +44,7 @@ namespace Auth
bool
InternalPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
LMS_LOG(AUTH, DEBUG, "Checking internal password for user '" << loginName << "'");
Database::User::PasswordHash passwordHash;
{
@@ -54,7 +54,7 @@ namespace Auth
const Database::User::pointer user {Database::User::find(session, loginName)};
if (!user)
{
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
LMS_LOG(AUTH, DEBUG, "hashing random stuff");
// hash random stuff here to waste some time
hashRandomPassword();
return false;
@@ -28,175 +28,173 @@
#include "services/auth/Types.hpp"
#include "services/database/Session.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Auth
{
class PAMError
{
public:
PAMError(std::string_view msg, pam_handle_t *pamh, int err)
{
_errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err);
}
namespace
{
class PAMError
{
public:
PAMError(std::string_view msg, pam_handle_t* pamh, int err)
{
_errorMsg = std::string{ msg } + ": " + pam_strerror(pamh, err);
}
std::string_view message() const { return _errorMsg; }
std::string_view message() const { return _errorMsg; }
private:
std::string _errorMsg;
};
private:
std::string _errorMsg;
};
class PAMContext
{
public:
PAMContext(std::string_view loginName)
{
int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)};
if (err != PAM_SUCCESS)
throw PAMError {"start failed", _pamh, err};
}
class PAMContext
{
public:
PAMContext(std::string_view loginName)
{
int err{ pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh) };
if (err != PAM_SUCCESS)
throw PAMError{ "start failed", _pamh, err };
}
~PAMContext()
{
int err {pam_end(_pamh, 0)};
if (err != PAM_SUCCESS)
LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err);
}
~PAMContext()
{
int err{ pam_end(_pamh, 0) };
if (err != PAM_SUCCESS)
LMS_LOG(AUTH, ERROR, "end failed: " << pam_strerror(_pamh, err));
}
void authenticate(std::string_view password)
{
AuthenticateConvContext authContext {password};
ScopedConvContextSetter scopedContext {*this, authContext};
void authenticate(std::string_view password)
{
AuthenticateConvContext authContext{ password };
ScopedConvContextSetter scopedContext{ *this, authContext };
int err {pam_authenticate(_pamh, 0)};
if (err != PAM_SUCCESS)
throw PAMError {"authenticate failed", _pamh, err};
}
int err{ pam_authenticate(_pamh, 0) };
if (err != PAM_SUCCESS)
throw PAMError{ "authenticate failed", _pamh, err };
}
void validateAccount()
{
int err {pam_acct_mgmt(_pamh, PAM_SILENT)};
if (err != PAM_SUCCESS)
throw PAMError {"acct_mgmt failed", _pamh, err};
}
void validateAccount()
{
int err{ pam_acct_mgmt(_pamh, PAM_SILENT) };
if (err != PAM_SUCCESS)
throw PAMError{ "acct_mgmt failed", _pamh, err };
}
private:
class ConvContext
{
public:
virtual ~ConvContext() = default;
};
private:
class ConvContext
{
public:
virtual ~ConvContext() = default;
};
class AuthenticateConvContext final : public ConvContext
{
public:
AuthenticateConvContext(std::string_view password) : _password {password} {}
class AuthenticateConvContext final : public ConvContext
{
public:
AuthenticateConvContext(std::string_view password) : _password{ password } {}
std::string_view getPassword() const { return _password; }
std::string_view getPassword() const { return _password; }
private:
std::string_view _password;
};
private:
std::string_view _password;
};
class ScopedConvContextSetter
{
public:
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
: _pamContext {pamContext}
{
_pamContext._convContext = &convContext;
}
class ScopedConvContextSetter
{
public:
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
: _pamContext{ pamContext }
{
_pamContext._convContext = &convContext;
}
~ScopedConvContextSetter()
{
_pamContext._convContext = nullptr;
}
~ScopedConvContextSetter()
{
_pamContext._convContext = nullptr;
}
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
private:
PAMContext& _pamContext;
};
private:
PAMContext& _pamContext;
};
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
{
if (msgCount < 1)
return PAM_CONV_ERR;
if (!resps || !msgs || !userData)
return PAM_CONV_ERR;
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
{
if (msgCount < 1)
return PAM_CONV_ERR;
if (!resps || !msgs || !userData)
return PAM_CONV_ERR;
PAMContext& context{ *static_cast<PAMContext*>(userData) };
PAMContext& context {*static_cast<PAMContext*>(userData)};
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
if (!authenticateContext)
{
LMS_LOG(AUTH, ERROR, "Unexpected conv!");
return PAM_CONV_ERR;
}
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
if (!authenticateContext)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv!";
return PAM_CONV_ERR;
}
// Only expect a PAM_PROMPT_ECHO_OFF msg
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
{
LMS_LOG(AUTH, ERROR, "Unexpected conv message. Count = " << msgCount);
return PAM_CONV_ERR;
}
// Only expect a PAM_PROMPT_ECHO_OFF msg
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount;
return PAM_CONV_ERR;
}
pam_response* response{ static_cast<pam_response*>(malloc(sizeof(pam_response))) };
if (!response)
return PAM_CONV_ERR;
pam_response* response {static_cast<pam_response*>(malloc(sizeof(pam_response)))};
if (!response)
return PAM_CONV_ERR;
response->resp = strdup(std::string{ authenticateContext->getPassword() }.c_str());
response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str());
*resps = response;
return PAM_SUCCESS;
}
*resps = response;
return PAM_SUCCESS;
}
ConvContext* _convContext{};
pam_conv _conv{ &PAMContext::conv, this };
pam_handle_t* _pamh{};
};
}
ConvContext* _convContext {};
pam_conv _conv {&PAMContext::conv, this};
pam_handle_t *_pamh {};
};
bool PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
try
{
LMS_LOG(AUTH, DEBUG, "Checking PAM password for user '" << loginName << "'");
PAMContext pamContext{ loginName };
bool
PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
try
{
LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'";
PAMContext pamContext {loginName};
pamContext.authenticate(password);
pamContext.validateAccount();
pamContext.authenticate(password);
pamContext.validateAccount();
return true;
}
catch (const PAMError& error)
{
LMS_LOG(AUTH, ERROR, "PAM error: " << error.message());
return false;
}
}
return true;
}
catch (const PAMError& error)
{
LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message();
return false;
}
}
bool PAMPasswordService::canSetPasswords() const
{
return false;
}
bool
PAMPasswordService::canSetPasswords() const
{
return false;
}
IPasswordService::PasswordAcceptabilityResult PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
{
throw NotImplementedException{};
}
IPasswordService::PasswordAcceptabilityResult
PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
{
throw NotImplementedException {};
}
void
PAMPasswordService::setPassword(Database::UserId, std::string_view)
{
throw NotImplementedException {};
}
void PAMPasswordService::setPassword(Database::UserId, std::string_view)
{
throw NotImplementedException{};
}
} // namespace Auth
+12 -12
View File
@@ -29,7 +29,7 @@
#include "image/Exception.hpp"
#include "image/IRawImage.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
#include "utils/String.hpp"
#include "utils/Utils.hpp"
@@ -110,10 +110,10 @@ namespace Cover
{
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
LMS_LOG(COVER, INFO, "Default cover path = '" << _defaultCoverPath.string() << "'");
LMS_LOG(COVER, INFO, "Max cache size = " << _maxCacheSize);
LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize);
LMS_LOG(COVER, INFO, "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ","));
#if LMS_SUPPORT_IMAGE_GM
GraphicsMagick::init(execPath);
@@ -148,7 +148,7 @@ namespace Cover
}
catch (const Image::ImageException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
}
});
@@ -167,7 +167,7 @@ namespace Cover
}
catch (const ImageException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
LMS_LOG(COVER, ERROR, "Cannot read cover in file '" << p.string() << "': " << e.what());
}
return image;
@@ -190,7 +190,7 @@ namespace Cover
std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
_defaultCoverCache[width] = image;
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
LMS_LOG(COVER, DEBUG, "Default cache entries = " << _defaultCoverCache.size());
return image;
}
@@ -269,7 +269,7 @@ namespace Cover
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
{
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
LMS_LOG(COVER, INFO, "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize);
return false;
}
@@ -306,7 +306,7 @@ namespace Cover
}
catch (Av::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p.string() << ": " << e.what());
}
return image;
@@ -404,7 +404,7 @@ namespace Cover
{
std::unique_lock lock{ _cacheMutex };
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
LMS_LOG(COVER, DEBUG, "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize);
_cacheHits = 0;
_cacheMisses = 0;
_cacheSize = 0;
@@ -415,7 +415,7 @@ namespace Cover
{
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
}
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
+1 -1
View File
@@ -25,7 +25,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
#include "EnumSetTraits.hpp"
+1 -1
View File
@@ -157,7 +157,7 @@ namespace Database
{
}
ClusterType::pointer ClusterType::create(Session& session, const std::string& name)
ClusterType::pointer ClusterType::create(Session& session, std::string_view name)
{
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
}
+6 -6
View File
@@ -26,7 +26,7 @@
#include "services/database/User.hpp"
#include "utils/IConfig.hpp"
#include "utils/Service.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Database
{
@@ -65,18 +65,18 @@ namespace Database
void prepare()
{
LMS_LOG(DB, DEBUG) << "Setting per-connection settings...";
LMS_LOG(DB, DEBUG, "Setting per-connection settings...");
executeSql("pragma journal_mode=WAL");
executeSql("pragma synchronous=normal");
executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries
LMS_LOG(DB, DEBUG) << "Setting per-connection settings done!";
LMS_LOG(DB, DEBUG, "Setting per-connection settings done!");
}
void optimize()
{
LMS_LOG(DB, DEBUG) << "connection close: Running pragma optimize...";
LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize...");
executeSql("pragma optimize");
LMS_LOG(DB, DEBUG) << "connection close: pragma optimize complete";
LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete");
}
std::filesystem::path _dbPath;
@@ -86,7 +86,7 @@ namespace Database
// Session living class handling the database and the login
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
{
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath.string());
auto connection{ std::make_unique<Connection>(dbPath.string()) };
if (IConfig * config{ Service<IConfig>::get() })// may not be here on testU
@@ -26,7 +26,7 @@
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Database
{
@@ -282,11 +282,11 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
try
{
version = VersionInfo::getOrCreate(session)->getVersion();
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
LMS_LOG(DB, INFO, "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION);
}
catch (std::exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
LMS_LOG(DB, ERROR, "Cannot get database version info: " << e.what());
throw LmsException{ outdatedMsg };
}
@@ -298,7 +298,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
while (version < LMS_DATABASE_VERSION)
{
LMS_LOG(DB, INFO) << "Migrating database from version " << version << " to " << version + 1 << "...";
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
auto itMigrationFunc{ migrationFunctions.find(version) };
assert(itMigrationFunc != std::cend(migrationFunctions));
@@ -306,7 +306,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
VersionInfo::get(session).modify()->setVersion(++version);
LMS_LOG(DB, INFO) << "Migration complete to version " << version;
LMS_LOG(DB, INFO, "Migration complete to version " << version);
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "SqlQuery.hpp"
#include "EnumSetTraits.hpp"
#include "IdTypeTraits.hpp"
@@ -22,131 +22,123 @@
#include <Wt/Dbo/WtSqlTraits.h>
#include "utils/Path.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/String.hpp"
#include "services/database/Cluster.hpp"
#include "services/database/Session.hpp"
namespace {
const std::set<std::string> defaultClusterTypeNames =
namespace Database
{
"GENRE",
"ALBUMGROUPING",
"MOOD",
"ALBUMMOOD",
};
namespace
{
}
const std::set<std::string_view> defaultClusterTypeNames =
{
"GENRE",
"ALBUMGROUPING",
"MOOD",
"ALBUMMOOD",
};
namespace Database {
}
void ScanSettings::init(Session& session)
{
session.checkWriteTransaction();
void
ScanSettings::init(Session& session)
{
session.checkWriteTransaction();
pointer settings{ get(session) };
if (settings)
return;
pointer settings {get(session)};
if (settings)
return;
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
settings.modify()->setClusterTypes(session, defaultClusterTypeNames);
}
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
settings.modify()->setClusterTypes(session, defaultClusterTypeNames );
}
ScanSettings::pointer ScanSettings::get(Session& session)
{
session.checkReadTransaction();
ScanSettings::pointer
ScanSettings::get(Session& session)
{
session.checkReadTransaction();
return session.getDboSession().find<ScanSettings>().resultValue();
}
return session.getDboSession().find<ScanSettings>().resultValue();
}
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
{
const auto extensions{ StringUtils::splitString(_audioFileExtensions, " ") };
std::vector<std::filesystem::path>
ScanSettings::getAudioFileExtensions() const
{
const auto extensions {StringUtils::splitString(_audioFileExtensions, " ")};
std::vector<std::filesystem::path> res(std::cbegin(extensions), std::cend(extensions));
std::sort(std::begin(res), std::end(res));
res.erase(std::unique(std::begin(res), std::end(res)), std::end(res));
std::vector<std::filesystem::path> res (std::cbegin(extensions), std::cend(extensions));
std::sort(std::begin(res), std::end(res));
res.erase(std::unique( std::begin(res), std::end(res)), std::end(res));
return res;
}
return res;
}
void ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
{
_audioFileExtensions += " " + ext.string();
}
void
ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
{
_audioFileExtensions += " " + ext.string();
}
std::vector<ClusterType::pointer> ScanSettings::getClusterTypes() const
{
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
}
std::vector<ClusterType::pointer>
ScanSettings::getClusterTypes() const
{
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
}
void ScanSettings::setMediaDirectory(const std::filesystem::path& p)
{
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
}
void
ScanSettings::setMediaDirectory(const std::filesystem::path& p)
{
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
}
template <typename It>
std::set<std::string> getNames(It begin, It end)
{
std::set<std::string> names;
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
[](const ClusterType::pointer& clusterType)
{
return clusterType->getName();
});
template <typename It>
std::set<std::string> getNames(It begin, It end)
{
std::set<std::string> names;
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
[](const ClusterType::pointer& clusterType)
{
return clusterType->getName();
});
return names;
}
return names;
}
void ScanSettings::setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames)
{
session.checkWriteTransaction();
void
ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames)
{
session.checkWriteTransaction();
bool needRescan{};
bool needRescan {};
// Create any missing cluster type
for (const std::string_view clusterTypeName : clusterTypeNames)
{
ClusterType::pointer clusterType{ ClusterType::find(session, clusterTypeName) };
if (!clusterType)
{
LMS_LOG(DB, INFO, "Creating cluster type " << clusterTypeName);
clusterType = session.create<ClusterType>(clusterTypeName);
_clusterTypes.insert(getDboPtr(clusterType));
// Create any missing cluster type
for (const std::string& clusterTypeName : clusterTypeNames)
{
ClusterType::pointer clusterType {ClusterType::find(session, clusterTypeName)};
if (!clusterType)
{
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
clusterType = session.create<ClusterType>(clusterTypeName);
_clusterTypes.insert(getDboPtr(clusterType));
needRescan = true;
}
}
needRescan = true;
}
}
// Delete no longer existing cluster types
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
{
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
[clusterType](std::string_view name) { return name == clusterType->getName(); }))
{
LMS_LOG(DB, INFO, "Deleting cluster type " << clusterType->getName());
clusterType.remove();
}
}
// Delete no longer existing cluster types
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
{
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
[clusterType](const std::string& name) { return name == clusterType->getName(); }))
{
LMS_LOG(DB, INFO) << "Deleting cluster type " << clusterType->getName();
clusterType.remove();
}
}
if (needRescan)
_scanVersion += 1;
}
if (needRescan)
_scanVersion += 1;
}
void
ScanSettings::incScanVersion()
{
_scanVersion += 1;
}
void
ScanSettings::incScanVersion()
{
_scanVersion += 1;
}
} // namespace Database
+9 -9
View File
@@ -22,7 +22,7 @@
#include <cassert>
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "services/database/Artist.hpp"
#include "services/database/AuthToken.hpp"
@@ -107,20 +107,20 @@ namespace Database
void Session::prepareTables()
{
LMS_LOG(DB, INFO) << "Preparing tables...";
LMS_LOG(DB, INFO, "Preparing tables...");
// Initial creation case
try
{
_session.createTables();
LMS_LOG(DB, INFO) << "Tables created";
LMS_LOG(DB, INFO, "Tables created");
}
catch (Wt::Dbo::Exception& e)
{
LMS_LOG(DB, DEBUG) << "Cannot create tables: " << e.what();
LMS_LOG(DB, DEBUG, "Cannot create tables: " << e.what());
if (std::string_view{ e.what() }.find("already exists") == std::string_view::npos)
{
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
LMS_LOG(DB, ERROR, "Cannot create tables: " << e.what());
throw e;
}
}
@@ -182,22 +182,22 @@ namespace Database
void Session::analyze()
{
LMS_LOG(DB, INFO) << "Analyzing database...";
LMS_LOG(DB, INFO, "Analyzing database...");
{
auto transaction{ createWriteTransaction() };
_session.execute("ANALYZE");
}
LMS_LOG(DB, INFO) << "Database Analyze complete";
LMS_LOG(DB, INFO, "Database Analyze complete");
}
void Session::optimize()
{
LMS_LOG(DB, INFO) << "Optimizing database...";
LMS_LOG(DB, INFO, "Optimizing database...");
{
auto transaction{ createWriteTransaction() };
_session.execute("PRAGMA optimize");
}
LMS_LOG(DB, INFO) << "Database optimizing complete";
LMS_LOG(DB, INFO, "Database optimizing complete");
}
} // namespace Database
+2 -2
View File
@@ -28,7 +28,7 @@
#include "services/database/TrackFeatures.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
@@ -536,7 +536,7 @@ namespace Database
for (auto artist : track->getArtists({ TrackArtistLinkType::Artist }))
os << " - " << artist->getName();
for (auto cluster : track->getClusters())
os << " {" + cluster->getType()->getName() << "-" << cluster->getName() << "}";
os << " {" << cluster->getType()->getName() << "-" << cluster->getName() << "}";
}
else
{
@@ -24,7 +24,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
@@ -111,7 +111,7 @@ namespace Database {
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
LMS_LOG(DB, ERROR, "Track " << _track.id() << ": ptree exception: " << error.what());
res.clear();
}
@@ -20,7 +20,7 @@
#include <cassert>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Cluster.hpp"
+1 -1
View File
@@ -23,7 +23,7 @@
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
@@ -126,7 +126,7 @@ namespace Database {
static void remove(Session& session, const std::string& name);
// Accessors
const std::string& getName() const { return _name; }
std::string_view getName() const { return _name; }
std::vector<Cluster::pointer> getClusters() const;
Cluster::pointer getCluster(const std::string& name) const;
@@ -141,7 +141,7 @@ namespace Database {
private:
friend class Session;
ClusterType(std::string_view name);
static pointer create(Session& session, const std::string& name);
static pointer create(Session& session, std::string_view name);
static const std::size_t _maxNameLength = 128;
@@ -20,6 +20,8 @@
#pragma once
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include <Wt/Dbo/Dbo.h>
@@ -74,7 +76,7 @@ namespace Database {
void setMediaDirectory(const std::filesystem::path& p);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
void setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames);
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
void incScanVersion();
@@ -29,7 +29,7 @@
#include "services/database/StarredTrack.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
@@ -44,15 +44,15 @@ namespace Feedback
FeedbackService::FeedbackService(boost::asio::io_context& ioContext, Db& db)
: _db{ db }
{
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
LMS_LOG(SCROBBLING, INFO, "Starting service...");
_backends.emplace(Database::FeedbackBackend::Internal, std::make_unique<InternalBackend>(_db));
_backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
LMS_LOG(SCROBBLING, INFO) << "Service started!";
LMS_LOG(SCROBBLING, INFO, "Service started!");
}
FeedbackService::~FeedbackService()
{
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
LMS_LOG(SCROBBLING, INFO, "Service stopped!");
}
std::optional<Database::FeedbackBackend> FeedbackService::getUserFeedbackBackend(UserId userId)
@@ -57,7 +57,7 @@ namespace Feedback::ListenBrainz
const Wt::Json::Array& feedbacks = root.get("feedback");
LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks";
LOG(DEBUG, "Got " << feedbacks.size() << " feedbacks");
if (feedbacks.empty())
return res;
@@ -72,17 +72,17 @@ namespace Feedback::ListenBrainz
}
catch (const Exception& e)
{
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
LOG(DEBUG, "Cannot parse feedback: " << e.what() << ", skipping");
}
catch (const Wt::WException& e)
{
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
LOG(DEBUG, "Cannot parse feedback: " << e.what() << ", skipping");
}
}
}
catch (const Wt::WException& error)
{
LOG(ERROR) << "Cannot parse 'feedback' result: " << error.what();
LOG(ERROR, "Cannot parse 'feedback' result: " << error.what());
}
return res;
@@ -53,7 +53,7 @@ namespace Feedback::ListenBrainz
}
catch (const Wt::WException& e)
{
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
LOG(ERROR, "Cannot parse listen count response: " << e.what());
return std::nullopt;
}
}
@@ -66,7 +66,7 @@ namespace Feedback::ListenBrainz
, _maxSyncFeedbackCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000) }
, _syncFeedbacksPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1) }
{
LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours";
LOG(INFO, "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours");
scheduleSync(std::chrono::seconds{ 30 });
}
@@ -95,7 +95,7 @@ namespace Feedback::ListenBrainz
case FeedbackType::Erase:
if (!recordingMBID)
{
LOG(DEBUG) << "Track has no recording MBID: erasing star";
LOG(DEBUG, "Track has no recording MBID: erasing star");
starredTrack.remove();
}
else
@@ -112,7 +112,7 @@ namespace Feedback::ListenBrainz
if (!recordingMBID)
{
LOG(DEBUG) << "Track has no recording MBID: skipping";
LOG(DEBUG, "Track has no recording MBID: skipping");
return;
}
@@ -142,7 +142,7 @@ namespace Feedback::ListenBrainz
}
catch (Exception& e)
{
LOG(DEBUG) << "Cannot send feedback: " << e.what();
LOG(DEBUG, "Cannot send feedback: " << e.what());
}
}
@@ -156,7 +156,7 @@ namespace Feedback::ListenBrainz
Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) };
if (!starredTrack)
{
LOG(DEBUG) << "Starred track not found. deleted?";
LOG(DEBUG, "Starred track not found. deleted?");
return;
}
@@ -166,23 +166,23 @@ namespace Feedback::ListenBrainz
{
case FeedbackType::Love:
starredTrack.modify()->setSyncState(Database::SyncState::Synchronized);
LOG(DEBUG) << "State set to synchronized";
LOG(DEBUG, "State set to synchronized");
if (userContext.feedbackCount)
{
(*userContext.feedbackCount)++;
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
LOG(DEBUG, "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'");
}
break;
case FeedbackType::Erase:
starredTrack.remove();
LOG(DEBUG) << "Removed starred track";
LOG(DEBUG, "Removed starred track");
if (userContext.feedbackCount && *userContext.feedbackCount > 0)
{
(*userContext.feedbackCount)--;
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
LOG(DEBUG, "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'");
}
break;
@@ -211,7 +211,7 @@ namespace Feedback::ListenBrainz
pendingFeedbacks = StarredTrack::find(session, params);
}
LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks";
LOG(DEBUG, "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks");
for (const StarredTrackId starredTrackId : pendingFeedbacks.results)
enqueFeedback(feedbackType, starredTrackId);
@@ -247,13 +247,13 @@ namespace Feedback::ListenBrainz
if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0)
return;
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
LOG(DEBUG, "Scheduled sync in " << fromNow.count() << " seconds...");
_syncTimer.expires_after(fromNow);
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG) << "getFeedbacks aborted";
LOG(DEBUG, "getFeedbacks aborted");
return;
}
else if (ec)
@@ -267,7 +267,7 @@ namespace Feedback::ListenBrainz
void FeedbacksSynchronizer::startSync()
{
LOG(DEBUG) << "Starting sync!";
LOG(DEBUG, "Starting sync!");
assert(!isSyncing());
assert(_strand.running_in_this_thread());
@@ -303,7 +303,7 @@ namespace Feedback::ListenBrainz
{
_strand.dispatch([this, &context]
{
LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount;
LOG(INFO, "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount);
context.syncing = false;
if (!isSyncing())
@@ -356,11 +356,11 @@ namespace Feedback::ListenBrainz
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context]
{
LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'";
LOG(DEBUG, "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'");
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
if (totalFeedbackCount)
LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount;
LOG(DEBUG, "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount);
bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) };
context.feedbackCount = totalFeedbackCount;
@@ -416,7 +416,7 @@ namespace Feedback::ListenBrainz
{
const FeedbacksParser::Result parseResult{ FeedbacksParser::parse(msgBody) };
LOG(DEBUG) << "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries";
LOG(DEBUG, "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries");
context.fetchedFeedbackCount += parseResult.feedbackCount;
for (const Feedback& feedback : parseResult.feedbacks)
@@ -441,12 +441,12 @@ namespace Feedback::ListenBrainz
const std::vector<Track::pointer> tracks{ Track::findByRecordingMBID(session, feedback.recordingMBID) };
if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found";
LOG(DEBUG, "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found");
return;
}
else if (tracks.empty())
{
LOG(DEBUG) << "Cannot match feedback '" << feedback << "': no track found for this recording MBID";
LOG(DEBUG, "Cannot match feedback '" << feedback << "': no track found for this recording MBID");
return;
}
@@ -461,7 +461,7 @@ namespace Feedback::ListenBrainz
if (needImport)
{
LOG(DEBUG) << "Importing feedback '" << feedback << "'";
LOG(DEBUG, "Importing feedback '" << feedback << "'");
auto transaction{ session.createWriteTransaction() };
@@ -481,7 +481,7 @@ namespace Feedback::ListenBrainz
}
else
{
LOG(DEBUG) << "No need to import feedback '" << feedback << "', already imported";
LOG(DEBUG, "No need to import feedback '" << feedback << "', already imported");
context.matchedFeedbackCount++;
}
}
@@ -26,7 +26,7 @@
#include "services/database/Track.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "Utils.hpp"
@@ -63,12 +63,12 @@ namespace Feedback::ListenBrainz
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
, _feedbacksSynchronizer{ _ioContext, db, *_client }
{
LOG(INFO) << "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'";
LOG(INFO, "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'");
}
ListenBrainzBackend::~ListenBrainzBackend()
{
LOG(INFO) << "Stopped ListenBrainz feedback backend!";
LOG(INFO, "Stopped ListenBrainz feedback backend!");
}
void ListenBrainzBackend::onStarred(Database::StarredArtistId starredArtistId)
@@ -46,13 +46,13 @@ namespace Feedback::ListenBrainz::Utils
Wt::Json::Object root;
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
{
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
LOG(ERROR, "Cannot parse 'validate-token' result: " << error.what());
return listenBrainzUserName;
}
if (!root.get("valid").orIfNull(false))
{
LOG(INFO) << "Invalid listenbrainz user";
LOG(INFO, "Invalid listenbrainz user");
return listenBrainzUserName;
}
@@ -20,10 +20,10 @@
#pragma once
#include "services/database/UserId.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/UUID.hpp"
#define LOG(sev) LMS_LOG(FEEDBACK, sev) << "[listenbrainz] "
#define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message)
namespace Database
{
@@ -26,7 +26,7 @@
#include "playlist-constraints/ConsecutiveArtists.hpp"
#include "playlist-constraints/ConsecutiveReleases.hpp"
#include "playlist-constraints/DuplicateTracks.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Recommendation
{
@@ -48,7 +48,7 @@ namespace Recommendation
std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
LMS_LOG(RECOMMENDATION, DEBUG, "Requested to extend playlist by " << maxCount << " similar tracks");
// supposed to be ordered from most similar to least similar
std::vector<TrackId> similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints
@@ -29,7 +29,7 @@
#include "services/database/Session.hpp"
#include "services/database/ScanSettings.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Recommendation
{
@@ -30,397 +30,384 @@
#include "services/database/TrackFeatures.hpp"
#include "services/database/TrackList.hpp"
#include "som/DataNormalizer.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Random.hpp"
namespace Recommendation {
using namespace Database;
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
namespace Recommendation
{
return std::make_unique<FeaturesEngine>(db);
}
const FeatureSettingsMap&
FeaturesEngine::getDefaultTrainFeatureSettings()
{
static const FeatureSettingsMap defaultTrainFeatureSettings
{
{ "lowlevel.spectral_energyband_high.mean", {1}},
{ "lowlevel.spectral_rolloff.median", {1}},
{ "lowlevel.spectral_contrast_valleys.var", {1}},
{ "lowlevel.erbbands.mean", {1}},
{ "lowlevel.gfcc.mean", {1}},
};
return defaultTrainFeatureSettings;
}
static
std::optional<SOM::InputVector>
convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
{
std::size_t i {};
std::optional<SOM::InputVector> res {SOM::InputVector {nbDimensions}};
for (const auto& [featureName, values] : featureValuesMap)
{
if (values.size() != getFeatureDef(featureName).nbDimensions)
{
LMS_LOG(RECOMMENDATION, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size();
res.reset();
break;
}
for (double val : values)
(*res)[i++] = val;
}
return res;
}
static
SOM::InputVector
getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
{
SOM::InputVector weights {nbDimensions};
std::size_t index {};
for (const auto& [featureName, featureSettings] : featureSettingsMap)
{
const std::size_t featureNbDimensions {getFeatureDef(featureName).nbDimensions};
for (std::size_t i {}; i < featureNbDimensions; ++i)
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
}
assert(index == nbDimensions);
return weights;
}
void
FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
{
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
std::unordered_set<FeatureName> featureNames;
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
const std::size_t nbDimensions {std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; })};
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
Session& session {_db.getTLSSession()};
RangeResults<TrackFeaturesId> trackFeaturesIds;
{
auto transaction {session.createReadTransaction()};
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features...";
trackFeaturesIds = TrackFeatures::find(session);
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)";
}
std::vector<SOM::InputVector> samples;
std::vector<TrackId> samplesTrackIds;
samples.reserve(trackFeaturesIds.results.size());
samplesTrackIds.reserve(trackFeaturesIds.results.size());
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
// TODO handle errors using exceptions
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
{
if (_loadCancelled)
return;
auto transaction {session.createReadTransaction()};
TrackFeatures::pointer trackFeatures {TrackFeatures::find(session, trackFeaturesId)};
if (!trackFeatures)
continue;
FeatureValuesMap featureValuesMap {trackFeatures->getFeatureValuesMap(featureNames)};
if (featureValuesMap.empty())
continue;
std::optional<SOM::InputVector> inputVector {convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions)};
if (!inputVector)
continue;
samples.emplace_back(std::move(*inputVector));
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
}
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE";
if (samples.empty())
{
LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
return;
}
LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
SOM::DataNormalizer dataNormalizer {nbDimensions};
dataNormalizer.computeNormalizationFactors(samples);
for (auto& sample : samples)
dataNormalizer.normalizeData(sample);
using namespace Database;
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
{
return std::make_unique<FeaturesEngine>(db);
}
namespace
{
std::optional<SOM::InputVector> convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
{
std::size_t i{};
std::optional<SOM::InputVector> res{ SOM::InputVector {nbDimensions} };
for (const auto& [featureName, values] : featureValuesMap)
{
if (values.size() != getFeatureDef(featureName).nbDimensions)
{
LMS_LOG(RECOMMENDATION, WARNING, "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size());
res.reset();
break;
}
for (double val : values)
(*res)[i++] = val;
}
return res;
}
SOM::InputVector getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
{
SOM::InputVector weights{ nbDimensions };
std::size_t index{};
for (const auto& [featureName, featureSettings] : featureSettingsMap)
{
const std::size_t featureNbDimensions{ getFeatureDef(featureName).nbDimensions };
for (std::size_t i{}; i < featureNbDimensions; ++i)
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
}
assert(index == nbDimensions);
return weights;
}
}
const FeatureSettingsMap& FeaturesEngine::getDefaultTrainFeatureSettings()
{
static const FeatureSettingsMap defaultTrainFeatureSettings
{
{ "lowlevel.spectral_energyband_high.mean", {1}},
{ "lowlevel.spectral_rolloff.median", {1}},
{ "lowlevel.spectral_contrast_valleys.var", {1}},
{ "lowlevel.erbbands.mean", {1}},
{ "lowlevel.gfcc.mean", {1}},
};
return defaultTrainFeatureSettings;
}
void FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
{
LMS_LOG(RECOMMENDATION, INFO, "Constructing features classifier...");
std::unordered_set<FeatureName> featureNames;
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
const std::size_t nbDimensions{ std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; }) };
LMS_LOG(RECOMMENDATION, DEBUG, "Features dimension = " << nbDimensions);
Session & session{ _db.getTLSSession() };
RangeResults<TrackFeaturesId> trackFeaturesIds;
{
auto transaction{ session.createReadTransaction() };
LMS_LOG(RECOMMENDATION, DEBUG, "Getting Track features...");
trackFeaturesIds = TrackFeatures::find(session);
LMS_LOG(RECOMMENDATION, DEBUG, "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)");
}
std::vector<SOM::InputVector> samples;
std::vector<TrackId> samplesTrackIds;
samples.reserve(trackFeaturesIds.results.size());
samplesTrackIds.reserve(trackFeaturesIds.results.size());
LMS_LOG(RECOMMENDATION, DEBUG, "Extracting features...");
// TODO handle errors using exceptions
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
{
if (_loadCancelled)
return;
auto transaction{ session.createReadTransaction() };
TrackFeatures::pointer trackFeatures{ TrackFeatures::find(session, trackFeaturesId) };
if (!trackFeatures)
continue;
FeatureValuesMap featureValuesMap{ trackFeatures->getFeatureValuesMap(featureNames) };
if (featureValuesMap.empty())
continue;
std::optional<SOM::InputVector> inputVector{ convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions) };
if (!inputVector)
continue;
samples.emplace_back(std::move(*inputVector));
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
}
LMS_LOG(RECOMMENDATION, DEBUG, "Extracting features DONE");
if (samples.empty())
{
LMS_LOG(RECOMMENDATION, INFO, "Nothing to classify!");
return;
}
SOM::Coordinate size {static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))};
if (size < 2)
{
LMS_LOG(RECOMMENDATION, WARNING) << "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors";
size = 2;
}
LMS_LOG(RECOMMENDATION, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network";
LMS_LOG(RECOMMENDATION, DEBUG, "Normalizing data...");
SOM::DataNormalizer dataNormalizer{ nbDimensions };
SOM::Network network {size, size, nbDimensions};
SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)};
network.setDataWeights(weights);
auto somProgressCallback{[&](const SOM::Network::CurrentIteration& iter)
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount;
progressCallback(Progress {iter.idIteration, iter.iterationCount});
}};
dataNormalizer.computeNormalizationFactors(samples);
for (auto& sample : samples)
dataNormalizer.normalizeData(sample);
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network...";
network.train(samples, trainSettings.iterationCount,
progressCallback ? somProgressCallback : SOM::Network::ProgressCallback {},
[this] { return _loadCancelled; });
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
SOM::Coordinate size{ static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron)) };
if (size < 2)
{
LMS_LOG(RECOMMENDATION, WARNING, "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors");
size = 2;
}
LMS_LOG(RECOMMENDATION, INFO, "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network");
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
TrackPositions trackPositions;
for (std::size_t i {}; i < samples.size(); ++i)
{
if (_loadCancelled)
return;
SOM::Network network{ size, size, nbDimensions };
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
SOM::InputVector weights{ getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions) };
network.setDataWeights(weights);
trackPositions[samplesTrackIds[i]].push_back(position);
}
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
auto somProgressCallback{ [&](const SOM::Network::CurrentIteration& iter)
{
LMS_LOG(RECOMMENDATION, DEBUG, "Current pass = " << iter.idIteration << " / " << iter.iterationCount);
progressCallback(Progress {iter.idIteration, iter.iterationCount});
} };
LMS_LOG(RECOMMENDATION, DEBUG, "Training network...");
network.train(samples, trainSettings.iterationCount,
progressCallback ? somProgressCallback : SOM::Network::ProgressCallback{},
[this] { return _loadCancelled; });
LMS_LOG(RECOMMENDATION, DEBUG, "Training network DONE");
load(std::move(network), std::move(trackPositions));
}
LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks...");
TrackPositions trackPositions;
for (std::size_t i{}; i < samples.size(); ++i)
{
if (_loadCancelled)
return;
void
FeaturesEngine::loadFromCache(FeaturesEngineCache&& cache)
{
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache...";
load(std::move(cache._network), cache._trackPositions);
}
TrackContainer
FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
{
const TrackContainer trackIds {[&]
{
TrackContainer res;
Session& session {_db.getTLSSession()};
auto transaction {session.createReadTransaction()};
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
if (trackList)
res = trackList->getTrackIds();
return res;
}()};
return findSimilarTracks(trackIds, maxCount);
}
TrackContainer
FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
{
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
Session& session {_db.getTLSSession()};
{
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
auto transaction {session.createReadTransaction()};
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
[&](TrackId trackId)
{
return !Track::exists(session, trackId);
}), std::end(similarTrackIds));
}
return similarTrackIds;
}
ReleaseContainer
FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
{
auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
Session& session {_db.getTLSSession()};
if (!similarReleaseIds.empty())
{
// Report only existing ids
auto transaction {session.createReadTransaction()};
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
[&](ReleaseId releaseId)
{
return !Release::exists(session, releaseId);
}), std::end(similarReleaseIds));
}
return similarReleaseIds;
}
ArtistContainer
FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
{
auto getSimilarArtistIdsForLinkType {[&] (TrackArtistLinkType linkType)
{
ArtistContainer similarArtistIds;
const auto itArtists {_artistMatrix.find(linkType)};
if (itArtists == std::cend(_artistMatrix))
{
return similarArtistIds;
}
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
}};
std::unordered_set<ArtistId> similarArtistIds;
for (TrackArtistLinkType linkType : linkTypes)
{
const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)};
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
}
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
Session& session {_db.getTLSSession()};
{
// Report only existing ids
auto transaction {session.createReadTransaction()};
res.erase(std::remove_if(std::begin(res), std::end(res),
[&](ArtistId artistId)
{
return !Artist::exists(session, artistId);
}), std::end(res));
}
while (res.size() > maxCount)
res.erase(Random::pickRandom(res));
return res;
}
FeaturesEngineCache
FeaturesEngine::toCache() const
{
return FeaturesEngineCache {*_network, _trackPositions};
}
void
FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
{
if (forceReload)
{
FeaturesEngineCache::invalidate();
}
else if (std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()})
{
loadFromCache(std::move(*cache));
return;
}
TrainSettings trainSettings;
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
loadFromTraining(trainSettings, progressCallback);
if (!_loadCancelled && _network)
toCache().write();
}
void
FeaturesEngine::requestCancelLoad()
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation";
_loadCancelled = true;
}
void
FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
{
using namespace Database;
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
LMS_LOG(RECOMMENDATION, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian;
const SOM::Coordinate width {network.getWidth()};
const SOM::Coordinate height {network.getHeight()};
_releaseMatrix = ReleaseMatrix {width, height};
_trackMatrix = TrackMatrix {width, height};
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
Session& session {_db.getTLSSession()};
for (const auto& [trackId, positions] : trackPositions)
{
if (_loadCancelled)
return;
auto transaction {session.createReadTransaction()};
const Track::pointer track {Track::find(session, trackId)};
if (!track)
continue;
for (const SOM::Position& position : positions)
{
Utils::push_back_if_not_present(_trackPositions[trackId], position);
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
if (Release::pointer release {track->getRelease()})
{
const ReleaseId releaseId {release->getId()};
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
}
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
{
const ArtistId artistId {artistLink->getArtist()->getId()};
Utils::push_back_if_not_present(_artistPositions[artistId], position);
auto itArtists {_artistMatrix.find(artistLink->getType())};
if (itArtists == std::cend(_artistMatrix))
{
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix {width, height});
assert(inserted);
itArtists = it;
}
Utils::push_back_if_not_present(itArtists->second[position], artistId);
}
}
}
_network = std::make_unique<SOM::Network>(network);
LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully loaded!";
}
const SOM::Position position{ network.getClosestRefVectorPosition(samples[i]) };
trackPositions[samplesTrackIds[i]].push_back(position);
}
LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks DONE");
load(std::move(network), std::move(trackPositions));
}
void FeaturesEngine::loadFromCache(FeaturesEngineCache&& cache)
{
LMS_LOG(RECOMMENDATION, INFO, "Constructing features classifier from cache...");
load(std::move(cache._network), cache._trackPositions);
}
TrackContainer FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
{
const TrackContainer trackIds{ [&]
{
TrackContainer res;
Session& session {_db.getTLSSession()};
auto transaction {session.createReadTransaction()};
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
if (trackList)
res = trackList->getTrackIds();
return res;
}() };
return findSimilarTracks(trackIds, maxCount);
}
TrackContainer FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
{
auto similarTrackIds{ getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount) };
Session& session{ _db.getTLSSession() };
{
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
auto transaction{ session.createReadTransaction() };
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
[&](TrackId trackId)
{
return !Track::exists(session, trackId);
}), std::end(similarTrackIds));
}
return similarTrackIds;
}
ReleaseContainer FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
{
auto similarReleaseIds{ getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount) };
Session& session{ _db.getTLSSession() };
if (!similarReleaseIds.empty())
{
// Report only existing ids
auto transaction{ session.createReadTransaction() };
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
[&](ReleaseId releaseId)
{
return !Release::exists(session, releaseId);
}), std::end(similarReleaseIds));
}
return similarReleaseIds;
}
ArtistContainer FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
{
auto getSimilarArtistIdsForLinkType{ [&](TrackArtistLinkType linkType)
{
ArtistContainer similarArtistIds;
const auto itArtists {_artistMatrix.find(linkType)};
if (itArtists == std::cend(_artistMatrix))
{
return similarArtistIds;
}
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
} };
std::unordered_set<ArtistId> similarArtistIds;
for (TrackArtistLinkType linkType : linkTypes)
{
const auto similarArtistIdsForLinkType{ getSimilarArtistIdsForLinkType(linkType) };
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
}
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
Session& session{ _db.getTLSSession() };
{
// Report only existing ids
auto transaction{ session.createReadTransaction() };
res.erase(std::remove_if(std::begin(res), std::end(res),
[&](ArtistId artistId)
{
return !Artist::exists(session, artistId);
}), std::end(res));
}
while (res.size() > maxCount)
res.erase(Random::pickRandom(res));
return res;
}
FeaturesEngineCache FeaturesEngine::toCache() const
{
return FeaturesEngineCache{ *_network, _trackPositions };
}
void FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
{
if (forceReload)
{
FeaturesEngineCache::invalidate();
}
else if (std::optional<FeaturesEngineCache> cache{ FeaturesEngineCache::read() })
{
loadFromCache(std::move(*cache));
return;
}
TrainSettings trainSettings;
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
loadFromTraining(trainSettings, progressCallback);
if (!_loadCancelled && _network)
toCache().write();
}
void FeaturesEngine::requestCancelLoad()
{
LMS_LOG(RECOMMENDATION, DEBUG, "Requesting init cancellation");
_loadCancelled = true;
}
void FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
{
using namespace Database;
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
LMS_LOG(RECOMMENDATION, DEBUG, "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian);
const SOM::Coordinate width{ network.getWidth() };
const SOM::Coordinate height{ network.getHeight() };
_releaseMatrix = ReleaseMatrix{ width, height };
_trackMatrix = TrackMatrix{ width, height };
LMS_LOG(RECOMMENDATION, DEBUG, "Constructing maps...");
Session & session{ _db.getTLSSession() };
for (const auto& [trackId, positions] : trackPositions)
{
if (_loadCancelled)
return;
auto transaction{ session.createReadTransaction() };
const Track::pointer track{ Track::find(session, trackId) };
if (!track)
continue;
for (const SOM::Position& position : positions)
{
Utils::push_back_if_not_present(_trackPositions[trackId], position);
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
if (Release::pointer release{ track->getRelease() })
{
const ReleaseId releaseId{ release->getId() };
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
}
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
{
const ArtistId artistId{ artistLink->getArtist()->getId() };
Utils::push_back_if_not_present(_artistPositions[artistId], position);
auto itArtists{ _artistMatrix.find(artistLink->getType()) };
if (itArtists == std::cend(_artistMatrix))
{
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix{ width, height });
assert(inserted);
itArtists = it;
}
Utils::push_back_if_not_present(itArtists->second[position], artistId);
}
}
}
_network = std::make_unique<SOM::Network>(network);
LMS_LOG(RECOMMENDATION, INFO, "Classifier successfully loaded!");
}
} // ns Recommendation
@@ -23,233 +23,226 @@
#include <boost/property_tree/xml_parser.hpp>
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
namespace Recommendation {
static
std::filesystem::path getCacheDirectory()
namespace Recommendation
{
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
}
namespace
{
std::filesystem::path getCacheDirectory()
{
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
}
static std::filesystem::path getCacheNetworkFilePath()
{
return getCacheDirectory() / "network";
}
std::filesystem::path getCacheNetworkFilePath()
{
return getCacheDirectory() / "network";
}
static std::filesystem::path getCacheTrackPositionsFilePath()
{
return getCacheDirectory() / "track_positions";
}
std::filesystem::path getCacheTrackPositionsFilePath()
{
return getCacheDirectory() / "track_positions";
}
static
bool
networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
{
try
{
boost::property_tree::ptree root;
bool networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
{
try
{
boost::property_tree::ptree root;
root.put("width", network.getWidth());
root.put("height", network.getHeight());
root.put("dim_count", network.getInputDimCount());
root.put("width", network.getWidth());
root.put("height", network.getHeight());
root.put("dim_count", network.getInputDimCount());
for (SOM::InputVector::value_type weight : network.getDataWeights())
root.add("weights.weight", weight);
for (SOM::InputVector::value_type weight : network.getDataWeights())
root.add("weights.weight", weight);
for (SOM::Coordinate x = 0; x < network.getWidth(); ++x)
{
for (SOM::Coordinate y = 0; y < network.getWidth(); ++y)
{
const auto& refVector = network.getRefVector({x, y});
for (SOM::Coordinate x = 0; x < network.getWidth(); ++x)
{
for (SOM::Coordinate y = 0; y < network.getWidth(); ++y)
{
const auto& refVector = network.getRefVector({ x, y });
boost::property_tree::ptree node;
for (auto value : refVector)
node.add("values.value", value);
boost::property_tree::ptree node;
for (auto value : refVector)
node.add("values.value", value);
node.put("coord_x", x);
node.put("coord_y", y);
node.put("coord_x", x);
node.put("coord_y", y);
root.add_child("ref_vectors.ref_vector", node);
}
}
root.add_child("ref_vectors.ref_vector", node);
}
}
boost::property_tree::write_xml(path.string(), root);
boost::property_tree::write_xml(path.string(), root);
LMS_LOG(RECOMMENDATION, DEBUG) << "Created network cache";
return true;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create network cache: " << error.what();
return false;
}
}
LMS_LOG(RECOMMENDATION, DEBUG, "Created network cache");
return true;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot create network cache: " << error.what());
return false;
}
}
}
std::optional<SOM::Network>
FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
{
if (!std::filesystem::exists(path))
return std::nullopt;
std::optional<SOM::Network> FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
{
if (!std::filesystem::exists(path))
return std::nullopt;
try
{
LMS_LOG(RECOMMENDATION, INFO) << "Reading network from cache...";
try
{
LMS_LOG(RECOMMENDATION, INFO, "Reading network from cache...");
boost::property_tree::ptree root;
boost::property_tree::ptree root;
boost::property_tree::read_xml(path.string(), root);
boost::property_tree::read_xml(path.string(), root);
SOM::Coordinate width {root.get<SOM::Coordinate>("width")};
SOM::Coordinate height {root.get<SOM::Coordinate>("height")};
std::size_t dimCount {root.get<std::size_t>("dim_count")};
SOM::Coordinate width{ root.get<SOM::Coordinate>("width") };
SOM::Coordinate height{ root.get<SOM::Coordinate>("height") };
std::size_t dimCount{ root.get<std::size_t>("dim_count") };
SOM::Network res {width, height, dimCount};
SOM::Network res{ width, height, dimCount };
{
SOM::InputVector weights {dimCount};
std::size_t i {};
for (const auto& val : root.get_child("weights"))
weights[i++] = val.second.get_value<double>();
{
SOM::InputVector weights{ dimCount };
std::size_t i{};
for (const auto& val : root.get_child("weights"))
weights[i++] = val.second.get_value<double>();
res.setDataWeights(weights);
}
res.setDataWeights(weights);
}
for (const auto& node : root.get_child("ref_vectors"))
{
SOM::Coordinate x {node.second.get<SOM::Coordinate>("coord_x")};
SOM::Coordinate y {node.second.get<SOM::Coordinate>("coord_y")};
for (const auto& node : root.get_child("ref_vectors"))
{
SOM::Coordinate x{ node.second.get<SOM::Coordinate>("coord_x") };
SOM::Coordinate y{ node.second.get<SOM::Coordinate>("coord_y") };
SOM::InputVector refVector {dimCount};
std::size_t i {};
for (const auto& val : node.second.get_child("values"))
refVector[i++] = val.second.get_value<SOM::InputVector::value_type>();
SOM::InputVector refVector{ dimCount };
std::size_t i{};
for (const auto& val : node.second.get_child("values"))
refVector[i++] = val.second.get_value<SOM::InputVector::value_type>();
res.setRefVector({x, y}, refVector);
}
res.setRefVector({ x, y }, refVector);
}
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read network from cache";
LMS_LOG(RECOMMENDATION, INFO, "Successfully read network from cache");
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot read network cache: " << error.what();
return std::nullopt;
}
}
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot read network cache: " << error.what());
return std::nullopt;
}
}
bool
FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
{
try
{
boost::property_tree::ptree root;
bool FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
{
try
{
boost::property_tree::ptree root;
for (const auto& [id, positions] : trackPositions)
{
boost::property_tree::ptree node;
for (const auto& [id, positions] : trackPositions)
{
boost::property_tree::ptree node;
node.put("id", id.getValue());
node.put("id", id.getValue());
for (const SOM::Position& position : positions)
{
boost::property_tree::ptree positionNode;
positionNode.put("x", position.x);
positionNode.put("y", position.y);
for (const SOM::Position& position : positions)
{
boost::property_tree::ptree positionNode;
positionNode.put("x", position.x);
positionNode.put("y", position.y);
node.add_child("position.position", positionNode);
}
node.add_child("position.position", positionNode);
}
root.add_child("objects.object", node);
}
root.add_child("objects.object", node);
}
boost::property_tree::write_xml(path.string(), root);
return true;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot cache object position: " << error.what();
return false;
}
}
boost::property_tree::write_xml(path.string(), root);
return true;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot cache object position: " << error.what());
return false;
}
}
std::optional<FeaturesEngineCache::TrackPositions>
FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
{
try
{
LMS_LOG(RECOMMENDATION, INFO) << "Reading object position from cache...";
std::optional<FeaturesEngineCache::TrackPositions> FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
{
try
{
LMS_LOG(RECOMMENDATION, INFO, "Reading object position from cache...");
boost::property_tree::ptree root;
boost::property_tree::ptree root;
boost::property_tree::read_xml(path.string(), root);
boost::property_tree::read_xml(path.string(), root);
TrackPositions res;
TrackPositions res;
for (const auto& object : root.get_child("objects"))
{
const Database::TrackId id {object.second.get<Database::IdType::ValueType>("id")};
for (const auto& position : object.second.get_child("position"))
{
auto x = position.second.get<SOM::Coordinate>("x");
auto y = position.second.get<SOM::Coordinate>("y");
for (const auto& object : root.get_child("objects"))
{
const Database::TrackId id{ object.second.get<Database::IdType::ValueType>("id") };
for (const auto& position : object.second.get_child("position"))
{
auto x = position.second.get<SOM::Coordinate>("x");
auto y = position.second.get<SOM::Coordinate>("y");
res[id].push_back({x, y});
}
}
res[id].push_back({ x, y });
}
}
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read object position from cache";
LMS_LOG(RECOMMENDATION, INFO, "Successfully read object position from cache");
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create object position from cache file: " << error.what();
return std::nullopt;
}
}
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot create object position from cache file: " << error.what());
return std::nullopt;
}
}
void
FeaturesEngineCache::invalidate()
{
std::filesystem::remove(getCacheNetworkFilePath());
std::filesystem::remove(getCacheTrackPositionsFilePath());
}
void FeaturesEngineCache::invalidate()
{
std::filesystem::remove(getCacheNetworkFilePath());
std::filesystem::remove(getCacheTrackPositionsFilePath());
}
std::optional<FeaturesEngineCache>
FeaturesEngineCache::read()
{
auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())};
if (!network)
return std::nullopt;
std::optional<FeaturesEngineCache> FeaturesEngineCache::read()
{
auto network{ createNetworkFromCacheFile(getCacheNetworkFilePath()) };
if (!network)
return std::nullopt;
auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())};
if (!trackPositions)
return std::nullopt;
auto trackPositions{ createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath()) };
if (!trackPositions)
return std::nullopt;
return FeaturesEngineCache {std::move(*network), std::move(*trackPositions)};
}
return FeaturesEngineCache{ std::move(*network), std::move(*trackPositions) };
}
void
FeaturesEngineCache::write() const
{
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
void FeaturesEngineCache::write() const
{
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
{
invalidate();
}
}
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
{
invalidate();
}
}
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
: _network {std::move(network)},
_trackPositions {std::move(trackPositions)}
{
}
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
: _network{ std::move(network) },
_trackPositions{ std::move(trackPositions) }
{
}
} // namespace Recommendation
@@ -25,7 +25,7 @@
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Recommendation::PlaylistGeneratorConstraint
{
@@ -23,7 +23,7 @@
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Recommendation::PlaylistGeneratorConstraint
{
@@ -22,7 +22,7 @@
#include "services/database/Db.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
namespace Scanner
{
@@ -45,13 +45,13 @@ namespace Scanner
const Track::pointer track{ Track::find(session, trackId) };
if (auto trackMBID{ track->getTrackMBID() })
{
LMS_LOG(DBUPDATER, INFO) << "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName();
LMS_LOG(DBUPDATER, INFO, "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName());
context.stats.duplicates.emplace_back(ScanDuplicate{ track->getId(), DuplicateReason::SameTrackMBID });
context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats);
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Found " << context.currentStepStats.processedElems << " duplicated audio files";
LMS_LOG(DBUPDATER, DEBUG, "Found " << context.currentStepStats.processedElems << " duplicated audio files");
}
}
@@ -21,7 +21,7 @@
#include "services/database/Db.hpp"
#include "services/database/Cluster.hpp"
#include "services/database/Session.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
namespace Scanner
@@ -84,6 +84,6 @@ namespace Scanner
return true;
});
LMS_LOG(DBUPDATER, DEBUG) << "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!";
LMS_LOG(DBUPDATER, DEBUG, "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!");
}
}
@@ -18,7 +18,7 @@
*/
#include "ScanStepDiscoverFiles.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
namespace Scanner
@@ -42,6 +42,6 @@ namespace Scanner
context.stats.filesScanned = context.currentStepStats.processedElems;
LMS_LOG(DBUPDATER, DEBUG) << "Discovered " << context.stats.filesScanned << " files in '" << context.directory << "'";
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << context.stats.filesScanned << " files in '" << context.directory << "'");
}
}
@@ -25,7 +25,7 @@
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
namespace Scanner
@@ -87,14 +87,14 @@ namespace Scanner
Session& session{ _db.getTLSSession() };
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
LMS_LOG(DBUPDATER, DEBUG, "Checking tracks to be removed...");
std::size_t trackCount{};
{
auto transaction{ session.createReadTransaction() };
trackCount = Track::getCount(session);
}
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
LMS_LOG(DBUPDATER, DEBUG, trackCount << " tracks to be checked...");
context.currentStepStats.totalElems = trackCount;
@@ -143,24 +143,24 @@ namespace Scanner
break;
}
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
LMS_LOG(DBUPDATER, DEBUG, trackCount << " tracks checked!");
}
void ScanStepRemoveOrphanDbFiles::removeOrphanClusters()
{
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters...";
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan clusters...");
removeOrphanEntries<Database::Cluster>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
{
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists...";
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists...");
removeOrphanEntries<Database::Artist>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
{
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan releases...");
removeOrphanEntries<Database::Release>(_db.getTLSSession(), _abortScan);
}
@@ -172,19 +172,19 @@ namespace Scanner
// and still belongs to a media directory
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing";
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': missing");
return false;
}
if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName))
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory";
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': out of media directory");
return false;
}
if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions))
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled";
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': file format no longer handled");
return false;
}
@@ -192,7 +192,7 @@ namespace Scanner
}
catch (std::filesystem::filesystem_error& e)
{
LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what();
LMS_LOG(DBUPDATER, ERROR, "Caught exception while checking file '" << p.string() << "': " << e.what());
return false;
}
}
@@ -30,7 +30,7 @@
#include "services/database/TrackArtistLink.hpp"
#include "utils/Exception.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
using namespace Database;
@@ -258,7 +258,7 @@ namespace Scanner
MetaData::ParserReadStyle getParserReadStyle()
{
std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "accurate") };
std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "average") };
if (readStyle == "fast")
return MetaData::ParserReadStyle::Fast;
@@ -290,7 +290,7 @@ namespace Scanner
if (ec)
{
LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message();
LMS_LOG(DBUPDATER, ERROR, "Cannot process entry '" << path.string() << "': " << ec.message());
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
}
else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
@@ -319,7 +319,7 @@ namespace Scanner
}
catch (LmsException& e)
{
LMS_LOG(DBUPDATER, ERROR) << e.what();
LMS_LOG(DBUPDATER, ERROR, e.what());
stats.skips++;
return;
}
@@ -365,7 +365,7 @@ namespace Scanner
std::error_code ec;
if (!std::filesystem::exists(otherTrack->getPath(), ec))
{
LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'");
track = otherTrack;
track.modify()->setPath(file);
}
@@ -384,7 +384,7 @@ namespace Scanner
if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName))
continue;
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')";
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')");
// As this MBID already exists, just remove what we just scanned
if (track)
{
@@ -399,7 +399,7 @@ namespace Scanner
// We estimate this is an audio file if the duration is not null
if (trackInfo->duration == std::chrono::milliseconds::zero())
{
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
// If Track exists here, delete it!
if (track)
@@ -427,12 +427,12 @@ namespace Scanner
if (!track)
{
track = dbSession.create<Track>(file);
LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Adding '" << file.string() << "'");
stats.additions++;
}
else
{
LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Updating '" << file.string() << "'");
stats.updates++;
}
@@ -27,7 +27,7 @@
#include "services/database/ScanSettings.hpp"
#include "utils/Exception.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Path.hpp"
#include "utils/Tuple.hpp"
@@ -82,9 +82,9 @@ namespace Scanner
ScannerService::~ScannerService()
{
LMS_LOG(DBUPDATER, INFO) << "Stopping service...";
LMS_LOG(DBUPDATER, INFO, "Stopping service...");
stop();
LMS_LOG(DBUPDATER, INFO) << "Service stopped!";
LMS_LOG(DBUPDATER, INFO, "Service stopped!");
}
void ScannerService::start()
@@ -113,15 +113,15 @@ namespace Scanner
void ScannerService::abortScan()
{
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
LMS_LOG(DBUPDATER, DEBUG, "Aborting scan...");
std::scoped_lock lock{ _controlMutex };
LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort...";
LMS_LOG(DBUPDATER, DEBUG, "Waiting for the scan to abort...");
_abortScan = true;
_scheduleTimer.cancel();
_ioService.stop();
LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!";
LMS_LOG(DBUPDATER, DEBUG, "Scan abort done!");
_abortScan = false;
_ioService.start();
@@ -167,7 +167,7 @@ namespace Scanner
void ScannerService::scheduleNextScan()
{
LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan";
LMS_LOG(DBUPDATER, DEBUG, "Scheduling next scan");
refreshScanSettings();
@@ -202,7 +202,7 @@ namespace Scanner
break;
case ScanSettings::UpdatePeriod::Never:
LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!";
LMS_LOG(DBUPDATER, INFO, "Auto scan disabled!");
break;
}
@@ -230,7 +230,7 @@ namespace Scanner
if (dateTime.isNull())
{
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan right now");
_scheduleTimer.expires_from_now(std::chrono::seconds{ 0 });
_scheduleTimer.async_wait(cb);
}
@@ -240,7 +240,7 @@ namespace Scanner
std::time_t t{ std::chrono::system_clock::to_time_t(timePoint) };
char ctimeStr[26];
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr));
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr)));
_scheduleTimer.expires_at(timePoint);
_scheduleTimer.async_wait(cb);
}
@@ -257,7 +257,7 @@ namespace Scanner
}
LMS_LOG(UI, INFO) << "New scan started!";
LMS_LOG(UI, INFO, "New scan started!");
refreshScanSettings();
@@ -267,16 +267,16 @@ namespace Scanner
for (auto& scanStep : _scanSteps)
{
LMS_LOG(DBUPDATER, DEBUG) << "Starting scan step '" << scanStep->getStepName() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'");
scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() };
notifyInProgress(scanContext.currentStepStats);
scanStep->process(scanContext);
notifyInProgress(scanContext.currentStepStats);
LMS_LOG(DBUPDATER, DEBUG) << "Completed scan step '" << scanStep->getStepName() << "'";
LMS_LOG(DBUPDATER, DEBUG, "Completed scan step '" << scanStep->getStepName() << "'");
}
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size();
LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size());
_dbSession.analyze();
@@ -290,14 +290,14 @@ namespace Scanner
_currentScanStepStats.reset();
}
LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!";
LMS_LOG(DBUPDATER, DEBUG, "Scan not aborted, scheduling next scan!");
scheduleNextScan();
_events.scanComplete.emit(stats);
}
else
{
LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!";
LMS_LOG(DBUPDATER, DEBUG, "Scan aborted, not scheduling next scan!");
std::unique_lock lock{ _statusMutex };
@@ -312,9 +312,9 @@ namespace Scanner
if (_settings == newSettings)
return;
LMS_LOG(DBUPDATER, DEBUG) << "Scanner settings updated";
LMS_LOG(DBUPDATER, DEBUG) << "skipDuplicateMBID = " << newSettings.skipDuplicateMBID;
LMS_LOG(DBUPDATER, DEBUG) << "Using scan settings version " << newSettings.scanVersion;
LMS_LOG(DBUPDATER, DEBUG, "Scanner settings updated");
LMS_LOG(DBUPDATER, DEBUG, "skipDuplicateMBID = " << newSettings.skipDuplicateMBID);
LMS_LOG(DBUPDATER, DEBUG, "Using scan settings version " << newSettings.scanVersion);
_settings = std::move(newSettings);
@@ -366,7 +366,7 @@ namespace Scanner
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
[](ClusterType::pointer clusterType) { return clusterType->getName(); });
[](const ClusterType::pointer& clusterType) { return std::string{ clusterType->getName() }; });
newSettings.clusterTypeNames = std::move(clusterTypeNames);
}
@@ -26,7 +26,7 @@
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
@@ -43,15 +43,15 @@ namespace Scrobbling
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
: _db{ db }
{
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
LMS_LOG(SCROBBLING, INFO, "Starting service...");
_scrobblingBackends.emplace(ScrobblingBackend::Internal, std::make_unique<InternalBackend>(_db));
_scrobblingBackends.emplace(ScrobblingBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
LMS_LOG(SCROBBLING, INFO) << "Service started!";
LMS_LOG(SCROBBLING, INFO, "Service started!");
}
ScrobblingService::~ScrobblingService()
{
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
LMS_LOG(SCROBBLING, INFO, "Service stopped!");
}
void ScrobblingService::listenStarted(const Listen& listen)
@@ -24,7 +24,7 @@
#include "services/database/Track.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "Utils.hpp"
@@ -44,7 +44,7 @@ namespace Scrobbling::ListenBrainz
const bool res{ duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2) };
if (!res)
LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s";
LOG(DEBUG, "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s");
return res;
}
@@ -57,12 +57,12 @@ namespace Scrobbling::ListenBrainz
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
, _listensSynchronizer{ _ioContext, db, *_client }
{
LOG(INFO) << "Starting ListenBrainz backend... API endpoint = '" << _baseAPIUrl << "'";
LOG(INFO, "Starting ListenBrainz backend... API endpoint = '" << _baseAPIUrl << "'");
}
ListenBrainzBackend::~ListenBrainzBackend()
{
LOG(INFO) << "Stopped ListenBrainz backend!";
LOG(INFO, "Stopped ListenBrainz backend!");
}
void ListenBrainzBackend::listenStarted(const Listen& listen)
@@ -24,86 +24,82 @@
#include <Wt/Json/Value.h>
#include <Wt/Json/Parser.h>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "Utils.hpp"
namespace
{
using namespace Scrobbling::ListenBrainz;
Listen
parseListen(const Wt::Json::Object& listenObject)
{
Listen listen;
// Mandatory fields
const Wt::Json::Object& metadata = listenObject.get("track_metadata");
listen.trackName = static_cast<std::string>(metadata.get("track_name"));
listen.artistName = static_cast<std::string>(metadata.get("artist_name"));
// Optional fields
listen.releaseName = static_cast<std::string>(metadata.get("release_name").orIfNull(""));
if (listenObject.type("listened_at") == Wt::Json::Type::Number)
listen.listenedAt = Wt::WDateTime::fromTime_t(static_cast<int>(listenObject.get("listened_at")));
if (!listen.listenedAt.isValid())
LOG(ERROR) << "Invalid or missing 'listened_at' field!";
if (metadata.type("additional_info") == Wt::Json::Type::Object)
{
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
listen.trackMBID = UUID::fromString(additionalInfo.get("track_mbid").orIfNull(""));
listen.recordingMBID = UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""));
listen.releaseMBID = UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""));
// tracknumber should be an integer but some players encode as strings
int trackNumber {additionalInfo.get("tracknumber").toNumber().orIfNull(-1)};
if (trackNumber > 0)
listen.trackNumber = trackNumber;
}
return listen;
}
} // namespace
namespace Scrobbling::ListenBrainz
{
ListensParser::Result
ListensParser::parse(std::string_view msgBody)
{
Result result;
namespace
{
Listen parseListen(const Wt::Json::Object& listenObject)
{
Listen listen;
try
{
Wt::Json::Object root;
Wt::Json::parse(std::string {msgBody}, root);
// Mandatory fields
const Wt::Json::Object& metadata = listenObject.get("track_metadata");
listen.trackName = static_cast<std::string>(metadata.get("track_name"));
listen.artistName = static_cast<std::string>(metadata.get("artist_name"));
const Wt::Json::Object& payload = root.get("payload");
const Wt::Json::Array& listens = payload.get("listens");
// Optional fields
listen.releaseName = static_cast<std::string>(metadata.get("release_name").orIfNull(""));
if (listenObject.type("listened_at") == Wt::Json::Type::Number)
listen.listenedAt = Wt::WDateTime::fromTime_t(static_cast<int>(listenObject.get("listened_at")));
if (!listen.listenedAt.isValid())
LOG(ERROR, "Invalid or missing 'listened_at' field!");
LOG(DEBUG) << "Parsing " << listens.size() << " listens...";
result.listenCount = listens.size();
if (metadata.type("additional_info") == Wt::Json::Type::Object)
{
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
listen.trackMBID = UUID::fromString(additionalInfo.get("track_mbid").orIfNull(""));
listen.recordingMBID = UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""));
listen.releaseMBID = UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""));
if (listens.empty())
return result;
// tracknumber should be an integer but some players encode as strings
int trackNumber{ additionalInfo.get("tracknumber").toNumber().orIfNull(-1) };
if (trackNumber > 0)
listen.trackNumber = trackNumber;
}
for (const Wt::Json::Value& value : listens)
{
try
{
const Wt::Json::Object& listen = value;
result.listens.push_back(parseListen(listen));
}
catch (const Wt::WException& error)
{
LOG(ERROR) << "Cannot parse 'listen': " << error.what();
}
}
}
catch (const Wt::WException& error)
{
LOG(ERROR) << "Cannot parse 'listens': " << error.what();
}
return listen;
}
} // namespace
return result;
}
ListensParser::Result ListensParser::parse(std::string_view msgBody)
{
Result result;
try
{
Wt::Json::Object root;
Wt::Json::parse(std::string{ msgBody }, root);
const Wt::Json::Object& payload = root.get("payload");
const Wt::Json::Array& listens = payload.get("listens");
LOG(DEBUG, "Parsing " << listens.size() << " listens...");
result.listenCount = listens.size();
if (listens.empty())
return result;
for (const Wt::Json::Value& value : listens)
{
try
{
const Wt::Json::Object& listen = value;
result.listens.push_back(parseListen(listen));
}
catch (const Wt::WException& error)
{
LOG(ERROR, "Cannot parse 'listen': " << error.what());
}
}
}
catch (const Wt::WException& error)
{
LOG(ERROR, "Cannot parse 'listens': " << error.what());
}
return result;
}
} // Scrobbling::ListenBrainz
@@ -58,7 +58,7 @@ namespace
if (artists.empty())
{
LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist";
LOG(DEBUG, "Track cannot be scrobbled since it does not have any artist");
return std::nullopt;
}
@@ -134,7 +134,7 @@ namespace
}
catch (const Wt::WException& e)
{
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
LOG(ERROR, "Cannot parse listen count response: " << e.what());
return std::nullopt;
}
}
@@ -152,12 +152,12 @@ namespace
// if duplicated files, do not record it (let the user correct its database)
if (tracks.size() == 1)
{
LOG(DEBUG) << "Matched listen '" << listen << "' using track MBID";
LOG(DEBUG, "Matched listen '" << listen << "' using track MBID");
return tracks.front()->getId();
}
else if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using track MBID!";
LOG(DEBUG, "Too many matches for listen '" << listen << "' using track MBID!");
return {};
}
}
@@ -168,12 +168,12 @@ namespace
// if duplicated files, do not record it (let the user correct its database)
if (tracks.size() == 1)
{
LOG(DEBUG) << "Matched listen '" << listen << "' using recording MBID";
LOG(DEBUG, "Matched listen '" << listen << "' using recording MBID");
return tracks.front()->getId();
}
else if (tracks.size() > 1)
{
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using recording MBID!";
LOG(DEBUG, "Too many matches for listen '" << listen << "' using recording MBID!");
return {};
}
}
@@ -192,16 +192,16 @@ namespace
// conservative behavior: in case of multiple matches: reject
if (tracks.results.size() == 1)
{
LOG(DEBUG) << "Matched listen '" << listen << "' using metadata";
LOG(DEBUG, "Matched listen '" << listen << "' using metadata");
return tracks.results.front();
}
else if (tracks.results.size() > 1)
{
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using metadata";
LOG(DEBUG, "Too many matches for listen '" << listen << "' using metadata");
return {};
}
LOG(DEBUG) << "No match for listen '" << listen << "'";
LOG(DEBUG, "No match for listen '" << listen << "'");
return {};
}
}
@@ -215,7 +215,7 @@ namespace Scrobbling::ListenBrainz
, _maxSyncListenCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-listen-count", 1000) }
, _syncListensPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-listens-period-hours", 1) }
{
LOG(INFO) << "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours";
LOG(INFO, "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours");
scheduleSync(std::chrono::seconds{ 30 });
}
@@ -267,14 +267,14 @@ namespace Scrobbling::ListenBrainz
std::string bodyText{ listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now") };
if (bodyText.empty())
{
LOG(DEBUG) << "Cannot convert listen to json: skipping";
LOG(DEBUG, "Cannot convert listen to json: skipping");
return;
}
const std::optional<UUID> listenBrainzToken{ Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) };
if (!listenBrainzToken)
{
LOG(DEBUG) << "No listenbrainz token found: skipping";
LOG(DEBUG, "No listenbrainz token found: skipping");
return;
}
@@ -305,7 +305,7 @@ namespace Scrobbling::ListenBrainz
dbListen = session.create<Database::Listen>(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt);
dbListen.modify()->setSyncState(scrobblingState);
LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString();
LOG(DEBUG, "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString());
return true;
}
@@ -347,7 +347,7 @@ namespace Scrobbling::ListenBrainz
}
}
LOG(DEBUG) << "Queing " << pendingListens.size() << " pending listen";
LOG(DEBUG, "Queing " << pendingListens.size() << " pending listen");
for (const TimedListen& pendingListen : pendingListens)
enqueListen(pendingListen);
@@ -379,13 +379,13 @@ namespace Scrobbling::ListenBrainz
if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0)
return;
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
LOG(DEBUG, "Scheduled sync in " << fromNow.count() << " seconds...");
_syncTimer.expires_after(fromNow);
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG) << "getListens aborted";
LOG(DEBUG, "getListens aborted");
return;
}
else if (ec)
@@ -399,7 +399,7 @@ namespace Scrobbling::ListenBrainz
void ListensSynchronizer::startSync()
{
LOG(DEBUG) << "Starting sync!";
LOG(DEBUG, "Starting sync!");
assert(!isSyncing());
@@ -435,7 +435,7 @@ namespace Scrobbling::ListenBrainz
{
_strand.dispatch([this, &context]
{
LOG(INFO) << "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount;
LOG(INFO, "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount);
context.syncing = false;
if (!isSyncing())
@@ -489,7 +489,7 @@ namespace Scrobbling::ListenBrainz
{
const auto listenCount = parseListenCount(msgBody);
if (listenCount)
LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount;
LOG(DEBUG, "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount);
bool needSync{ listenCount && (!context.listenCount || *context.listenCount != *listenCount) };
context.listenCount = listenCount;
@@ -551,7 +551,7 @@ namespace Scrobbling::ListenBrainz
// update oldest listen for the next query
if (!parsedListen.listenedAt.isValid())
{
LOG(DEBUG) << "Skipping entry due to invalid listenedAt";
LOG(DEBUG, "Skipping entry due to invalid listenedAt");
continue;
}
@@ -27,38 +27,36 @@
namespace Scrobbling::ListenBrainz::Utils
{
std::optional<UUID>
getListenBrainzToken(Database::Session& session, Database::UserId userId)
{
auto transaction {session.createReadTransaction()};
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId)
{
auto transaction{ session.createReadTransaction() };
const Database::User::pointer user {Database::User::find(session, userId)};
if (!user)
return std::nullopt;
const Database::User::pointer user{ Database::User::find(session, userId) };
if (!user)
return std::nullopt;
return user->getListenBrainzToken();
}
return user->getListenBrainzToken();
}
std::string
parseValidateToken(std::string_view msgBody)
{
std::string listenBrainzUserName;
std::string parseValidateToken(std::string_view msgBody)
{
std::string listenBrainzUserName;
Wt::Json::ParseError error;
Wt::Json::Object root;
if (!Wt::Json::parse(std::string {msgBody}, root, error))
{
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
return listenBrainzUserName;
}
Wt::Json::ParseError error;
Wt::Json::Object root;
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
{
LOG(ERROR, "Cannot parse 'validate-token' result: " << error.what());
return listenBrainzUserName;
}
if (!root.get("valid").orIfNull(false))
{
LOG(INFO) << "Invalid listenbrainz user";
return listenBrainzUserName;
}
if (!root.get("valid").orIfNull(false))
{
LOG(INFO, "Invalid listenbrainz user");
return listenBrainzUserName;
}
listenBrainzUserName = root.get("user_name").orIfNull("");
return listenBrainzUserName;
}
listenBrainzUserName = root.get("user_name").orIfNull("");
return listenBrainzUserName;
}
}
@@ -20,10 +20,10 @@
#pragma once
#include "services/database/UserId.hpp"
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/UUID.hpp"
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] "
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, message << "[listenbrainz] ")
namespace Database
{
@@ -19,16 +19,16 @@
#include <gtest/gtest.h>
#include "utils/Logger.hpp"
#include "utils/ILogger.hpp"
#include "utils/Service.hpp"
#include "utils/StreamLogger.hpp"
int main(int argc, char **argv)
int main(int argc, char** argv)
{
// log to stdout
Service<Logger> logger {std::make_unique<StreamLogger>(std::cout, EnumSet<Severity> {Severity::FATAL, Severity::ERROR})};
// log to stdout
Service<ILogger> logger{ std::make_unique<StreamLogger>(std::cout, EnumSet<Severity> {Severity::FATAL, Severity::ERROR}) };
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}