Auto reformatted the base, ref #470

This commit is contained in:
emeric
2024-05-24 23:31:52 +02:00
parent 83b868673c
commit 39941d90a3
460 changed files with 8583 additions and 8514 deletions
@@ -19,11 +19,10 @@
#include "AuthServiceBase.hpp"
#include <cstdlib>
#include "core/ILogger.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "core/ILogger.hpp"
namespace lms::auth
{
@@ -31,7 +30,8 @@ namespace lms::auth
AuthServiceBase::AuthServiceBase(Db& db)
: _db{ db }
{}
{
}
UserId AuthServiceBase::getOrCreateUser(std::string_view loginName)
{
@@ -91,4 +91,4 @@ namespace lms::auth
{
return _db.getTLSSession();
}
}
} // namespace lms::auth
@@ -20,13 +20,14 @@
#pragma once
#include <string_view>
#include "database/UserId.hpp"
namespace lms::db
{
class Db;
class Session;
}
} // namespace lms::db
namespace lms::auth
{
@@ -35,12 +36,12 @@ namespace lms::auth
protected:
AuthServiceBase(db::Db& db);
db::UserId getOrCreateUser(std::string_view loginName);
void onUserAuthenticated(db::UserId userId);
db::UserId getOrCreateUser(std::string_view loginName);
void onUserAuthenticated(db::UserId userId);
db::Session& getDbSession();
db::Session& getDbSession();
private:
db::Db& _db;
};
}
} // namespace lms::auth
@@ -23,121 +23,121 @@
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/WRandom.h>
#include "services/auth/Types.hpp"
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "database/AuthToken.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "services/auth/Types.hpp"
namespace lms::auth
{
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
{
return std::make_unique<AuthTokenService>(db, maxThrottlerEntries);
}
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
{
return std::make_unique<AuthTokenService>(db, maxThrottlerEntries);
}
static const Wt::Auth::SHA1HashFunction sha1Function;
static const Wt::Auth::SHA1HashFunction sha1Function;
AuthTokenService::AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
: AuthServiceBase {db}
, _loginThrottler {maxThrottlerEntries}
{
}
AuthTokenService::AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
: AuthServiceBase{ db }
, _loginThrottler{ maxThrottlerEntries }
{
}
std::string
AuthTokenService::createAuthToken(db::UserId userId, const Wt::WDateTime& expiry)
{
const std::string secret {Wt::WRandom::generateId(32)};
const std::string secretHash {sha1Function.compute(secret, {})};
std::string
AuthTokenService::createAuthToken(db::UserId userId, const Wt::WDateTime& expiry)
{
const std::string secret{ Wt::WRandom::generateId(32) };
const std::string secretHash{ sha1Function.compute(secret, {}) };
db::Session& session {getDbSession()};
db::Session& session{ getDbSession() };
auto transaction {session.createWriteTransaction()};
auto transaction{ session.createWriteTransaction() };
db::User::pointer user {db::User::find(session, userId)};
if (!user)
throw Exception {"User deleted"};
db::User::pointer user{ db::User::find(session, userId) };
if (!user)
throw Exception{ "User deleted" };
db::AuthToken::pointer authToken {session.create<db::AuthToken>(secretHash, expiry, user)};
db::AuthToken::pointer authToken{ session.create<db::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)
db::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
if (user->getAuthTokensCount() >= 50)
db::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
return secret;
}
return secret;
}
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
AuthTokenService::processAuthToken(std::string_view secret)
{
const std::string secretHash {sha1Function.compute(std::string {secret}, {})};
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
AuthTokenService::processAuthToken(std::string_view secret)
{
const std::string secretHash{ sha1Function.compute(std::string{ secret }, {}) };
db::Session& session {getDbSession()};
auto transaction {session.createWriteTransaction()};
db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() };
db::AuthToken::pointer authToken {db::AuthToken::find(session, secretHash)};
if (!authToken)
return std::nullopt;
db::AuthToken::pointer authToken{ db::AuthToken::find(session, secretHash) };
if (!authToken)
return std::nullopt;
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
{
authToken.remove();
return std::nullopt;
}
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
{
authToken.remove();
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();
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res{ authToken->getUser()->getId(), authToken->getExpiry() };
authToken.remove();
return res;
}
return res;
}
AuthTokenService::AuthTokenProcessResult
AuthTokenService::processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
{
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock lock {_mutex};
AuthTokenService::AuthTokenProcessResult
AuthTokenService::processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
{
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock lock{ _mutex };
if (_loginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
}
if (_loginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Throttled };
}
auto res {processAuthToken(tokenValue)};
{
std::unique_lock lock {_mutex};
auto res{ processAuthToken(tokenValue) };
{
std::unique_lock lock{ _mutex };
if (_loginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
if (_loginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Throttled };
if (!res)
{
_loginThrottler.onBadClientAttempt(clientAddress);
return AuthTokenProcessResult {AuthTokenProcessResult::State::Denied};
}
if (!res)
{
_loginThrottler.onBadClientAttempt(clientAddress);
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Denied };
}
_loginThrottler.onGoodClientAttempt(clientAddress);
onUserAuthenticated(res->userId);
return AuthTokenProcessResult {AuthTokenProcessResult::State::Granted, std::move(*res)};
}
}
_loginThrottler.onGoodClientAttempt(clientAddress);
onUserAuthenticated(res->userId);
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Granted, std::move(*res) };
}
}
void
AuthTokenService::clearAuthTokens(db::UserId userId)
{
db::Session& session {getDbSession()};
void
AuthTokenService::clearAuthTokens(db::UserId userId)
{
db::Session& session{ getDbSession() };
auto transaction {session.createWriteTransaction()};
auto transaction{ session.createWriteTransaction() };
db::User::pointer user {db::User::find(session, userId)};
if (!user)
throw Exception {"User deleted"};
db::User::pointer user{ db::User::find(session, userId) };
if (!user)
throw Exception{ "User deleted" };
user.modify()->clearAuthTokens();
}
user.modify()->clearAuthTokens();
}
} // namespace lms::auth
@@ -22,34 +22,35 @@
#include <shared_mutex>
#include "services/auth/IAuthTokenService.hpp"
#include "AuthServiceBase.hpp"
#include "LoginThrottler.hpp"
namespace lms::db
{
class Session;
class Session;
}
namespace lms::auth
{
class AuthTokenService : public IAuthTokenService, public AuthServiceBase
{
public:
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries);
class AuthTokenService : public IAuthTokenService, public AuthServiceBase
{
public:
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries);
AuthTokenService(const AuthTokenService&) = delete;
AuthTokenService& operator=(const AuthTokenService&) = delete;
AuthTokenService(AuthTokenService&&) = delete;
AuthTokenService& operator=(AuthTokenService&&) = delete;
AuthTokenService(const AuthTokenService&) = delete;
AuthTokenService& operator=(const AuthTokenService&) = delete;
AuthTokenService(AuthTokenService&&) = delete;
AuthTokenService& operator=(AuthTokenService&&) = delete;
private:
AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
std::string createAuthToken(db::UserId userId, const Wt::WDateTime& expiry) override;
void clearAuthTokens(db::UserId userId) override;
private:
AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
std::string createAuthToken(db::UserId userId, const Wt::WDateTime& expiry) override;
void clearAuthTokens(db::UserId userId) override;
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> processAuthToken(std::string_view secret);
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> processAuthToken(std::string_view secret);
std::shared_mutex _mutex;
LoginThrottler _loginThrottler;
};
}
std::shared_mutex _mutex;
LoginThrottler _loginThrottler;
};
} // namespace lms::auth
+9 -8
View File
@@ -20,16 +20,17 @@
#include "services/auth/IEnvService.hpp"
#include "services/auth/Types.hpp"
#include "http-headers/HttpHeadersEnvService.hpp"
namespace lms::auth
{
std::unique_ptr<IEnvService>
createEnvService(std::string_view backendName, db::Db& db)
{
if (backendName == "http-headers")
return std::make_unique<HttpHeadersEnvService>(db);
std::unique_ptr<IEnvService>
createEnvService(std::string_view backendName, db::Db& db)
{
if (backendName == "http-headers")
return std::make_unique<HttpHeadersEnvService>(db);
throw Exception {"Authentication backend '" + std::string {backendName} + "' is not supported!"};
}
}
throw Exception{ "Authentication backend '" + std::string{ backendName } + "' is not supported!" };
}
} // namespace lms::auth
@@ -17,8 +17,6 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
/* This file contains some classes in order to get info from file using the libavconv */
#include "LoginThrottler.hpp"
#include "core/ILogger.hpp"
@@ -44,13 +42,13 @@ namespace lms::auth
{
return address.is_v6() ? getAddressWithMask(address.to_v6(), 64) : address;
}
}
} // namespace
void LoginThrottler::removeOutdatedEntries()
{
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
for (auto it{ std::begin(_attemptsInfo) }; it != std::end(_attemptsInfo); )
for (auto it{ std::begin(_attemptsInfo) }; it != std::end(_attemptsInfo);)
{
if (it->second.nextAttempt <= now)
it = _attemptsInfo.erase(it);
@@ -110,4 +108,4 @@ namespace lms::auth
return it->second.nextAttempt > Wt::WDateTime::currentDateTime();
}
}
} // namespace lms::auth
+23 -23
View File
@@ -24,34 +24,34 @@
#include <Wt/WDateTime.h>
#include "core/NetAddress.hpp"
#include "core/Exception.hpp"
#include "core/NetAddress.hpp"
namespace lms::auth
{
class LoginThrottler
{
public:
LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {}
class LoginThrottler
{
public:
LoginThrottler(std::size_t maxEntries)
: _maxEntries{ maxEntries } {}
// user must lock these calls to avoid races
bool isClientThrottled(const boost::asio::ip::address& address) const;
void onBadClientAttempt(const boost::asio::ip::address& address);
void onGoodClientAttempt(const boost::asio::ip::address& address);
// user must lock these calls to avoid races
bool isClientThrottled(const boost::asio::ip::address& address) const;
void onBadClientAttempt(const boost::asio::ip::address& address);
void onGoodClientAttempt(const boost::asio::ip::address& address);
private:
void removeOutdatedEntries();
private:
void removeOutdatedEntries();
const std::size_t _maxEntries;
static constexpr std::size_t _maxBadConsecutiveAttemptCount {5};
static constexpr std::chrono::seconds _throttlingDuration {3};
struct AttemptInfo
{
Wt::WDateTime nextAttempt;
std::size_t badConsecutiveAttemptCount{};
};
std::unordered_map<boost::asio::ip::address, AttemptInfo> _attemptsInfo;
};
} // Auth
const std::size_t _maxEntries;
static constexpr std::size_t _maxBadConsecutiveAttemptCount{ 5 };
static constexpr std::chrono::seconds _throttlingDuration{ 3 };
struct AttemptInfo
{
Wt::WDateTime nextAttempt;
std::size_t badConsecutiveAttemptCount{};
};
std::unordered_map<boost::asio::ip::address, AttemptInfo> _attemptsInfo;
};
} // namespace lms::auth
@@ -24,73 +24,72 @@
#include "internal/InternalPasswordService.hpp"
#ifdef LMS_SUPPORT_PAM
#include "pam/PAMPasswordService.hpp"
#include "pam/PAMPasswordService.hpp"
#endif // LMS_SUPPORT_PAM
#include "services/auth/Types.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "services/auth/Types.hpp"
namespace lms::auth
{
static const Wt::Auth::SHA1HashFunction sha1Function;
static const Wt::Auth::SHA1HashFunction sha1Function;
std::unique_ptr<IPasswordService>
createPasswordService(std::string_view passwordAuthenticationBackend, db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
{
if (passwordAuthenticationBackend == "internal")
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntries, authTokenService);
std::unique_ptr<IPasswordService>
createPasswordService(std::string_view passwordAuthenticationBackend, db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
{
if (passwordAuthenticationBackend == "internal")
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntries, authTokenService);
#ifdef LMS_SUPPORT_PAM
else if (passwordAuthenticationBackend == "pam")
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntries, authTokenService);
else if (passwordAuthenticationBackend == "pam")
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntries, authTokenService);
#endif // LMS_SUPPORT_PAM
throw Exception {"Authentication backend '" + std::string {passwordAuthenticationBackend} + "' is not supported!"};
}
throw Exception{ "Authentication backend '" + std::string{ passwordAuthenticationBackend } + "' is not supported!" };
}
PasswordServiceBase::PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
: AuthServiceBase {db}
, _loginThrottler {maxThrottlerEntries}
, _authTokenService {authTokenService}
{
}
PasswordServiceBase::PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
: AuthServiceBase{ db }
, _loginThrottler{ maxThrottlerEntries }
, _authTokenService{ authTokenService }
{
}
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 << "'");
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 << "'");
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock lock {_mutex};
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock lock{ _mutex };
if (_loginThrottler.isClientThrottled(clientAddress))
return {CheckResult::State::Throttled};
}
if (_loginThrottler.isClientThrottled(clientAddress))
return { CheckResult::State::Throttled };
}
const bool match {checkUserPassword(loginName, password)};
{
std::unique_lock lock {_mutex};
const bool match{ checkUserPassword(loginName, password) };
{
std::unique_lock lock{ _mutex };
if (_loginThrottler.isClientThrottled(clientAddress))
return {CheckResult::State::Throttled};
if (_loginThrottler.isClientThrottled(clientAddress))
return { CheckResult::State::Throttled };
if (match)
{
_loginThrottler.onGoodClientAttempt(clientAddress);
if (match)
{
_loginThrottler.onGoodClientAttempt(clientAddress);
const db::UserId userId {getOrCreateUser(loginName)};
onUserAuthenticated(userId);
return {CheckResult::State::Granted, userId};
}
else
{
_loginThrottler.onBadClientAttempt(clientAddress);
return {CheckResult::State::Denied};
}
}
}
const db::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId };
}
else
{
_loginThrottler.onBadClientAttempt(clientAddress);
return { CheckResult::State::Denied };
}
}
}
} // namespace lms::auth
@@ -21,40 +21,40 @@
#include <shared_mutex>
#include "services/auth/IPasswordService.hpp"
#include "AuthServiceBase.hpp"
#include "LoginThrottler.hpp"
#include "services/auth/IPasswordService.hpp"
namespace lms::db
{
class Db;
class Session;
}
class Db;
class Session;
} // namespace lms::db
namespace lms::auth
{
class PasswordServiceBase : public IPasswordService, public AuthServiceBase
{
public:
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
class PasswordServiceBase : public IPasswordService, public AuthServiceBase
{
public:
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
PasswordServiceBase(const PasswordServiceBase&) = delete;
PasswordServiceBase& operator=(const PasswordServiceBase&) = delete;
PasswordServiceBase(PasswordServiceBase&&) = delete;
PasswordServiceBase& operator=(PasswordServiceBase&&) = delete;
PasswordServiceBase(const PasswordServiceBase&) = delete;
PasswordServiceBase& operator=(const PasswordServiceBase&) = delete;
PasswordServiceBase(PasswordServiceBase&&) = delete;
PasswordServiceBase& operator=(PasswordServiceBase&&) = delete;
protected:
IAuthTokenService& getAuthTokenService() { return _authTokenService; }
protected:
IAuthTokenService& getAuthTokenService() { return _authTokenService; }
private:
virtual bool checkUserPassword(std::string_view loginName, std::string_view password) = 0;
private:
virtual bool checkUserPassword(std::string_view loginName, std::string_view password) = 0;
CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
std::string_view loginName,
std::string_view password) override;
CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
std::string_view loginName,
std::string_view password) override;
std::shared_mutex _mutex;
LoginThrottler _loginThrottler;
IAuthTokenService& _authTokenService;
};
}
std::shared_mutex _mutex;
LoginThrottler _loginThrottler;
IAuthTokenService& _authTokenService;
};
} // namespace lms::auth
@@ -20,21 +20,21 @@
#pragma once
#include "services/auth/IEnvService.hpp"
#include "AuthServiceBase.hpp"
namespace lms::auth
{
class HttpHeadersEnvService : public IEnvService, public AuthServiceBase
{
public:
HttpHeadersEnvService(db::Db& db);
class HttpHeadersEnvService : public IEnvService, public AuthServiceBase
{
public:
HttpHeadersEnvService(db::Db& db);
private:
CheckResult processEnv(const Wt::WEnvironment& env) override;
CheckResult processRequest(const Wt::Http::Request& request) override;
private:
CheckResult processEnv(const Wt::WEnvironment& env) override;
CheckResult processRequest(const Wt::Http::Request& request) override;
std::string _fieldName;
};
std::string _fieldName;
};
} // namespace lms::auth
@@ -18,14 +18,15 @@
*/
#include "InternalPasswordService.hpp"
#include <Wt/WRandom.h>
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/Types.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/Types.hpp"
namespace lms::auth
{
@@ -120,14 +121,13 @@ namespace lms::auth
{
const std::string salt{ Wt::WRandom::generateId(32) };
return { salt, _hashFunc.compute(std::string {password}, salt) };
return { salt, _hashFunc.compute(std::string{ password }, salt) };
}
void
InternalPasswordService::hashRandomPassword() const
InternalPasswordService::hashRandomPassword() const
{
hashPassword(Wt::WRandom::generateId(32));
}
} // namespace lms::auth
@@ -23,8 +23,9 @@
#include <Wt/Auth/PasswordStrengthValidator.h>
#include "database/User.hpp"
#include "PasswordServiceBase.hpp"
#include "LoginThrottler.hpp"
#include "PasswordServiceBase.hpp"
namespace lms::auth
{
@@ -36,17 +37,17 @@ namespace lms::auth
InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
private:
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
bool canSetPasswords() const override;
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
void setPassword(db::UserId userId, std::string_view newPassword) override;
bool canSetPasswords() const override;
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
void setPassword(db::UserId userId, std::string_view newPassword) override;
db::User::PasswordHash hashPassword(std::string_view password) const;
void hashRandomPassword() const;
db::User::PasswordHash hashPassword(std::string_view password) const;
void hashRandomPassword() const;
const Wt::Auth::BCryptHashFunction _hashFunc{ 7 }; // TODO parametrize this
Wt::Auth::PasswordStrengthValidator _validator;
const Wt::Auth::BCryptHashFunction _hashFunc{ 7 }; // TODO parametrize this
Wt::Auth::PasswordStrengthValidator _validator;
};
}
} // namespace lms::auth
@@ -20,15 +20,15 @@
#include "PAMPasswordService.hpp"
#ifndef LMS_SUPPORT_PAM
#error "Should not compile this"
#error "Should not compile this"
#endif
#include <cstring>
#include <security/pam_appl.h>
#include "services/auth/Types.hpp"
#include "database/Session.hpp"
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "services/auth/Types.hpp"
namespace lms::auth
{
@@ -53,7 +53,7 @@ namespace lms::auth
public:
PAMContext(std::string_view loginName)
{
int err{ pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh) };
int err{ pam_start("lms", std::string{ loginName }.c_str(), &_conv, &_pamh) };
if (err != PAM_SUCCESS)
throw PAMError{ "start failed", _pamh, err };
}
@@ -92,7 +92,8 @@ namespace lms::auth
class AuthenticateConvContext final : public ConvContext
{
public:
AuthenticateConvContext(std::string_view password) : _password{ password } {}
AuthenticateConvContext(std::string_view password)
: _password{ password } {}
std::string_view getPassword() const { return _password; }
@@ -160,7 +161,7 @@ namespace lms::auth
pam_conv _conv{ &PAMContext::conv, this };
pam_handle_t* _pamh{};
};
}
} // namespace
bool PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
@@ -197,4 +198,3 @@ namespace lms::auth
}
} // namespace lms::auth
@@ -25,15 +25,15 @@
namespace lms::auth
{
class PAMPasswordService: public PasswordServiceBase
{
public:
using PasswordServiceBase::PasswordServiceBase;
class PAMPasswordService : public PasswordServiceBase
{
public:
using PasswordServiceBase::PasswordServiceBase;
private:
bool checkUserPassword(std::string_view loginName,std::string_view password) override;
bool canSetPasswords() const override;
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
void setPassword(db::UserId userId, std::string_view newPassword) override;
};
}
private:
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
bool canSetPasswords() const override;
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
void setPassword(db::UserId userId, std::string_view newPassword) override;
};
} // namespace lms::auth
@@ -17,58 +17,56 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
/* This file contains some classes in order to get info from file using the libavconv */
#pragma once
#include <Wt/WDateTime.h>
#include <boost/asio/ip/address.hpp>
#include <optional>
#include <string>
#include <string_view>
#include <boost/asio/ip/address.hpp>
#include <Wt/WDateTime.h>
#include "database/UserId.hpp"
namespace lms::db
{
class Db;
class User;
}
class Db;
class User;
} // namespace lms::db
namespace lms::auth
{
class IAuthTokenService
{
public:
virtual ~IAuthTokenService() = default;
class IAuthTokenService
{
public:
virtual ~IAuthTokenService() = default;
// Auth Token services
struct AuthTokenProcessResult
{
enum class State
{
Granted,
Throttled,
Denied,
};
// Auth Token services
struct AuthTokenProcessResult
{
enum class State
{
Granted,
Throttled,
Denied,
};
struct AuthTokenInfo
{
db::UserId userId;
Wt::WDateTime expiry;
};
struct AuthTokenInfo
{
db::UserId userId;
Wt::WDateTime expiry;
};
State state {State::Denied};
std::optional<AuthTokenInfo> authTokenInfo {};
};
State state{ State::Denied };
std::optional<AuthTokenInfo> authTokenInfo{};
};
// Provided token is only accepted once
virtual AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
// Provided token is only accepted once
virtual AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
// Returns a one time token
virtual std::string createAuthToken(db::UserId userid, const Wt::WDateTime& expiry) = 0;
virtual void clearAuthTokens(db::UserId userid) = 0;
};
// Returns a one time token
virtual std::string createAuthToken(db::UserId userid, const Wt::WDateTime& expiry) = 0;
virtual void clearAuthTokens(db::UserId userid) = 0;
};
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
}
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
} // namespace lms::auth
@@ -26,44 +26,44 @@
namespace lms::db
{
class Db;
class Session;
}
class Db;
class Session;
} // namespace lms::db
namespace Wt
{
class WEnvironment;
class WEnvironment;
}
namespace Wt::Http
{
class Request;
class Request;
}
namespace lms::auth
{
class IEnvService
{
public:
virtual ~IEnvService() = default;
class IEnvService
{
public:
virtual ~IEnvService() = default;
// Auth Token services
struct CheckResult
{
enum class State
{
Granted,
Denied,
Throttled,
};
// Auth Token services
struct CheckResult
{
enum class State
{
Granted,
Denied,
Throttled,
};
State state {State::Denied};
std::optional<db::UserId> userId {};
};
State state{ State::Denied };
std::optional<db::UserId> userId{};
};
virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0;
virtual CheckResult processRequest(const Wt::Http::Request& request) = 0;
};
virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0;
virtual CheckResult processRequest(const Wt::Http::Request& request) = 0;
};
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName, db::Db& db);
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName, db::Db& db);
} // namespace lms::auth
@@ -19,59 +19,59 @@
#pragma once
#include <string_view>
#include <optional>
#include <string_view>
#include <boost/asio/ip/address.hpp>
#include <Wt/WDateTime.h>
#include <Wt/Dbo/ptr.h>
#include <Wt/WDateTime.h>
#include <boost/asio/ip/address.hpp>
#include "services/auth/Types.hpp"
#include "database/UserId.hpp"
#include "services/auth/Types.hpp"
namespace lms::db
{
class Db;
class User;
}
class Db;
class User;
} // namespace lms::db
namespace lms::auth
{
class IAuthTokenService;
class IAuthTokenService;
class IPasswordService
{
public:
virtual ~IPasswordService() = default;
class IPasswordService
{
public:
virtual ~IPasswordService() = default;
struct CheckResult
{
enum class State
{
Granted,
Denied,
Throttled,
};
State state {State::Denied};
std::optional<db::UserId> userId {};
std::optional<Wt::WDateTime> expiry {};
};
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
std::string_view loginName,
std::string_view password) = 0;
struct CheckResult
{
enum class State
{
Granted,
Denied,
Throttled,
};
State state{ State::Denied };
std::optional<db::UserId> userId{};
std::optional<Wt::WDateTime> expiry{};
};
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
std::string_view loginName,
std::string_view password)
= 0;
virtual bool canSetPasswords() const = 0;
virtual bool canSetPasswords() const = 0;
enum class PasswordAcceptabilityResult
{
OK,
TooWeak,
MustMatchLoginName,
};
virtual PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const = 0;
virtual void setPassword(db::UserId userId, std::string_view newPassword) = 0;
};
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, db::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
}
enum class PasswordAcceptabilityResult
{
OK,
TooWeak,
MustMatchLoginName,
};
virtual PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const = 0;
virtual void setPassword(db::UserId userId, std::string_view newPassword) = 0;
};
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, db::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
} // namespace lms::auth
@@ -20,50 +20,54 @@
#pragma once
#include <string>
#include "database/Types.hpp"
#include "core/Exception.hpp"
#include "database/Types.hpp"
namespace lms::auth
{
class Exception : public core::LmsException
{
using core::LmsException::LmsException;
};
class Exception : public core::LmsException
{
using core::LmsException::LmsException;
};
class NotImplementedException : public Exception
{
public:
NotImplementedException() : Exception {"Not implemented"} {}
};
class NotImplementedException : public Exception
{
public:
NotImplementedException()
: Exception{ "Not implemented" } {}
};
class UserNotFoundException : public Exception
{
public:
UserNotFoundException() : Exception {"User not found"} {}
};
class UserNotFoundException : public Exception
{
public:
UserNotFoundException()
: Exception{ "User not found" } {}
};
struct PasswordValidationContext
{
std::string loginName;
db::UserType userType;
};
struct PasswordValidationContext
{
std::string loginName;
db::UserType userType;
};
class PasswordException : public Exception
{
public:
using Exception::Exception;
};
class PasswordException : public Exception
{
public:
using Exception::Exception;
};
class PasswordTooWeakException : public PasswordException
{
public:
PasswordTooWeakException() : PasswordException {"Password too weak"} {}
};
class PasswordMustMatchLoginNameException : public PasswordException
{
public:
PasswordMustMatchLoginNameException() : PasswordException {"Password must match login name"} {}
};
}
class PasswordTooWeakException : public PasswordException
{
public:
PasswordTooWeakException()
: PasswordException{ "Password too weak" } {}
};
class PasswordMustMatchLoginNameException : public PasswordException
{
public:
PasswordMustMatchLoginNameException()
: PasswordException{ "Password must match login name" } {}
};
} // namespace lms::auth
+51 -58
View File
@@ -22,21 +22,19 @@
#include <set>
#include "av/IAudioFile.hpp"
#include "database/Db.hpp"
#include "database/Artist.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "image/Exception.hpp"
#include "image/Image.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
#include "core/Random.hpp"
#include "core/String.hpp"
#include "core/Utils.hpp"
#include "database/Artist.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "image/Exception.hpp"
#include "image/Image.hpp"
namespace lms::cover
{
@@ -80,10 +78,10 @@ namespace lms::cover
std::vector<std::string> res;
core::Service<core::IConfig>::get()->visitStrings("cover-preferred-file-names",
[&res](std::string_view fileName)
{
[&res](std::string_view fileName) {
res.emplace_back(fileName);
}, { "cover", "front" });
},
{ "cover", "front" });
return res;
}
@@ -93,10 +91,10 @@ namespace lms::cover
std::vector<std::string> res;
core::Service<core::IConfig>::get()->visitStrings("artist-image-file-names",
[&res](std::string_view fileName)
{
[&res](std::string_view fileName) {
res.emplace_back(fileName);
}, { "artist" });
},
{ "artist" });
return res;
}
@@ -105,7 +103,7 @@ namespace lms::cover
{
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
}
}
} // namespace
std::unique_ptr<ICoverService> createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath)
{
@@ -136,22 +134,21 @@ namespace lms::cover
{
std::unique_ptr<IEncodedImage> image;
input.visitAttachedPictures([&](const av::Picture& picture)
{
if (image)
return;
input.visitAttachedPictures([&](const av::Picture& picture) {
if (image)
return;
try
{
std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
rawImage->resize(width);
image = rawImage->encodeToJPEG(_jpegQuality);
}
catch (const image::Exception& e)
{
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
}
});
try
{
std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
rawImage->resize(width);
image = rawImage->encodeToJPEG(_jpegQuality);
}
catch (const image::Exception& e)
{
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
}
});
return image;
}
@@ -183,19 +180,18 @@ namespace lms::cover
{
const std::multimap<std::string, std::filesystem::path> coverPaths{ getCoverPaths(directory) };
auto tryLoadImageFromFilename = [&](std::string_view fileName)
{
std::unique_ptr<IEncodedImage> image;
auto tryLoadImageFromFilename = [&](std::string_view fileName) {
std::unique_ptr<IEncodedImage> image;
auto range{ coverPaths.equal_range(std::string {fileName}) };
for (auto it{ range.first }; it != range.second; ++it)
{
image = getFromCoverFile(it->second, width);
if (image)
break;
}
return image;
};
auto range{ coverPaths.equal_range(std::string{ fileName }) };
for (auto it{ range.first }; it != range.second; ++it)
{
image = getFromCoverFile(it->second, width);
if (image)
break;
}
return image;
};
std::unique_ptr<IEncodedImage> image;
@@ -353,14 +349,13 @@ namespace lms::cover
Session& session{ _db.getTLSSession() };
auto getReleaseInfo{ [&]
{
auto getReleaseInfo{ [&] {
std::optional<ReleaseInfo> res;
auto transaction{ session.createReadTransaction() };
// get a track in this release, consider the release is in a single directory
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange(Range{ 0, 1 }).setSortMethod(TrackSortMethod::Release)) };
const auto tracks{ Track::find(session, Track::FindParameters{}.setRelease(releaseId).setRange(Range{ 0, 1 }).setSortMethod(TrackSortMethod::Release)) };
if (!tracks.results.empty())
{
const Track::pointer& track{ tracks.results.front() };
@@ -416,18 +411,17 @@ namespace lms::cover
Track::FindParameters params;
params.setArtist(artistId, { TrackArtistLinkType::ReleaseArtist });
Track::find(session, params, [&](const Track::pointer& track)
{
Artist::FindParameters artistFindParams;
artistFindParams.setTrack(track->getId());
artistFindParams.setLinkType(TrackArtistLinkType::ReleaseArtist);
Track::find(session, params, [&](const Track::pointer& track) {
Artist::FindParameters artistFindParams;
artistFindParams.setTrack(track->getId());
artistFindParams.setLinkType(TrackArtistLinkType::ReleaseArtist);
const auto releaseArtists{ Artist::findIds(session, artistFindParams) };
if (releaseArtists.results.size() == 1)
releasePaths.insert(track->getAbsoluteFilePath().parent_path());
else
multiArtistReleasePaths.insert(track->getAbsoluteFilePath().parent_path());
});
const auto releaseArtists{ Artist::findIds(session, artistFindParams) };
if (releaseArtists.results.size() == 1)
releasePaths.insert(track->getAbsoluteFilePath().parent_path());
else
multiArtistReleasePaths.insert(track->getAbsoluteFilePath().parent_path());
});
}
std::vector<std::string> artistFileNames;
@@ -497,4 +491,3 @@ namespace lms::cover
}
} // namespace lms::cover
+17 -17
View File
@@ -23,9 +23,10 @@
#include <map>
#include <vector>
#include "services/cover/ICoverService.hpp"
#include "image/IEncodedImage.hpp"
#include "database/Types.hpp"
#include "image/IEncodedImage.hpp"
#include "services/cover/ICoverService.hpp"
#include "ImageCache.hpp"
namespace lms::db
@@ -49,23 +50,23 @@ namespace lms::cover
CoverService(const CoverService&) = delete;
CoverService& operator=(const CoverService&) = delete;
std::shared_ptr<image::IEncodedImage> getFromTrack(db::TrackId trackId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getFromRelease(db::ReleaseId releaseId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getFromArtist(db::ArtistId artistId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getDefaultSvgCover() override;
void flushCache() override;
void setJpegQuality(unsigned quality) override;
std::shared_ptr<image::IEncodedImage> getFromTrack(db::TrackId trackId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getFromRelease(db::ReleaseId releaseId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getFromArtist(db::ArtistId artistId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getDefaultSvgCover() override;
void flushCache() override;
void setJpegQuality(unsigned quality) override;
std::shared_ptr<image::IEncodedImage> getFromTrack(db::Session& dbSession, db::TrackId trackId, image::ImageSize width, bool allowReleaseFallback);
std::unique_ptr<image::IEncodedImage> getFromAvMediaFile(const av::IAudioFile& input, image::ImageSize width) const;
std::unique_ptr<image::IEncodedImage> getFromCoverFile(const std::filesystem::path& p, image::ImageSize width) const;
std::shared_ptr<image::IEncodedImage> getFromTrack(db::Session& dbSession, db::TrackId trackId, image::ImageSize width, bool allowReleaseFallback);
std::unique_ptr<image::IEncodedImage> getFromAvMediaFile(const av::IAudioFile& input, image::ImageSize width) const;
std::unique_ptr<image::IEncodedImage> getFromCoverFile(const std::filesystem::path& p, image::ImageSize width) const;
std::unique_ptr<image::IEncodedImage> getFromTrack(const std::filesystem::path& path, image::ImageSize width) const;
std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
std::unique_ptr<image::IEncodedImage> getFromDirectory(const std::filesystem::path& directory, image::ImageSize width, const std::vector<std::string>& preferredFileNames, bool allowPickRandom) const;
std::unique_ptr<image::IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, image::ImageSize width) const;
std::unique_ptr<image::IEncodedImage> getFromTrack(const std::filesystem::path& path, image::ImageSize width) const;
std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
std::unique_ptr<image::IEncodedImage> getFromDirectory(const std::filesystem::path& directory, image::ImageSize width, const std::vector<std::string>& preferredFileNames, bool allowPickRandom) const;
std::unique_ptr<image::IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, image::ImageSize width) const;
bool checkCoverFile(const std::filesystem::path& filePath) const;
bool checkCoverFile(const std::filesystem::path& filePath) const;
db::Db& _db;
@@ -80,4 +81,3 @@ namespace lms::cover
};
} // namespace lms::cover
+4 -3
View File
@@ -21,14 +21,15 @@
#include <mutex>
#include "core/Random.hpp"
#include "core/ILogger.hpp"
#include "core/Random.hpp"
namespace lms::cover
{
ImageCache::ImageCache(std::size_t maxCacheSize)
: _maxCacheSize{ maxCacheSize }
{}
{
}
void ImageCache::addImage(const EntryDesc& entryDesc, std::shared_ptr<image::IEncodedImage> image)
{
@@ -70,4 +71,4 @@ namespace lms::cover
_cacheSize = 0;
_cache.clear();
}
}
} // namespace lms::cover
+4 -4
View File
@@ -65,8 +65,8 @@ namespace lms::cover
};
std::unordered_map<EntryDesc, std::shared_ptr<image::IEncodedImage>, EntryHasher> _cache;
std::size_t _cacheSize{};
mutable std::atomic<std::size_t> _cacheMisses{};
mutable std::atomic<std::size_t> _cacheHits{};
std::size_t _cacheSize{};
mutable std::atomic<std::size_t> _cacheMisses{};
mutable std::atomic<std::size_t> _cacheHits{};
};
}
} // namespace lms::cover
@@ -52,5 +52,4 @@ namespace lms::cover
std::unique_ptr<ICoverService> createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath);
} // namespace lms::coverArt
} // namespace lms::cover
@@ -20,6 +20,7 @@
#include "FeedbackService.hpp"
#include "FeedbackService.impl.hpp"
#include "core/ILogger.hpp"
#include "database/Artist.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
@@ -29,7 +30,6 @@
#include "database/StarredTrack.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "core/ILogger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
@@ -187,5 +187,4 @@ namespace lms::feedback
return Track::findIds(session, searchParams);
}
} // ns Feedback
} // namespace lms::feedback
@@ -23,6 +23,7 @@
#include <unordered_map>
#include "services/feedback/IFeedbackService.hpp"
#include "IFeedbackBackend.hpp"
namespace lms::db
@@ -46,7 +47,7 @@ namespace lms::feedback
void unstar(db::UserId userId, db::ArtistId artistId) override;
bool isStarred(db::UserId userId, db::ArtistId artistId) override;
Wt::WDateTime getStarredDateTime(db::UserId userId, db::ArtistId artistId) override;
ArtistContainer findStarredArtists(const ArtistFindParameters& params) override;
ArtistContainer findStarredArtists(const ArtistFindParameters& params) override;
void star(db::UserId userId, db::ReleaseId releaseId) override;
void unstar(db::UserId userId, db::ReleaseId releaseId) override;
@@ -62,17 +63,17 @@ namespace lms::feedback
std::optional<db::FeedbackBackend> getUserFeedbackBackend(db::UserId userId);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
template<typename ObjType, typename ObjIdType, typename StarredObjType>
void star(db::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
template<typename ObjType, typename ObjIdType, typename StarredObjType>
void unstar(db::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
template<typename ObjType, typename ObjIdType, typename StarredObjType>
bool isStarred(db::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
template<typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime getStarredDateTime(db::UserId userId, ObjIdType id);
db::Db& _db;
std::unordered_map<db::FeedbackBackend, std::unique_ptr<IFeedbackBackend>> _backends;
};
} // ns Feedback
} // namespace lms::feedback
@@ -27,7 +27,7 @@ namespace lms::feedback
{
using namespace db;
template <typename ObjType, typename ObjIdType, typename StarredObjType>
template<typename ObjType, typename ObjIdType, typename StarredObjType>
void FeedbackService::star(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
@@ -58,7 +58,7 @@ namespace lms::feedback
_backends[*backend]->onStarred(starredObjId);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
template<typename ObjType, typename ObjIdType, typename StarredObjType>
void FeedbackService::unstar(UserId userId, ObjIdType objId)
{
const auto backend{ getUserFeedbackBackend(userId) };
@@ -79,7 +79,7 @@ namespace lms::feedback
_backends[*backend]->onUnstarred(starredObjId);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
template<typename ObjType, typename ObjIdType, typename StarredObjType>
bool FeedbackService::isStarred(UserId userId, ObjIdType objId)
{
Session& session{ _db.getTLSSession() };
@@ -89,7 +89,7 @@ namespace lms::feedback
return starredObj && (starredObj->getSyncState() != SyncState::PendingRemove);
}
template <typename ObjType, typename ObjIdType, typename StarredObjType>
template<typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ObjIdType objId)
{
Session& session{ _db.getTLSSession() };
@@ -102,4 +102,4 @@ namespace lms::feedback
return {};
}
} // ns Feedback
} // namespace lms::feedback
@@ -40,4 +40,4 @@ namespace lms::feedback
std::unique_ptr<IFeedbackBackend> createFeedbackBackend(std::string_view backendName);
} // ns Feedback
} // namespace lms::feedback
@@ -29,7 +29,7 @@ namespace lms::feedback
{
namespace details
{
template <typename StarredObjType>
template<typename StarredObjType>
void onStarred(db::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createWriteTransaction() };
@@ -38,7 +38,7 @@ namespace lms::feedback
starredObj.modify()->setSyncState(db::SyncState::Synchronized);
}
template <typename StarredObjType>
template<typename StarredObjType>
void onUnstarred(db::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createWriteTransaction() };
@@ -46,11 +46,12 @@ namespace lms::feedback
if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.remove();
}
}
} // namespace details
InternalBackend::InternalBackend(db::Db& db)
: _db{ db }
{}
{
}
void InternalBackend::onStarred(db::StarredArtistId starredArtistId)
{
@@ -81,4 +82,4 @@ namespace lms::feedback
{
details::onUnstarred<db::StarredTrack>(_db.getTLSSession(), starredTrackId);
}
} // Feedback
} // namespace lms::feedback
@@ -43,5 +43,4 @@ namespace lms::feedback
db::Db& _db;
};
} // Feedback
} // namespace lms::feedback
@@ -23,9 +23,9 @@
namespace lms::feedback::listenBrainz
{
class Exception : public feedback::Exception
{
public:
using feedback::Exception::Exception;
};
}
class Exception : public feedback::Exception
{
public:
using feedback::Exception::Exception;
};
} // namespace lms::feedback::listenBrainz
@@ -21,10 +21,9 @@
namespace lms::feedback::listenBrainz
{
std::ostream&
operator<<(std::ostream& os, const Feedback& feedback)
{
os << "created = '" << feedback.created.toString() << "', recording MBID = '" << feedback.recordingMBID.getAsString() << "', score = " << static_cast<int>(feedback.score);
return os;
}
} // feedback::ListenBrainz
std::ostream& operator<<(std::ostream& os, const Feedback& feedback)
{
os << "created = '" << feedback.created.toString() << "', recording MBID = '" << feedback.recordingMBID.getAsString() << "', score = " << static_cast<int>(feedback.score);
return os;
}
} // namespace lms::feedback::listenBrainz
@@ -20,26 +20,28 @@
#pragma once
#include <ostream>
#include <Wt/WDateTime.h>
#include "core/UUID.hpp"
namespace lms::feedback::listenBrainz
{
// See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc
enum class FeedbackType
{
Love = 1,
Hate = -1,
Erase = 0,
};
// See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc
enum class FeedbackType
{
Love = 1,
Hate = -1,
Erase = 0,
};
struct Feedback
{
Wt::WDateTime created;
core::UUID recordingMBID;
FeedbackType score;
};
struct Feedback
{
Wt::WDateTime created;
core::UUID recordingMBID;
FeedbackType score;
};
std::ostream& operator<<(std::ostream& os, const Feedback& feedback);
std::ostream& operator<<(std::ostream& os, const Feedback& feedback);
} // feedback::ListenBrainz
} // namespace lms::feedback::listenBrainz
@@ -21,8 +21,8 @@
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Parser.h>
#include <Wt/Json/Value.h>
#include "Exception.hpp"
#include "Utils.hpp"
@@ -37,14 +37,13 @@ namespace lms::feedback::listenBrainz
if (!recordingMBID)
throw Exception{ "MBID not found!" };
return Feedback
{
return Feedback{
Wt::WDateTime::fromTime_t(static_cast<int>(feedbackObj.get("created"))),
*recordingMBID,
static_cast<FeedbackType>(static_cast<int>(feedbackObj.get("score")))
*recordingMBID,
static_cast<FeedbackType>(static_cast<int>(feedbackObj.get("score")))
};
}
}
} // namespace
FeedbacksParser::Result FeedbacksParser::parse(std::string_view msgBody)
{
@@ -87,4 +86,4 @@ namespace lms::feedback::listenBrainz
return res;
}
} // feedback::ListenBrainz
} // namespace lms::feedback::listenBrainz
@@ -25,16 +25,16 @@
namespace lms::feedback::listenBrainz
{
class FeedbacksParser
{
public:
struct Result
{
std::size_t feedbackCount {}; // >= feedbacks.size()
std::vector<Feedback> feedbacks;
};
class FeedbacksParser
{
public:
struct Result
{
std::size_t feedbackCount{}; // >= feedbacks.size()
std::vector<Feedback> feedbacks;
};
static Result parse(std::string_view msgBody);
};
static Result parse(std::string_view msgBody);
};
} // feedback::ListenBrainz
} // namespace lms::feedback::listenBrainz
@@ -19,20 +19,20 @@
#include "FeedbacksSynchronizer.hpp"
#include <boost/asio/bind_executor.hpp>
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h>
#include <Wt/Json/Value.h>
#include <boost/asio/bind_executor.hpp>
#include "core/IConfig.hpp"
#include "core/Service.hpp"
#include "core/http/IClient.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/StarredTrack.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "core/IConfig.hpp"
#include "core/http/IClient.hpp"
#include "core/Service.hpp"
#include "Exception.hpp"
#include "FeedbacksParser.hpp"
@@ -57,7 +57,7 @@ namespace lms::feedback::listenBrainz
return std::nullopt;
}
}
}
} // namespace
FeedbacksSynchronizer::FeedbacksSynchronizer(boost::asio::io_context& ioContext, db::Db& db, core::http::IClient& client)
: _ioContext{ ioContext }
@@ -125,19 +125,17 @@ namespace lms::feedback::listenBrainz
request.message.addHeader("Authorization", "Token " + std::string{ listenBrainzToken->getAsString() });
Wt::Json::Object root;
root["recording_mbid"] = Wt::Json::Value{ std::string {recordingMBID->getAsString()} };
root["recording_mbid"] = Wt::Json::Value{ std::string{ recordingMBID->getAsString() } };
root["score"] = Wt::Json::Value{ static_cast<int>(type) };
request.message.addBodyText(Wt::Json::serialize(root));
request.message.addHeader("Content-Type", "application/json");
request.onSuccessFunc = [this, type, starredTrackId](std::string_view /*msgBody*/)
{
_strand.dispatch([this, type, starredTrackId]
{
onFeedbackSent(type, starredTrackId);
});
};
request.onSuccessFunc = [this, type, starredTrackId](std::string_view /*msgBody*/) {
_strand.dispatch([this, type, starredTrackId] {
onFeedbackSent(type, starredTrackId);
});
};
_client.sendPOSTRequest(std::move(request));
}
catch (Exception& e)
@@ -193,20 +191,19 @@ namespace lms::feedback::listenBrainz
void FeedbacksSynchronizer::enquePendingFeedbacks()
{
using namespace db;
using namespace db;
auto processPendingFeedbacks{ [this](SyncState scrobblingState, FeedbackType feedbackType)
{
auto processPendingFeedbacks{ [this](SyncState scrobblingState, FeedbackType feedbackType) {
RangeResults<StarredTrackId> pendingFeedbacks;
{
db::Session& session {_db.getTLSSession()};
db::Session& session{ _db.getTLSSession() };
auto transaction {session.createReadTransaction()};
auto transaction{ session.createReadTransaction() };
StarredTrack::FindParameters params;
params.setFeedbackBackend(db::FeedbackBackend::ListenBrainz, scrobblingState)
.setRange(db::Range {0, 100}); // don't flood too much?
.setRange(db::Range{ 0, 100 }); // don't flood too much?
pendingFeedbacks = StarredTrack::find(session, params);
}
@@ -236,10 +233,9 @@ namespace lms::feedback::listenBrainz
bool FeedbacksSynchronizer::isSyncing() const
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
return contextEntry.second.syncing;
});
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) {
return contextEntry.second.syncing;
});
}
void FeedbacksSynchronizer::scheduleSync(std::chrono::seconds fromNow)
@@ -249,20 +245,19 @@ namespace lms::feedback::listenBrainz
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)
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec) {
if (ec == boost::asio::error::operation_aborted)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG, "getFeedbacks aborted");
return;
}
else if (ec)
{
throw Exception{ "GetFeedbacks timer failure: " + std::string {ec.message()} };
}
LOG(DEBUG, "getFeedbacks aborted");
return;
}
else if (ec)
{
throw Exception{ "GetFeedbacks timer failure: " + std::string{ ec.message() } };
}
startSync();
}));
startSync();
}));
}
void FeedbacksSynchronizer::startSync()
@@ -301,14 +296,13 @@ namespace lms::feedback::listenBrainz
void FeedbacksSynchronizer::onSyncEnded(UserContext& context)
{
_strand.dispatch([this, &context]
{
LOG(INFO, "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount);
context.syncing = false;
_strand.dispatch([this, &context] {
LOG(INFO, "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount);
context.syncing = false;
if (!isSyncing())
scheduleSync(_syncFeedbacksPeriod);
});
if (!isSyncing())
scheduleSync(_syncFeedbacksPeriod);
});
}
void FeedbacksSynchronizer::enqueValidateToken(UserContext& context)
@@ -325,21 +319,19 @@ namespace lms::feedback::listenBrainz
core::http::ClientGETRequestParameters request;
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.relativeUrl = "/1/validate-token";
request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} };
request.onSuccessFunc = [this, &context](std::string_view msgBody)
{
context.listenBrainzUserName = utils::parseValidateToken(msgBody);
if (context.listenBrainzUserName.empty())
{
onSyncEnded(context);
return;
}
enqueGetFeedbackCount(context);
};
request.onFailureFunc = [this, &context]
request.headers = { { "Authorization", "Token " + std::string{ listenBrainzToken->getAsString() } } };
request.onSuccessFunc = [this, &context](std::string_view msgBody) {
context.listenBrainzUserName = utils::parseValidateToken(msgBody);
if (context.listenBrainzUserName.empty())
{
onSyncEnded(context);
};
return;
}
enqueGetFeedbackCount(context);
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
@@ -351,30 +343,27 @@ namespace lms::feedback::listenBrainz
core::http::ClientGETRequestParameters request;
request.relativeUrl = "/1/feedback/user/" + std::string{ context.listenBrainzUserName } + "/get-feedback?score=1&count=0";
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody)
{
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context]
{
LOG(DEBUG, "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'");
request.onSuccessFunc = [this, &context](std::string_view msgBody) {
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context] {
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);
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
if (totalFeedbackCount)
LOG(DEBUG, "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount);
bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) };
context.feedbackCount = totalFeedbackCount;
bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) };
context.feedbackCount = totalFeedbackCount;
if (needSync)
enqueGetFeedbacks(context);
else
onSyncEnded(context);
});
};
request.onFailureFunc = [this, &context]
{
onSyncEnded(context);
};
if (needSync)
enqueGetFeedbacks(context);
else
onSyncEnded(context);
});
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
@@ -386,28 +375,25 @@ namespace lms::feedback::listenBrainz
core::http::ClientGETRequestParameters request;
request.relativeUrl = "/1/feedback/user/" + context.listenBrainzUserName + "/get-feedback?offset=" + std::to_string(context.fetchedFeedbackCount);
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody)
{
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context]
{
const std::size_t fetchedFeedbackCount{ processGetFeedbacks(msgBodyCopy, context) };
if (fetchedFeedbackCount == 0 // no more thing available on server
|| context.fetchedFeedbackCount >= context.feedbackCount // we may miss something, but we will get it next time
|| context.fetchedFeedbackCount >= _maxSyncFeedbackCount)
{
onSyncEnded(context);
}
else
{
enqueGetFeedbacks(context);
}
});
};
request.onFailureFunc = [this, &context]
{
onSyncEnded(context);
};
request.onSuccessFunc = [this, &context](std::string_view msgBody) {
std::string msgBodyCopy{ msgBody };
_strand.dispatch([this, msgBodyCopy, &context] {
const std::size_t fetchedFeedbackCount{ processGetFeedbacks(msgBodyCopy, context) };
if (fetchedFeedbackCount == 0 // no more thing available on server
|| context.fetchedFeedbackCount >= context.feedbackCount // we may miss something, but we will get it next time
|| context.fetchedFeedbackCount >= _maxSyncFeedbackCount)
{
onSyncEnded(context);
}
else
{
enqueGetFeedbacks(context);
}
});
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
@@ -21,12 +21,13 @@
#include <optional>
#include <unordered_map>
#include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp>
#include <boost/asio/steady_timer.hpp>
#include "database/Types.hpp"
#include "database/StarredTrackId.hpp"
#include "database/Types.hpp"
#include "database/UserId.hpp"
#include "FeedbackTypes.hpp"
@@ -35,13 +36,13 @@ namespace lms
{
namespace core::http
{
class IClient;
class IClient;
}
namespace db
{
class Db;
}
}
} // namespace lms
namespace lms::feedback::listenBrainz
{
@@ -59,22 +60,23 @@ namespace lms::feedback::listenBrainz
struct UserContext
{
UserContext(db::UserId id) : userId{ id } {}
UserContext(db::UserId id)
: userId{ id } {}
UserContext(const UserContext&) = delete;
UserContext& operator=(const UserContext&) = delete;
const db::UserId userId;
bool syncing{};
std::optional<std::size_t> feedbackCount{};
const db::UserId userId;
bool syncing{};
std::optional<std::size_t> feedbackCount{};
// resetted at each sync
std::string listenBrainzUserName; // need to be resolved first
std::string listenBrainzUserName; // need to be resolved first
std::size_t currentOffset{};
std::size_t fetchedFeedbackCount{};
std::size_t matchedFeedbackCount{};
std::size_t importedFeedbackCount{};
std::size_t currentOffset{};
std::size_t fetchedFeedbackCount{};
std::size_t matchedFeedbackCount{};
std::size_t importedFeedbackCount{};
};
UserContext& getUserContext(db::UserId userId);
@@ -90,15 +92,14 @@ namespace lms::feedback::listenBrainz
void tryImportFeedback(const Feedback& feedback, UserContext& context);
boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand{ _ioContext };
boost::asio::io_context::strand _strand{ _ioContext };
db::Db& _db;
boost::asio::steady_timer _syncTimer{ _ioContext };
boost::asio::steady_timer _syncTimer{ _ioContext };
core::http::IClient& _client;
std::unordered_map<db::UserId, UserContext> _userContexts;
const std::size_t _maxSyncFeedbackCount;
const std::chrono::hours _syncFeedbacksPeriod;
const std::size_t _maxSyncFeedbackCount;
const std::chrono::hours _syncFeedbacksPeriod;
};
} // feedback::ListenBrainz
} // namespace lms::feedback::listenBrainz
@@ -19,22 +19,23 @@
#include "ListenBrainzBackend.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "core/http/IClient.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/StarredArtist.hpp"
#include "database/StarredRelease.hpp"
#include "database/Track.hpp"
#include "core/IConfig.hpp"
#include "core/http/IClient.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "Utils.hpp"
namespace lms::feedback::listenBrainz
{
namespace details
{
template <typename StarredObjType>
template<typename StarredObjType>
void onStarred(db::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createWriteTransaction() };
@@ -46,7 +47,7 @@ namespace lms::feedback::listenBrainz
}
}
template <typename StarredObjType>
template<typename StarredObjType>
void onUnstarred(db::Session& session, typename StarredObjType::IdType id)
{
auto transaction{ session.createWriteTransaction() };
@@ -54,7 +55,7 @@ namespace lms::feedback::listenBrainz
if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.remove();
}
}
} // namespace details
ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, db::Db& db)
: _ioContext{ ioContext }
@@ -100,4 +101,4 @@ namespace lms::feedback::listenBrainz
{
_feedbacksSynchronizer.enqueFeedback(FeedbackType::Erase, starredtrackId);
}
} // namespace lms::scrobbling::listenBrainz
} // namespace lms::feedback::listenBrainz
@@ -19,12 +19,12 @@
#pragma once
#include <boost/asio/io_context.hpp>
#include <memory>
#include <string>
#include <boost/asio/io_context.hpp>
#include "IFeedbackBackend.hpp"
#include "FeedbacksSynchronizer.hpp"
#include "IFeedbackBackend.hpp"
namespace lms::db
{
@@ -52,8 +52,8 @@ namespace lms::feedback::listenBrainz
boost::asio::io_context& _ioContext;
db::Db& _db;
std::string _baseAPIUrl;
std::unique_ptr<core::http::IClient> _client;
FeedbacksSynchronizer _feedbacksSynchronizer;
std::string _baseAPIUrl;
std::unique_ptr<core::http::IClient> _client;
FeedbacksSynchronizer _feedbacksSynchronizer;
};
}
} // namespace lms::feedback::listenBrainz
@@ -59,4 +59,4 @@ namespace lms::feedback::listenBrainz::utils
listenBrainzUserName = root.get("user_name").orIfNull("");
return listenBrainzUserName;
}
}
} // namespace lms::feedback::listenBrainz::utils
@@ -19,11 +19,11 @@
#pragma once
#include "database/UserId.hpp"
#include "core/ILogger.hpp"
#include "core/UUID.hpp"
#include "database/UserId.hpp"
#define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message)
#define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message)
namespace lms::db
{
@@ -32,6 +32,6 @@ namespace lms::db
namespace lms::feedback::listenBrainz::utils
{
std::optional<core::UUID> getListenBrainzToken(db::Session& session, db::UserId userId);
std::optional<core::UUID> getListenBrainzToken(db::Session& session, db::UserId userId);
std::string parseValidateToken(std::string_view msgBody);
}
} // namespace lms::feedback::listenBrainz::utils
@@ -28,4 +28,4 @@ namespace lms::feedback
public:
using LmsException::LmsException;
};
}
} // namespace lms::feedback
@@ -22,17 +22,17 @@
#include <memory>
#include <optional>
#include <span>
#include <boost/asio/io_service.hpp>
#include <Wt/WDateTime.h>
#include "database/Types.hpp"
#include <Wt/WDateTime.h>
#include <boost/asio/io_service.hpp>
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
#include "database/UserId.hpp"
#include "database/Types.hpp"
#include "database/UserId.hpp"
namespace lms::db
{
@@ -52,51 +52,78 @@ namespace lms::feedback
struct FindParameters
{
db::UserId user;
std::vector<db::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::optional<db::Range> range;
db::MediaLibraryId library;
db::UserId user;
std::vector<db::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::optional<db::Range> range;
db::MediaLibraryId library;
FindParameters& setUser(const db::UserId _user) { user = _user; return *this; }
FindParameters& setClusters(std::span<const db::ClusterId> _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setRange(std::optional<db::Range> _range) { range = _range; return *this; }
FindParameters& setMediaLibrary(db::MediaLibraryId _library) { library = _library; return *this; }
FindParameters& setUser(const db::UserId _user)
{
user = _user;
return *this;
}
FindParameters& setClusters(std::span<const db::ClusterId> _clusters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
return *this;
}
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
{
keywords = _keywords;
return *this;
}
FindParameters& setRange(std::optional<db::Range> _range)
{
range = _range;
return *this;
}
FindParameters& setMediaLibrary(db::MediaLibraryId _library)
{
library = _library;
return *this;
}
};
// Artists
struct ArtistFindParameters : public FindParameters
{
std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None };
std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None };
ArtistFindParameters& setLinkType(std::optional<db::TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
ArtistFindParameters& setSortMethod(db::ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
ArtistFindParameters& setLinkType(std::optional<db::TrackArtistLinkType> _linkType)
{
linkType = _linkType;
return *this;
}
ArtistFindParameters& setSortMethod(db::ArtistSortMethod _sortMethod)
{
sortMethod = _sortMethod;
return *this;
}
};
virtual void star(db::UserId userId, db::ArtistId artistId) = 0;
virtual void unstar(db::UserId userId, db::ArtistId artistId) = 0;
virtual bool isStarred(db::UserId userId, db::ArtistId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::ArtistId artistId) = 0;
virtual ArtistContainer findStarredArtists(const ArtistFindParameters& params) = 0;
virtual void star(db::UserId userId, db::ArtistId artistId) = 0;
virtual void unstar(db::UserId userId, db::ArtistId artistId) = 0;
virtual bool isStarred(db::UserId userId, db::ArtistId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::ArtistId artistId) = 0;
virtual ArtistContainer findStarredArtists(const ArtistFindParameters& params) = 0;
// Releases
virtual void star(db::UserId userId, db::ReleaseId releaseId) = 0;
virtual void unstar(db::UserId userId, db::ReleaseId releaseId) = 0;
virtual bool isStarred(db::UserId userId, db::ReleaseId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::ReleaseId artistId) = 0;
virtual ReleaseContainer findStarredReleases(const FindParameters& params) = 0;
virtual void star(db::UserId userId, db::ReleaseId releaseId) = 0;
virtual void unstar(db::UserId userId, db::ReleaseId releaseId) = 0;
virtual bool isStarred(db::UserId userId, db::ReleaseId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::ReleaseId artistId) = 0;
virtual ReleaseContainer findStarredReleases(const FindParameters& params) = 0;
// Tracks
virtual void star(db::UserId userId, db::TrackId trackId) = 0;
virtual void unstar(db::UserId userId, db::TrackId trackId) = 0;
virtual bool isStarred(db::UserId userId, db::TrackId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::TrackId artistId) = 0;
virtual TrackContainer findStarredTracks(const FindParameters& params) = 0;
virtual void star(db::UserId userId, db::TrackId trackId) = 0;
virtual void unstar(db::UserId userId, db::TrackId trackId) = 0;
virtual bool isStarred(db::UserId userId, db::TrackId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(db::UserId userId, db::TrackId artistId) = 0;
virtual TrackContainer findStarredTracks(const FindParameters& params) = 0;
};
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_service& ioService, db::Db& db);
} // ns Feedback
} // namespace lms::feedback
@@ -23,12 +23,11 @@
namespace lms::db
{
class Db;
class Db;
}
namespace lms::recommendation
{
class IEngine;
std::unique_ptr<IEngine> createClustersEngine(db::Db& db);
}
class IEngine;
std::unique_ptr<IEngine> createClustersEngine(db::Db& db);
} // namespace lms::recommendation
@@ -20,15 +20,15 @@
#pragma once
#include <memory>
#include "IEngine.hpp"
namespace lms::db
{
class Db;
class Db;
}
namespace lms::recommendation
{
std::unique_ptr<IEngine> createFeaturesEngine(db::Db& db);
std::unique_ptr<IEngine> createFeaturesEngine(db::Db& db);
}
@@ -20,33 +20,33 @@
#pragma once
#include <memory>
#include "database/Types.hpp"
#include "database/TrackListId.hpp"
#include "services/recommendation/Types.hpp"
#include "core/EnumSet.hpp"
#include "database/TrackListId.hpp"
#include "database/Types.hpp"
#include "services/recommendation/Types.hpp"
namespace lms::db
{
class Db;
class Db;
}
namespace lms::recommendation
{
class IEngine
{
public:
virtual ~IEngine() = default;
class IEngine
{
public:
virtual ~IEngine() = default;
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
virtual void requestCancelLoad() = 0;
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
virtual void requestCancelLoad() = 0;
virtual TrackContainer findSimilarTracksFromTrackList(db::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual TrackContainer findSimilarTracks(const std::vector<db::TrackId>& tracksId, std::size_t maxCount) const = 0;
virtual ReleaseContainer getSimilarReleases(db::ReleaseId releaseId, std::size_t maxCount) const = 0;
virtual ArtistContainer getSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
};
virtual TrackContainer findSimilarTracksFromTrackList(db::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual TrackContainer findSimilarTracks(const std::vector<db::TrackId>& tracksId, std::size_t maxCount) const = 0;
virtual ReleaseContainer getSimilarReleases(db::ReleaseId releaseId, std::size_t maxCount) const = 0;
virtual ArtistContainer getSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
};
std::unique_ptr<IEngine> createEngine(db::Db& db);
} // ns Recommendation
std::unique_ptr<IEngine> createEngine(db::Db& db);
} // namespace lms::recommendation
@@ -19,14 +19,15 @@
#include "PlaylistGeneratorService.hpp"
#include "core/ILogger.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "playlist-constraints/ConsecutiveArtists.hpp"
#include "playlist-constraints/ConsecutiveReleases.hpp"
#include "playlist-constraints/DuplicateTracks.hpp"
#include "core/ILogger.hpp"
namespace lms::recommendation
{
@@ -111,4 +112,4 @@ namespace lms::recommendation
return tracks;
}
}
} // namespace lms::recommendation
@@ -21,22 +21,23 @@
#include "services/recommendation/IPlaylistGeneratorService.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "playlist-constraints/IConstraint.hpp"
namespace lms::recommendation
{
class PlaylistGeneratorService : public IPlaylistGeneratorService
{
public:
PlaylistGeneratorService(db::Db& db, IRecommendationService& recommendationService);
class PlaylistGeneratorService : public IPlaylistGeneratorService
{
public:
PlaylistGeneratorService(db::Db& db, IRecommendationService& recommendationService);
private:
TrackContainer extendPlaylist(db::TrackListId tracklistId, std::size_t maxCount) const override;
private:
TrackContainer extendPlaylist(db::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer getTracksFromTrackList(db::TrackListId tracklistId) const;
TrackContainer getTracksFromTrackList(db::TrackListId tracklistId) const;
db::Db& _db;
IRecommendationService& _recommendationService;
std::vector<std::unique_ptr<PlaylistGeneratorConstraint::IConstraint>> _constraints;
};
} // namespace Radio
db::Db& _db;
IRecommendationService& _recommendationService;
std::vector<std::unique_ptr<PlaylistGeneratorConstraint::IConstraint>> _constraints;
};
} // namespace lms::recommendation
@@ -22,14 +22,14 @@
#include <unordered_map>
#include <vector>
#include "ClustersEngineCreator.hpp"
#include "FeaturesEngineCreator.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/ScanSettings.hpp"
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "database/Db.hpp"
#include "database/ScanSettings.hpp"
#include "database/Session.hpp"
#include "ClustersEngineCreator.hpp"
#include "FeaturesEngineCreator.hpp"
namespace lms::recommendation
{
@@ -41,7 +41,7 @@ namespace lms::recommendation
return db::ScanSettings::get(session)->getSimilarityEngineType();
}
}
} // namespace
std::unique_ptr<IRecommendationService> createRecommendationService(db::Db& db)
{
@@ -81,7 +81,8 @@ namespace lms::recommendation
if (!_engine)
return res;
return _engine->getSimilarReleases(releaseId, maxCount);;
return _engine->getSimilarReleases(releaseId, maxCount);
;
}
ArtistContainer RecommendationService::getSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
@@ -120,4 +121,4 @@ namespace lms::recommendation
if (_engine)
_engine->load(false);
}
} // ns Similarity
} // namespace lms::recommendation
@@ -22,6 +22,7 @@
#include <optional>
#include "services/recommendation/IRecommendationService.hpp"
#include "IEngine.hpp"
namespace lms::db
@@ -63,5 +64,4 @@ namespace lms::recommendation
std::unique_ptr<IEngine> _engine;
};
} // ns Recommendation
} // namespace lms::recommendation
@@ -27,7 +27,8 @@
#include "database/Track.hpp"
#include "database/TrackList.hpp"
namespace lms::recommendation {
namespace lms::recommendation
{
using namespace db;
@@ -44,7 +45,7 @@ namespace lms::recommendation {
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createReadTransaction() };
const auto similarTrackIds{ Track::findSimilarTrackIds(dbSession, trackIds, Range {0, maxCount}) };
const auto similarTrackIds{ Track::findSimilarTrackIds(dbSession, trackIds, Range{ 0, maxCount }) };
return std::move(similarTrackIds.results);
}
@@ -104,7 +105,7 @@ namespace lms::recommendation {
if (!artist)
return {};
const auto similarArtistIds{ artist->findSimilarArtistIds(artistLinkTypes, Range {0, maxCount}) };
const auto similarArtistIds{ artist->findSimilarArtistIds(artistLinkTypes, Range{ 0, maxCount }) };
return std::move(similarArtistIds.results);
}
@@ -24,27 +24,27 @@
namespace lms::recommendation
{
class ClusterEngine : public IEngine
{
public:
ClusterEngine(db::Db& db) : _db {db} {}
class ClusterEngine : public IEngine
{
public:
ClusterEngine(db::Db& db)
: _db{ db } {}
ClusterEngine(const ClusterEngine&) = delete;
ClusterEngine(ClusterEngine&&) = delete;
ClusterEngine& operator=(const ClusterEngine&) = delete;
ClusterEngine& operator=(ClusterEngine&&) = delete;
ClusterEngine(const ClusterEngine&) = delete;
ClusterEngine(ClusterEngine&&) = delete;
ClusterEngine& operator=(const ClusterEngine&) = delete;
ClusterEngine& operator=(ClusterEngine&&) = delete;
private:
void load(bool, const ProgressCallback&) override {}
void requestCancelLoad() override {}
private:
void load(bool, const ProgressCallback&) override {}
void requestCancelLoad() override {}
TrackContainer findSimilarTracksFromTrackList(db::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer findSimilarTracks(const std::vector<db::TrackId>& tracksId, std::size_t maxCount) const override;
ReleaseContainer getSimilarReleases(db::ReleaseId releaseId, std::size_t maxCount) const override;
ArtistContainer getSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
TrackContainer findSimilarTracksFromTrackList(db::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer findSimilarTracks(const std::vector<db::TrackId>& tracksId, std::size_t maxCount) const override;
ReleaseContainer getSimilarReleases(db::ReleaseId releaseId, std::size_t maxCount) const override;
ArtistContainer getSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
db::Db& _db;
};
db::Db& _db;
};
} // namespace lms::recommendation
@@ -24,370 +24,367 @@
#include "core/Exception.hpp"
namespace lms::recommendation {
static const std::unordered_map<FeatureName, FeatureDef> featureDefinitions
namespace lms::recommendation
{
{ "lowlevel.average_loudness", {1}},
{ "lowlevel.barkbands.dmean", {27}},
{ "lowlevel.barkbands.dmean2", {27}},
{ "lowlevel.barkbands.dvar", {27}},
{ "lowlevel.barkbands.dvar2", {27}},
{ "lowlevel.barkbands.max", {27}},
{ "lowlevel.barkbands.mean", {27}},
{ "lowlevel.barkbands.median", {27}},
{ "lowlevel.barkbands.min", {27}},
{ "lowlevel.barkbands.var", {27}},
{ "lowlevel.barkbands_crest.dmean", {1}},
{ "lowlevel.barkbands_crest.dmean2", {1}},
{ "lowlevel.barkbands_crest.dvar", {1}},
{ "lowlevel.barkbands_crest.dvar2", {1}},
{ "lowlevel.barkbands_crest.max", {1}},
{ "lowlevel.barkbands_crest.mean", {1}},
{ "lowlevel.barkbands_crest.median", {1}},
{ "lowlevel.barkbands_crest.min", {1}},
{ "lowlevel.barkbands_crest.var", {1}},
{ "lowlevel.barkbands_flatness_db.dmean", {1}},
{ "lowlevel.barkbands_flatness_db.dmean2", {1}},
{ "lowlevel.barkbands_flatness_db.dvar", {1}},
{ "lowlevel.barkbands_flatness_db.dvar2", {1}},
{ "lowlevel.barkbands_flatness_db.max", {1}},
{ "lowlevel.barkbands_flatness_db.mean", {1}},
{ "lowlevel.barkbands_flatness_db.median", {1}},
{ "lowlevel.barkbands_flatness_db.min", {1}},
{ "lowlevel.barkbands_flatness_db.var", {1}},
{ "lowlevel.barkbands_kurtosis.dmean", {1}},
{ "lowlevel.barkbands_kurtosis.dmean2", {1}},
{ "lowlevel.barkbands_kurtosis.dvar", {1}},
{ "lowlevel.barkbands_kurtosis.dvar2", {1}},
{ "lowlevel.barkbands_kurtosis.max", {1}},
{ "lowlevel.barkbands_kurtosis.mean", {1}},
{ "lowlevel.barkbands_kurtosis.median", {1}},
{ "lowlevel.barkbands_kurtosis.min", {1}},
{ "lowlevel.barkbands_kurtosis.var", {1}},
{ "lowlevel.barkbands_skewness.dmean", {1}},
{ "lowlevel.barkbands_skewness.dmean2", {1}},
{ "lowlevel.barkbands_skewness.dvar", {1}},
{ "lowlevel.barkbands_skewness.dvar2", {1}},
{ "lowlevel.barkbands_skewness.max", {1}},
{ "lowlevel.barkbands_skewness.mean", {1}},
{ "lowlevel.barkbands_skewness.median", {1}},
{ "lowlevel.barkbands_skewness.min", {1}},
{ "lowlevel.barkbands_skewness.var", {1}},
{ "lowlevel.barkbands_spread.dmean", {1}},
{ "lowlevel.barkbands_spread.dmean2", {1}},
{ "lowlevel.barkbands_spread.dvar", {1}},
{ "lowlevel.barkbands_spread.dvar2", {1}},
{ "lowlevel.barkbands_spread.max", {1}},
{ "lowlevel.barkbands_spread.mean", {1}},
{ "lowlevel.barkbands_spread.median", {1}},
{ "lowlevel.barkbands_spread.min", {1}},
{ "lowlevel.barkbands_spread.var", {1}},
{ "lowlevel.dissonance.dmean", {1}},
{ "lowlevel.dissonance.dmean2", {1}},
{ "lowlevel.dissonance.dvar", {1}},
{ "lowlevel.dissonance.dvar2", {1}},
{ "lowlevel.dissonance.max", {1}},
{ "lowlevel.dissonance.mean", {1}},
{ "lowlevel.dissonance.median", {1}},
{ "lowlevel.dissonance.min", {1}},
{ "lowlevel.dissonance.var", {1}},
{ "lowlevel.dynamic_complexity", {1}},
{ "lowlevel.erbbands.dmean", {40}},
{ "lowlevel.erbbands.dmean2", {40}},
{ "lowlevel.erbbands.dvar", {40}},
{ "lowlevel.erbbands.dvar2", {40}},
{ "lowlevel.erbbands.max", {40}},
{ "lowlevel.erbbands.mean", {40}},
{ "lowlevel.erbbands.median", {40}},
{ "lowlevel.erbbands.min", {40}},
{ "lowlevel.erbbands.var", {40}},
{ "lowlevel.gfcc.mean", {13}},
{ "lowlevel.hfc.dmean", {1}},
{ "lowlevel.hfc.dmean2", {1}},
{ "lowlevel.hfc.dvar", {1}},
{ "lowlevel.hfc.dvar2", {1}},
{ "lowlevel.hfc.max", {1}},
{ "lowlevel.hfc.mean", {1}},
{ "lowlevel.hfc.median", {1}},
{ "lowlevel.hfc.min", {1}},
{ "lowlevel.hfc.var", {1}},
{ "tonal.hpcp.median", {36}},
{ "lowlevel.melbands.dmean", {40}},
{ "lowlevel.melbands.dmean2", {40}},
{ "lowlevel.melbands.dvar", {40}},
{ "lowlevel.melbands.dvar2", {40}},
{ "lowlevel.melbands.max", {40}},
{ "lowlevel.melbands.mean", {40}},
{ "lowlevel.melbands.median", {40}},
{ "lowlevel.melbands.min", {40}},
{ "lowlevel.melbands.var", {40}},
{ "lowlevel.melbands_crest.dmean", {1}},
{ "lowlevel.melbands_crest.dmean2", {1}},
{ "lowlevel.melbands_crest.dvar", {1}},
{ "lowlevel.melbands_crest.dvar2", {1}},
{ "lowlevel.melbands_crest.max", {1}},
{ "lowlevel.melbands_crest.mean", {1}},
{ "lowlevel.melbands_crest.median", {1}},
{ "lowlevel.melbands_crest.min", {1}},
{ "lowlevel.melbands_crest.var", {1}},
{ "lowlevel.melbands_flatness_db.dmean", {1}},
{ "lowlevel.melbands_flatness_db.dmean2", {1}},
{ "lowlevel.melbands_flatness_db.dvar", {1}},
{ "lowlevel.melbands_flatness_db.dvar2", {1}},
{ "lowlevel.melbands_flatness_db.max", {1}},
{ "lowlevel.melbands_flatness_db.mean", {1}},
{ "lowlevel.melbands_flatness_db.median", {1}},
{ "lowlevel.melbands_flatness_db.min", {1}},
{ "lowlevel.melbands_flatness_db.var", {1}},
{ "lowlevel.melbands_kurtosis.dmean", {1}},
{ "lowlevel.melbands_kurtosis.dmean2", {1}},
{ "lowlevel.melbands_kurtosis.dvar", {1}},
{ "lowlevel.melbands_kurtosis.dvar2", {1}},
{ "lowlevel.melbands_kurtosis.max", {1}},
{ "lowlevel.melbands_kurtosis.mean", {1}},
{ "lowlevel.melbands_kurtosis.median", {1}},
{ "lowlevel.melbands_kurtosis.min", {1}},
{ "lowlevel.melbands_kurtosis.var", {1}},
{ "lowlevel.melbands_skewness.dmean", {1}},
{ "lowlevel.melbands_skewness.dmean2", {1}},
{ "lowlevel.melbands_skewness.dvar", {1}},
{ "lowlevel.melbands_skewness.dvar2", {1}},
{ "lowlevel.melbands_skewness.max", {1}},
{ "lowlevel.melbands_skewness.mean", {1}},
{ "lowlevel.melbands_skewness.median", {1}},
{ "lowlevel.melbands_skewness.min", {1}},
{ "lowlevel.melbands_skewness.var", {1}},
{ "lowlevel.melbands_spread.dmean", {1}},
{ "lowlevel.melbands_spread.dmean2", {1}},
{ "lowlevel.melbands_spread.dvar", {1}},
{ "lowlevel.melbands_spread.dvar2", {1}},
{ "lowlevel.melbands_spread.max", {1}},
{ "lowlevel.melbands_spread.mean", {1}},
{ "lowlevel.melbands_spread.median", {1}},
{ "lowlevel.melbands_spread.min", {1}},
{ "lowlevel.melbands_spread.var", {1}},
{ "lowlevel.mfcc.mean", {13}},
{ "lowlevel.pitch_salience.dmean", {1}},
{ "lowlevel.pitch_salience.dmean2", {1}},
{ "lowlevel.pitch_salience.dvar", {1}},
{ "lowlevel.pitch_salience.dvar2", {1}},
{ "lowlevel.pitch_salience.max", {1}},
{ "lowlevel.pitch_salience.mean", {1}},
{ "lowlevel.pitch_salience.median", {1}},
{ "lowlevel.pitch_salience.min", {1}},
{ "lowlevel.pitch_salience.var", {1}},
{ "lowlevel.silence_rate_30dB.dmean", {1}},
{ "lowlevel.silence_rate_30dB.dmean2", {1}},
{ "lowlevel.silence_rate_30dB.dvar", {1}},
{ "lowlevel.silence_rate_30dB.dvar2", {1}},
{ "lowlevel.silence_rate_30dB.max", {1}},
{ "lowlevel.silence_rate_30dB.mean", {1}},
{ "lowlevel.silence_rate_30dB.median", {1}},
{ "lowlevel.silence_rate_30dB.min", {1}},
{ "lowlevel.silence_rate_30dB.var", {1}},
{ "lowlevel.silence_rate_60dB.dmean", {1}},
{ "lowlevel.silence_rate_60dB.dmean2", {1}},
{ "lowlevel.silence_rate_60dB.dvar", {1}},
{ "lowlevel.silence_rate_60dB.dvar2", {1}},
{ "lowlevel.silence_rate_60dB.max", {1}},
{ "lowlevel.silence_rate_60dB.mean", {1}},
{ "lowlevel.silence_rate_60dB.median", {1}},
{ "lowlevel.silence_rate_60dB.min", {1}},
{ "lowlevel.silence_rate_60dB.var", {1}},
{ "lowlevel.spectral_centroid.dmean", {1}},
{ "lowlevel.spectral_centroid.dmean2", {1}},
{ "lowlevel.spectral_centroid.dvar", {1}},
{ "lowlevel.spectral_centroid.dvar2", {1}},
{ "lowlevel.spectral_centroid.max", {1}},
{ "lowlevel.spectral_centroid.mean", {1}},
{ "lowlevel.spectral_centroid.median", {1}},
{ "lowlevel.spectral_centroid.min", {1}},
{ "lowlevel.spectral_centroid.var", {1}},
{ "lowlevel.spectral_complexity.dmean", {1}},
{ "lowlevel.spectral_complexity.dmean2", {1}},
{ "lowlevel.spectral_complexity.dvar", {1}},
{ "lowlevel.spectral_complexity.dvar2", {1}},
{ "lowlevel.spectral_complexity.max", {1}},
{ "lowlevel.spectral_complexity.mean", {1}},
{ "lowlevel.spectral_complexity.median", {1}},
{ "lowlevel.spectral_complexity.min", {1}},
{ "lowlevel.spectral_complexity.var", {1}},
{ "lowlevel.spectral_contrast_coeffs.dmean", {6}},
{ "lowlevel.spectral_contrast_coeffs.dmean2", {6}},
{ "lowlevel.spectral_contrast_coeffs.dvar", {6}},
{ "lowlevel.spectral_contrast_coeffs.dvar2", {6}},
{ "lowlevel.spectral_contrast_coeffs.max", {6}},
{ "lowlevel.spectral_contrast_coeffs.mean", {6}},
{ "lowlevel.spectral_contrast_coeffs.median", {6}},
{ "lowlevel.spectral_contrast_coeffs.min", {6}},
{ "lowlevel.spectral_contrast_coeffs.var", {6}},
{ "lowlevel.spectral_contrast_valleys.dmean", {6}},
{ "lowlevel.spectral_contrast_valleys.dmean2", {6}},
{ "lowlevel.spectral_contrast_valleys.dvar", {6}},
{ "lowlevel.spectral_contrast_valleys.dvar2", {6}},
{ "lowlevel.spectral_contrast_valleys.max", {6}},
{ "lowlevel.spectral_contrast_valleys.mean", {6}},
{ "lowlevel.spectral_contrast_valleys.median", {6}},
{ "lowlevel.spectral_contrast_valleys.min", {6}},
{ "lowlevel.spectral_contrast_valleys.var", {6}},
{ "lowlevel.spectral_decrease.dmean", {1}},
{ "lowlevel.spectral_decrease.dmean2", {1}},
{ "lowlevel.spectral_decrease.dvar", {1}},
{ "lowlevel.spectral_decrease.dvar2", {1}},
{ "lowlevel.spectral_decrease.max", {1}},
{ "lowlevel.spectral_decrease.mean", {1}},
{ "lowlevel.spectral_decrease.median", {1}},
{ "lowlevel.spectral_decrease.min", {1}},
{ "lowlevel.spectral_decrease.var", {1}},
{ "lowlevel.spectral_energy.dmean", {1}},
{ "lowlevel.spectral_energy.dmean2", {1}},
{ "lowlevel.spectral_energy.dvar", {1}},
{ "lowlevel.spectral_energy.dvar2", {1}},
{ "lowlevel.spectral_energy.max", {1}},
{ "lowlevel.spectral_energy.mean", {1}},
{ "lowlevel.spectral_energy.median", {1}},
{ "lowlevel.spectral_energy.min", {1}},
{ "lowlevel.spectral_energy.var", {1}},
{ "lowlevel.spectral_energyband_high.dmean", {1}},
{ "lowlevel.spectral_energyband_high.dmean2", {1}},
{ "lowlevel.spectral_energyband_high.dvar", {1}},
{ "lowlevel.spectral_energyband_high.dvar2", {1}},
{ "lowlevel.spectral_energyband_high.max", {1}},
{ "lowlevel.spectral_energyband_high.mean", {1}},
{ "lowlevel.spectral_energyband_high.median", {1}},
{ "lowlevel.spectral_energyband_high.min", {1}},
{ "lowlevel.spectral_energyband_high.var", {1}},
{ "lowlevel.spectral_energyband_low.dmean", {1}},
{ "lowlevel.spectral_energyband_low.dmean2", {1}},
{ "lowlevel.spectral_energyband_low.dvar", {1}},
{ "lowlevel.spectral_energyband_low.dvar2", {1}},
{ "lowlevel.spectral_energyband_low.max", {1}},
{ "lowlevel.spectral_energyband_low.mean", {1}},
{ "lowlevel.spectral_energyband_low.median", {1}},
{ "lowlevel.spectral_energyband_low.min", {1}},
{ "lowlevel.spectral_energyband_low.var", {1}},
{ "lowlevel.spectral_energyband_middle_high.dmean", {1}},
{ "lowlevel.spectral_energyband_middle_high.dmean2", {1}},
{ "lowlevel.spectral_energyband_middle_high.dvar", {1}},
{ "lowlevel.spectral_energyband_middle_high.dvar2", {1}},
{ "lowlevel.spectral_energyband_middle_high.max", {1}},
{ "lowlevel.spectral_energyband_middle_high.mean", {1}},
{ "lowlevel.spectral_energyband_middle_high.median", {1}},
{ "lowlevel.spectral_energyband_middle_high.min", {1}},
{ "lowlevel.spectral_energyband_middle_high.var", {1}},
{ "lowlevel.spectral_energyband_middle_low.dmean", {1}},
{ "lowlevel.spectral_energyband_middle_low.dmean2", {1}},
{ "lowlevel.spectral_energyband_middle_low.dvar", {1}},
{ "lowlevel.spectral_energyband_middle_low.dvar2", {1}},
{ "lowlevel.spectral_energyband_middle_low.max", {1}},
{ "lowlevel.spectral_energyband_middle_low.mean", {1}},
{ "lowlevel.spectral_energyband_middle_low.median", {1}},
{ "lowlevel.spectral_energyband_middle_low.min", {1}},
{ "lowlevel.spectral_energyband_middle_low.var", {1}},
{ "lowlevel.spectral_entropy.dmean", {1}},
{ "lowlevel.spectral_entropy.dmean2", {1}},
{ "lowlevel.spectral_entropy.dvar", {1}},
{ "lowlevel.spectral_entropy.dvar2", {1}},
{ "lowlevel.spectral_entropy.max", {1}},
{ "lowlevel.spectral_entropy.mean", {1}},
{ "lowlevel.spectral_entropy.median", {1}},
{ "lowlevel.spectral_entropy.min", {1}},
{ "lowlevel.spectral_entropy.var", {1}},
{ "lowlevel.spectral_flux.dmean", {1}},
{ "lowlevel.spectral_flux.dmean2", {1}},
{ "lowlevel.spectral_flux.dvar", {1}},
{ "lowlevel.spectral_flux.dvar2", {1}},
{ "lowlevel.spectral_flux.max", {1}},
{ "lowlevel.spectral_flux.mean", {1}},
{ "lowlevel.spectral_flux.median", {1}},
{ "lowlevel.spectral_flux.min", {1}},
{ "lowlevel.spectral_flux.var", {1}},
{ "lowlevel.spectral_kurtosis.dmean", {1}},
{ "lowlevel.spectral_kurtosis.dmean2", {1}},
{ "lowlevel.spectral_kurtosis.dvar", {1}},
{ "lowlevel.spectral_kurtosis.dvar2", {1}},
{ "lowlevel.spectral_kurtosis.max", {1}},
{ "lowlevel.spectral_kurtosis.mean", {1}},
{ "lowlevel.spectral_kurtosis.median", {1}},
{ "lowlevel.spectral_kurtosis.min", {1}},
{ "lowlevel.spectral_kurtosis.var", {1}},
{ "lowlevel.spectral_rms.dmean", {1}},
{ "lowlevel.spectral_rms.dmean2", {1}},
{ "lowlevel.spectral_rms.dvar", {1}},
{ "lowlevel.spectral_rms.dvar2", {1}},
{ "lowlevel.spectral_rms.max", {1}},
{ "lowlevel.spectral_rms.mean", {1}},
{ "lowlevel.spectral_rms.median", {1}},
{ "lowlevel.spectral_rms.min", {1}},
{ "lowlevel.spectral_rms.var", {1}},
{ "lowlevel.spectral_rolloff.dmean", {1}},
{ "lowlevel.spectral_rolloff.dmean2", {1}},
{ "lowlevel.spectral_rolloff.dvar", {1}},
{ "lowlevel.spectral_rolloff.dvar2", {1}},
{ "lowlevel.spectral_rolloff.max", {1}},
{ "lowlevel.spectral_rolloff.mean", {1}},
{ "lowlevel.spectral_rolloff.median", {1}},
{ "lowlevel.spectral_rolloff.min", {1}},
{ "lowlevel.spectral_rolloff.var", {1}},
{ "lowlevel.spectral_skewness.dmean", {1}},
{ "lowlevel.spectral_skewness.dmean2", {1}},
{ "lowlevel.spectral_skewness.dvar", {1}},
{ "lowlevel.spectral_skewness.dvar2", {1}},
{ "lowlevel.spectral_skewness.max", {1}},
{ "lowlevel.spectral_skewness.mean", {1}},
{ "lowlevel.spectral_skewness.median", {1}},
{ "lowlevel.spectral_skewness.min", {1}},
{ "lowlevel.spectral_skewness.var", {1}},
{ "lowlevel.spectral_spread.dmean", {1}},
{ "lowlevel.spectral_spread.dmean2", {1}},
{ "lowlevel.spectral_spread.dvar", {1}},
{ "lowlevel.spectral_spread.dvar2", {1}},
{ "lowlevel.spectral_spread.max", {1}},
{ "lowlevel.spectral_spread.mean", {1}},
{ "lowlevel.spectral_spread.median", {1}},
{ "lowlevel.spectral_spread.min", {1}},
{ "lowlevel.spectral_spread.var", {1}},
{ "lowlevel.spectral_strongpeak.dmean", {1}},
{ "lowlevel.spectral_strongpeak.dmean2", {1}},
{ "lowlevel.spectral_strongpeak.dvar", {1}},
{ "lowlevel.spectral_strongpeak.dvar2", {1}},
{ "lowlevel.spectral_strongpeak.max", {1}},
{ "lowlevel.spectral_strongpeak.mean", {1}},
{ "lowlevel.spectral_strongpeak.median", {1}},
{ "lowlevel.spectral_strongpeak.min", {1}},
{ "lowlevel.spectral_strongpeak.var", {1}},
{ "lowlevel.zerocrossingrate.dmean", {1}},
{ "lowlevel.zerocrossingrate.dmean2", {1}},
{ "lowlevel.zerocrossingrate.dvar", {1}},
{ "lowlevel.zerocrossingrate.dvar2", {1}},
{ "lowlevel.zerocrossingrate.max", {1}},
{ "lowlevel.zerocrossingrate.mean", {1}},
{ "lowlevel.zerocrossingrate.median", {1}},
{ "lowlevel.zerocrossingrate.min", {1}},
{ "lowlevel.zerocrossingrate.var", {1}},
};
FeatureDef
getFeatureDef(const FeatureName& featureName)
{
auto it {featureDefinitions.find(featureName)};
if (it == std::cend(featureDefinitions))
throw core::LmsException {"Unhandled requested feature '" + featureName + "'"};
static const std::unordered_map<FeatureName, FeatureDef> featureDefinitions{
{ "lowlevel.average_loudness", { 1 } },
{ "lowlevel.barkbands.dmean", { 27 } },
{ "lowlevel.barkbands.dmean2", { 27 } },
{ "lowlevel.barkbands.dvar", { 27 } },
{ "lowlevel.barkbands.dvar2", { 27 } },
{ "lowlevel.barkbands.max", { 27 } },
{ "lowlevel.barkbands.mean", { 27 } },
{ "lowlevel.barkbands.median", { 27 } },
{ "lowlevel.barkbands.min", { 27 } },
{ "lowlevel.barkbands.var", { 27 } },
{ "lowlevel.barkbands_crest.dmean", { 1 } },
{ "lowlevel.barkbands_crest.dmean2", { 1 } },
{ "lowlevel.barkbands_crest.dvar", { 1 } },
{ "lowlevel.barkbands_crest.dvar2", { 1 } },
{ "lowlevel.barkbands_crest.max", { 1 } },
{ "lowlevel.barkbands_crest.mean", { 1 } },
{ "lowlevel.barkbands_crest.median", { 1 } },
{ "lowlevel.barkbands_crest.min", { 1 } },
{ "lowlevel.barkbands_crest.var", { 1 } },
{ "lowlevel.barkbands_flatness_db.dmean", { 1 } },
{ "lowlevel.barkbands_flatness_db.dmean2", { 1 } },
{ "lowlevel.barkbands_flatness_db.dvar", { 1 } },
{ "lowlevel.barkbands_flatness_db.dvar2", { 1 } },
{ "lowlevel.barkbands_flatness_db.max", { 1 } },
{ "lowlevel.barkbands_flatness_db.mean", { 1 } },
{ "lowlevel.barkbands_flatness_db.median", { 1 } },
{ "lowlevel.barkbands_flatness_db.min", { 1 } },
{ "lowlevel.barkbands_flatness_db.var", { 1 } },
{ "lowlevel.barkbands_kurtosis.dmean", { 1 } },
{ "lowlevel.barkbands_kurtosis.dmean2", { 1 } },
{ "lowlevel.barkbands_kurtosis.dvar", { 1 } },
{ "lowlevel.barkbands_kurtosis.dvar2", { 1 } },
{ "lowlevel.barkbands_kurtosis.max", { 1 } },
{ "lowlevel.barkbands_kurtosis.mean", { 1 } },
{ "lowlevel.barkbands_kurtosis.median", { 1 } },
{ "lowlevel.barkbands_kurtosis.min", { 1 } },
{ "lowlevel.barkbands_kurtosis.var", { 1 } },
{ "lowlevel.barkbands_skewness.dmean", { 1 } },
{ "lowlevel.barkbands_skewness.dmean2", { 1 } },
{ "lowlevel.barkbands_skewness.dvar", { 1 } },
{ "lowlevel.barkbands_skewness.dvar2", { 1 } },
{ "lowlevel.barkbands_skewness.max", { 1 } },
{ "lowlevel.barkbands_skewness.mean", { 1 } },
{ "lowlevel.barkbands_skewness.median", { 1 } },
{ "lowlevel.barkbands_skewness.min", { 1 } },
{ "lowlevel.barkbands_skewness.var", { 1 } },
{ "lowlevel.barkbands_spread.dmean", { 1 } },
{ "lowlevel.barkbands_spread.dmean2", { 1 } },
{ "lowlevel.barkbands_spread.dvar", { 1 } },
{ "lowlevel.barkbands_spread.dvar2", { 1 } },
{ "lowlevel.barkbands_spread.max", { 1 } },
{ "lowlevel.barkbands_spread.mean", { 1 } },
{ "lowlevel.barkbands_spread.median", { 1 } },
{ "lowlevel.barkbands_spread.min", { 1 } },
{ "lowlevel.barkbands_spread.var", { 1 } },
{ "lowlevel.dissonance.dmean", { 1 } },
{ "lowlevel.dissonance.dmean2", { 1 } },
{ "lowlevel.dissonance.dvar", { 1 } },
{ "lowlevel.dissonance.dvar2", { 1 } },
{ "lowlevel.dissonance.max", { 1 } },
{ "lowlevel.dissonance.mean", { 1 } },
{ "lowlevel.dissonance.median", { 1 } },
{ "lowlevel.dissonance.min", { 1 } },
{ "lowlevel.dissonance.var", { 1 } },
{ "lowlevel.dynamic_complexity", { 1 } },
{ "lowlevel.erbbands.dmean", { 40 } },
{ "lowlevel.erbbands.dmean2", { 40 } },
{ "lowlevel.erbbands.dvar", { 40 } },
{ "lowlevel.erbbands.dvar2", { 40 } },
{ "lowlevel.erbbands.max", { 40 } },
{ "lowlevel.erbbands.mean", { 40 } },
{ "lowlevel.erbbands.median", { 40 } },
{ "lowlevel.erbbands.min", { 40 } },
{ "lowlevel.erbbands.var", { 40 } },
{ "lowlevel.gfcc.mean", { 13 } },
{ "lowlevel.hfc.dmean", { 1 } },
{ "lowlevel.hfc.dmean2", { 1 } },
{ "lowlevel.hfc.dvar", { 1 } },
{ "lowlevel.hfc.dvar2", { 1 } },
{ "lowlevel.hfc.max", { 1 } },
{ "lowlevel.hfc.mean", { 1 } },
{ "lowlevel.hfc.median", { 1 } },
{ "lowlevel.hfc.min", { 1 } },
{ "lowlevel.hfc.var", { 1 } },
{ "tonal.hpcp.median", { 36 } },
{ "lowlevel.melbands.dmean", { 40 } },
{ "lowlevel.melbands.dmean2", { 40 } },
{ "lowlevel.melbands.dvar", { 40 } },
{ "lowlevel.melbands.dvar2", { 40 } },
{ "lowlevel.melbands.max", { 40 } },
{ "lowlevel.melbands.mean", { 40 } },
{ "lowlevel.melbands.median", { 40 } },
{ "lowlevel.melbands.min", { 40 } },
{ "lowlevel.melbands.var", { 40 } },
{ "lowlevel.melbands_crest.dmean", { 1 } },
{ "lowlevel.melbands_crest.dmean2", { 1 } },
{ "lowlevel.melbands_crest.dvar", { 1 } },
{ "lowlevel.melbands_crest.dvar2", { 1 } },
{ "lowlevel.melbands_crest.max", { 1 } },
{ "lowlevel.melbands_crest.mean", { 1 } },
{ "lowlevel.melbands_crest.median", { 1 } },
{ "lowlevel.melbands_crest.min", { 1 } },
{ "lowlevel.melbands_crest.var", { 1 } },
{ "lowlevel.melbands_flatness_db.dmean", { 1 } },
{ "lowlevel.melbands_flatness_db.dmean2", { 1 } },
{ "lowlevel.melbands_flatness_db.dvar", { 1 } },
{ "lowlevel.melbands_flatness_db.dvar2", { 1 } },
{ "lowlevel.melbands_flatness_db.max", { 1 } },
{ "lowlevel.melbands_flatness_db.mean", { 1 } },
{ "lowlevel.melbands_flatness_db.median", { 1 } },
{ "lowlevel.melbands_flatness_db.min", { 1 } },
{ "lowlevel.melbands_flatness_db.var", { 1 } },
{ "lowlevel.melbands_kurtosis.dmean", { 1 } },
{ "lowlevel.melbands_kurtosis.dmean2", { 1 } },
{ "lowlevel.melbands_kurtosis.dvar", { 1 } },
{ "lowlevel.melbands_kurtosis.dvar2", { 1 } },
{ "lowlevel.melbands_kurtosis.max", { 1 } },
{ "lowlevel.melbands_kurtosis.mean", { 1 } },
{ "lowlevel.melbands_kurtosis.median", { 1 } },
{ "lowlevel.melbands_kurtosis.min", { 1 } },
{ "lowlevel.melbands_kurtosis.var", { 1 } },
{ "lowlevel.melbands_skewness.dmean", { 1 } },
{ "lowlevel.melbands_skewness.dmean2", { 1 } },
{ "lowlevel.melbands_skewness.dvar", { 1 } },
{ "lowlevel.melbands_skewness.dvar2", { 1 } },
{ "lowlevel.melbands_skewness.max", { 1 } },
{ "lowlevel.melbands_skewness.mean", { 1 } },
{ "lowlevel.melbands_skewness.median", { 1 } },
{ "lowlevel.melbands_skewness.min", { 1 } },
{ "lowlevel.melbands_skewness.var", { 1 } },
{ "lowlevel.melbands_spread.dmean", { 1 } },
{ "lowlevel.melbands_spread.dmean2", { 1 } },
{ "lowlevel.melbands_spread.dvar", { 1 } },
{ "lowlevel.melbands_spread.dvar2", { 1 } },
{ "lowlevel.melbands_spread.max", { 1 } },
{ "lowlevel.melbands_spread.mean", { 1 } },
{ "lowlevel.melbands_spread.median", { 1 } },
{ "lowlevel.melbands_spread.min", { 1 } },
{ "lowlevel.melbands_spread.var", { 1 } },
{ "lowlevel.mfcc.mean", { 13 } },
{ "lowlevel.pitch_salience.dmean", { 1 } },
{ "lowlevel.pitch_salience.dmean2", { 1 } },
{ "lowlevel.pitch_salience.dvar", { 1 } },
{ "lowlevel.pitch_salience.dvar2", { 1 } },
{ "lowlevel.pitch_salience.max", { 1 } },
{ "lowlevel.pitch_salience.mean", { 1 } },
{ "lowlevel.pitch_salience.median", { 1 } },
{ "lowlevel.pitch_salience.min", { 1 } },
{ "lowlevel.pitch_salience.var", { 1 } },
{ "lowlevel.silence_rate_30dB.dmean", { 1 } },
{ "lowlevel.silence_rate_30dB.dmean2", { 1 } },
{ "lowlevel.silence_rate_30dB.dvar", { 1 } },
{ "lowlevel.silence_rate_30dB.dvar2", { 1 } },
{ "lowlevel.silence_rate_30dB.max", { 1 } },
{ "lowlevel.silence_rate_30dB.mean", { 1 } },
{ "lowlevel.silence_rate_30dB.median", { 1 } },
{ "lowlevel.silence_rate_30dB.min", { 1 } },
{ "lowlevel.silence_rate_30dB.var", { 1 } },
{ "lowlevel.silence_rate_60dB.dmean", { 1 } },
{ "lowlevel.silence_rate_60dB.dmean2", { 1 } },
{ "lowlevel.silence_rate_60dB.dvar", { 1 } },
{ "lowlevel.silence_rate_60dB.dvar2", { 1 } },
{ "lowlevel.silence_rate_60dB.max", { 1 } },
{ "lowlevel.silence_rate_60dB.mean", { 1 } },
{ "lowlevel.silence_rate_60dB.median", { 1 } },
{ "lowlevel.silence_rate_60dB.min", { 1 } },
{ "lowlevel.silence_rate_60dB.var", { 1 } },
{ "lowlevel.spectral_centroid.dmean", { 1 } },
{ "lowlevel.spectral_centroid.dmean2", { 1 } },
{ "lowlevel.spectral_centroid.dvar", { 1 } },
{ "lowlevel.spectral_centroid.dvar2", { 1 } },
{ "lowlevel.spectral_centroid.max", { 1 } },
{ "lowlevel.spectral_centroid.mean", { 1 } },
{ "lowlevel.spectral_centroid.median", { 1 } },
{ "lowlevel.spectral_centroid.min", { 1 } },
{ "lowlevel.spectral_centroid.var", { 1 } },
{ "lowlevel.spectral_complexity.dmean", { 1 } },
{ "lowlevel.spectral_complexity.dmean2", { 1 } },
{ "lowlevel.spectral_complexity.dvar", { 1 } },
{ "lowlevel.spectral_complexity.dvar2", { 1 } },
{ "lowlevel.spectral_complexity.max", { 1 } },
{ "lowlevel.spectral_complexity.mean", { 1 } },
{ "lowlevel.spectral_complexity.median", { 1 } },
{ "lowlevel.spectral_complexity.min", { 1 } },
{ "lowlevel.spectral_complexity.var", { 1 } },
{ "lowlevel.spectral_contrast_coeffs.dmean", { 6 } },
{ "lowlevel.spectral_contrast_coeffs.dmean2", { 6 } },
{ "lowlevel.spectral_contrast_coeffs.dvar", { 6 } },
{ "lowlevel.spectral_contrast_coeffs.dvar2", { 6 } },
{ "lowlevel.spectral_contrast_coeffs.max", { 6 } },
{ "lowlevel.spectral_contrast_coeffs.mean", { 6 } },
{ "lowlevel.spectral_contrast_coeffs.median", { 6 } },
{ "lowlevel.spectral_contrast_coeffs.min", { 6 } },
{ "lowlevel.spectral_contrast_coeffs.var", { 6 } },
{ "lowlevel.spectral_contrast_valleys.dmean", { 6 } },
{ "lowlevel.spectral_contrast_valleys.dmean2", { 6 } },
{ "lowlevel.spectral_contrast_valleys.dvar", { 6 } },
{ "lowlevel.spectral_contrast_valleys.dvar2", { 6 } },
{ "lowlevel.spectral_contrast_valleys.max", { 6 } },
{ "lowlevel.spectral_contrast_valleys.mean", { 6 } },
{ "lowlevel.spectral_contrast_valleys.median", { 6 } },
{ "lowlevel.spectral_contrast_valleys.min", { 6 } },
{ "lowlevel.spectral_contrast_valleys.var", { 6 } },
{ "lowlevel.spectral_decrease.dmean", { 1 } },
{ "lowlevel.spectral_decrease.dmean2", { 1 } },
{ "lowlevel.spectral_decrease.dvar", { 1 } },
{ "lowlevel.spectral_decrease.dvar2", { 1 } },
{ "lowlevel.spectral_decrease.max", { 1 } },
{ "lowlevel.spectral_decrease.mean", { 1 } },
{ "lowlevel.spectral_decrease.median", { 1 } },
{ "lowlevel.spectral_decrease.min", { 1 } },
{ "lowlevel.spectral_decrease.var", { 1 } },
{ "lowlevel.spectral_energy.dmean", { 1 } },
{ "lowlevel.spectral_energy.dmean2", { 1 } },
{ "lowlevel.spectral_energy.dvar", { 1 } },
{ "lowlevel.spectral_energy.dvar2", { 1 } },
{ "lowlevel.spectral_energy.max", { 1 } },
{ "lowlevel.spectral_energy.mean", { 1 } },
{ "lowlevel.spectral_energy.median", { 1 } },
{ "lowlevel.spectral_energy.min", { 1 } },
{ "lowlevel.spectral_energy.var", { 1 } },
{ "lowlevel.spectral_energyband_high.dmean", { 1 } },
{ "lowlevel.spectral_energyband_high.dmean2", { 1 } },
{ "lowlevel.spectral_energyband_high.dvar", { 1 } },
{ "lowlevel.spectral_energyband_high.dvar2", { 1 } },
{ "lowlevel.spectral_energyband_high.max", { 1 } },
{ "lowlevel.spectral_energyband_high.mean", { 1 } },
{ "lowlevel.spectral_energyband_high.median", { 1 } },
{ "lowlevel.spectral_energyband_high.min", { 1 } },
{ "lowlevel.spectral_energyband_high.var", { 1 } },
{ "lowlevel.spectral_energyband_low.dmean", { 1 } },
{ "lowlevel.spectral_energyband_low.dmean2", { 1 } },
{ "lowlevel.spectral_energyband_low.dvar", { 1 } },
{ "lowlevel.spectral_energyband_low.dvar2", { 1 } },
{ "lowlevel.spectral_energyband_low.max", { 1 } },
{ "lowlevel.spectral_energyband_low.mean", { 1 } },
{ "lowlevel.spectral_energyband_low.median", { 1 } },
{ "lowlevel.spectral_energyband_low.min", { 1 } },
{ "lowlevel.spectral_energyband_low.var", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.dmean", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.dmean2", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.dvar", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.dvar2", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.max", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.mean", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.median", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.min", { 1 } },
{ "lowlevel.spectral_energyband_middle_high.var", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.dmean", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.dmean2", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.dvar", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.dvar2", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.max", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.mean", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.median", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.min", { 1 } },
{ "lowlevel.spectral_energyband_middle_low.var", { 1 } },
{ "lowlevel.spectral_entropy.dmean", { 1 } },
{ "lowlevel.spectral_entropy.dmean2", { 1 } },
{ "lowlevel.spectral_entropy.dvar", { 1 } },
{ "lowlevel.spectral_entropy.dvar2", { 1 } },
{ "lowlevel.spectral_entropy.max", { 1 } },
{ "lowlevel.spectral_entropy.mean", { 1 } },
{ "lowlevel.spectral_entropy.median", { 1 } },
{ "lowlevel.spectral_entropy.min", { 1 } },
{ "lowlevel.spectral_entropy.var", { 1 } },
{ "lowlevel.spectral_flux.dmean", { 1 } },
{ "lowlevel.spectral_flux.dmean2", { 1 } },
{ "lowlevel.spectral_flux.dvar", { 1 } },
{ "lowlevel.spectral_flux.dvar2", { 1 } },
{ "lowlevel.spectral_flux.max", { 1 } },
{ "lowlevel.spectral_flux.mean", { 1 } },
{ "lowlevel.spectral_flux.median", { 1 } },
{ "lowlevel.spectral_flux.min", { 1 } },
{ "lowlevel.spectral_flux.var", { 1 } },
{ "lowlevel.spectral_kurtosis.dmean", { 1 } },
{ "lowlevel.spectral_kurtosis.dmean2", { 1 } },
{ "lowlevel.spectral_kurtosis.dvar", { 1 } },
{ "lowlevel.spectral_kurtosis.dvar2", { 1 } },
{ "lowlevel.spectral_kurtosis.max", { 1 } },
{ "lowlevel.spectral_kurtosis.mean", { 1 } },
{ "lowlevel.spectral_kurtosis.median", { 1 } },
{ "lowlevel.spectral_kurtosis.min", { 1 } },
{ "lowlevel.spectral_kurtosis.var", { 1 } },
{ "lowlevel.spectral_rms.dmean", { 1 } },
{ "lowlevel.spectral_rms.dmean2", { 1 } },
{ "lowlevel.spectral_rms.dvar", { 1 } },
{ "lowlevel.spectral_rms.dvar2", { 1 } },
{ "lowlevel.spectral_rms.max", { 1 } },
{ "lowlevel.spectral_rms.mean", { 1 } },
{ "lowlevel.spectral_rms.median", { 1 } },
{ "lowlevel.spectral_rms.min", { 1 } },
{ "lowlevel.spectral_rms.var", { 1 } },
{ "lowlevel.spectral_rolloff.dmean", { 1 } },
{ "lowlevel.spectral_rolloff.dmean2", { 1 } },
{ "lowlevel.spectral_rolloff.dvar", { 1 } },
{ "lowlevel.spectral_rolloff.dvar2", { 1 } },
{ "lowlevel.spectral_rolloff.max", { 1 } },
{ "lowlevel.spectral_rolloff.mean", { 1 } },
{ "lowlevel.spectral_rolloff.median", { 1 } },
{ "lowlevel.spectral_rolloff.min", { 1 } },
{ "lowlevel.spectral_rolloff.var", { 1 } },
{ "lowlevel.spectral_skewness.dmean", { 1 } },
{ "lowlevel.spectral_skewness.dmean2", { 1 } },
{ "lowlevel.spectral_skewness.dvar", { 1 } },
{ "lowlevel.spectral_skewness.dvar2", { 1 } },
{ "lowlevel.spectral_skewness.max", { 1 } },
{ "lowlevel.spectral_skewness.mean", { 1 } },
{ "lowlevel.spectral_skewness.median", { 1 } },
{ "lowlevel.spectral_skewness.min", { 1 } },
{ "lowlevel.spectral_skewness.var", { 1 } },
{ "lowlevel.spectral_spread.dmean", { 1 } },
{ "lowlevel.spectral_spread.dmean2", { 1 } },
{ "lowlevel.spectral_spread.dvar", { 1 } },
{ "lowlevel.spectral_spread.dvar2", { 1 } },
{ "lowlevel.spectral_spread.max", { 1 } },
{ "lowlevel.spectral_spread.mean", { 1 } },
{ "lowlevel.spectral_spread.median", { 1 } },
{ "lowlevel.spectral_spread.min", { 1 } },
{ "lowlevel.spectral_spread.var", { 1 } },
{ "lowlevel.spectral_strongpeak.dmean", { 1 } },
{ "lowlevel.spectral_strongpeak.dmean2", { 1 } },
{ "lowlevel.spectral_strongpeak.dvar", { 1 } },
{ "lowlevel.spectral_strongpeak.dvar2", { 1 } },
{ "lowlevel.spectral_strongpeak.max", { 1 } },
{ "lowlevel.spectral_strongpeak.mean", { 1 } },
{ "lowlevel.spectral_strongpeak.median", { 1 } },
{ "lowlevel.spectral_strongpeak.min", { 1 } },
{ "lowlevel.spectral_strongpeak.var", { 1 } },
{ "lowlevel.zerocrossingrate.dmean", { 1 } },
{ "lowlevel.zerocrossingrate.dmean2", { 1 } },
{ "lowlevel.zerocrossingrate.dvar", { 1 } },
{ "lowlevel.zerocrossingrate.dvar2", { 1 } },
{ "lowlevel.zerocrossingrate.max", { 1 } },
{ "lowlevel.zerocrossingrate.mean", { 1 } },
{ "lowlevel.zerocrossingrate.median", { 1 } },
{ "lowlevel.zerocrossingrate.min", { 1 } },
{ "lowlevel.zerocrossingrate.var", { 1 } },
};
return it->second;
}
FeatureDef getFeatureDef(const FeatureName& featureName)
{
auto it{ featureDefinitions.find(featureName) };
if (it == std::cend(featureDefinitions))
throw core::LmsException{ "Unhandled requested feature '" + featureName + "'" };
FeatureNames
getFeatureNames()
{
FeatureNames res;
return it->second;
}
std::transform(std::cbegin(featureDefinitions), std::cend(featureDefinitions),
std::inserter(res, std::begin(res)), [](auto itFeature) { return itFeature.first; });
FeatureNames getFeatureNames()
{
FeatureNames res;
return res;
}
std::transform(std::cbegin(featureDefinitions), std::cend(featureDefinitions),
std::inserter(res, std::begin(res)), [](auto itFeature) { return itFeature.first; });
return res;
}
} // namespace lms::recommendation
@@ -24,26 +24,27 @@
#include <unordered_set>
#include <vector>
namespace lms::recommendation {
using FeatureName = std::string;
using FeatureNames = std::unordered_set<FeatureName>;
using FeatureValue = double;
using FeatureValues = std::vector<FeatureValue>;
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
struct FeatureDef
namespace lms::recommendation
{
std::size_t nbDimensions {};
};
FeatureDef getFeatureDef(const FeatureName& featureName);
FeatureNames getFeatureNames();
using FeatureName = std::string;
using FeatureNames = std::unordered_set<FeatureName>;
using FeatureValue = double;
using FeatureValues = std::vector<FeatureValue>;
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
struct FeatureSettings
{
double weight {};
};
using FeatureSettingsMap = std::unordered_map<FeatureName, FeatureSettings>;
struct FeatureDef
{
std::size_t nbDimensions{};
};
FeatureDef getFeatureDef(const FeatureName& featureName);
FeatureNames getFeatureNames();
struct FeatureSettings
{
double weight{};
};
using FeatureSettingsMap = std::unordered_map<FeatureName, FeatureSettings>;
} // namespace lms::recommendation
@@ -21,6 +21,8 @@
#include <numeric>
#include "core/ILogger.hpp"
#include "core/Random.hpp"
#include "database/Artist.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
@@ -30,8 +32,6 @@
#include "database/TrackFeatures.hpp"
#include "database/TrackList.hpp"
#include "som/DataNormalizer.hpp"
#include "core/ILogger.hpp"
#include "core/Random.hpp"
namespace lms::recommendation
{
@@ -47,7 +47,7 @@ namespace lms::recommendation
std::optional<som::InputVector> convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
{
std::size_t i{};
std::optional<som::InputVector> res{ som::InputVector {nbDimensions} };
std::optional<som::InputVector> res{ som::InputVector{ nbDimensions } };
for (const auto& [featureName, values] : featureValuesMap)
{
if (values.size() != getFeatureDef(featureName).nbDimensions)
@@ -80,17 +80,16 @@ namespace lms::recommendation
return weights;
}
}
} // namespace
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}},
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;
@@ -104,12 +103,12 @@ namespace lms::recommendation
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
const std::size_t nbDimensions{ std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; }) };
const std::size_t nbDimensions{ std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t{ 0 },
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; }) };
LMS_LOG(RECOMMENDATION, DEBUG, "Features dimension = " << nbDimensions);
Session & session{ _db.getTLSSession() };
Session& session{ _db.getTLSSession() };
RangeResults<TrackFeaturesId> trackFeaturesIds;
{
@@ -178,10 +177,9 @@ namespace lms::recommendation
som::InputVector weights{ getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions) };
network.setDataWeights(weights);
auto somProgressCallback{ [&](const som::Network::CurrentIteration& iter)
{
auto somProgressCallback{ [&](const som::Network::CurrentIteration& iter) {
LMS_LOG(RECOMMENDATION, DEBUG, "Current pass = " << iter.idIteration << " / " << iter.iterationCount);
progressCallback(Progress {iter.idIteration, iter.iterationCount});
progressCallback(Progress{ iter.idIteration, iter.iterationCount });
} };
LMS_LOG(RECOMMENDATION, DEBUG, "Training network...");
@@ -216,15 +214,14 @@ namespace lms::recommendation
TrackContainer FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
{
const TrackContainer trackIds{ [&]
{
const TrackContainer trackIds{ [&] {
TrackContainer res;
Session& session {_db.getTLSSession()};
Session& session{ _db.getTLSSession() };
auto transaction {session.createReadTransaction()};
auto transaction{ session.createReadTransaction() };
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
const TrackList::pointer trackList{ TrackList::find(session, trackListId) };
if (trackList)
res = trackList->getTrackIds();
@@ -245,10 +242,10 @@ namespace lms::recommendation
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));
[&](TrackId trackId) {
return !Track::exists(session, trackId);
}),
std::end(similarTrackIds));
}
return similarTrackIds;
@@ -256,7 +253,7 @@ namespace lms::recommendation
ReleaseContainer FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
{
auto similarReleaseIds{ getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount) };
auto similarReleaseIds{ getSimilarObjects({ releaseId }, _releaseMatrix, _releasePositions, maxCount) };
Session& session{ _db.getTLSSession() };
@@ -266,10 +263,10 @@ namespace lms::recommendation
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));
[&](ReleaseId releaseId) {
return !Release::exists(session, releaseId);
}),
std::end(similarReleaseIds));
}
return similarReleaseIds;
@@ -277,17 +274,16 @@ namespace lms::recommendation
ArtistContainer FeaturesEngine::getSimilarArtists(ArtistId artistId, core::EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
{
auto getSimilarArtistIdsForLinkType{ [&](TrackArtistLinkType linkType)
{
auto getSimilarArtistIdsForLinkType{ [&](TrackArtistLinkType linkType) {
ArtistContainer similarArtistIds;
const auto itArtists {_artistMatrix.find(linkType)};
const auto itArtists{ _artistMatrix.find(linkType) };
if (itArtists == std::cend(_artistMatrix))
{
return similarArtistIds;
}
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
return getSimilarObjects({ artistId }, itArtists->second, _artistPositions, maxCount);
} };
std::unordered_set<ArtistId> similarArtistIds;
@@ -306,10 +302,10 @@ namespace lms::recommendation
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));
[&](ArtistId artistId) {
return !Artist::exists(session, artistId);
}),
std::end(res));
}
while (res.size() > maxCount)
@@ -364,7 +360,7 @@ namespace lms::recommendation
LMS_LOG(RECOMMENDATION, DEBUG, "Constructing maps...");
Session & session{ _db.getTLSSession() };
Session& session{ _db.getTLSSession() };
for (const auto& [trackId, positions] : trackPositions)
{
@@ -410,4 +406,4 @@ namespace lms::recommendation
LMS_LOG(RECOMMENDATION, INFO, "Classifier successfully loaded!");
}
} // ns Recommendation
} // namespace lms::recommendation
@@ -21,17 +21,18 @@
#include <algorithm>
#include <functional>
#include <unordered_map>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "core/Utils.hpp"
#include "som/DataNormalizer.hpp"
#include "som/Network.hpp"
#include "core/Utils.hpp"
#include "IEngine.hpp"
#include "FeaturesEngineCache.hpp"
#include "FeaturesDefs.hpp"
#include "FeaturesEngineCache.hpp"
#include "IEngine.hpp"
namespace lms::db
{
@@ -45,7 +46,8 @@ namespace lms::recommendation
class FeaturesEngine : public IEngine
{
public:
FeaturesEngine(db::Db& db) : _db{ db } {}
FeaturesEngine(db::Db& db)
: _db{ db } {}
FeaturesEngine(const FeaturesEngine&) = delete;
FeaturesEngine(FeaturesEngine&&) = delete;
@@ -74,14 +76,14 @@ namespace lms::recommendation
};
void loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback);
template <typename IdType>
template<typename IdType>
using ObjectPositions = std::unordered_map<IdType, std::vector<som::Position>>;
using ArtistPositions = ObjectPositions<db::ArtistId>;
using ReleasePositions = ObjectPositions<db::ReleaseId>;
using TrackPositions = ObjectPositions<db::TrackId>;
template <typename IdType>
template<typename IdType>
using ObjectMatrix = som::Matrix<std::vector<IdType>>;
using ArtistMatrix = ObjectMatrix<db::ArtistId>;
using ReleaseMatrix = ObjectMatrix<db::ReleaseId>;
@@ -91,34 +93,34 @@ namespace lms::recommendation
FeaturesEngineCache toCache() const;
template <typename IdType>
template<typename IdType>
static std::vector<som::Position> getMatchingRefVectorsPosition(const std::vector<IdType>& ids, const ObjectPositions<IdType>& objectPositions);
template <typename IdType>
template<typename IdType>
static std::vector<IdType> getObjectsIds(const std::vector<som::Position>& positions, const ObjectMatrix<IdType>& objectsMatrix);
template <typename IdType>
template<typename IdType>
std::vector<IdType> getSimilarObjects(const std::vector<IdType>& ids,
const ObjectMatrix<IdType>& objectMatrix,
const ObjectPositions<IdType>& objectPositions,
std::size_t maxCount) const;
db::Db& _db;
bool _loadCancelled{};
std::unique_ptr<som::Network> _network;
double _networkRefVectorsDistanceMedian{};
bool _loadCancelled{};
std::unique_ptr<som::Network> _network;
double _networkRefVectorsDistanceMedian{};
ArtistPositions _artistPositions;
ArtistPositions _artistPositions;
std::unordered_map<db::TrackArtistLinkType, ArtistMatrix> _artistMatrix;
ReleasePositions _releasePositions;
ReleaseMatrix _releaseMatrix;
ReleasePositions _releasePositions;
ReleaseMatrix _releaseMatrix;
TrackPositions _trackPositions;
TrackMatrix _trackMatrix;
TrackPositions _trackPositions;
TrackMatrix _trackMatrix;
};
template <typename IdType>
template<typename IdType>
std::vector<som::Position> FeaturesEngine::getMatchingRefVectorsPosition(const std::vector<IdType>& ids, const ObjectPositions<IdType>& objectPositions)
{
std::vector<som::Position> res;
@@ -139,7 +141,7 @@ namespace lms::recommendation
return res;
}
template <typename IdType>
template<typename IdType>
std::vector<IdType> FeaturesEngine::getObjectsIds(const std::vector<som::Position>& positions, const ObjectMatrix<IdType>& objectMatrix)
{
std::vector<IdType> res;
@@ -153,11 +155,11 @@ namespace lms::recommendation
return res;
}
template <typename IdType>
template<typename IdType>
std::vector<IdType> FeaturesEngine::getSimilarObjects(const std::vector<IdType>& ids,
const ObjectMatrix<IdType>& objectMatrix,
const ObjectPositions<IdType>& objectPositions,
std::size_t maxCount) const
const ObjectMatrix<IdType>& objectMatrix,
const ObjectPositions<IdType>& objectPositions,
std::size_t maxCount) const
{
std::vector<IdType> res;
@@ -171,11 +173,10 @@ namespace lms::recommendation
// Remove objects that are already in input or already reported
closestObjectIds.erase(std::remove_if(std::begin(closestObjectIds), std::end(closestObjectIds),
[&](IdType id)
{
return std::find(std::cbegin(ids), std::cend(ids), id) != std::cend(ids);
})
, std::end(closestObjectIds));
[&](IdType id) {
return std::find(std::cbegin(ids), std::cend(ids), id) != std::cend(ids);
}),
std::end(closestObjectIds));
for (IdType id : closestObjectIds)
{
@@ -198,4 +199,4 @@ namespace lms::recommendation
return res;
}
}
} // namespace lms::recommendation
@@ -86,7 +86,7 @@ namespace lms::recommendation
return false;
}
}
}
} // namespace
std::optional<som::Network> FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
{
@@ -240,8 +240,8 @@ namespace lms::recommendation
}
FeaturesEngineCache::FeaturesEngineCache(som::Network network, TrackPositions trackPositions)
: _network{ std::move(network) },
_trackPositions{ std::move(trackPositions) }
: _network{ std::move(network) }
, _trackPositions{ std::move(trackPositions) }
{
}
@@ -47,8 +47,8 @@ namespace lms::recommendation
friend class FeaturesEngine;
som::Network _network;
TrackPositions _trackPositions;
som::Network _network;
TrackPositions _trackPositions;
};
} // namespace lms::recommendation
@@ -21,77 +21,73 @@
#include <algorithm>
#include "core/ILogger.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "core/ILogger.hpp"
namespace lms::recommendation::PlaylistGeneratorConstraint
{
namespace
{
std::size_t
countCommonArtists(const ArtistContainer& artists1, const ArtistContainer& artists2)
{
ArtistContainer intersection;
namespace
{
std::size_t countCommonArtists(const ArtistContainer& artists1, const ArtistContainer& artists2)
{
ArtistContainer intersection;
std::set_intersection(std::cbegin(artists1), std::cend(artists1),
std::cbegin(artists2), std::cend(artists2),
std::back_inserter(intersection));
std::set_intersection(std::cbegin(artists1), std::cend(artists1),
std::cbegin(artists2), std::cend(artists2),
std::back_inserter(intersection));
return intersection.size();
}
}
return intersection.size();
}
} // namespace
ConsecutiveArtists::ConsecutiveArtists(db::Db& db)
: _db {db}
{}
ConsecutiveArtists::ConsecutiveArtists(db::Db& db)
: _db{ db }
{
}
float
ConsecutiveArtists::computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex)
{
assert(!trackIds.empty());
assert(trackIndex <= trackIds.size() - 1);
float ConsecutiveArtists::computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex)
{
assert(!trackIds.empty());
assert(trackIndex <= trackIds.size() - 1);
const ArtistContainer artists {getArtists(trackIds[trackIndex])};
const ArtistContainer artists{ getArtists(trackIds[trackIndex]) };
constexpr std::size_t rangeSize{ 3 }; // check up to rangeSize tracks before/after the target track
static_assert(rangeSize > 0);
constexpr std::size_t rangeSize{ 3 }; // check up to rangeSize tracks before/after the target track
static_assert(rangeSize > 0);
float score {};
for (std::size_t i {1}; i < rangeSize; ++i)
{
if (trackIndex >= i)
score += countCommonArtists(artists, getArtists(trackIds[trackIndex - i])) / static_cast<float>(i);
float score{};
for (std::size_t i{ 1 }; i < rangeSize; ++i)
{
if (trackIndex >= i)
score += countCommonArtists(artists, getArtists(trackIds[trackIndex - i])) / static_cast<float>(i);
if (trackIndex + i < trackIds.size())
score += countCommonArtists(artists, getArtists(trackIds[trackIndex + i])) / static_cast<float>(i);
}
if (trackIndex + i < trackIds.size())
score += countCommonArtists(artists, getArtists(trackIds[trackIndex + i])) / static_cast<float>(i);
}
return score;
}
return score;
}
ArtistContainer
ConsecutiveArtists::getArtists(db::TrackId trackId)
{
using namespace db;
ArtistContainer ConsecutiveArtists::getArtists(db::TrackId trackId)
{
using namespace db;
ArtistContainer res;
ArtistContainer res;
Session& dbSession {_db.getTLSSession()};
auto transaction {dbSession.createReadTransaction()};
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createReadTransaction() };
const Track::pointer track {Track::find(dbSession, trackId)};
if (!track)
return res;
const Track::pointer track{ Track::find(dbSession, trackId) };
if (!track)
return res;
res = track->getArtistIds({});
std::sort(std::begin(res), std::end(res));
res = track->getArtistIds({});
std::sort(std::begin(res), std::end(res));
return res;
}
} // namespace lms::recommendation
return res;
}
} // namespace lms::recommendation::PlaylistGeneratorConstraint
@@ -25,21 +25,20 @@
namespace lms::db
{
class Db;
class Db;
}
namespace lms::recommendation::PlaylistGeneratorConstraint
{
class ConsecutiveArtists : public IConstraint
{
public:
ConsecutiveArtists(db::Db& db);
class ConsecutiveArtists : public IConstraint
{
public:
ConsecutiveArtists(db::Db& db);
private:
float computeScore(const TrackContainer& trackIds, std::size_t trackIndex) override;
ArtistContainer getArtists(db::TrackId trackId);
private:
float computeScore(const TrackContainer& trackIds, std::size_t trackIndex) override;
ArtistContainer getArtists(db::TrackId trackId);
db::Db& _db;
};
db::Db& _db;
};
} // namespace lms::recommendation::PlaylistGeneratorConstraint
@@ -19,59 +19,57 @@
#include "ConsecutiveReleases.hpp"
#include "core/ILogger.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "core/ILogger.hpp"
namespace lms::recommendation::PlaylistGeneratorConstraint
{
ConsecutiveReleases::ConsecutiveReleases(db::Db& db)
: _db {db}
{}
ConsecutiveReleases::ConsecutiveReleases(db::Db& db)
: _db{ db }
{
}
float
ConsecutiveReleases::computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex)
{
assert(!trackIds.empty());
assert(trackIndex <= trackIds.size() - 1);
float ConsecutiveReleases::computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex)
{
assert(!trackIds.empty());
assert(trackIndex <= trackIds.size() - 1);
const db::ReleaseId releaseId {getReleaseId(trackIds[trackIndex])};
const db::ReleaseId releaseId{ getReleaseId(trackIds[trackIndex]) };
constexpr std::size_t rangeSize{ 3 }; // check up to rangeSize tracks before/after the target track
static_assert(rangeSize > 0);
constexpr std::size_t rangeSize{ 3 }; // check up to rangeSize tracks before/after the target track
static_assert(rangeSize > 0);
float score {};
for (std::size_t i {1}; i < rangeSize; ++i)
{
if ((trackIndex >= i) && getReleaseId(trackIds[trackIndex - i]) == releaseId)
score += (1.f / static_cast<float>(i));
float score{};
for (std::size_t i{ 1 }; i < rangeSize; ++i)
{
if ((trackIndex >= i) && getReleaseId(trackIds[trackIndex - i]) == releaseId)
score += (1.f / static_cast<float>(i));
if ((trackIndex + i < trackIds.size()) && getReleaseId(trackIds[trackIndex + i]) == releaseId)
score += (1.f / static_cast<float>(i));
}
if ((trackIndex + i < trackIds.size()) && getReleaseId(trackIds[trackIndex + i]) == releaseId)
score += (1.f / static_cast<float>(i));
}
return score;
}
return score;
}
db::ReleaseId
ConsecutiveReleases::getReleaseId(db::TrackId trackId)
{
using namespace db;
db::ReleaseId ConsecutiveReleases::getReleaseId(db::TrackId trackId)
{
using namespace db;
Session& dbSession {_db.getTLSSession()};
auto transaction {dbSession.createReadTransaction()};
Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createReadTransaction() };
const Track::pointer track {Track::find(dbSession, trackId)};
if (!track)
return {};
const Track::pointer track{ Track::find(dbSession, trackId) };
if (!track)
return {};
const Release::pointer release {track->getRelease()};
if (!release)
return {};
return release->getId();
}
} // namespace lms::recommendation
const Release::pointer release{ track->getRelease() };
if (!release)
return {};
return release->getId();
}
} // namespace lms::recommendation::PlaylistGeneratorConstraint
@@ -25,22 +25,21 @@
namespace lms::db
{
class Db;
class Db;
}
namespace lms::recommendation::PlaylistGeneratorConstraint
{
class ConsecutiveReleases : public IConstraint
{
public:
ConsecutiveReleases(db::Db& db);
class ConsecutiveReleases : public IConstraint
{
public:
ConsecutiveReleases(db::Db& db);
private:
float computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex) override;
private:
float computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex) override;
db::ReleaseId getReleaseId(db::TrackId trackId);
db::Db& _db;
};
} // namespace lms::recommendation
db::ReleaseId getReleaseId(db::TrackId trackId);
db::Db& _db;
};
} // namespace lms::recommendation::PlaylistGeneratorConstraint
@@ -23,11 +23,9 @@
namespace lms::recommendation::PlaylistGeneratorConstraint
{
float
DuplicateTracks::computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex)
{
const auto count {std::count(std::cbegin(trackIds), std::cend(trackIds), trackIds[trackIndex])};
return count == 1 ? 0 : 1000;
}
} // namespace lms::recommendation
float DuplicateTracks::computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex)
{
const auto count{ std::count(std::cbegin(trackIds), std::cend(trackIds), trackIds[trackIndex]) };
return count == 1 ? 0 : 1000;
}
} // namespace lms::recommendation::PlaylistGeneratorConstraint
@@ -23,10 +23,9 @@
namespace lms::recommendation::PlaylistGeneratorConstraint
{
class DuplicateTracks : public IConstraint
{
private:
float computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex) override;
};
} // namespace lms::recommendation::PlaylistGeneratorConstraints
class DuplicateTracks : public IConstraint
{
private:
float computeScore(const std::vector<db::TrackId>& trackIds, std::size_t trackIndex) override;
};
} // namespace lms::recommendation::PlaylistGeneratorConstraint
@@ -25,15 +25,15 @@
namespace lms::recommendation::PlaylistGeneratorConstraint
{
class IConstraint
{
public:
virtual ~IConstraint() = default;
class IConstraint
{
public:
virtual ~IConstraint() = default;
// compute the score of the track at index trackIndex
// 0: best
// 1: worst
// > 1 : violation
virtual float computeScore(const TrackContainer& trackIds, std::size_t trackIndex) = 0;
};
} // namespace lms::recommendation
// compute the score of the track at index trackIndex
// 0: best
// 1: worst
// > 1 : violation
virtual float computeScore(const TrackContainer& trackIds, std::size_t trackIndex) = 0;
};
} // namespace lms::recommendation::PlaylistGeneratorConstraint
@@ -20,27 +20,27 @@
#pragma once
#include <memory>
#include "database/TrackListId.hpp"
#include "database/Types.hpp"
#include "services/recommendation/Types.hpp"
namespace lms::db
{
class Db;
class Db;
}
namespace lms::recommendation
{
class IRecommendationService;
class IPlaylistGeneratorService
{
public:
virtual ~IPlaylistGeneratorService() = default;
class IRecommendationService;
class IPlaylistGeneratorService
{
public:
virtual ~IPlaylistGeneratorService() = default;
// extend an existing playlist with similar tracks (but use playlist contraints)
virtual TrackContainer extendPlaylist(db::TrackListId tracklistId, std::size_t maxCount) const = 0;
};
std::unique_ptr<IPlaylistGeneratorService> createPlaylistGeneratorService(db::Db& db, IRecommendationService& recommandationService);
} // ns Recommendation
// extend an existing playlist with similar tracks (but use playlist contraints)
virtual TrackContainer extendPlaylist(db::TrackListId tracklistId, std::size_t maxCount) const = 0;
};
std::unique_ptr<IPlaylistGeneratorService> createPlaylistGeneratorService(db::Db& db, IRecommendationService& recommandationService);
} // namespace lms::recommendation
@@ -21,6 +21,7 @@
#include <memory>
#include <vector>
#include "core/EnumSet.hpp"
#include "database/TrackListId.hpp"
#include "database/Types.hpp"
@@ -28,24 +29,23 @@
namespace lms::db
{
class Db;
class Db;
}
namespace lms::recommendation
{
class IRecommendationService
{
public:
virtual ~IRecommendationService() = default;
class IRecommendationService
{
public:
virtual ~IRecommendationService() = default;
virtual void load() = 0;
virtual void load() = 0;
virtual TrackContainer findSimilarTracks(db::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual TrackContainer findSimilarTracks(const std::vector<db::TrackId>& tracksId, std::size_t maxCount) const = 0;
virtual ReleaseContainer getSimilarReleases(db::ReleaseId releaseId, std::size_t maxCount) const = 0;
virtual ArtistContainer getSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
};
std::unique_ptr<IRecommendationService> createRecommendationService(db::Db& db);
} // ns Recommendation
virtual TrackContainer findSimilarTracks(db::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual TrackContainer findSimilarTracks(const std::vector<db::TrackId>& tracksId, std::size_t maxCount) const = 0;
virtual ReleaseContainer getSimilarReleases(db::ReleaseId releaseId, std::size_t maxCount) const = 0;
virtual ArtistContainer getSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
};
std::unique_ptr<IRecommendationService> createRecommendationService(db::Db& db);
} // namespace lms::recommendation
@@ -1,24 +1,25 @@
#pragma once
#include <functional>
#include "database/ArtistId.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
namespace lms::recommendation
{
struct Progress
{
std::size_t totalElems {};
std::size_t processedElems {};
};
using ProgressCallback = std::function<void(const Progress&)>;
struct Progress
{
std::size_t totalElems{};
std::size_t processedElems{};
};
using ProgressCallback = std::function<void(const Progress&)>;
template <typename IdType>
using ResultContainer = std::vector<IdType>;
template<typename IdType>
using ResultContainer = std::vector<IdType>;
using ArtistContainer = ResultContainer<db::ArtistId>;
using ReleaseContainer = ResultContainer<db::ReleaseId>;
using TrackContainer = ResultContainer<db::TrackId>;
using ArtistContainer = ResultContainer<db::ArtistId>;
using ReleaseContainer = ResultContainer<db::ReleaseId>;
using TrackContainer = ResultContainer<db::TrackId>;
} // namespace lms::recommendation
+1 -1
View File
@@ -43,4 +43,4 @@ namespace lms::scanner
};
virtual void process(ScanContext& context) = 0;
};
}
} // namespace lms::scanner
+28 -26
View File
@@ -22,40 +22,42 @@
#include <functional>
#include "services/scanner/ScannerStats.hpp"
#include "IScanStep.hpp"
#include "ScannerSettings.hpp"
namespace lms::db
{
class Db;
class Db;
}
namespace lms::scanner
{
class ScanStepBase : public IScanStep
{
public:
static inline const std::filesystem::path excludeDirFileName {".lmsignore"};
using ProgressCallback = std::function<void(const ScanStepStats& stats)>;
class ScanStepBase : public IScanStep
{
public:
static inline const std::filesystem::path excludeDirFileName{ ".lmsignore" };
using ProgressCallback = std::function<void(const ScanStepStats& stats)>;
struct InitParams
{
const ScannerSettings& settings;
ProgressCallback progressCallback;
bool& abortScan;
db::Db& db;
};
ScanStepBase(InitParams& initParams)
: _settings {initParams.settings}
, _progressCallback {initParams.progressCallback}
, _abortScan {initParams.abortScan}
, _db {initParams.db}
{}
struct InitParams
{
const ScannerSettings& settings;
ProgressCallback progressCallback;
bool& abortScan;
db::Db& db;
};
ScanStepBase(InitParams& initParams)
: _settings{ initParams.settings }
, _progressCallback{ initParams.progressCallback }
, _abortScan{ initParams.abortScan }
, _db{ initParams.db }
{
}
protected:
const ScannerSettings& _settings;
ProgressCallback _progressCallback;
bool& _abortScan;
db::Db& _db;
};
}
protected:
const ScannerSettings& _settings;
ProgressCallback _progressCallback;
bool& _abortScan;
db::Db& _db;
};
} // namespace lms::scanner
@@ -19,10 +19,10 @@
#include "ScanStepCheckDuplicatedDbFiles.hpp"
#include "core/ILogger.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "core/ILogger.hpp"
namespace lms::scanner
{
@@ -33,8 +33,8 @@ namespace lms::scanner
if (_abortScan)
return;
Session& session {_db.getTLSSession()};
auto transaction {session.createReadTransaction()};
Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
const RangeResults<TrackId> tracks = Track::findIdsTrackMBIDDuplicates(session);
for (const TrackId trackId : tracks.results)
@@ -54,4 +54,4 @@ namespace lms::scanner
LMS_LOG(DBUPDATER, DEBUG, "Found " << context.currentStepStats.processedElems << " duplicated audio files");
}
}
} // namespace lms::scanner
@@ -23,14 +23,14 @@
namespace lms::scanner
{
class ScanStepCheckDuplicatedDbFiles : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
class ScanStepCheckDuplicatedDbFiles : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
private:
core::LiteralString getStepName() const override { return "Check for duplicated files"; }
ScanStep getStep() const override { return ScanStep::CheckForDuplicateFiles; }
void process(ScanContext& context) override;
};
}
private:
core::LiteralString getStepName() const override { return "Check for duplicated files"; }
ScanStep getStep() const override { return ScanStep::CheckForDuplicateFiles; }
void process(ScanContext& context) override;
};
} // namespace lms::scanner
@@ -30,4 +30,4 @@ namespace lms::scanner
if (context.scanOptions.compact)
_db.getTLSSession().vacuum();
}
}
} // namespace lms::scanner
@@ -23,14 +23,14 @@
namespace lms::scanner
{
class ScanStepCompact : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
class ScanStepCompact : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
private:
ScanStep getStep() const override { return ScanStep::Compact; }
core::LiteralString getStepName() const override { return "Compact"; }
void process(ScanContext& context) override;
};
}
private:
ScanStep getStep() const override { return ScanStep::Compact; }
core::LiteralString getStepName() const override { return "Compact"; }
void process(ScanContext& context) override;
};
} // namespace lms::scanner
@@ -18,11 +18,11 @@
*/
#include "ScanStepComputeClusterStats.hpp"
#include "database/Db.hpp"
#include "database/Cluster.hpp"
#include "database/Session.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
namespace lms::scanner
{
@@ -38,53 +38,51 @@ namespace lms::scanner
const std::size_t clusterCount{ [&] {
auto transaction{ dbSession.createReadTransaction() };
return Cluster::getCount(dbSession);
}() };
}() };
context.currentStepStats.totalElems = clusterCount;
foreachSubRange(Range{ 0, clusterCount }, 100, [&](Range range)
foreachSubRange(Range{ 0, clusterCount }, 100, [&](Range range) {
const std::vector<ClusterId> clusterIds{ [&] {
Cluster::FindParameters params;
params.setRange(range);
{
auto transaction{ dbSession.createReadTransaction() };
return std::move(Cluster::findIds(dbSession, params).results);
}
}() };
for (const ClusterId clusterId : clusterIds)
{
const std::vector<ClusterId> clusterIds{ [&]
if (_abortScan)
break;
std::size_t trackCount;
std::size_t releaseCount;
{
Cluster::FindParameters params;
params.setRange(range);
auto transaction{ dbSession.createReadTransaction() };
{
auto transaction{ dbSession.createReadTransaction() };
return std::move(Cluster::findIds(dbSession, params).results);
}
}() };
for (const ClusterId clusterId : clusterIds)
{
if (_abortScan)
break;
std::size_t trackCount;
std::size_t releaseCount;
{
auto transaction{ dbSession.createReadTransaction() };
trackCount = Cluster::computeTrackCount(dbSession, clusterId);
releaseCount = Cluster::computeReleaseCount(dbSession, clusterId);
}
{
auto transaction{ dbSession.createWriteTransaction() };
auto cluster{ Cluster::find(dbSession, clusterId) };
cluster.modify()->setTrackCount(trackCount);
cluster.modify()->setReleaseCount(releaseCount);
}
context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats);
trackCount = Cluster::computeTrackCount(dbSession, clusterId);
releaseCount = Cluster::computeReleaseCount(dbSession, clusterId);
}
return true;
});
{
auto transaction{ dbSession.createWriteTransaction() };
auto cluster{ Cluster::find(dbSession, clusterId) };
cluster.modify()->setTrackCount(trackCount);
cluster.modify()->setReleaseCount(releaseCount);
}
context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats);
}
return true;
});
LMS_LOG(DBUPDATER, DEBUG, "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!");
}
}
} // namespace lms::scanner
@@ -33,4 +33,4 @@ namespace lms::scanner
core::LiteralString getStepName() const override { return "Compute cluster stats"; }
void process(ScanContext& context) override;
};
}
} // namespace lms::scanner
@@ -31,8 +31,8 @@ namespace lms::scanner
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
{
std::size_t currentDirectoryProcessElemsCount{};
core::pathUtils::exploreFilesRecursive(mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path)
{
core::pathUtils::exploreFilesRecursive(
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) {
if (_abortScan)
return false;
@@ -44,7 +44,8 @@ namespace lms::scanner
}
return true;
}, &excludeDirFileName);
},
&excludeDirFileName);
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << currentDirectoryProcessElemsCount << " files in '" << mediaLibrary.rootDirectory << "'");
}
@@ -53,4 +54,4 @@ namespace lms::scanner
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << context.stats.filesScanned << " files in all directories");
}
}
} // namespace lms::scanner
@@ -23,14 +23,14 @@
namespace lms::scanner
{
class ScanStepDiscoverFiles : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
class ScanStepDiscoverFiles : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
private:
ScanStep getStep() const override { return ScanStep::DiscoverFiles; }
core::LiteralString getStepName() const override { return "Discover files"; }
void process(ScanContext& context) override;
};
}
private:
ScanStep getStep() const override { return ScanStep::DiscoverFiles; }
core::LiteralString getStepName() const override { return "Discover files"; }
void process(ScanContext& context) override;
};
} // namespace lms::scanner
@@ -53,4 +53,4 @@ namespace lms::scanner
LMS_LOG(DBUPDATER, INFO, "Database analyze complete");
}
}
}
} // namespace lms::scanner
@@ -23,14 +23,14 @@
namespace lms::scanner
{
class ScanStepOptimize : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
class ScanStepOptimize : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
private:
ScanStep getStep() const override { return ScanStep::Optimize; }
core::LiteralString getStepName() const override { return "Optimize"; }
void process(ScanContext& context) override;
};
}
private:
ScanStep getStep() const override { return ScanStep::Optimize; }
core::LiteralString getStepName() const override { return "Optimize"; }
void process(ScanContext& context) override;
};
} // namespace lms::scanner
@@ -19,14 +19,14 @@
#include "ScanStepRemoveOrphanDbFiles.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
namespace lms::scanner
{
@@ -36,7 +36,7 @@ namespace lms::scanner
{
constexpr std::size_t batchSize = 100;
template <typename T>
template<typename T>
void removeOrphanEntries(Session& session, bool& abortScan)
{
using IdType = typename T::IdType;
@@ -68,7 +68,7 @@ namespace lms::scanner
break;
}
}
}
} // namespace
void ScanStepRemoveOrphanDbFiles::process(ScanContext& context)
{
@@ -110,15 +110,14 @@ namespace lms::scanner
auto transaction{ session.createReadTransaction() };
endReached = true;
Track::find(session, lastCheckedTrackID, batchSize, [&](const Track::pointer& track)
{
endReached = false;
Track::find(session, lastCheckedTrackID, batchSize, [&](const Track::pointer& track) {
endReached = false;
if (!checkFile(track->getAbsoluteFilePath()))
tracksToRemove.push_back(track);
if (!checkFile(track->getAbsoluteFilePath()))
tracksToRemove.push_back(track);
context.currentStepStats.processedElems++;
});
context.currentStepStats.processedElems++;
});
}
if (!tracksToRemove.empty())
@@ -175,10 +174,9 @@ namespace lms::scanner
}
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
[&](const ScannerSettings::MediaLibraryInfo& libraryInfo)
{
return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName);
}))
[&](const ScannerSettings::MediaLibraryInfo& libraryInfo) {
return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName);
}))
{
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': out of media directory");
return false;
@@ -198,4 +196,4 @@ namespace lms::scanner
return false;
}
}
}
} // namespace lms::scanner
@@ -25,21 +25,21 @@
namespace lms::scanner
{
class ScanStepRemoveOrphanDbFiles : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
class ScanStepRemoveOrphanDbFiles : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
private:
core::LiteralString getStepName() const override { return "Check orphaned entries"; }
ScanStep getStep() const override { return ScanStep::CheckForMissingFiles; }
void process(ScanContext& context) override;
private:
core::LiteralString getStepName() const override { return "Check orphaned entries"; }
ScanStep getStep() const override { return ScanStep::CheckForMissingFiles; }
void process(ScanContext& context) override;
void removeOrphanTracks(ScanContext& context);
void removeOrphanClusters();
void removeOrphanClusterTypes();
void removeOrphanArtists();
void removeOrphanReleases();
bool checkFile(const std::filesystem::path& p);
};
}
void removeOrphanTracks(ScanContext& context);
void removeOrphanClusters();
void removeOrphanClusterTypes();
void removeOrphanArtists();
void removeOrphanReleases();
bool checkFile(const std::filesystem::path& p);
};
} // namespace lms::scanner
@@ -19,6 +19,11 @@
#include "ScanStepScanFiles.hpp"
#include "core/Exception.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "core/Path.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
@@ -26,15 +31,10 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackFeatures.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackFeatures.hpp"
#include "metadata/Exception.hpp"
#include "metadata/IParser.hpp"
#include "core/Exception.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
#include "core/ITraceLogger.hpp"
namespace lms::scanner
{
@@ -249,8 +249,7 @@ namespace lms::scanner
{
std::vector<Cluster::pointer> clusters;
auto getOrCreateClusters{ [&](std::string tag, std::span<const std::string> values)
{
auto getOrCreateClusters{ [&](std::string tag, std::span<const std::string> values) {
auto clusterType = ClusterType::find(session, tag);
if (!clusterType)
clusterType = session.create<ClusterType>(tag);
@@ -306,7 +305,8 @@ namespace lms::scanner
: _metadataParser{ parser }
, _scanContextRunner{ _scanContext, threadCount, "ScannerMetadata" }
, _abort{ abort }
{}
{
}
void ScanStepScanFiles::MetadataScanQueue::pushScanRequest(const std::filesystem::path& path)
{
@@ -315,38 +315,37 @@ namespace lms::scanner
_ongoingScanCount += 1;
}
_scanContext.post([=, this]
_scanContext.post([=, this] {
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "AudioFileParseJob");
std::unique_ptr<metadata::Track> track;
if (_abort)
{
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "AudioFileParseJob");
std::scoped_lock lock{ _mutex };
_ongoingScanCount -= 1;
}
else
{
try
{
track = _metadataParser.parse(path);
}
catch (const metadata::Exception& e)
{
LMS_LOG(DBUPDATER, INFO, "Failed to parse '" << path.string() << "'");
}
std::unique_ptr<metadata::Track> track;
if (_abort)
{
std::scoped_lock lock{ _mutex };
_scanResults.emplace_back(MetaDataScanResult{ std::move(path), std::move(track) });
_ongoingScanCount -= 1;
}
else
{
try
{
track = _metadataParser.parse(path);
}
catch (const metadata::Exception& e)
{
LMS_LOG(DBUPDATER, INFO, "Failed to parse '" << path.string() << "'");
}
}
{
std::scoped_lock lock{ _mutex };
_scanResults.emplace_back(MetaDataScanResult{ std::move(path), std::move(track) });
_ongoingScanCount -= 1;
}
}
_condVar.notify_all();
});
_condVar.notify_all();
});
}
std::size_t ScanStepScanFiles::MetadataScanQueue::getResultsCount() const
@@ -407,8 +406,8 @@ namespace lms::scanner
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
{
core::pathUtils::exploreFilesRecursive(mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path)
{
core::pathUtils::exploreFilesRecursive(
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) {
LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile");
if (_abortScan)
@@ -437,7 +436,8 @@ namespace lms::scanner
_metadataScanQueue.wait(scanQueueMaxScanRequestCount);
return true;
}, &excludeDirFileName);
},
&excludeDirFileName);
_metadataScanQueue.wait();
@@ -469,8 +469,7 @@ namespace lms::scanner
if (track
&& track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t()
&& track->getScanVersion() == _settings.scanVersion
)
&& track->getScanVersion() == _settings.scanVersion)
{
// this file may have been moved from one library to another, then we just need to update the media library id instead of a full rescan
const auto trackMediaLibrary{ track->getMediaLibrary() };
@@ -568,10 +567,9 @@ namespace lms::scanner
// Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
[&](const ScannerSettings::MediaLibraryInfo& libraryInfo)
{
return core::pathUtils::isPathInRootPath(file, libraryInfo.rootDirectory, &excludeDirFileName);
}))
[&](const ScannerSettings::MediaLibraryInfo& libraryInfo) {
return core::pathUtils::isPathInRootPath(file, libraryInfo.rootDirectory, &excludeDirFileName);
}))
{
continue;
}
@@ -723,4 +721,4 @@ namespace lms::scanner
stats.updates++;
}
}
}
} // namespace lms::scanner
@@ -27,8 +27,9 @@
#include <string>
#include <vector>
#include "metadata/IParser.hpp"
#include "core/IOContextRunner.hpp"
#include "metadata/IParser.hpp"
#include "ScanStepBase.hpp"
namespace lms::scanner
@@ -52,36 +53,36 @@ namespace lms::scanner
void processMetaDataScanResults(ScanContext& context, std::span<const MetaDataScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
void processFileMetaData(ScanContext& context, const std::filesystem::path& file, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo);
std::unique_ptr<metadata::IParser> _metadataParser;
const std::vector<std::string> _extraTagsToParse;
std::unique_ptr<metadata::IParser> _metadataParser;
const std::vector<std::string> _extraTagsToParse;
class MetadataScanQueue
{
public:
MetadataScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort);
public:
MetadataScanQueue(metadata::IParser& parser, std::size_t threadCount, bool& abort);
std::size_t getThreadCount() const { return _scanContextRunner.getThreadCount(); }
std::size_t getThreadCount() const { return _scanContextRunner.getThreadCount(); }
void pushScanRequest(const std::filesystem::path& path);
void pushScanRequest(const std::filesystem::path& path);
std::size_t getResultsCount() const;
size_t popResults(std::vector<MetaDataScanResult>& results, std::size_t maxCount);
std::size_t getResultsCount() const;
size_t popResults(std::vector<MetaDataScanResult>& results, std::size_t maxCount);
void wait(std::size_t maxScanRequestCount = 0); // wait until ongoing scan request count <= maxScanRequestCount
void wait(std::size_t maxScanRequestCount = 0); // wait until ongoing scan request count <= maxScanRequestCount
private:
metadata::IParser& _metadataParser;
boost::asio::io_context _scanContext;
core::IOContextRunner _scanContextRunner;
private:
metadata::IParser& _metadataParser;
boost::asio::io_context _scanContext;
core::IOContextRunner _scanContextRunner;
mutable std::mutex _mutex ;
std::size_t _ongoingScanCount{};
std::deque<MetaDataScanResult> _scanResults;
std::condition_variable _condVar;
bool& _abort;
mutable std::mutex _mutex;
std::size_t _ongoingScanCount{};
std::deque<MetaDataScanResult> _scanResults;
std::condition_variable _condVar;
bool& _abort;
};
MetadataScanQueue _metadataScanQueue;
std::deque<MetaDataScanResult> _metaDataScanResults;
};
}
} // namespace lms::scanner
@@ -21,14 +21,14 @@
#include <ctime>
#include "database/MediaLibrary.hpp"
#include "database/TrackFeatures.hpp"
#include "database/ScanSettings.hpp"
#include "core/Exception.hpp"
#include "core/Path.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "core/Path.hpp"
#include "database/MediaLibrary.hpp"
#include "database/ScanSettings.hpp"
#include "database/TrackFeatures.hpp"
#include "ScanStepCheckDuplicatedDbFiles.hpp"
#include "ScanStepCompact.hpp"
@@ -91,13 +91,12 @@ namespace lms::scanner
{
std::scoped_lock lock{ _controlMutex };
_ioService.post([this]
{
if (_abortScan)
return;
_ioService.post([this] {
if (_abortScan)
return;
scheduleNextScan();
});
scheduleNextScan();
});
_ioService.start();
}
@@ -139,25 +138,23 @@ namespace lms::scanner
void ScannerService::requestImmediateScan(const ScanOptions& scanOptions)
{
abortScan();
_ioService.post([this, scanOptions]
{
if (_abortScan)
return;
_ioService.post([this, scanOptions] {
if (_abortScan)
return;
scheduleScan(scanOptions);
});
scheduleScan(scanOptions);
});
}
void ScannerService::requestReload()
{
abortScan();
_ioService.post([this]()
{
if (_abortScan)
return;
_ioService.post([this]() {
if (_abortScan)
return;
scheduleNextScan();
});
scheduleNextScan();
});
}
ScannerService::Status ScannerService::getStatus() const
@@ -229,8 +226,7 @@ namespace lms::scanner
void ScannerService::scheduleScan(const ScanOptions& scanOptions, const Wt::WDateTime& dateTime)
{
auto cb{ [this, scanOptions](boost::system::error_code ec)
{
auto cb{ [this, scanOptions](boost::system::error_code ec) {
if (ec)
return;
@@ -270,7 +266,7 @@ namespace lms::scanner
refreshScanSettings();
IScanStep::ScanContext scanContext{ scanOptions, ScanStats {}, ScanStepStats {} };
IScanStep::ScanContext scanContext{ scanOptions, ScanStats{}, ScanStepStats{} };
ScanStats& stats{ scanContext.stats };
stats.startTime = Wt::WDateTime::currentDateTime();
@@ -329,13 +325,11 @@ namespace lms::scanner
_settings = std::move(newSettings);
auto cbFunc{ [this](const ScanStepStats& stats)
{
notifyInProgressIfNeeded(stats);
} };
auto cbFunc{ [this](const ScanStepStats& stats) {
notifyInProgressIfNeeded(stats);
} };
ScanStepBase::InitParams params
{
ScanStepBase::InitParams params{
_settings,
cbFunc,
_abortScan,
@@ -373,14 +367,13 @@ namespace lms::scanner
[](const std::filesystem::path& extension) { return std::filesystem::path{ core::stringUtils::stringToLower(extension.string()) }; });
}
MediaLibrary::find(_db.getTLSSession(), [&](const MediaLibrary::pointer& mediaLibrary)
{
newSettings.mediaLibraries.push_back(ScannerSettings::MediaLibraryInfo{ mediaLibrary->getId(), mediaLibrary->getPath().lexically_normal() });
});
MediaLibrary::find(_db.getTLSSession(), [&](const MediaLibrary::pointer& mediaLibrary) {
newSettings.mediaLibraries.push_back(ScannerSettings::MediaLibraryInfo{ mediaLibrary->getId(), mediaLibrary->getPath().lexically_normal() });
});
{
const auto& tags{ scanSettings->getExtraTagsToScan() };
std::transform(std::cbegin(tags), std::cend(tags), std::back_inserter(newSettings.extraTags), [](std::string_view tag) { return std::string{ tag };});
std::transform(std::cbegin(tags), std::cend(tags), std::back_inserter(newSettings.extraTags), [](std::string_view tag) { return std::string{ tag }; });
}
newSettings.artistTagDelimiters = scanSettings->getArtistTagDelimiters();
@@ -20,23 +20,22 @@
#pragma once
#include <chrono>
#include <shared_mutex>
#include <optional>
#include <shared_mutex>
#include <vector>
#include <Wt/WDateTime.h>
#include <Wt/WIOService.h>
#include <Wt/WSignal.h>
#include <boost/asio/system_timer.hpp>
#include "IScanStep.hpp"
#include "ScannerSettings.hpp"
#include "core/Path.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/Types.hpp"
#include "services/scanner/IScannerService.hpp"
#include "core/Path.hpp"
#include "IScanStep.hpp"
#include "ScannerSettings.hpp"
namespace lms::scanner
{
@@ -53,7 +52,7 @@ namespace lms::scanner
void requestReload() override;
void requestImmediateScan(const ScanOptions& scanOptions) override;
Status getStatus() const override;
Status getStatus() const override;
Events& getEvents() override { return _events; }
private:
@@ -78,23 +77,22 @@ namespace lms::scanner
void notifyInProgressIfNeeded(const ScanStepStats& stats);
void notifyInProgress(const ScanStepStats& stats);
std::vector<std::unique_ptr<IScanStep>> _scanSteps;
std::vector<std::unique_ptr<IScanStep>> _scanSteps;
std::mutex _controlMutex;
bool _abortScan{};
Wt::WIOService _ioService;
boost::asio::system_timer _scheduleTimer{ _ioService };
Events _events;
std::chrono::system_clock::time_point _lastScanInProgressEmit{};
std::mutex _controlMutex;
bool _abortScan{};
Wt::WIOService _ioService;
boost::asio::system_timer _scheduleTimer{ _ioService };
Events _events;
std::chrono::system_clock::time_point _lastScanInProgressEmit{};
db::Db& _db;
mutable std::shared_mutex _statusMutex;
State _curState{ State::NotScheduled };
std::optional<ScanStats> _lastCompleteScanStats;
std::optional<ScanStepStats> _currentScanStepStats;
Wt::WDateTime _nextScheduledScan;
mutable std::shared_mutex _statusMutex;
State _curState{ State::NotScheduled };
std::optional<ScanStats> _lastCompleteScanStats;
std::optional<ScanStepStats> _currentScanStepStats;
Wt::WDateTime _nextScheduledScan;
ScannerSettings _settings;
ScannerSettings _settings;
};
} // Scanner
} // namespace lms::scanner
@@ -22,7 +22,9 @@
#include <filesystem>
#include <string>
#include <vector>
#include <Wt/WDateTime.h>
#include "database/MediaLibraryId.hpp"
#include "database/ScanSettings.hpp"
@@ -30,14 +32,14 @@ namespace lms::scanner
{
struct ScannerSettings
{
std::size_t scanVersion{};
Wt::WTime startTime;
db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never };
std::vector<std::filesystem::path> supportedExtensions;
bool skipDuplicateMBID{};
std::vector<std::string> extraTags;
std::vector<std::string> artistTagDelimiters;
std::vector<std::string> defaultTagDelimiters;
std::size_t scanVersion{};
Wt::WTime startTime;
db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never };
std::vector<std::filesystem::path> supportedExtensions;
bool skipDuplicateMBID{};
std::vector<std::string> extraTags;
std::vector<std::string> artistTagDelimiters;
std::vector<std::string> defaultTagDelimiters;
struct MediaLibraryInfo
{
@@ -46,8 +48,8 @@ namespace lms::scanner
auto operator<=>(const MediaLibraryInfo& other) const = default;
};
std::vector<MediaLibraryInfo> mediaLibraries;
std::vector<MediaLibraryInfo> mediaLibraries;
bool operator==(const ScannerSettings& rhs) const = default;
};
}
} // namespace lms::scanner
@@ -22,9 +22,9 @@
namespace lms::scanner
{
ScanError::ScanError(const std::filesystem::path& _file, ScanErrorType _error, const std::string& _systemError)
: file{ _file },
error{ _error },
systemError{ _systemError }
: file{ _file }
, error{ _error }
, systemError{ _systemError }
{
}
@@ -43,4 +43,3 @@ namespace lms::scanner
return (processedElems / static_cast<float>(totalElems ? totalElems : 1)) * 100;
}
} // namespace lms::scanner
@@ -50,10 +50,10 @@ namespace lms::scanner
struct Status
{
State currentState{ State::NotScheduled };
Wt::WDateTime nextScheduledScan;
std::optional<ScanStats> lastCompleteScanStats;
std::optional<ScanStepStats> currentScanStepStats;
State currentState{ State::NotScheduled };
Wt::WDateTime nextScheduledScan;
std::optional<ScanStats> lastCompleteScanStats;
std::optional<ScanStepStats> currentScanStepStats;
};
virtual Status getStatus() const = 0;
@@ -62,4 +62,4 @@ namespace lms::scanner
};
std::unique_ptr<IScannerService> createScannerService(db::Db& db);
} // Scanner
} // namespace lms::scanner
@@ -30,20 +30,19 @@ namespace lms::scanner
struct Events
{
// Called if scan was aborted
Wt::Signal<> scanAborted;
Wt::Signal<> scanAborted;
// Called just after scan start
Wt::Signal<> scanStarted;
Wt::Signal<> scanStarted;
// Called just after scan complete (true if changes have been made)
Wt::Signal<ScanStats> scanComplete;
Wt::Signal<ScanStats> scanComplete;
// Called during scan in progress
Wt::Signal<ScanStepStats> scanInProgress;
Wt::Signal<ScanStepStats> scanInProgress;
// Called after a schedule
Wt::Signal<Wt::WDateTime> scanScheduled;
Wt::Signal<Wt::WDateTime> scanScheduled;
};
} // ns Scanner
} // namespace lms::scanner
@@ -23,8 +23,8 @@ namespace lms::scanner
{
struct ScanOptions
{
bool fullScan{}; // scan files even if not changed
bool fullScan{}; // scan files even if not changed
bool forceOptimize{}; // force optimize database
bool compact{}; // compact the database
bool compact{}; // compact the database
};
}
} // namespace lms::scanner
@@ -30,10 +30,10 @@ namespace lms::scanner
{
enum class ScanErrorType
{
CannotReadFile, // cannot read file
CannotParseFile, // cannot parse file
NoAudioTrack, // no audio track found
BadDuration, // bad duration
CannotReadFile, // cannot read file
CannotParseFile, // cannot parse file
NoAudioTrack, // no audio track found
BadDuration, // bad duration
};
enum class DuplicateReason
@@ -44,17 +44,17 @@ namespace lms::scanner
struct ScanError
{
std::filesystem::path file;
ScanErrorType error;
std::string systemError;
std::filesystem::path file;
ScanErrorType error;
std::string systemError;
ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = "");
};
struct ScanDuplicate
{
db::TrackId trackId;
DuplicateReason reason;
db::TrackId trackId;
DuplicateReason reason;
};
enum class ScanStep
@@ -74,38 +74,37 @@ namespace lms::scanner
// reduced scan stats
struct ScanStepStats
{
Wt::WDateTime startTime;
Wt::WDateTime startTime;
std::size_t stepIndex{};
ScanStep currentStep;
std::size_t totalElems{};
std::size_t processedElems{};
std::size_t totalElems{};
std::size_t processedElems{};
unsigned progress() const;
unsigned progress() const;
};
struct ScanStats
{
Wt::WDateTime startTime;
Wt::WDateTime stopTime;
Wt::WDateTime startTime;
Wt::WDateTime stopTime;
std::size_t filesScanned{}; // Total number of files scanned (estimated)
std::size_t filesScanned{}; // Total number of files scanned (estimated)
std::size_t skips{}; // no change since last scan
std::size_t scans{}; // actually scanned filed
std::size_t skips{}; // no change since last scan
std::size_t scans{}; // actually scanned filed
std::size_t additions{}; // added in DB
std::size_t deletions{}; // removed from DB
std::size_t updates{}; // updated file in DB
std::size_t additions{}; // added in DB
std::size_t deletions{}; // removed from DB
std::size_t updates{}; // updated file in DB
std::size_t featuresFetched{}; // features fetched in DB
std::size_t featuresFetched{}; // features fetched in DB
std::vector<ScanError> errors;
std::vector<ScanDuplicate> duplicates;
std::vector<ScanError> errors;
std::vector<ScanDuplicate> duplicates;
std::size_t nbFiles() const;
std::size_t nbChanges() const;
std::size_t nbFiles() const;
std::size_t nbChanges() const;
};
} // namespace lms::scanner
@@ -30,7 +30,7 @@ namespace lms::db
class Session;
class TrackList;
class User;
}
} // namespace lms::db
namespace lms::scrobbling
{
@@ -44,5 +44,4 @@ namespace lms::scrobbling
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
virtual void addTimedListen(const TimedListen& listen) = 0;
};
} // ns Scrobbling
} // namespace lms::scrobbling
@@ -19,6 +19,7 @@
#include "ScrobblingService.hpp"
#include "core/ILogger.hpp"
#include "database/Artist.hpp"
#include "database/Db.hpp"
#include "database/Listen.hpp"
@@ -26,7 +27,6 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "core/ILogger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
@@ -54,7 +54,7 @@ namespace lms::scrobbling
{
return db::Listen::ArtistStatsFindParameters{ convertToListenFindParameters(static_cast<const ScrobblingService::FindParameters&>(params)), params.linkType };
}
}
} // namespace
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_context& ioContext, Db& db)
{
@@ -253,5 +253,4 @@ namespace lms::scrobbling
res = db::Listen::getTopTracks(session, listenFindParams);
return res;
}
} // ns Scrobbling
} // namespace lms::scrobbling
@@ -24,6 +24,7 @@
#include <unordered_map>
#include "services/scrobbling/IScrobblingService.hpp"
#include "IScrobblingBackend.hpp"
namespace lms::scrobbling
@@ -59,5 +60,4 @@ namespace lms::scrobbling
std::unordered_map<db::ScrobblingBackend, std::unique_ptr<IScrobblingBackend>> _scrobblingBackends;
};
} // ns Scrobbling
} // namespace lms::scrobbling
@@ -29,7 +29,8 @@ namespace lms::scrobbling
{
InternalBackend::InternalBackend(db::Db& db)
: _db{ db }
{}
{
}
void InternalBackend::listenStarted(const Listen&)
{
@@ -64,5 +65,4 @@ namespace lms::scrobbling
auto dbListen{ session.create<db::Listen>(user, track, db::ScrobblingBackend::Internal, listen.listenedAt) };
dbListen.modify()->setSyncState(db::SyncState::Synchronized);
}
} // Scrobbling
} // namespace lms::scrobbling
@@ -40,5 +40,4 @@ namespace lms::scrobbling
db::Db& _db;
};
} // Scrobbling
} // namespace lms::scrobbling
@@ -23,9 +23,9 @@
namespace lms::scrobbling::listenBrainz
{
class Exception : public scrobbling::Exception
{
public:
using scrobbling::Exception::Exception;
};
}
class Exception : public scrobbling::Exception
{
public:
using scrobbling::Exception::Exception;
};
} // namespace lms::scrobbling::listenBrainz
@@ -19,13 +19,14 @@
#include "ListenBrainzBackend.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "core/http/IClient.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "core/IConfig.hpp"
#include "core/http/IClient.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "Utils.hpp"
namespace lms::scrobbling::listenBrainz
@@ -48,7 +49,7 @@ namespace lms::scrobbling::listenBrainz
return res;
}
}
} // namespace
ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, Db& db)
: _ioContext{ ioContext }
@@ -84,4 +85,3 @@ namespace lms::scrobbling::listenBrainz
_listensSynchronizer.enqueListen(timedListen);
}
} // namespace lms::scrobbling::listenBrainz
@@ -19,8 +19,9 @@
#pragma once
#include <string>
#include <optional>
#include <string>
#include <boost/asio/io_context.hpp>
#include "IScrobblingBackend.hpp"
@@ -50,11 +51,10 @@ namespace lms::scrobbling::listenBrainz
// Submit listens
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
boost::asio::io_context& _ioContext;
db::Db& _db;
std::string _baseAPIUrl;
std::unique_ptr<core::http::IClient> _client;
ListensSynchronizer _listensSynchronizer;
boost::asio::io_context& _ioContext;
db::Db& _db;
std::string _baseAPIUrl;
std::unique_ptr<core::http::IClient> _client;
ListensSynchronizer _listensSynchronizer;
};
} // scrobbling::ListenBrainz
} // namespace lms::scrobbling::listenBrainz
@@ -21,21 +21,20 @@
namespace lms::scrobbling::listenBrainz
{
std::ostream&
operator<<(std::ostream& os, const Listen& listen)
{
os << "track name = '" << listen.trackName << "', artistName = '" << listen.artistName << "'";
if (listen.listenedAt.isValid())
os << ", listenedAt = " << listen.listenedAt.toString();
if (!listen.releaseName.empty())
os << ", releaseName = '" << listen.releaseName << "'";
if (listen.trackNumber)
os << ", trackNumber = " << *listen.trackNumber;
if (listen.trackMBID)
os << ", trackMBID = '" << listen.trackMBID->getAsString() << "'";
if (listen.recordingMBID)
os << ", recordingMBID = '" << listen.recordingMBID->getAsString() << "'";
std::ostream& operator<<(std::ostream& os, const Listen& listen)
{
os << "track name = '" << listen.trackName << "', artistName = '" << listen.artistName << "'";
if (listen.listenedAt.isValid())
os << ", listenedAt = " << listen.listenedAt.toString();
if (!listen.releaseName.empty())
os << ", releaseName = '" << listen.releaseName << "'";
if (listen.trackNumber)
os << ", trackNumber = " << *listen.trackNumber;
if (listen.trackMBID)
os << ", trackMBID = '" << listen.trackMBID->getAsString() << "'";
if (listen.recordingMBID)
os << ", recordingMBID = '" << listen.recordingMBID->getAsString() << "'";
return os;
}
} // scrobbling::ListenBrainz
return os;
}
} // namespace lms::scrobbling::listenBrainz

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