Auto reformatted the base, ref #470
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
+36
-38
@@ -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
|
||||
|
||||
+11
-12
@@ -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
|
||||
|
||||
+12
-12
@@ -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
|
||||
|
||||
+14
-14
@@ -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
|
||||
|
||||
@@ -43,4 +43,4 @@ namespace lms::scanner
|
||||
};
|
||||
virtual void process(ScanContext& context) = 0;
|
||||
};
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user