Simplified logger configuration, it no longer depends on Wt
This commit is contained in:
+2
-2
@@ -10,8 +10,8 @@ ffmpeg-file = "/usr/bin/ffmpeg";
|
|||||||
# Log files, empty means stdout
|
# Log files, empty means stdout
|
||||||
log-file = "";
|
log-file = "";
|
||||||
access-log-file = "";
|
access-log-file = "";
|
||||||
# Logger configuration, see log-config in https://webtoolkit.eu/wt/doc/reference/html/overview.html#config_general
|
# Minimum severity, can be "debug", "info", "warning", "error" or "fatal"
|
||||||
log-config = "* -debug -info:WebRequest";
|
log-min-severity = "info";
|
||||||
# Output db queries on stdout
|
# Output db queries on stdout
|
||||||
db-show-queries = false;
|
db-show-queries = false;
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ extern "C"
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
namespace Av
|
namespace Av
|
||||||
@@ -105,14 +105,14 @@ namespace Av
|
|||||||
int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) };
|
int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) };
|
||||||
if (error < 0)
|
if (error < 0)
|
||||||
{
|
{
|
||||||
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
|
LMS_LOG(AV, ERROR, "Cannot open " << _p.string() << ": " << averror_to_string(error));
|
||||||
throw AudioFileException{ error };
|
throw AudioFileException{ error };
|
||||||
}
|
}
|
||||||
|
|
||||||
error = avformat_find_stream_info(_context, nullptr);
|
error = avformat_find_stream_info(_context, nullptr);
|
||||||
if (error < 0)
|
if (error < 0)
|
||||||
{
|
{
|
||||||
LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
|
LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error));
|
||||||
avformat_close_input(&_context);
|
avformat_close_input(&_context);
|
||||||
throw AudioFileException{ error };
|
throw AudioFileException{ error };
|
||||||
}
|
}
|
||||||
@@ -233,7 +233,7 @@ namespace Av
|
|||||||
|
|
||||||
if (avstream->codecpar == nullptr)
|
if (avstream->codecpar == nullptr)
|
||||||
{
|
{
|
||||||
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
|
LMS_LOG(AV, ERROR, "Skipping stream " << i << " since no codecpar is set");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ namespace Av
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
picture.mimeType = "application/octet-stream";
|
picture.mimeType = "application/octet-stream";
|
||||||
LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion";
|
LMS_LOG(AV, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
|
||||||
}
|
}
|
||||||
|
|
||||||
const AVPacket& pkt{ avstream->attached_pic };
|
const AVPacket& pkt{ avstream->attached_pic };
|
||||||
@@ -271,7 +271,7 @@ namespace Av
|
|||||||
|
|
||||||
if (!avstream->codecpar)
|
if (!avstream->codecpar)
|
||||||
{
|
{
|
||||||
LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
|
LMS_LOG(AV, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
#include "utils/IChildProcessManager.hpp"
|
#include "utils/IChildProcessManager.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Path.hpp"
|
#include "utils/Path.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
namespace Av::Transcoding
|
namespace Av::Transcoding
|
||||||
{
|
{
|
||||||
|
|
||||||
#define LOG(sev) LMS_LOG(TRANSCODING, sev) << "[" << _debugId << "] - "
|
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
|
||||||
|
|
||||||
static std::atomic<size_t> globalId{};
|
static std::atomic<size_t> globalId{};
|
||||||
static std::filesystem::path ffmpegPath;
|
static std::filesystem::path ffmpegPath;
|
||||||
@@ -84,7 +84,7 @@ namespace Av::Transcoding
|
|||||||
throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
|
throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(INFO) << "Transcoding file '" << _inputParameters.trackPath.string() << "'";
|
LOG(INFO, "Transcoding file '" << _inputParameters.trackPath.string() << "'");
|
||||||
|
|
||||||
std::vector<std::string> args;
|
std::vector<std::string> args;
|
||||||
|
|
||||||
@@ -176,9 +176,9 @@ namespace Av::Transcoding
|
|||||||
|
|
||||||
args.emplace_back("pipe:1");
|
args.emplace_back("pipe:1");
|
||||||
|
|
||||||
LOG(DEBUG) << "Dumping args (" << args.size() << ")";
|
LOG(DEBUG, "Dumping args (" << args.size() << ")");
|
||||||
for (const std::string& arg : args)
|
for (const std::string& arg : args)
|
||||||
LOG(DEBUG) << "Arg = '" << arg << "'";
|
LOG(DEBUG, "Arg = '" << arg << "'");
|
||||||
|
|
||||||
// Caution: stdin must have been closed before
|
// Caution: stdin must have been closed before
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "TranscodingResourceHandler.hpp"
|
#include "TranscodingResourceHandler.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Av::Transcoding
|
namespace Av::Transcoding
|
||||||
{
|
{
|
||||||
@@ -43,9 +43,9 @@ namespace Av::Transcoding
|
|||||||
, _transcoder{ inputParameters, outputParameters }
|
, _transcoder{ inputParameters, outputParameters }
|
||||||
{
|
{
|
||||||
if (_estimatedContentLength)
|
if (_estimatedContentLength)
|
||||||
LMS_LOG(TRANSCODING, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
|
LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength);
|
||||||
else
|
else
|
||||||
LMS_LOG(TRANSCODING, DEBUG) << "Not using estimated content length";
|
LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length");
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||||
@@ -53,11 +53,11 @@ namespace Av::Transcoding
|
|||||||
if (_estimatedContentLength)
|
if (_estimatedContentLength)
|
||||||
response.setContentLength(*_estimatedContentLength);
|
response.setContentLength(*_estimatedContentLength);
|
||||||
response.setMimeType(_transcoder.getOutputMimeType());
|
response.setMimeType(_transcoder.getOutputMimeType());
|
||||||
LMS_LOG(TRANSCODING, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
|
LMS_LOG(TRANSCODING, DEBUG, "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType());
|
||||||
|
|
||||||
if (_bytesReadyCount > 0)
|
if (_bytesReadyCount > 0)
|
||||||
{
|
{
|
||||||
LMS_LOG(TRANSCODING, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
|
LMS_LOG(TRANSCODING, DEBUG, "Writing " << _bytesReadyCount << " bytes back to client");
|
||||||
|
|
||||||
response.out().write(reinterpret_cast<const char*>(&_buffer[0]), _bytesReadyCount);
|
response.out().write(reinterpret_cast<const char*>(&_buffer[0]), _bytesReadyCount);
|
||||||
_totalServedByteCount += _bytesReadyCount;
|
_totalServedByteCount += _bytesReadyCount;
|
||||||
@@ -70,7 +70,7 @@ namespace Av::Transcoding
|
|||||||
continuation->waitForMoreData();
|
continuation->waitForMoreData();
|
||||||
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
|
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
|
||||||
{
|
{
|
||||||
LMS_LOG(TRANSCODING, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
|
LMS_LOG(TRANSCODING, DEBUG, "Have " << nbBytesRead << " more bytes to send back");
|
||||||
|
|
||||||
assert(_bytesReadyCount == 0);
|
assert(_bytesReadyCount == 0);
|
||||||
_bytesReadyCount = nbBytesRead;
|
_bytesReadyCount = nbBytesRead;
|
||||||
@@ -86,7 +86,7 @@ namespace Av::Transcoding
|
|||||||
{
|
{
|
||||||
const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
|
const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
|
||||||
|
|
||||||
LMS_LOG(TRANSCODING, DEBUG) << "Adding " << padSize << " padding bytes";
|
LMS_LOG(TRANSCODING, DEBUG, "Adding " << padSize << " padding bytes");
|
||||||
|
|
||||||
for (std::size_t i{}; i < padSize; ++i)
|
for (std::size_t i{}; i < padSize; ++i)
|
||||||
response.out().put(0);
|
response.out().put(0);
|
||||||
@@ -94,7 +94,7 @@ namespace Av::Transcoding
|
|||||||
_totalServedByteCount += padSize;
|
_totalServedByteCount += padSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(TRANSCODING, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
|
LMS_LOG(TRANSCODING, DEBUG, "Transcoding finished. Total served byte count = " << _totalServedByteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include "RawImage.hpp"
|
#include "RawImage.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Image::GraphicsMagick
|
namespace Image::GraphicsMagick
|
||||||
{
|
{
|
||||||
@@ -36,7 +36,7 @@ namespace Image::GraphicsMagick
|
|||||||
}
|
}
|
||||||
catch (Magick::Exception& e)
|
catch (Magick::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
|
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||||
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
#include "JPEGImage.hpp"
|
#include "JPEGImage.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Image
|
namespace Image
|
||||||
{
|
{
|
||||||
@@ -43,16 +43,16 @@ namespace Image
|
|||||||
Magick::InitializeMagick(path.string().c_str());
|
Magick::InitializeMagick(path.string().c_str());
|
||||||
|
|
||||||
if (auto nbThreads {MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource)}; nbThreads != 1)
|
if (auto nbThreads {MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource)}; nbThreads != 1)
|
||||||
LMS_LOG(COVER, WARNING) << "Consider setting env var OMP_NUM_THREADS=1 to save resources";
|
LMS_LOG(COVER, WARNING, "Consider setting env var OMP_NUM_THREADS=1 to save resources");
|
||||||
|
|
||||||
if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1))
|
if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1))
|
||||||
LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 1!";
|
LMS_LOG(COVER, ERROR, "Cannot set Magick thread resource limit to 1!");
|
||||||
|
|
||||||
if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0))
|
if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0))
|
||||||
LMS_LOG(COVER, ERROR) << "Cannot set Magick disk resource limit to 0!";
|
LMS_LOG(COVER, ERROR, "Cannot set Magick disk resource limit to 0!");
|
||||||
|
|
||||||
LMS_LOG(COVER, INFO) << "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource);
|
LMS_LOG(COVER, INFO, "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource));
|
||||||
LMS_LOG(COVER, INFO) << "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource);
|
LMS_LOG(COVER, INFO, "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,16 +68,16 @@ RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
|||||||
}
|
}
|
||||||
catch (Magick::WarningCoder& e)
|
catch (Magick::WarningCoder& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder: " << e.what();
|
LMS_LOG(COVER, WARNING, "Caught Magick WarningCoder: " << e.what());
|
||||||
}
|
}
|
||||||
catch (Magick::Warning& e)
|
catch (Magick::Warning& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, WARNING) << "Caught Magick warning: " << e.what();
|
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
|
||||||
throw ImageException {std::string {"Magick read warning: "} + e.what()};
|
throw ImageException {std::string {"Magick read warning: "} + e.what()};
|
||||||
}
|
}
|
||||||
catch (Magick::Exception& e)
|
catch (Magick::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
|
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||||
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,16 +90,16 @@ RawImage::RawImage(const std::filesystem::path& p)
|
|||||||
}
|
}
|
||||||
catch (Magick::WarningCoder& e)
|
catch (Magick::WarningCoder& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder: " << e.what();
|
LMS_LOG(COVER, WARNING, "Caught Magick WarningCoder: " << e.what());
|
||||||
}
|
}
|
||||||
catch (Magick::Warning& e)
|
catch (Magick::Warning& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, WARNING) << "Caught Magick warning: " << e.what();
|
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
|
||||||
throw ImageException {std::string {"Magick read warning: "} + e.what()};
|
throw ImageException {std::string {"Magick read warning: "} + e.what()};
|
||||||
}
|
}
|
||||||
catch (Magick::Exception& e)
|
catch (Magick::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
|
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||||
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ RawImage::resize(ImageSize width)
|
|||||||
}
|
}
|
||||||
catch (Magick::Exception& e)
|
catch (Magick::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception while resizing: " << e.what();
|
LMS_LOG(COVER, ERROR, "Caught Magick exception while resizing: " << e.what());
|
||||||
throw ImageException {std::string {"Magick resize error: "} + e.what()};
|
throw ImageException {std::string {"Magick resize error: "} + e.what()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include "av/IAudioFile.hpp"
|
#include "av/IAudioFile.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
#include "metadata/IParser.hpp"
|
#include "metadata/IParser.hpp"
|
||||||
|
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "AvFormatParser.hpp"
|
#include "AvFormatParser.hpp"
|
||||||
#include "TagLibParser.hpp"
|
#include "TagLibParser.hpp"
|
||||||
@@ -34,10 +34,10 @@ namespace MetaData
|
|||||||
switch (parserType)
|
switch (parserType)
|
||||||
{
|
{
|
||||||
case ParserType::TagLib:
|
case ParserType::TagLib:
|
||||||
LMS_LOG(METADATA, INFO) << "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle);
|
LMS_LOG(METADATA, INFO, "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle));
|
||||||
return std::make_unique<TagLibParser>(parserReadStyle);
|
return std::make_unique<TagLibParser>(parserReadStyle);
|
||||||
case ParserType::AvFormat:
|
case ParserType::AvFormat:
|
||||||
LMS_LOG(METADATA, INFO) << "Creating AvFormat parser";
|
LMS_LOG(METADATA, INFO, "Creating AvFormat parser");
|
||||||
return std::make_unique<AvFormatParser>();
|
return std::make_unique<AvFormatParser>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
@@ -391,7 +391,7 @@ namespace MetaData
|
|||||||
|
|
||||||
if (f.isNull())
|
if (f.isNull())
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed";
|
LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ namespace MetaData
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
|
LMS_LOG(METADATA, INFO, "File '" << p.string() << "': no audio properties");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Auth
|
namespace Auth
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ namespace Auth
|
|||||||
{
|
{
|
||||||
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
|
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
|
||||||
|
|
||||||
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN);
|
LMS_LOG(AUTH, DEBUG, "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN));
|
||||||
|
|
||||||
user = session.create<User>(loginName);
|
user = session.create<User>(loginName);
|
||||||
user.modify()->setType(type);
|
user.modify()->setType(type);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Auth
|
namespace Auth
|
||||||
{
|
{
|
||||||
@@ -62,7 +62,7 @@ namespace Auth
|
|||||||
|
|
||||||
Database::AuthToken::pointer authToken {session.create<Database::AuthToken>(secretHash, expiry, user)};
|
Database::AuthToken::pointer authToken {session.create<Database::AuthToken>(secretHash, expiry, user)};
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
|
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString());
|
||||||
|
|
||||||
if (user->getAuthTokensCount() >= 50)
|
if (user->getAuthTokensCount() >= 50)
|
||||||
Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
|
Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
|
||||||
@@ -88,7 +88,7 @@ namespace Auth
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
|
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!");
|
||||||
|
|
||||||
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser()->getId(), authToken->getExpiry()};
|
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser()->getId(), authToken->getExpiry()};
|
||||||
authToken.remove();
|
authToken.remove();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include "LoginThrottler.hpp"
|
#include "LoginThrottler.hpp"
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Random.hpp"
|
#include "utils/Random.hpp"
|
||||||
|
|
||||||
namespace Auth {
|
namespace Auth {
|
||||||
@@ -81,10 +81,10 @@ LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
|
|||||||
|
|
||||||
attemptInfo.badConsecutiveAttemptCount += 1;
|
attemptInfo.badConsecutiveAttemptCount += 1;
|
||||||
|
|
||||||
LMS_LOG(AUTH, DEBUG) << "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount;
|
LMS_LOG(AUTH, DEBUG, "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount);
|
||||||
if (attemptInfo.badConsecutiveAttemptCount >= _maxBadConsecutiveAttemptCount)
|
if (attemptInfo.badConsecutiveAttemptCount >= _maxBadConsecutiveAttemptCount)
|
||||||
{
|
{
|
||||||
LMS_LOG(AUTH, DEBUG) << "Throttling '" << clientAddress.to_string() << "'";
|
LMS_LOG(AUTH, DEBUG, "Throttling '" << clientAddress.to_string() << "'");
|
||||||
attemptInfo.nextAttempt = now.addMSecs(std::chrono::duration_cast<std::chrono::milliseconds>(_throttlingDuration).count());
|
attemptInfo.nextAttempt = now.addMSecs(std::chrono::duration_cast<std::chrono::milliseconds>(_throttlingDuration).count());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Auth
|
namespace Auth
|
||||||
{
|
{
|
||||||
@@ -61,7 +61,7 @@ namespace Auth
|
|||||||
PasswordServiceBase::CheckResult
|
PasswordServiceBase::CheckResult
|
||||||
PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
|
PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
|
||||||
{
|
{
|
||||||
LMS_LOG(AUTH, DEBUG) << "Checking password for user '" << loginName << "'";
|
LMS_LOG(AUTH, DEBUG, "Checking password for user '" << loginName << "'");
|
||||||
|
|
||||||
// Do not waste too much resource on brute force attacks (optim)
|
// Do not waste too much resource on brute force attacks (optim)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,46 +22,41 @@
|
|||||||
#include <Wt/WEnvironment.h>
|
#include <Wt/WEnvironment.h>
|
||||||
|
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
namespace Auth
|
namespace Auth
|
||||||
{
|
{
|
||||||
|
HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db)
|
||||||
|
: AuthServiceBase{ db }
|
||||||
|
, _fieldName{ Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User") }
|
||||||
|
{
|
||||||
|
LMS_LOG(AUTH, INFO, "Using http header field = '" << _fieldName << "'");
|
||||||
|
}
|
||||||
|
|
||||||
HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db)
|
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
|
||||||
: AuthServiceBase {db}
|
{
|
||||||
, _fieldName {Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User")}
|
const std::string loginName{ env.headerValue(_fieldName) };
|
||||||
{
|
if (loginName.empty())
|
||||||
LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'";
|
return { CheckResult::State::Denied };
|
||||||
}
|
|
||||||
|
|
||||||
HttpHeadersEnvService::CheckResult
|
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
|
||||||
HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
|
|
||||||
{
|
|
||||||
const std::string loginName {env.headerValue(_fieldName)};
|
|
||||||
if (loginName.empty())
|
|
||||||
return {CheckResult::State::Denied};
|
|
||||||
|
|
||||||
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
|
const Database::UserId userId{ getOrCreateUser(loginName) };
|
||||||
|
onUserAuthenticated(userId);
|
||||||
|
return { CheckResult::State::Granted, userId };
|
||||||
|
}
|
||||||
|
|
||||||
const Database::UserId userId {getOrCreateUser(loginName)};
|
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
|
||||||
onUserAuthenticated(userId);
|
{
|
||||||
return {CheckResult::State::Granted, userId};
|
const std::string loginName{ request.headerValue(_fieldName) };
|
||||||
}
|
if (loginName.empty())
|
||||||
|
return { CheckResult::State::Denied };
|
||||||
|
|
||||||
HttpHeadersEnvService::CheckResult
|
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
|
||||||
HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
|
|
||||||
{
|
|
||||||
const std::string loginName {request.headerValue(_fieldName)};
|
|
||||||
if (loginName.empty())
|
|
||||||
return {CheckResult::State::Denied};
|
|
||||||
|
|
||||||
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
|
|
||||||
|
|
||||||
const Database::UserId userId {getOrCreateUser(loginName)};
|
|
||||||
onUserAuthenticated(userId);
|
|
||||||
return {CheckResult::State::Granted, userId};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const Database::UserId userId{ getOrCreateUser(loginName) };
|
||||||
|
onUserAuthenticated(userId);
|
||||||
|
return { CheckResult::State::Granted, userId };
|
||||||
|
}
|
||||||
} // namespace Auth
|
} // namespace Auth
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Auth
|
namespace Auth
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ namespace Auth
|
|||||||
bool
|
bool
|
||||||
InternalPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
InternalPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
||||||
{
|
{
|
||||||
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
|
LMS_LOG(AUTH, DEBUG, "Checking internal password for user '" << loginName << "'");
|
||||||
|
|
||||||
Database::User::PasswordHash passwordHash;
|
Database::User::PasswordHash passwordHash;
|
||||||
{
|
{
|
||||||
@@ -54,7 +54,7 @@ namespace Auth
|
|||||||
const Database::User::pointer user {Database::User::find(session, loginName)};
|
const Database::User::pointer user {Database::User::find(session, loginName)};
|
||||||
if (!user)
|
if (!user)
|
||||||
{
|
{
|
||||||
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
|
LMS_LOG(AUTH, DEBUG, "hashing random stuff");
|
||||||
// hash random stuff here to waste some time
|
// hash random stuff here to waste some time
|
||||||
hashRandomPassword();
|
hashRandomPassword();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -28,175 +28,173 @@
|
|||||||
|
|
||||||
#include "services/auth/Types.hpp"
|
#include "services/auth/Types.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Auth
|
namespace Auth
|
||||||
{
|
{
|
||||||
class PAMError
|
namespace
|
||||||
{
|
{
|
||||||
public:
|
class PAMError
|
||||||
PAMError(std::string_view msg, pam_handle_t *pamh, int err)
|
{
|
||||||
{
|
public:
|
||||||
_errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err);
|
PAMError(std::string_view msg, pam_handle_t* pamh, int err)
|
||||||
}
|
{
|
||||||
|
_errorMsg = std::string{ msg } + ": " + pam_strerror(pamh, err);
|
||||||
|
}
|
||||||
|
|
||||||
std::string_view message() const { return _errorMsg; }
|
std::string_view message() const { return _errorMsg; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string _errorMsg;
|
std::string _errorMsg;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PAMContext
|
class PAMContext
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PAMContext(std::string_view loginName)
|
PAMContext(std::string_view loginName)
|
||||||
{
|
{
|
||||||
int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)};
|
int err{ pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh) };
|
||||||
if (err != PAM_SUCCESS)
|
if (err != PAM_SUCCESS)
|
||||||
throw PAMError {"start failed", _pamh, err};
|
throw PAMError{ "start failed", _pamh, err };
|
||||||
}
|
}
|
||||||
|
|
||||||
~PAMContext()
|
~PAMContext()
|
||||||
{
|
{
|
||||||
int err {pam_end(_pamh, 0)};
|
int err{ pam_end(_pamh, 0) };
|
||||||
if (err != PAM_SUCCESS)
|
if (err != PAM_SUCCESS)
|
||||||
LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err);
|
LMS_LOG(AUTH, ERROR, "end failed: " << pam_strerror(_pamh, err));
|
||||||
}
|
}
|
||||||
|
|
||||||
void authenticate(std::string_view password)
|
void authenticate(std::string_view password)
|
||||||
{
|
{
|
||||||
AuthenticateConvContext authContext {password};
|
AuthenticateConvContext authContext{ password };
|
||||||
ScopedConvContextSetter scopedContext {*this, authContext};
|
ScopedConvContextSetter scopedContext{ *this, authContext };
|
||||||
|
|
||||||
int err {pam_authenticate(_pamh, 0)};
|
int err{ pam_authenticate(_pamh, 0) };
|
||||||
if (err != PAM_SUCCESS)
|
if (err != PAM_SUCCESS)
|
||||||
throw PAMError {"authenticate failed", _pamh, err};
|
throw PAMError{ "authenticate failed", _pamh, err };
|
||||||
}
|
}
|
||||||
|
|
||||||
void validateAccount()
|
void validateAccount()
|
||||||
{
|
{
|
||||||
int err {pam_acct_mgmt(_pamh, PAM_SILENT)};
|
int err{ pam_acct_mgmt(_pamh, PAM_SILENT) };
|
||||||
if (err != PAM_SUCCESS)
|
if (err != PAM_SUCCESS)
|
||||||
throw PAMError {"acct_mgmt failed", _pamh, err};
|
throw PAMError{ "acct_mgmt failed", _pamh, err };
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class ConvContext
|
class ConvContext
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~ConvContext() = default;
|
virtual ~ConvContext() = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AuthenticateConvContext final : public ConvContext
|
class AuthenticateConvContext final : public ConvContext
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AuthenticateConvContext(std::string_view password) : _password {password} {}
|
AuthenticateConvContext(std::string_view password) : _password{ password } {}
|
||||||
|
|
||||||
std::string_view getPassword() const { return _password; }
|
std::string_view getPassword() const { return _password; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string_view _password;
|
std::string_view _password;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ScopedConvContextSetter
|
class ScopedConvContextSetter
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
|
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
|
||||||
: _pamContext {pamContext}
|
: _pamContext{ pamContext }
|
||||||
{
|
{
|
||||||
_pamContext._convContext = &convContext;
|
_pamContext._convContext = &convContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
~ScopedConvContextSetter()
|
~ScopedConvContextSetter()
|
||||||
{
|
{
|
||||||
_pamContext._convContext = nullptr;
|
_pamContext._convContext = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
|
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
|
||||||
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
|
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
|
||||||
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
|
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
|
||||||
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
|
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PAMContext& _pamContext;
|
PAMContext& _pamContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
|
||||||
|
{
|
||||||
|
if (msgCount < 1)
|
||||||
|
return PAM_CONV_ERR;
|
||||||
|
if (!resps || !msgs || !userData)
|
||||||
|
return PAM_CONV_ERR;
|
||||||
|
|
||||||
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
|
PAMContext& context{ *static_cast<PAMContext*>(userData) };
|
||||||
{
|
|
||||||
if (msgCount < 1)
|
|
||||||
return PAM_CONV_ERR;
|
|
||||||
if (!resps || !msgs || !userData)
|
|
||||||
return PAM_CONV_ERR;
|
|
||||||
|
|
||||||
PAMContext& context {*static_cast<PAMContext*>(userData)};
|
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
|
||||||
|
if (!authenticateContext)
|
||||||
|
{
|
||||||
|
LMS_LOG(AUTH, ERROR, "Unexpected conv!");
|
||||||
|
return PAM_CONV_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
|
// Only expect a PAM_PROMPT_ECHO_OFF msg
|
||||||
if (!authenticateContext)
|
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
|
||||||
{
|
{
|
||||||
LMS_LOG(AUTH, ERROR) << "Unexpected conv!";
|
LMS_LOG(AUTH, ERROR, "Unexpected conv message. Count = " << msgCount);
|
||||||
return PAM_CONV_ERR;
|
return PAM_CONV_ERR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only expect a PAM_PROMPT_ECHO_OFF msg
|
pam_response* response{ static_cast<pam_response*>(malloc(sizeof(pam_response))) };
|
||||||
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
|
if (!response)
|
||||||
{
|
return PAM_CONV_ERR;
|
||||||
LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount;
|
|
||||||
return PAM_CONV_ERR;
|
|
||||||
}
|
|
||||||
|
|
||||||
pam_response* response {static_cast<pam_response*>(malloc(sizeof(pam_response)))};
|
response->resp = strdup(std::string{ authenticateContext->getPassword() }.c_str());
|
||||||
if (!response)
|
|
||||||
return PAM_CONV_ERR;
|
|
||||||
|
|
||||||
response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str());
|
*resps = response;
|
||||||
|
return PAM_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
*resps = response;
|
ConvContext* _convContext{};
|
||||||
return PAM_SUCCESS;
|
pam_conv _conv{ &PAMContext::conv, this };
|
||||||
}
|
pam_handle_t* _pamh{};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
ConvContext* _convContext {};
|
bool PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
||||||
pam_conv _conv {&PAMContext::conv, this};
|
{
|
||||||
pam_handle_t *_pamh {};
|
try
|
||||||
};
|
{
|
||||||
|
LMS_LOG(AUTH, DEBUG, "Checking PAM password for user '" << loginName << "'");
|
||||||
|
PAMContext pamContext{ loginName };
|
||||||
|
|
||||||
bool
|
pamContext.authenticate(password);
|
||||||
PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
pamContext.validateAccount();
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'";
|
|
||||||
PAMContext pamContext {loginName};
|
|
||||||
|
|
||||||
pamContext.authenticate(password);
|
return true;
|
||||||
pamContext.validateAccount();
|
}
|
||||||
|
catch (const PAMError& error)
|
||||||
|
{
|
||||||
|
LMS_LOG(AUTH, ERROR, "PAM error: " << error.message());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
bool PAMPasswordService::canSetPasswords() const
|
||||||
}
|
{
|
||||||
catch (const PAMError& error)
|
return false;
|
||||||
{
|
}
|
||||||
LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
IPasswordService::PasswordAcceptabilityResult PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
|
||||||
PAMPasswordService::canSetPasswords() const
|
{
|
||||||
{
|
throw NotImplementedException{};
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
IPasswordService::PasswordAcceptabilityResult
|
void PAMPasswordService::setPassword(Database::UserId, std::string_view)
|
||||||
PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
|
{
|
||||||
{
|
throw NotImplementedException{};
|
||||||
throw NotImplementedException {};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PAMPasswordService::setPassword(Database::UserId, std::string_view)
|
|
||||||
{
|
|
||||||
throw NotImplementedException {};
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Auth
|
} // namespace Auth
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "image/IRawImage.hpp"
|
#include "image/IRawImage.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Random.hpp"
|
#include "utils/Random.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "utils/Utils.hpp"
|
#include "utils/Utils.hpp"
|
||||||
@@ -110,10 +110,10 @@ namespace Cover
|
|||||||
{
|
{
|
||||||
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
||||||
|
|
||||||
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
|
LMS_LOG(COVER, INFO, "Default cover path = '" << _defaultCoverPath.string() << "'");
|
||||||
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
|
LMS_LOG(COVER, INFO, "Max cache size = " << _maxCacheSize);
|
||||||
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
|
LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize);
|
||||||
LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
|
LMS_LOG(COVER, INFO, "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ","));
|
||||||
|
|
||||||
#if LMS_SUPPORT_IMAGE_GM
|
#if LMS_SUPPORT_IMAGE_GM
|
||||||
GraphicsMagick::init(execPath);
|
GraphicsMagick::init(execPath);
|
||||||
@@ -148,7 +148,7 @@ namespace Cover
|
|||||||
}
|
}
|
||||||
catch (const Image::ImageException& e)
|
catch (const Image::ImageException& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
|
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ namespace Cover
|
|||||||
}
|
}
|
||||||
catch (const ImageException& e)
|
catch (const ImageException& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
|
LMS_LOG(COVER, ERROR, "Cannot read cover in file '" << p.string() << "': " << e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
@@ -190,7 +190,7 @@ namespace Cover
|
|||||||
|
|
||||||
std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
|
std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
|
||||||
_defaultCoverCache[width] = image;
|
_defaultCoverCache[width] = image;
|
||||||
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
|
LMS_LOG(COVER, DEBUG, "Default cache entries = " << _defaultCoverCache.size());
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
@@ -269,7 +269,7 @@ namespace Cover
|
|||||||
|
|
||||||
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
|
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
|
LMS_LOG(COVER, INFO, "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ namespace Cover
|
|||||||
}
|
}
|
||||||
catch (Av::Exception& e)
|
catch (Av::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
|
LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p.string() << ": " << e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
@@ -404,7 +404,7 @@ namespace Cover
|
|||||||
{
|
{
|
||||||
std::unique_lock lock{ _cacheMutex };
|
std::unique_lock lock{ _cacheMutex };
|
||||||
|
|
||||||
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
|
LMS_LOG(COVER, DEBUG, "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize);
|
||||||
_cacheHits = 0;
|
_cacheHits = 0;
|
||||||
_cacheMisses = 0;
|
_cacheMisses = 0;
|
||||||
_cacheSize = 0;
|
_cacheSize = 0;
|
||||||
@@ -415,7 +415,7 @@ namespace Cover
|
|||||||
{
|
{
|
||||||
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
|
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
|
||||||
|
|
||||||
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
|
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
|
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "EnumSetTraits.hpp"
|
#include "EnumSetTraits.hpp"
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ namespace Database
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
ClusterType::pointer ClusterType::create(Session& session, const std::string& name)
|
ClusterType::pointer ClusterType::create(Session& session, std::string_view name)
|
||||||
{
|
{
|
||||||
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
|
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -65,18 +65,18 @@ namespace Database
|
|||||||
|
|
||||||
void prepare()
|
void prepare()
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, DEBUG) << "Setting per-connection settings...";
|
LMS_LOG(DB, DEBUG, "Setting per-connection settings...");
|
||||||
executeSql("pragma journal_mode=WAL");
|
executeSql("pragma journal_mode=WAL");
|
||||||
executeSql("pragma synchronous=normal");
|
executeSql("pragma synchronous=normal");
|
||||||
executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries
|
executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries
|
||||||
LMS_LOG(DB, DEBUG) << "Setting per-connection settings done!";
|
LMS_LOG(DB, DEBUG, "Setting per-connection settings done!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void optimize()
|
void optimize()
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, DEBUG) << "connection close: Running pragma optimize...";
|
LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize...");
|
||||||
executeSql("pragma optimize");
|
executeSql("pragma optimize");
|
||||||
LMS_LOG(DB, DEBUG) << "connection close: pragma optimize complete";
|
LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path _dbPath;
|
std::filesystem::path _dbPath;
|
||||||
@@ -86,7 +86,7 @@ namespace Database
|
|||||||
// Session living class handling the database and the login
|
// Session living class handling the database and the login
|
||||||
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
|
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath.string());
|
||||||
|
|
||||||
auto connection{ std::make_unique<Connection>(dbPath.string()) };
|
auto connection{ std::make_unique<Connection>(dbPath.string()) };
|
||||||
if (IConfig * config{ Service<IConfig>::get() })// may not be here on testU
|
if (IConfig * config{ Service<IConfig>::get() })// may not be here on testU
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -282,11 +282,11 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
version = VersionInfo::getOrCreate(session)->getVersion();
|
version = VersionInfo::getOrCreate(session)->getVersion();
|
||||||
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
|
LMS_LOG(DB, INFO, "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION);
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
|
LMS_LOG(DB, ERROR, "Cannot get database version info: " << e.what());
|
||||||
throw LmsException{ outdatedMsg };
|
throw LmsException{ outdatedMsg };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +298,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
|
|
||||||
while (version < LMS_DATABASE_VERSION)
|
while (version < LMS_DATABASE_VERSION)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, INFO) << "Migrating database from version " << version << " to " << version + 1 << "...";
|
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
|
||||||
|
|
||||||
auto itMigrationFunc{ migrationFunctions.find(version) };
|
auto itMigrationFunc{ migrationFunctions.find(version) };
|
||||||
assert(itMigrationFunc != std::cend(migrationFunctions));
|
assert(itMigrationFunc != std::cend(migrationFunctions));
|
||||||
@@ -306,7 +306,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
|
|
||||||
VersionInfo::get(session).modify()->setVersion(++version);
|
VersionInfo::get(session).modify()->setVersion(++version);
|
||||||
|
|
||||||
LMS_LOG(DB, INFO) << "Migration complete to version " << version;
|
LMS_LOG(DB, INFO, "Migration complete to version " << version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "EnumSetTraits.hpp"
|
#include "EnumSetTraits.hpp"
|
||||||
#include "IdTypeTraits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
|
|||||||
@@ -22,131 +22,123 @@
|
|||||||
#include <Wt/Dbo/WtSqlTraits.h>
|
#include <Wt/Dbo/WtSqlTraits.h>
|
||||||
|
|
||||||
#include "utils/Path.hpp"
|
#include "utils/Path.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "services/database/Cluster.hpp"
|
#include "services/database/Cluster.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace Database
|
||||||
|
|
||||||
const std::set<std::string> defaultClusterTypeNames =
|
|
||||||
{
|
{
|
||||||
"GENRE",
|
namespace
|
||||||
"ALBUMGROUPING",
|
{
|
||||||
"MOOD",
|
|
||||||
"ALBUMMOOD",
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
const std::set<std::string_view> defaultClusterTypeNames =
|
||||||
|
{
|
||||||
|
"GENRE",
|
||||||
|
"ALBUMGROUPING",
|
||||||
|
"MOOD",
|
||||||
|
"ALBUMMOOD",
|
||||||
|
};
|
||||||
|
|
||||||
namespace Database {
|
}
|
||||||
|
void ScanSettings::init(Session& session)
|
||||||
|
{
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
|
||||||
void
|
pointer settings{ get(session) };
|
||||||
ScanSettings::init(Session& session)
|
if (settings)
|
||||||
{
|
return;
|
||||||
session.checkWriteTransaction();
|
|
||||||
|
|
||||||
pointer settings {get(session)};
|
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
|
||||||
if (settings)
|
settings.modify()->setClusterTypes(session, defaultClusterTypeNames);
|
||||||
return;
|
}
|
||||||
|
|
||||||
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
|
ScanSettings::pointer ScanSettings::get(Session& session)
|
||||||
settings.modify()->setClusterTypes(session, defaultClusterTypeNames );
|
{
|
||||||
}
|
session.checkReadTransaction();
|
||||||
|
|
||||||
ScanSettings::pointer
|
return session.getDboSession().find<ScanSettings>().resultValue();
|
||||||
ScanSettings::get(Session& session)
|
}
|
||||||
{
|
|
||||||
session.checkReadTransaction();
|
|
||||||
|
|
||||||
return session.getDboSession().find<ScanSettings>().resultValue();
|
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
|
||||||
}
|
{
|
||||||
|
const auto extensions{ StringUtils::splitString(_audioFileExtensions, " ") };
|
||||||
|
|
||||||
std::vector<std::filesystem::path>
|
std::vector<std::filesystem::path> res(std::cbegin(extensions), std::cend(extensions));
|
||||||
ScanSettings::getAudioFileExtensions() const
|
std::sort(std::begin(res), std::end(res));
|
||||||
{
|
res.erase(std::unique(std::begin(res), std::end(res)), std::end(res));
|
||||||
const auto extensions {StringUtils::splitString(_audioFileExtensions, " ")};
|
|
||||||
|
|
||||||
std::vector<std::filesystem::path> res (std::cbegin(extensions), std::cend(extensions));
|
return res;
|
||||||
std::sort(std::begin(res), std::end(res));
|
}
|
||||||
res.erase(std::unique( std::begin(res), std::end(res)), std::end(res));
|
|
||||||
|
|
||||||
return res;
|
void ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
|
||||||
}
|
{
|
||||||
|
_audioFileExtensions += " " + ext.string();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
std::vector<ClusterType::pointer> ScanSettings::getClusterTypes() const
|
||||||
ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
|
{
|
||||||
{
|
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
|
||||||
_audioFileExtensions += " " + ext.string();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<ClusterType::pointer>
|
void ScanSettings::setMediaDirectory(const std::filesystem::path& p)
|
||||||
ScanSettings::getClusterTypes() const
|
{
|
||||||
{
|
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
|
||||||
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
template <typename It>
|
||||||
ScanSettings::setMediaDirectory(const std::filesystem::path& p)
|
std::set<std::string> getNames(It begin, It end)
|
||||||
{
|
{
|
||||||
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
|
std::set<std::string> names;
|
||||||
}
|
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
|
||||||
|
[](const ClusterType::pointer& clusterType)
|
||||||
|
{
|
||||||
|
return clusterType->getName();
|
||||||
|
});
|
||||||
|
|
||||||
template <typename It>
|
return names;
|
||||||
std::set<std::string> getNames(It begin, It end)
|
}
|
||||||
{
|
|
||||||
std::set<std::string> names;
|
|
||||||
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
|
|
||||||
[](const ClusterType::pointer& clusterType)
|
|
||||||
{
|
|
||||||
return clusterType->getName();
|
|
||||||
});
|
|
||||||
|
|
||||||
return names;
|
void ScanSettings::setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames)
|
||||||
}
|
{
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
|
||||||
void
|
bool needRescan{};
|
||||||
ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames)
|
|
||||||
{
|
|
||||||
session.checkWriteTransaction();
|
|
||||||
|
|
||||||
bool needRescan {};
|
// Create any missing cluster type
|
||||||
|
for (const std::string_view clusterTypeName : clusterTypeNames)
|
||||||
|
{
|
||||||
|
ClusterType::pointer clusterType{ ClusterType::find(session, clusterTypeName) };
|
||||||
|
if (!clusterType)
|
||||||
|
{
|
||||||
|
LMS_LOG(DB, INFO, "Creating cluster type " << clusterTypeName);
|
||||||
|
clusterType = session.create<ClusterType>(clusterTypeName);
|
||||||
|
_clusterTypes.insert(getDboPtr(clusterType));
|
||||||
|
|
||||||
// Create any missing cluster type
|
needRescan = true;
|
||||||
for (const std::string& clusterTypeName : clusterTypeNames)
|
}
|
||||||
{
|
}
|
||||||
ClusterType::pointer clusterType {ClusterType::find(session, clusterTypeName)};
|
|
||||||
if (!clusterType)
|
|
||||||
{
|
|
||||||
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
|
|
||||||
clusterType = session.create<ClusterType>(clusterTypeName);
|
|
||||||
_clusterTypes.insert(getDboPtr(clusterType));
|
|
||||||
|
|
||||||
needRescan = true;
|
// Delete no longer existing cluster types
|
||||||
}
|
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
|
||||||
}
|
{
|
||||||
|
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
|
||||||
|
[clusterType](std::string_view name) { return name == clusterType->getName(); }))
|
||||||
|
{
|
||||||
|
LMS_LOG(DB, INFO, "Deleting cluster type " << clusterType->getName());
|
||||||
|
clusterType.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delete no longer existing cluster types
|
if (needRescan)
|
||||||
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
|
_scanVersion += 1;
|
||||||
{
|
}
|
||||||
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
|
|
||||||
[clusterType](const std::string& name) { return name == clusterType->getName(); }))
|
|
||||||
{
|
|
||||||
LMS_LOG(DB, INFO) << "Deleting cluster type " << clusterType->getName();
|
|
||||||
clusterType.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needRescan)
|
void
|
||||||
_scanVersion += 1;
|
ScanSettings::incScanVersion()
|
||||||
}
|
{
|
||||||
|
_scanVersion += 1;
|
||||||
void
|
}
|
||||||
ScanSettings::incScanVersion()
|
|
||||||
{
|
|
||||||
_scanVersion += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/AuthToken.hpp"
|
#include "services/database/AuthToken.hpp"
|
||||||
@@ -107,20 +107,20 @@ namespace Database
|
|||||||
|
|
||||||
void Session::prepareTables()
|
void Session::prepareTables()
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, INFO) << "Preparing tables...";
|
LMS_LOG(DB, INFO, "Preparing tables...");
|
||||||
|
|
||||||
// Initial creation case
|
// Initial creation case
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_session.createTables();
|
_session.createTables();
|
||||||
LMS_LOG(DB, INFO) << "Tables created";
|
LMS_LOG(DB, INFO, "Tables created");
|
||||||
}
|
}
|
||||||
catch (Wt::Dbo::Exception& e)
|
catch (Wt::Dbo::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, DEBUG) << "Cannot create tables: " << e.what();
|
LMS_LOG(DB, DEBUG, "Cannot create tables: " << e.what());
|
||||||
if (std::string_view{ e.what() }.find("already exists") == std::string_view::npos)
|
if (std::string_view{ e.what() }.find("already exists") == std::string_view::npos)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
|
LMS_LOG(DB, ERROR, "Cannot create tables: " << e.what());
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,22 +182,22 @@ namespace Database
|
|||||||
|
|
||||||
void Session::analyze()
|
void Session::analyze()
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, INFO) << "Analyzing database...";
|
LMS_LOG(DB, INFO, "Analyzing database...");
|
||||||
{
|
{
|
||||||
auto transaction{ createWriteTransaction() };
|
auto transaction{ createWriteTransaction() };
|
||||||
_session.execute("ANALYZE");
|
_session.execute("ANALYZE");
|
||||||
}
|
}
|
||||||
LMS_LOG(DB, INFO) << "Database Analyze complete";
|
LMS_LOG(DB, INFO, "Database Analyze complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::optimize()
|
void Session::optimize()
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, INFO) << "Optimizing database...";
|
LMS_LOG(DB, INFO, "Optimizing database...");
|
||||||
{
|
{
|
||||||
auto transaction{ createWriteTransaction() };
|
auto transaction{ createWriteTransaction() };
|
||||||
_session.execute("PRAGMA optimize");
|
_session.execute("PRAGMA optimize");
|
||||||
}
|
}
|
||||||
LMS_LOG(DB, INFO) << "Database optimizing complete";
|
LMS_LOG(DB, INFO, "Database optimizing complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
#include "services/database/TrackFeatures.hpp"
|
#include "services/database/TrackFeatures.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "IdTypeTraits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
@@ -536,7 +536,7 @@ namespace Database
|
|||||||
for (auto artist : track->getArtists({ TrackArtistLinkType::Artist }))
|
for (auto artist : track->getArtists({ TrackArtistLinkType::Artist }))
|
||||||
os << " - " << artist->getName();
|
os << " - " << artist->getName();
|
||||||
for (auto cluster : track->getClusters())
|
for (auto cluster : track->getClusters())
|
||||||
os << " {" + cluster->getType()->getName() << "-" << cluster->getName() << "}";
|
os << " {" << cluster->getType()->getName() << "-" << cluster->getName() << "}";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "IdTypeTraits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ namespace Database {
|
|||||||
}
|
}
|
||||||
catch (boost::property_tree::ptree_error& error)
|
catch (boost::property_tree::ptree_error& error)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
|
LMS_LOG(DB, ERROR, "Track " << _track.id() << ": ptree exception: " << error.what());
|
||||||
res.clear();
|
res.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Cluster.hpp"
|
#include "services/database/Cluster.hpp"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "IdTypeTraits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
#include "StringViewTraits.hpp"
|
#include "StringViewTraits.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ namespace Database {
|
|||||||
static void remove(Session& session, const std::string& name);
|
static void remove(Session& session, const std::string& name);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
const std::string& getName() const { return _name; }
|
std::string_view getName() const { return _name; }
|
||||||
std::vector<Cluster::pointer> getClusters() const;
|
std::vector<Cluster::pointer> getClusters() const;
|
||||||
Cluster::pointer getCluster(const std::string& name) const;
|
Cluster::pointer getCluster(const std::string& name) const;
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ namespace Database {
|
|||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
ClusterType(std::string_view name);
|
ClusterType(std::string_view name);
|
||||||
static pointer create(Session& session, const std::string& name);
|
static pointer create(Session& session, std::string_view name);
|
||||||
|
|
||||||
static const std::size_t _maxNameLength = 128;
|
static const std::size_t _maxNameLength = 128;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
@@ -74,7 +76,7 @@ namespace Database {
|
|||||||
void setMediaDirectory(const std::filesystem::path& p);
|
void setMediaDirectory(const std::filesystem::path& p);
|
||||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||||
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
|
void setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames);
|
||||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||||
void incScanVersion();
|
void incScanVersion();
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
#include "services/database/StarredTrack.hpp"
|
#include "services/database/StarredTrack.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "internal/InternalBackend.hpp"
|
#include "internal/InternalBackend.hpp"
|
||||||
#include "listenbrainz/ListenBrainzBackend.hpp"
|
#include "listenbrainz/ListenBrainzBackend.hpp"
|
||||||
@@ -44,15 +44,15 @@ namespace Feedback
|
|||||||
FeedbackService::FeedbackService(boost::asio::io_context& ioContext, Db& db)
|
FeedbackService::FeedbackService(boost::asio::io_context& ioContext, Db& db)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
{
|
{
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
LMS_LOG(SCROBBLING, INFO, "Starting service...");
|
||||||
_backends.emplace(Database::FeedbackBackend::Internal, std::make_unique<InternalBackend>(_db));
|
_backends.emplace(Database::FeedbackBackend::Internal, std::make_unique<InternalBackend>(_db));
|
||||||
_backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
|
_backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
LMS_LOG(SCROBBLING, INFO, "Service started!");
|
||||||
}
|
}
|
||||||
|
|
||||||
FeedbackService::~FeedbackService()
|
FeedbackService::~FeedbackService()
|
||||||
{
|
{
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
LMS_LOG(SCROBBLING, INFO, "Service stopped!");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Database::FeedbackBackend> FeedbackService::getUserFeedbackBackend(UserId userId)
|
std::optional<Database::FeedbackBackend> FeedbackService::getUserFeedbackBackend(UserId userId)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ namespace Feedback::ListenBrainz
|
|||||||
|
|
||||||
const Wt::Json::Array& feedbacks = root.get("feedback");
|
const Wt::Json::Array& feedbacks = root.get("feedback");
|
||||||
|
|
||||||
LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks";
|
LOG(DEBUG, "Got " << feedbacks.size() << " feedbacks");
|
||||||
|
|
||||||
if (feedbacks.empty())
|
if (feedbacks.empty())
|
||||||
return res;
|
return res;
|
||||||
@@ -72,17 +72,17 @@ namespace Feedback::ListenBrainz
|
|||||||
}
|
}
|
||||||
catch (const Exception& e)
|
catch (const Exception& e)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
|
LOG(DEBUG, "Cannot parse feedback: " << e.what() << ", skipping");
|
||||||
}
|
}
|
||||||
catch (const Wt::WException& e)
|
catch (const Wt::WException& e)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
|
LOG(DEBUG, "Cannot parse feedback: " << e.what() << ", skipping");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (const Wt::WException& error)
|
catch (const Wt::WException& error)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Cannot parse 'feedback' result: " << error.what();
|
LOG(ERROR, "Cannot parse 'feedback' result: " << error.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ namespace Feedback::ListenBrainz
|
|||||||
}
|
}
|
||||||
catch (const Wt::WException& e)
|
catch (const Wt::WException& e)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
|
LOG(ERROR, "Cannot parse listen count response: " << e.what());
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ namespace Feedback::ListenBrainz
|
|||||||
, _maxSyncFeedbackCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000) }
|
, _maxSyncFeedbackCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000) }
|
||||||
, _syncFeedbacksPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1) }
|
, _syncFeedbacksPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1) }
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours";
|
LOG(INFO, "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours");
|
||||||
|
|
||||||
scheduleSync(std::chrono::seconds{ 30 });
|
scheduleSync(std::chrono::seconds{ 30 });
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ namespace Feedback::ListenBrainz
|
|||||||
case FeedbackType::Erase:
|
case FeedbackType::Erase:
|
||||||
if (!recordingMBID)
|
if (!recordingMBID)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Track has no recording MBID: erasing star";
|
LOG(DEBUG, "Track has no recording MBID: erasing star");
|
||||||
starredTrack.remove();
|
starredTrack.remove();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -112,7 +112,7 @@ namespace Feedback::ListenBrainz
|
|||||||
|
|
||||||
if (!recordingMBID)
|
if (!recordingMBID)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Track has no recording MBID: skipping";
|
LOG(DEBUG, "Track has no recording MBID: skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ namespace Feedback::ListenBrainz
|
|||||||
}
|
}
|
||||||
catch (Exception& e)
|
catch (Exception& e)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Cannot send feedback: " << e.what();
|
LOG(DEBUG, "Cannot send feedback: " << e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ namespace Feedback::ListenBrainz
|
|||||||
Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) };
|
Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) };
|
||||||
if (!starredTrack)
|
if (!starredTrack)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Starred track not found. deleted?";
|
LOG(DEBUG, "Starred track not found. deleted?");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,23 +166,23 @@ namespace Feedback::ListenBrainz
|
|||||||
{
|
{
|
||||||
case FeedbackType::Love:
|
case FeedbackType::Love:
|
||||||
starredTrack.modify()->setSyncState(Database::SyncState::Synchronized);
|
starredTrack.modify()->setSyncState(Database::SyncState::Synchronized);
|
||||||
LOG(DEBUG) << "State set to synchronized";
|
LOG(DEBUG, "State set to synchronized");
|
||||||
|
|
||||||
if (userContext.feedbackCount)
|
if (userContext.feedbackCount)
|
||||||
{
|
{
|
||||||
(*userContext.feedbackCount)++;
|
(*userContext.feedbackCount)++;
|
||||||
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
|
LOG(DEBUG, "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FeedbackType::Erase:
|
case FeedbackType::Erase:
|
||||||
starredTrack.remove();
|
starredTrack.remove();
|
||||||
LOG(DEBUG) << "Removed starred track";
|
LOG(DEBUG, "Removed starred track");
|
||||||
|
|
||||||
if (userContext.feedbackCount && *userContext.feedbackCount > 0)
|
if (userContext.feedbackCount && *userContext.feedbackCount > 0)
|
||||||
{
|
{
|
||||||
(*userContext.feedbackCount)--;
|
(*userContext.feedbackCount)--;
|
||||||
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
|
LOG(DEBUG, "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ namespace Feedback::ListenBrainz
|
|||||||
pendingFeedbacks = StarredTrack::find(session, params);
|
pendingFeedbacks = StarredTrack::find(session, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks";
|
LOG(DEBUG, "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks");
|
||||||
|
|
||||||
for (const StarredTrackId starredTrackId : pendingFeedbacks.results)
|
for (const StarredTrackId starredTrackId : pendingFeedbacks.results)
|
||||||
enqueFeedback(feedbackType, starredTrackId);
|
enqueFeedback(feedbackType, starredTrackId);
|
||||||
@@ -247,13 +247,13 @@ namespace Feedback::ListenBrainz
|
|||||||
if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0)
|
if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
|
LOG(DEBUG, "Scheduled sync in " << fromNow.count() << " seconds...");
|
||||||
_syncTimer.expires_after(fromNow);
|
_syncTimer.expires_after(fromNow);
|
||||||
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
|
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
|
||||||
{
|
{
|
||||||
if (ec == boost::asio::error::operation_aborted)
|
if (ec == boost::asio::error::operation_aborted)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "getFeedbacks aborted";
|
LOG(DEBUG, "getFeedbacks aborted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (ec)
|
else if (ec)
|
||||||
@@ -267,7 +267,7 @@ namespace Feedback::ListenBrainz
|
|||||||
|
|
||||||
void FeedbacksSynchronizer::startSync()
|
void FeedbacksSynchronizer::startSync()
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Starting sync!";
|
LOG(DEBUG, "Starting sync!");
|
||||||
|
|
||||||
assert(!isSyncing());
|
assert(!isSyncing());
|
||||||
assert(_strand.running_in_this_thread());
|
assert(_strand.running_in_this_thread());
|
||||||
@@ -303,7 +303,7 @@ namespace Feedback::ListenBrainz
|
|||||||
{
|
{
|
||||||
_strand.dispatch([this, &context]
|
_strand.dispatch([this, &context]
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount;
|
LOG(INFO, "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount);
|
||||||
context.syncing = false;
|
context.syncing = false;
|
||||||
|
|
||||||
if (!isSyncing())
|
if (!isSyncing())
|
||||||
@@ -356,11 +356,11 @@ namespace Feedback::ListenBrainz
|
|||||||
std::string msgBodyCopy{ msgBody };
|
std::string msgBodyCopy{ msgBody };
|
||||||
_strand.dispatch([this, msgBodyCopy, &context]
|
_strand.dispatch([this, msgBodyCopy, &context]
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'";
|
LOG(DEBUG, "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'");
|
||||||
|
|
||||||
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
|
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
|
||||||
if (totalFeedbackCount)
|
if (totalFeedbackCount)
|
||||||
LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount;
|
LOG(DEBUG, "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount);
|
||||||
|
|
||||||
bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) };
|
bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) };
|
||||||
context.feedbackCount = totalFeedbackCount;
|
context.feedbackCount = totalFeedbackCount;
|
||||||
@@ -416,7 +416,7 @@ namespace Feedback::ListenBrainz
|
|||||||
{
|
{
|
||||||
const FeedbacksParser::Result parseResult{ FeedbacksParser::parse(msgBody) };
|
const FeedbacksParser::Result parseResult{ FeedbacksParser::parse(msgBody) };
|
||||||
|
|
||||||
LOG(DEBUG) << "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries";
|
LOG(DEBUG, "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries");
|
||||||
context.fetchedFeedbackCount += parseResult.feedbackCount;
|
context.fetchedFeedbackCount += parseResult.feedbackCount;
|
||||||
|
|
||||||
for (const Feedback& feedback : parseResult.feedbacks)
|
for (const Feedback& feedback : parseResult.feedbacks)
|
||||||
@@ -441,12 +441,12 @@ namespace Feedback::ListenBrainz
|
|||||||
const std::vector<Track::pointer> tracks{ Track::findByRecordingMBID(session, feedback.recordingMBID) };
|
const std::vector<Track::pointer> tracks{ Track::findByRecordingMBID(session, feedback.recordingMBID) };
|
||||||
if (tracks.size() > 1)
|
if (tracks.size() > 1)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found";
|
LOG(DEBUG, "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (tracks.empty())
|
else if (tracks.empty())
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Cannot match feedback '" << feedback << "': no track found for this recording MBID";
|
LOG(DEBUG, "Cannot match feedback '" << feedback << "': no track found for this recording MBID");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +461,7 @@ namespace Feedback::ListenBrainz
|
|||||||
|
|
||||||
if (needImport)
|
if (needImport)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Importing feedback '" << feedback << "'";
|
LOG(DEBUG, "Importing feedback '" << feedback << "'");
|
||||||
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
@@ -481,7 +481,7 @@ namespace Feedback::ListenBrainz
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "No need to import feedback '" << feedback << "', already imported";
|
LOG(DEBUG, "No need to import feedback '" << feedback << "', already imported");
|
||||||
context.matchedFeedbackCount++;
|
context.matchedFeedbackCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/http/IClient.hpp"
|
#include "utils/http/IClient.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
@@ -63,12 +63,12 @@ namespace Feedback::ListenBrainz
|
|||||||
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
|
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
|
||||||
, _feedbacksSynchronizer{ _ioContext, db, *_client }
|
, _feedbacksSynchronizer{ _ioContext, db, *_client }
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'";
|
LOG(INFO, "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
ListenBrainzBackend::~ListenBrainzBackend()
|
ListenBrainzBackend::~ListenBrainzBackend()
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Stopped ListenBrainz feedback backend!";
|
LOG(INFO, "Stopped ListenBrainz feedback backend!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListenBrainzBackend::onStarred(Database::StarredArtistId starredArtistId)
|
void ListenBrainzBackend::onStarred(Database::StarredArtistId starredArtistId)
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ namespace Feedback::ListenBrainz::Utils
|
|||||||
Wt::Json::Object root;
|
Wt::Json::Object root;
|
||||||
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
|
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
|
LOG(ERROR, "Cannot parse 'validate-token' result: " << error.what());
|
||||||
return listenBrainzUserName;
|
return listenBrainzUserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.get("valid").orIfNull(false))
|
if (!root.get("valid").orIfNull(false))
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Invalid listenbrainz user";
|
LOG(INFO, "Invalid listenbrainz user");
|
||||||
return listenBrainzUserName;
|
return listenBrainzUserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "services/database/UserId.hpp"
|
#include "services/database/UserId.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/UUID.hpp"
|
#include "utils/UUID.hpp"
|
||||||
|
|
||||||
#define LOG(sev) LMS_LOG(FEEDBACK, sev) << "[listenbrainz] "
|
#define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message)
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "playlist-constraints/ConsecutiveArtists.hpp"
|
#include "playlist-constraints/ConsecutiveArtists.hpp"
|
||||||
#include "playlist-constraints/ConsecutiveReleases.hpp"
|
#include "playlist-constraints/ConsecutiveReleases.hpp"
|
||||||
#include "playlist-constraints/DuplicateTracks.hpp"
|
#include "playlist-constraints/DuplicateTracks.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Recommendation
|
namespace Recommendation
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@ namespace Recommendation
|
|||||||
|
|
||||||
std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
|
std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
|
LMS_LOG(RECOMMENDATION, DEBUG, "Requested to extend playlist by " << maxCount << " similar tracks");
|
||||||
|
|
||||||
// supposed to be ordered from most similar to least similar
|
// supposed to be ordered from most similar to least similar
|
||||||
std::vector<TrackId> similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints
|
std::vector<TrackId> similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/ScanSettings.hpp"
|
#include "services/database/ScanSettings.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Recommendation
|
namespace Recommendation
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,397 +30,384 @@
|
|||||||
#include "services/database/TrackFeatures.hpp"
|
#include "services/database/TrackFeatures.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
#include "som/DataNormalizer.hpp"
|
#include "som/DataNormalizer.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Random.hpp"
|
#include "utils/Random.hpp"
|
||||||
|
|
||||||
|
namespace Recommendation
|
||||||
namespace Recommendation {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
|
|
||||||
{
|
{
|
||||||
return std::make_unique<FeaturesEngine>(db);
|
using namespace Database;
|
||||||
}
|
|
||||||
|
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
|
||||||
const FeatureSettingsMap&
|
{
|
||||||
FeaturesEngine::getDefaultTrainFeatureSettings()
|
return std::make_unique<FeaturesEngine>(db);
|
||||||
{
|
}
|
||||||
static const FeatureSettingsMap defaultTrainFeatureSettings
|
|
||||||
{
|
namespace
|
||||||
{ "lowlevel.spectral_energyband_high.mean", {1}},
|
{
|
||||||
{ "lowlevel.spectral_rolloff.median", {1}},
|
std::optional<SOM::InputVector> convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
|
||||||
{ "lowlevel.spectral_contrast_valleys.var", {1}},
|
{
|
||||||
{ "lowlevel.erbbands.mean", {1}},
|
std::size_t i{};
|
||||||
{ "lowlevel.gfcc.mean", {1}},
|
std::optional<SOM::InputVector> res{ SOM::InputVector {nbDimensions} };
|
||||||
};
|
for (const auto& [featureName, values] : featureValuesMap)
|
||||||
|
{
|
||||||
return defaultTrainFeatureSettings;
|
if (values.size() != getFeatureDef(featureName).nbDimensions)
|
||||||
}
|
{
|
||||||
|
LMS_LOG(RECOMMENDATION, WARNING, "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size());
|
||||||
static
|
res.reset();
|
||||||
std::optional<SOM::InputVector>
|
break;
|
||||||
convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
|
}
|
||||||
{
|
|
||||||
std::size_t i {};
|
for (double val : values)
|
||||||
std::optional<SOM::InputVector> res {SOM::InputVector {nbDimensions}};
|
(*res)[i++] = val;
|
||||||
for (const auto& [featureName, values] : featureValuesMap)
|
}
|
||||||
{
|
|
||||||
if (values.size() != getFeatureDef(featureName).nbDimensions)
|
return res;
|
||||||
{
|
}
|
||||||
LMS_LOG(RECOMMENDATION, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size();
|
|
||||||
res.reset();
|
SOM::InputVector getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
|
||||||
break;
|
{
|
||||||
}
|
SOM::InputVector weights{ nbDimensions };
|
||||||
|
std::size_t index{};
|
||||||
for (double val : values)
|
for (const auto& [featureName, featureSettings] : featureSettingsMap)
|
||||||
(*res)[i++] = val;
|
{
|
||||||
}
|
const std::size_t featureNbDimensions{ getFeatureDef(featureName).nbDimensions };
|
||||||
|
|
||||||
return res;
|
for (std::size_t i{}; i < featureNbDimensions; ++i)
|
||||||
}
|
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
|
||||||
|
}
|
||||||
static
|
|
||||||
SOM::InputVector
|
assert(index == nbDimensions);
|
||||||
getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
|
|
||||||
{
|
return weights;
|
||||||
SOM::InputVector weights {nbDimensions};
|
}
|
||||||
std::size_t index {};
|
}
|
||||||
for (const auto& [featureName, featureSettings] : featureSettingsMap)
|
|
||||||
{
|
const FeatureSettingsMap& FeaturesEngine::getDefaultTrainFeatureSettings()
|
||||||
const std::size_t featureNbDimensions {getFeatureDef(featureName).nbDimensions};
|
{
|
||||||
|
static const FeatureSettingsMap defaultTrainFeatureSettings
|
||||||
for (std::size_t i {}; i < featureNbDimensions; ++i)
|
{
|
||||||
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
|
{ "lowlevel.spectral_energyband_high.mean", {1}},
|
||||||
}
|
{ "lowlevel.spectral_rolloff.median", {1}},
|
||||||
|
{ "lowlevel.spectral_contrast_valleys.var", {1}},
|
||||||
assert(index == nbDimensions);
|
{ "lowlevel.erbbands.mean", {1}},
|
||||||
|
{ "lowlevel.gfcc.mean", {1}},
|
||||||
return weights;
|
};
|
||||||
}
|
|
||||||
|
return defaultTrainFeatureSettings;
|
||||||
void
|
}
|
||||||
FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
|
|
||||||
{
|
void FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
|
{
|
||||||
|
LMS_LOG(RECOMMENDATION, INFO, "Constructing features classifier...");
|
||||||
std::unordered_set<FeatureName> featureNames;
|
|
||||||
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
|
std::unordered_set<FeatureName> featureNames;
|
||||||
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
|
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
|
||||||
|
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
|
||||||
const std::size_t nbDimensions {std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
|
|
||||||
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; })};
|
const std::size_t nbDimensions{ std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
|
||||||
|
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; }) };
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
|
|
||||||
|
LMS_LOG(RECOMMENDATION, DEBUG, "Features dimension = " << nbDimensions);
|
||||||
Session& session {_db.getTLSSession()};
|
|
||||||
|
Session & session{ _db.getTLSSession() };
|
||||||
RangeResults<TrackFeaturesId> trackFeaturesIds;
|
|
||||||
{
|
RangeResults<TrackFeaturesId> trackFeaturesIds;
|
||||||
auto transaction {session.createReadTransaction()};
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features...";
|
|
||||||
trackFeaturesIds = TrackFeatures::find(session);
|
LMS_LOG(RECOMMENDATION, DEBUG, "Getting Track features...");
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)";
|
trackFeaturesIds = TrackFeatures::find(session);
|
||||||
}
|
LMS_LOG(RECOMMENDATION, DEBUG, "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)");
|
||||||
|
}
|
||||||
std::vector<SOM::InputVector> samples;
|
|
||||||
std::vector<TrackId> samplesTrackIds;
|
std::vector<SOM::InputVector> samples;
|
||||||
|
std::vector<TrackId> samplesTrackIds;
|
||||||
samples.reserve(trackFeaturesIds.results.size());
|
|
||||||
samplesTrackIds.reserve(trackFeaturesIds.results.size());
|
samples.reserve(trackFeaturesIds.results.size());
|
||||||
|
samplesTrackIds.reserve(trackFeaturesIds.results.size());
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
|
|
||||||
// TODO handle errors using exceptions
|
LMS_LOG(RECOMMENDATION, DEBUG, "Extracting features...");
|
||||||
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
|
// TODO handle errors using exceptions
|
||||||
{
|
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
|
||||||
if (_loadCancelled)
|
{
|
||||||
return;
|
if (_loadCancelled)
|
||||||
|
return;
|
||||||
auto transaction {session.createReadTransaction()};
|
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
TrackFeatures::pointer trackFeatures {TrackFeatures::find(session, trackFeaturesId)};
|
|
||||||
if (!trackFeatures)
|
TrackFeatures::pointer trackFeatures{ TrackFeatures::find(session, trackFeaturesId) };
|
||||||
continue;
|
if (!trackFeatures)
|
||||||
|
continue;
|
||||||
FeatureValuesMap featureValuesMap {trackFeatures->getFeatureValuesMap(featureNames)};
|
|
||||||
if (featureValuesMap.empty())
|
FeatureValuesMap featureValuesMap{ trackFeatures->getFeatureValuesMap(featureNames) };
|
||||||
continue;
|
if (featureValuesMap.empty())
|
||||||
|
continue;
|
||||||
std::optional<SOM::InputVector> inputVector {convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions)};
|
|
||||||
if (!inputVector)
|
std::optional<SOM::InputVector> inputVector{ convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions) };
|
||||||
continue;
|
if (!inputVector)
|
||||||
|
continue;
|
||||||
samples.emplace_back(std::move(*inputVector));
|
|
||||||
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
|
samples.emplace_back(std::move(*inputVector));
|
||||||
}
|
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE";
|
}
|
||||||
|
LMS_LOG(RECOMMENDATION, DEBUG, "Extracting features DONE");
|
||||||
if (samples.empty())
|
|
||||||
{
|
if (samples.empty())
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
|
{
|
||||||
return;
|
LMS_LOG(RECOMMENDATION, INFO, "Nothing to classify!");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
|
|
||||||
SOM::DataNormalizer dataNormalizer {nbDimensions};
|
|
||||||
|
|
||||||
dataNormalizer.computeNormalizationFactors(samples);
|
|
||||||
for (auto& sample : samples)
|
|
||||||
dataNormalizer.normalizeData(sample);
|
|
||||||
|
|
||||||
SOM::Coordinate size {static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))};
|
LMS_LOG(RECOMMENDATION, DEBUG, "Normalizing data...");
|
||||||
if (size < 2)
|
SOM::DataNormalizer dataNormalizer{ nbDimensions };
|
||||||
{
|
|
||||||
LMS_LOG(RECOMMENDATION, WARNING) << "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors";
|
|
||||||
size = 2;
|
|
||||||
}
|
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network";
|
|
||||||
|
|
||||||
SOM::Network network {size, size, nbDimensions};
|
dataNormalizer.computeNormalizationFactors(samples);
|
||||||
|
for (auto& sample : samples)
|
||||||
SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)};
|
dataNormalizer.normalizeData(sample);
|
||||||
network.setDataWeights(weights);
|
|
||||||
|
|
||||||
auto somProgressCallback{[&](const SOM::Network::CurrentIteration& iter)
|
|
||||||
{
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount;
|
|
||||||
progressCallback(Progress {iter.idIteration, iter.iterationCount});
|
|
||||||
}};
|
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network...";
|
SOM::Coordinate size{ static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron)) };
|
||||||
network.train(samples, trainSettings.iterationCount,
|
if (size < 2)
|
||||||
progressCallback ? somProgressCallback : SOM::Network::ProgressCallback {},
|
{
|
||||||
[this] { return _loadCancelled; });
|
LMS_LOG(RECOMMENDATION, WARNING, "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors");
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
|
size = 2;
|
||||||
|
}
|
||||||
|
LMS_LOG(RECOMMENDATION, INFO, "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network");
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
|
SOM::Network network{ size, size, nbDimensions };
|
||||||
TrackPositions trackPositions;
|
|
||||||
for (std::size_t i {}; i < samples.size(); ++i)
|
|
||||||
{
|
|
||||||
if (_loadCancelled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
|
SOM::InputVector weights{ getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions) };
|
||||||
|
network.setDataWeights(weights);
|
||||||
|
|
||||||
trackPositions[samplesTrackIds[i]].push_back(position);
|
auto somProgressCallback{ [&](const SOM::Network::CurrentIteration& iter)
|
||||||
}
|
{
|
||||||
|
LMS_LOG(RECOMMENDATION, DEBUG, "Current pass = " << iter.idIteration << " / " << iter.iterationCount);
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
|
progressCallback(Progress {iter.idIteration, iter.iterationCount});
|
||||||
|
} };
|
||||||
|
|
||||||
|
LMS_LOG(RECOMMENDATION, DEBUG, "Training network...");
|
||||||
|
network.train(samples, trainSettings.iterationCount,
|
||||||
|
progressCallback ? somProgressCallback : SOM::Network::ProgressCallback{},
|
||||||
|
[this] { return _loadCancelled; });
|
||||||
|
LMS_LOG(RECOMMENDATION, DEBUG, "Training network DONE");
|
||||||
|
|
||||||
load(std::move(network), std::move(trackPositions));
|
LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks...");
|
||||||
}
|
TrackPositions trackPositions;
|
||||||
|
for (std::size_t i{}; i < samples.size(); ++i)
|
||||||
|
{
|
||||||
|
if (_loadCancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
void
|
const SOM::Position position{ network.getClosestRefVectorPosition(samples[i]) };
|
||||||
FeaturesEngine::loadFromCache(FeaturesEngineCache&& cache)
|
|
||||||
{
|
trackPositions[samplesTrackIds[i]].push_back(position);
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache...";
|
}
|
||||||
|
|
||||||
load(std::move(cache._network), cache._trackPositions);
|
LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks DONE");
|
||||||
}
|
|
||||||
|
load(std::move(network), std::move(trackPositions));
|
||||||
TrackContainer
|
}
|
||||||
FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
|
|
||||||
{
|
void FeaturesEngine::loadFromCache(FeaturesEngineCache&& cache)
|
||||||
const TrackContainer trackIds {[&]
|
{
|
||||||
{
|
LMS_LOG(RECOMMENDATION, INFO, "Constructing features classifier from cache...");
|
||||||
TrackContainer res;
|
|
||||||
|
load(std::move(cache._network), cache._trackPositions);
|
||||||
Session& session {_db.getTLSSession()};
|
}
|
||||||
|
|
||||||
auto transaction {session.createReadTransaction()};
|
TrackContainer FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
|
||||||
|
{
|
||||||
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
|
const TrackContainer trackIds{ [&]
|
||||||
if (trackList)
|
{
|
||||||
res = trackList->getTrackIds();
|
TrackContainer res;
|
||||||
|
|
||||||
return res;
|
Session& session {_db.getTLSSession()};
|
||||||
}()};
|
|
||||||
|
auto transaction {session.createReadTransaction()};
|
||||||
return findSimilarTracks(trackIds, maxCount);
|
|
||||||
}
|
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
|
||||||
|
if (trackList)
|
||||||
TrackContainer
|
res = trackList->getTrackIds();
|
||||||
FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
|
|
||||||
{
|
return res;
|
||||||
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
|
}() };
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
return findSimilarTracks(trackIds, maxCount);
|
||||||
|
}
|
||||||
{
|
|
||||||
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
|
TrackContainer FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
|
||||||
auto transaction {session.createReadTransaction()};
|
{
|
||||||
|
auto similarTrackIds{ getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount) };
|
||||||
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
|
||||||
[&](TrackId trackId)
|
Session& session{ _db.getTLSSession() };
|
||||||
{
|
|
||||||
return !Track::exists(session, trackId);
|
{
|
||||||
}), std::end(similarTrackIds));
|
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
|
||||||
}
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
return similarTrackIds;
|
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
||||||
}
|
[&](TrackId trackId)
|
||||||
|
{
|
||||||
ReleaseContainer
|
return !Track::exists(session, trackId);
|
||||||
FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
|
}), std::end(similarTrackIds));
|
||||||
{
|
}
|
||||||
auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
|
|
||||||
|
return similarTrackIds;
|
||||||
Session& session {_db.getTLSSession()};
|
}
|
||||||
|
|
||||||
if (!similarReleaseIds.empty())
|
ReleaseContainer FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
// Report only existing ids
|
auto similarReleaseIds{ getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount) };
|
||||||
auto transaction {session.createReadTransaction()};
|
|
||||||
|
Session& session{ _db.getTLSSession() };
|
||||||
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
|
||||||
[&](ReleaseId releaseId)
|
if (!similarReleaseIds.empty())
|
||||||
{
|
{
|
||||||
return !Release::exists(session, releaseId);
|
// Report only existing ids
|
||||||
}), std::end(similarReleaseIds));
|
auto transaction{ session.createReadTransaction() };
|
||||||
}
|
|
||||||
|
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
||||||
return similarReleaseIds;
|
[&](ReleaseId releaseId)
|
||||||
}
|
{
|
||||||
|
return !Release::exists(session, releaseId);
|
||||||
ArtistContainer
|
}), std::end(similarReleaseIds));
|
||||||
FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
}
|
||||||
{
|
|
||||||
auto getSimilarArtistIdsForLinkType {[&] (TrackArtistLinkType linkType)
|
return similarReleaseIds;
|
||||||
{
|
}
|
||||||
ArtistContainer similarArtistIds;
|
|
||||||
|
ArtistContainer FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
||||||
const auto itArtists {_artistMatrix.find(linkType)};
|
{
|
||||||
if (itArtists == std::cend(_artistMatrix))
|
auto getSimilarArtistIdsForLinkType{ [&](TrackArtistLinkType linkType)
|
||||||
{
|
{
|
||||||
return similarArtistIds;
|
ArtistContainer similarArtistIds;
|
||||||
}
|
|
||||||
|
const auto itArtists {_artistMatrix.find(linkType)};
|
||||||
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
|
if (itArtists == std::cend(_artistMatrix))
|
||||||
}};
|
{
|
||||||
|
return similarArtistIds;
|
||||||
std::unordered_set<ArtistId> similarArtistIds;
|
}
|
||||||
|
|
||||||
for (TrackArtistLinkType linkType : linkTypes)
|
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
|
||||||
{
|
} };
|
||||||
const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)};
|
|
||||||
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
|
std::unordered_set<ArtistId> similarArtistIds;
|
||||||
}
|
|
||||||
|
for (TrackArtistLinkType linkType : linkTypes)
|
||||||
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
|
{
|
||||||
|
const auto similarArtistIdsForLinkType{ getSimilarArtistIdsForLinkType(linkType) };
|
||||||
Session& session {_db.getTLSSession()};
|
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
|
||||||
{
|
}
|
||||||
// Report only existing ids
|
|
||||||
auto transaction {session.createReadTransaction()};
|
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
|
||||||
|
|
||||||
res.erase(std::remove_if(std::begin(res), std::end(res),
|
Session& session{ _db.getTLSSession() };
|
||||||
[&](ArtistId artistId)
|
{
|
||||||
{
|
// Report only existing ids
|
||||||
return !Artist::exists(session, artistId);
|
auto transaction{ session.createReadTransaction() };
|
||||||
}), std::end(res));
|
|
||||||
}
|
res.erase(std::remove_if(std::begin(res), std::end(res),
|
||||||
|
[&](ArtistId artistId)
|
||||||
while (res.size() > maxCount)
|
{
|
||||||
res.erase(Random::pickRandom(res));
|
return !Artist::exists(session, artistId);
|
||||||
|
}), std::end(res));
|
||||||
return res;
|
}
|
||||||
}
|
|
||||||
|
while (res.size() > maxCount)
|
||||||
FeaturesEngineCache
|
res.erase(Random::pickRandom(res));
|
||||||
FeaturesEngine::toCache() const
|
|
||||||
{
|
return res;
|
||||||
return FeaturesEngineCache {*_network, _trackPositions};
|
}
|
||||||
}
|
|
||||||
|
FeaturesEngineCache FeaturesEngine::toCache() const
|
||||||
void
|
{
|
||||||
FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
|
return FeaturesEngineCache{ *_network, _trackPositions };
|
||||||
{
|
}
|
||||||
if (forceReload)
|
|
||||||
{
|
void FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
|
||||||
FeaturesEngineCache::invalidate();
|
{
|
||||||
}
|
if (forceReload)
|
||||||
else if (std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()})
|
{
|
||||||
{
|
FeaturesEngineCache::invalidate();
|
||||||
loadFromCache(std::move(*cache));
|
}
|
||||||
return;
|
else if (std::optional<FeaturesEngineCache> cache{ FeaturesEngineCache::read() })
|
||||||
}
|
{
|
||||||
|
loadFromCache(std::move(*cache));
|
||||||
TrainSettings trainSettings;
|
return;
|
||||||
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
|
}
|
||||||
|
|
||||||
loadFromTraining(trainSettings, progressCallback);
|
TrainSettings trainSettings;
|
||||||
if (!_loadCancelled && _network)
|
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
|
||||||
toCache().write();
|
|
||||||
}
|
loadFromTraining(trainSettings, progressCallback);
|
||||||
|
if (!_loadCancelled && _network)
|
||||||
void
|
toCache().write();
|
||||||
FeaturesEngine::requestCancelLoad()
|
}
|
||||||
{
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation";
|
void FeaturesEngine::requestCancelLoad()
|
||||||
_loadCancelled = true;
|
{
|
||||||
}
|
LMS_LOG(RECOMMENDATION, DEBUG, "Requesting init cancellation");
|
||||||
|
_loadCancelled = true;
|
||||||
void
|
}
|
||||||
FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
|
|
||||||
{
|
void FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
|
||||||
using namespace Database;
|
{
|
||||||
|
using namespace Database;
|
||||||
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian;
|
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
|
||||||
|
LMS_LOG(RECOMMENDATION, DEBUG, "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian);
|
||||||
const SOM::Coordinate width {network.getWidth()};
|
|
||||||
const SOM::Coordinate height {network.getHeight()};
|
const SOM::Coordinate width{ network.getWidth() };
|
||||||
|
const SOM::Coordinate height{ network.getHeight() };
|
||||||
_releaseMatrix = ReleaseMatrix {width, height};
|
|
||||||
_trackMatrix = TrackMatrix {width, height};
|
_releaseMatrix = ReleaseMatrix{ width, height };
|
||||||
|
_trackMatrix = TrackMatrix{ width, height };
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
|
|
||||||
|
LMS_LOG(RECOMMENDATION, DEBUG, "Constructing maps...");
|
||||||
Session& session {_db.getTLSSession()};
|
|
||||||
|
Session & session{ _db.getTLSSession() };
|
||||||
for (const auto& [trackId, positions] : trackPositions)
|
|
||||||
{
|
for (const auto& [trackId, positions] : trackPositions)
|
||||||
if (_loadCancelled)
|
{
|
||||||
return;
|
if (_loadCancelled)
|
||||||
|
return;
|
||||||
auto transaction {session.createReadTransaction()};
|
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
const Track::pointer track {Track::find(session, trackId)};
|
|
||||||
if (!track)
|
const Track::pointer track{ Track::find(session, trackId) };
|
||||||
continue;
|
if (!track)
|
||||||
|
continue;
|
||||||
for (const SOM::Position& position : positions)
|
|
||||||
{
|
for (const SOM::Position& position : positions)
|
||||||
Utils::push_back_if_not_present(_trackPositions[trackId], position);
|
{
|
||||||
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
|
Utils::push_back_if_not_present(_trackPositions[trackId], position);
|
||||||
|
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
|
||||||
if (Release::pointer release {track->getRelease()})
|
|
||||||
{
|
if (Release::pointer release{ track->getRelease() })
|
||||||
const ReleaseId releaseId {release->getId()};
|
{
|
||||||
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
|
const ReleaseId releaseId{ release->getId() };
|
||||||
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
|
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
|
||||||
}
|
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
|
||||||
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
|
}
|
||||||
{
|
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
|
||||||
const ArtistId artistId {artistLink->getArtist()->getId()};
|
{
|
||||||
|
const ArtistId artistId{ artistLink->getArtist()->getId() };
|
||||||
Utils::push_back_if_not_present(_artistPositions[artistId], position);
|
|
||||||
auto itArtists {_artistMatrix.find(artistLink->getType())};
|
Utils::push_back_if_not_present(_artistPositions[artistId], position);
|
||||||
if (itArtists == std::cend(_artistMatrix))
|
auto itArtists{ _artistMatrix.find(artistLink->getType()) };
|
||||||
{
|
if (itArtists == std::cend(_artistMatrix))
|
||||||
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix {width, height});
|
{
|
||||||
assert(inserted);
|
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix{ width, height });
|
||||||
itArtists = it;
|
assert(inserted);
|
||||||
}
|
itArtists = it;
|
||||||
Utils::push_back_if_not_present(itArtists->second[position], artistId);
|
}
|
||||||
}
|
Utils::push_back_if_not_present(itArtists->second[position], artistId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_network = std::make_unique<SOM::Network>(network);
|
|
||||||
|
_network = std::make_unique<SOM::Network>(network);
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully loaded!";
|
|
||||||
}
|
LMS_LOG(RECOMMENDATION, INFO, "Classifier successfully loaded!");
|
||||||
|
}
|
||||||
|
|
||||||
} // ns Recommendation
|
} // ns Recommendation
|
||||||
|
|||||||
@@ -23,233 +23,226 @@
|
|||||||
#include <boost/property_tree/xml_parser.hpp>
|
#include <boost/property_tree/xml_parser.hpp>
|
||||||
|
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
namespace Recommendation {
|
namespace Recommendation
|
||||||
|
|
||||||
|
|
||||||
static
|
|
||||||
std::filesystem::path getCacheDirectory()
|
|
||||||
{
|
{
|
||||||
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
|
namespace
|
||||||
}
|
{
|
||||||
|
std::filesystem::path getCacheDirectory()
|
||||||
|
{
|
||||||
|
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
|
||||||
|
}
|
||||||
|
|
||||||
static std::filesystem::path getCacheNetworkFilePath()
|
std::filesystem::path getCacheNetworkFilePath()
|
||||||
{
|
{
|
||||||
return getCacheDirectory() / "network";
|
return getCacheDirectory() / "network";
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::filesystem::path getCacheTrackPositionsFilePath()
|
std::filesystem::path getCacheTrackPositionsFilePath()
|
||||||
{
|
{
|
||||||
return getCacheDirectory() / "track_positions";
|
return getCacheDirectory() / "track_positions";
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
bool networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
|
||||||
bool
|
{
|
||||||
networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
|
try
|
||||||
{
|
{
|
||||||
try
|
boost::property_tree::ptree root;
|
||||||
{
|
|
||||||
boost::property_tree::ptree root;
|
|
||||||
|
|
||||||
root.put("width", network.getWidth());
|
root.put("width", network.getWidth());
|
||||||
root.put("height", network.getHeight());
|
root.put("height", network.getHeight());
|
||||||
root.put("dim_count", network.getInputDimCount());
|
root.put("dim_count", network.getInputDimCount());
|
||||||
|
|
||||||
for (SOM::InputVector::value_type weight : network.getDataWeights())
|
for (SOM::InputVector::value_type weight : network.getDataWeights())
|
||||||
root.add("weights.weight", weight);
|
root.add("weights.weight", weight);
|
||||||
|
|
||||||
for (SOM::Coordinate x = 0; x < network.getWidth(); ++x)
|
for (SOM::Coordinate x = 0; x < network.getWidth(); ++x)
|
||||||
{
|
{
|
||||||
for (SOM::Coordinate y = 0; y < network.getWidth(); ++y)
|
for (SOM::Coordinate y = 0; y < network.getWidth(); ++y)
|
||||||
{
|
{
|
||||||
const auto& refVector = network.getRefVector({x, y});
|
const auto& refVector = network.getRefVector({ x, y });
|
||||||
|
|
||||||
boost::property_tree::ptree node;
|
boost::property_tree::ptree node;
|
||||||
for (auto value : refVector)
|
for (auto value : refVector)
|
||||||
node.add("values.value", value);
|
node.add("values.value", value);
|
||||||
|
|
||||||
node.put("coord_x", x);
|
node.put("coord_x", x);
|
||||||
node.put("coord_y", y);
|
node.put("coord_y", y);
|
||||||
|
|
||||||
root.add_child("ref_vectors.ref_vector", node);
|
root.add_child("ref_vectors.ref_vector", node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::property_tree::write_xml(path.string(), root);
|
boost::property_tree::write_xml(path.string(), root);
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Created network cache";
|
LMS_LOG(RECOMMENDATION, DEBUG, "Created network cache");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (boost::property_tree::ptree_error& error)
|
catch (boost::property_tree::ptree_error& error)
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create network cache: " << error.what();
|
LMS_LOG(RECOMMENDATION, ERROR, "Cannot create network cache: " << error.what());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<SOM::Network>
|
std::optional<SOM::Network> FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
|
||||||
FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
|
{
|
||||||
{
|
if (!std::filesystem::exists(path))
|
||||||
if (!std::filesystem::exists(path))
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Reading network from cache...";
|
LMS_LOG(RECOMMENDATION, INFO, "Reading network from cache...");
|
||||||
|
|
||||||
boost::property_tree::ptree root;
|
boost::property_tree::ptree root;
|
||||||
|
|
||||||
boost::property_tree::read_xml(path.string(), root);
|
boost::property_tree::read_xml(path.string(), root);
|
||||||
|
|
||||||
SOM::Coordinate width {root.get<SOM::Coordinate>("width")};
|
SOM::Coordinate width{ root.get<SOM::Coordinate>("width") };
|
||||||
SOM::Coordinate height {root.get<SOM::Coordinate>("height")};
|
SOM::Coordinate height{ root.get<SOM::Coordinate>("height") };
|
||||||
std::size_t dimCount {root.get<std::size_t>("dim_count")};
|
std::size_t dimCount{ root.get<std::size_t>("dim_count") };
|
||||||
|
|
||||||
SOM::Network res {width, height, dimCount};
|
SOM::Network res{ width, height, dimCount };
|
||||||
|
|
||||||
{
|
{
|
||||||
SOM::InputVector weights {dimCount};
|
SOM::InputVector weights{ dimCount };
|
||||||
std::size_t i {};
|
std::size_t i{};
|
||||||
for (const auto& val : root.get_child("weights"))
|
for (const auto& val : root.get_child("weights"))
|
||||||
weights[i++] = val.second.get_value<double>();
|
weights[i++] = val.second.get_value<double>();
|
||||||
|
|
||||||
res.setDataWeights(weights);
|
res.setDataWeights(weights);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& node : root.get_child("ref_vectors"))
|
for (const auto& node : root.get_child("ref_vectors"))
|
||||||
{
|
{
|
||||||
SOM::Coordinate x {node.second.get<SOM::Coordinate>("coord_x")};
|
SOM::Coordinate x{ node.second.get<SOM::Coordinate>("coord_x") };
|
||||||
SOM::Coordinate y {node.second.get<SOM::Coordinate>("coord_y")};
|
SOM::Coordinate y{ node.second.get<SOM::Coordinate>("coord_y") };
|
||||||
|
|
||||||
SOM::InputVector refVector {dimCount};
|
SOM::InputVector refVector{ dimCount };
|
||||||
std::size_t i {};
|
std::size_t i{};
|
||||||
for (const auto& val : node.second.get_child("values"))
|
for (const auto& val : node.second.get_child("values"))
|
||||||
refVector[i++] = val.second.get_value<SOM::InputVector::value_type>();
|
refVector[i++] = val.second.get_value<SOM::InputVector::value_type>();
|
||||||
|
|
||||||
res.setRefVector({x, y}, refVector);
|
res.setRefVector({ x, y }, refVector);
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read network from cache";
|
LMS_LOG(RECOMMENDATION, INFO, "Successfully read network from cache");
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
catch (boost::property_tree::ptree_error& error)
|
catch (boost::property_tree::ptree_error& error)
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot read network cache: " << error.what();
|
LMS_LOG(RECOMMENDATION, ERROR, "Cannot read network cache: " << error.what());
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
|
||||||
FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
|
{
|
||||||
{
|
try
|
||||||
try
|
{
|
||||||
{
|
boost::property_tree::ptree root;
|
||||||
boost::property_tree::ptree root;
|
|
||||||
|
|
||||||
for (const auto& [id, positions] : trackPositions)
|
for (const auto& [id, positions] : trackPositions)
|
||||||
{
|
{
|
||||||
boost::property_tree::ptree node;
|
boost::property_tree::ptree node;
|
||||||
|
|
||||||
node.put("id", id.getValue());
|
node.put("id", id.getValue());
|
||||||
|
|
||||||
for (const SOM::Position& position : positions)
|
for (const SOM::Position& position : positions)
|
||||||
{
|
{
|
||||||
boost::property_tree::ptree positionNode;
|
boost::property_tree::ptree positionNode;
|
||||||
positionNode.put("x", position.x);
|
positionNode.put("x", position.x);
|
||||||
positionNode.put("y", position.y);
|
positionNode.put("y", position.y);
|
||||||
|
|
||||||
node.add_child("position.position", positionNode);
|
node.add_child("position.position", positionNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
root.add_child("objects.object", node);
|
root.add_child("objects.object", node);
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::property_tree::write_xml(path.string(), root);
|
boost::property_tree::write_xml(path.string(), root);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (boost::property_tree::ptree_error& error)
|
catch (boost::property_tree::ptree_error& error)
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot cache object position: " << error.what();
|
LMS_LOG(RECOMMENDATION, ERROR, "Cannot cache object position: " << error.what());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<FeaturesEngineCache::TrackPositions>
|
std::optional<FeaturesEngineCache::TrackPositions> FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
|
||||||
FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
|
{
|
||||||
{
|
try
|
||||||
try
|
{
|
||||||
{
|
LMS_LOG(RECOMMENDATION, INFO, "Reading object position from cache...");
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Reading object position from cache...";
|
|
||||||
|
|
||||||
boost::property_tree::ptree root;
|
boost::property_tree::ptree root;
|
||||||
|
|
||||||
boost::property_tree::read_xml(path.string(), root);
|
boost::property_tree::read_xml(path.string(), root);
|
||||||
|
|
||||||
TrackPositions res;
|
TrackPositions res;
|
||||||
|
|
||||||
for (const auto& object : root.get_child("objects"))
|
for (const auto& object : root.get_child("objects"))
|
||||||
{
|
{
|
||||||
const Database::TrackId id {object.second.get<Database::IdType::ValueType>("id")};
|
const Database::TrackId id{ object.second.get<Database::IdType::ValueType>("id") };
|
||||||
for (const auto& position : object.second.get_child("position"))
|
for (const auto& position : object.second.get_child("position"))
|
||||||
{
|
{
|
||||||
auto x = position.second.get<SOM::Coordinate>("x");
|
auto x = position.second.get<SOM::Coordinate>("x");
|
||||||
auto y = position.second.get<SOM::Coordinate>("y");
|
auto y = position.second.get<SOM::Coordinate>("y");
|
||||||
|
|
||||||
res[id].push_back({x, y});
|
res[id].push_back({ x, y });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read object position from cache";
|
LMS_LOG(RECOMMENDATION, INFO, "Successfully read object position from cache");
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
catch (boost::property_tree::ptree_error& error)
|
catch (boost::property_tree::ptree_error& error)
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create object position from cache file: " << error.what();
|
LMS_LOG(RECOMMENDATION, ERROR, "Cannot create object position from cache file: " << error.what());
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void FeaturesEngineCache::invalidate()
|
||||||
FeaturesEngineCache::invalidate()
|
{
|
||||||
{
|
std::filesystem::remove(getCacheNetworkFilePath());
|
||||||
std::filesystem::remove(getCacheNetworkFilePath());
|
std::filesystem::remove(getCacheTrackPositionsFilePath());
|
||||||
std::filesystem::remove(getCacheTrackPositionsFilePath());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<FeaturesEngineCache>
|
std::optional<FeaturesEngineCache> FeaturesEngineCache::read()
|
||||||
FeaturesEngineCache::read()
|
{
|
||||||
{
|
auto network{ createNetworkFromCacheFile(getCacheNetworkFilePath()) };
|
||||||
auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())};
|
if (!network)
|
||||||
if (!network)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())};
|
auto trackPositions{ createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath()) };
|
||||||
if (!trackPositions)
|
if (!trackPositions)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
return FeaturesEngineCache {std::move(*network), std::move(*trackPositions)};
|
return FeaturesEngineCache{ std::move(*network), std::move(*trackPositions) };
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void FeaturesEngineCache::write() const
|
||||||
FeaturesEngineCache::write() const
|
{
|
||||||
{
|
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
|
||||||
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
|
|
||||||
|
|
||||||
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|
||||||
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
|
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
|
||||||
{
|
{
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
|
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
|
||||||
: _network {std::move(network)},
|
: _network{ std::move(network) },
|
||||||
_trackPositions {std::move(trackPositions)}
|
_trackPositions{ std::move(trackPositions) }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Recommendation
|
} // namespace Recommendation
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Recommendation::PlaylistGeneratorConstraint
|
namespace Recommendation::PlaylistGeneratorConstraint
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Recommendation::PlaylistGeneratorConstraint
|
namespace Recommendation::PlaylistGeneratorConstraint
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Scanner
|
namespace Scanner
|
||||||
{
|
{
|
||||||
@@ -45,13 +45,13 @@ namespace Scanner
|
|||||||
const Track::pointer track{ Track::find(session, trackId) };
|
const Track::pointer track{ Track::find(session, trackId) };
|
||||||
if (auto trackMBID{ track->getTrackMBID() })
|
if (auto trackMBID{ track->getTrackMBID() })
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName();
|
LMS_LOG(DBUPDATER, INFO, "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName());
|
||||||
context.stats.duplicates.emplace_back(ScanDuplicate{ track->getId(), DuplicateReason::SameTrackMBID });
|
context.stats.duplicates.emplace_back(ScanDuplicate{ track->getId(), DuplicateReason::SameTrackMBID });
|
||||||
context.currentStepStats.processedElems++;
|
context.currentStepStats.processedElems++;
|
||||||
_progressCallback(context.currentStepStats);
|
_progressCallback(context.currentStepStats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Found " << context.currentStepStats.processedElems << " duplicated audio files";
|
LMS_LOG(DBUPDATER, DEBUG, "Found " << context.currentStepStats.processedElems << " duplicated audio files");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
#include "services/database/Cluster.hpp"
|
#include "services/database/Cluster.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Path.hpp"
|
#include "utils/Path.hpp"
|
||||||
|
|
||||||
namespace Scanner
|
namespace Scanner
|
||||||
@@ -84,6 +84,6 @@ namespace Scanner
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!";
|
LMS_LOG(DBUPDATER, DEBUG, "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ScanStepDiscoverFiles.hpp"
|
#include "ScanStepDiscoverFiles.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Path.hpp"
|
#include "utils/Path.hpp"
|
||||||
|
|
||||||
namespace Scanner
|
namespace Scanner
|
||||||
@@ -42,6 +42,6 @@ namespace Scanner
|
|||||||
|
|
||||||
context.stats.filesScanned = context.currentStepStats.processedElems;
|
context.stats.filesScanned = context.currentStepStats.processedElems;
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Discovered " << context.stats.filesScanned << " files in '" << context.directory << "'";
|
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << context.stats.filesScanned << " files in '" << context.directory << "'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Path.hpp"
|
#include "utils/Path.hpp"
|
||||||
|
|
||||||
namespace Scanner
|
namespace Scanner
|
||||||
@@ -87,14 +87,14 @@ namespace Scanner
|
|||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
|
LMS_LOG(DBUPDATER, DEBUG, "Checking tracks to be removed...");
|
||||||
std::size_t trackCount{};
|
std::size_t trackCount{};
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
trackCount = Track::getCount(session);
|
trackCount = Track::getCount(session);
|
||||||
}
|
}
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
|
LMS_LOG(DBUPDATER, DEBUG, trackCount << " tracks to be checked...");
|
||||||
|
|
||||||
context.currentStepStats.totalElems = trackCount;
|
context.currentStepStats.totalElems = trackCount;
|
||||||
|
|
||||||
@@ -143,24 +143,24 @@ namespace Scanner
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
|
LMS_LOG(DBUPDATER, DEBUG, trackCount << " tracks checked!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepRemoveOrphanDbFiles::removeOrphanClusters()
|
void ScanStepRemoveOrphanDbFiles::removeOrphanClusters()
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters...";
|
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan clusters...");
|
||||||
removeOrphanEntries<Database::Cluster>(_db.getTLSSession(), _abortScan);
|
removeOrphanEntries<Database::Cluster>(_db.getTLSSession(), _abortScan);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
|
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists...";
|
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists...");
|
||||||
removeOrphanEntries<Database::Artist>(_db.getTLSSession(), _abortScan);
|
removeOrphanEntries<Database::Artist>(_db.getTLSSession(), _abortScan);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
|
void ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
|
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan releases...");
|
||||||
removeOrphanEntries<Database::Release>(_db.getTLSSession(), _abortScan);
|
removeOrphanEntries<Database::Release>(_db.getTLSSession(), _abortScan);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,19 +172,19 @@ namespace Scanner
|
|||||||
// and still belongs to a media directory
|
// and still belongs to a media directory
|
||||||
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
|
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing";
|
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': missing");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName))
|
if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory";
|
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': out of media directory");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions))
|
if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled";
|
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': file format no longer handled");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
catch (std::filesystem::filesystem_error& e)
|
catch (std::filesystem::filesystem_error& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what();
|
LMS_LOG(DBUPDATER, ERROR, "Caught exception while checking file '" << p.string() << "': " << e.what());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Path.hpp"
|
#include "utils/Path.hpp"
|
||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
@@ -258,7 +258,7 @@ namespace Scanner
|
|||||||
|
|
||||||
MetaData::ParserReadStyle getParserReadStyle()
|
MetaData::ParserReadStyle getParserReadStyle()
|
||||||
{
|
{
|
||||||
std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "accurate") };
|
std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "average") };
|
||||||
|
|
||||||
if (readStyle == "fast")
|
if (readStyle == "fast")
|
||||||
return MetaData::ParserReadStyle::Fast;
|
return MetaData::ParserReadStyle::Fast;
|
||||||
@@ -290,7 +290,7 @@ namespace Scanner
|
|||||||
|
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message();
|
LMS_LOG(DBUPDATER, ERROR, "Cannot process entry '" << path.string() << "': " << ec.message());
|
||||||
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
|
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
|
||||||
}
|
}
|
||||||
else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
|
else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
|
||||||
@@ -319,7 +319,7 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
catch (LmsException& e)
|
catch (LmsException& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, ERROR) << e.what();
|
LMS_LOG(DBUPDATER, ERROR, e.what());
|
||||||
stats.skips++;
|
stats.skips++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ namespace Scanner
|
|||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!std::filesystem::exists(otherTrack->getPath(), ec))
|
if (!std::filesystem::exists(otherTrack->getPath(), ec))
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'";
|
LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'");
|
||||||
track = otherTrack;
|
track = otherTrack;
|
||||||
track.modify()->setPath(file);
|
track.modify()->setPath(file);
|
||||||
}
|
}
|
||||||
@@ -384,7 +384,7 @@ namespace Scanner
|
|||||||
if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName))
|
if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')";
|
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')");
|
||||||
// As this MBID already exists, just remove what we just scanned
|
// As this MBID already exists, just remove what we just scanned
|
||||||
if (track)
|
if (track)
|
||||||
{
|
{
|
||||||
@@ -399,7 +399,7 @@ namespace Scanner
|
|||||||
// We estimate this is an audio file if the duration is not null
|
// We estimate this is an audio file if the duration is not null
|
||||||
if (trackInfo->duration == std::chrono::milliseconds::zero())
|
if (trackInfo->duration == std::chrono::milliseconds::zero())
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
|
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
|
||||||
|
|
||||||
// If Track exists here, delete it!
|
// If Track exists here, delete it!
|
||||||
if (track)
|
if (track)
|
||||||
@@ -427,12 +427,12 @@ namespace Scanner
|
|||||||
if (!track)
|
if (!track)
|
||||||
{
|
{
|
||||||
track = dbSession.create<Track>(file);
|
track = dbSession.create<Track>(file);
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'";
|
LMS_LOG(DBUPDATER, DEBUG, "Adding '" << file.string() << "'");
|
||||||
stats.additions++;
|
stats.additions++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'";
|
LMS_LOG(DBUPDATER, DEBUG, "Updating '" << file.string() << "'");
|
||||||
|
|
||||||
stats.updates++;
|
stats.updates++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
#include "services/database/ScanSettings.hpp"
|
#include "services/database/ScanSettings.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Path.hpp"
|
#include "utils/Path.hpp"
|
||||||
#include "utils/Tuple.hpp"
|
#include "utils/Tuple.hpp"
|
||||||
|
|
||||||
@@ -82,9 +82,9 @@ namespace Scanner
|
|||||||
|
|
||||||
ScannerService::~ScannerService()
|
ScannerService::~ScannerService()
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Stopping service...";
|
LMS_LOG(DBUPDATER, INFO, "Stopping service...");
|
||||||
stop();
|
stop();
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Service stopped!";
|
LMS_LOG(DBUPDATER, INFO, "Service stopped!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScannerService::start()
|
void ScannerService::start()
|
||||||
@@ -113,15 +113,15 @@ namespace Scanner
|
|||||||
|
|
||||||
void ScannerService::abortScan()
|
void ScannerService::abortScan()
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
|
LMS_LOG(DBUPDATER, DEBUG, "Aborting scan...");
|
||||||
std::scoped_lock lock{ _controlMutex };
|
std::scoped_lock lock{ _controlMutex };
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort...";
|
LMS_LOG(DBUPDATER, DEBUG, "Waiting for the scan to abort...");
|
||||||
|
|
||||||
_abortScan = true;
|
_abortScan = true;
|
||||||
_scheduleTimer.cancel();
|
_scheduleTimer.cancel();
|
||||||
_ioService.stop();
|
_ioService.stop();
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!";
|
LMS_LOG(DBUPDATER, DEBUG, "Scan abort done!");
|
||||||
|
|
||||||
_abortScan = false;
|
_abortScan = false;
|
||||||
_ioService.start();
|
_ioService.start();
|
||||||
@@ -167,7 +167,7 @@ namespace Scanner
|
|||||||
|
|
||||||
void ScannerService::scheduleNextScan()
|
void ScannerService::scheduleNextScan()
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan";
|
LMS_LOG(DBUPDATER, DEBUG, "Scheduling next scan");
|
||||||
|
|
||||||
refreshScanSettings();
|
refreshScanSettings();
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ namespace Scanner
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ScanSettings::UpdatePeriod::Never:
|
case ScanSettings::UpdatePeriod::Never:
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!";
|
LMS_LOG(DBUPDATER, INFO, "Auto scan disabled!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ namespace Scanner
|
|||||||
|
|
||||||
if (dateTime.isNull())
|
if (dateTime.isNull())
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
|
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan right now");
|
||||||
_scheduleTimer.expires_from_now(std::chrono::seconds{ 0 });
|
_scheduleTimer.expires_from_now(std::chrono::seconds{ 0 });
|
||||||
_scheduleTimer.async_wait(cb);
|
_scheduleTimer.async_wait(cb);
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@ namespace Scanner
|
|||||||
std::time_t t{ std::chrono::system_clock::to_time_t(timePoint) };
|
std::time_t t{ std::chrono::system_clock::to_time_t(timePoint) };
|
||||||
char ctimeStr[26];
|
char ctimeStr[26];
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr));
|
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr)));
|
||||||
_scheduleTimer.expires_at(timePoint);
|
_scheduleTimer.expires_at(timePoint);
|
||||||
_scheduleTimer.async_wait(cb);
|
_scheduleTimer.async_wait(cb);
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
LMS_LOG(UI, INFO) << "New scan started!";
|
LMS_LOG(UI, INFO, "New scan started!");
|
||||||
|
|
||||||
refreshScanSettings();
|
refreshScanSettings();
|
||||||
|
|
||||||
@@ -267,16 +267,16 @@ namespace Scanner
|
|||||||
|
|
||||||
for (auto& scanStep : _scanSteps)
|
for (auto& scanStep : _scanSteps)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Starting scan step '" << scanStep->getStepName() << "'";
|
LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'");
|
||||||
scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() };
|
scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() };
|
||||||
|
|
||||||
notifyInProgress(scanContext.currentStepStats);
|
notifyInProgress(scanContext.currentStepStats);
|
||||||
scanStep->process(scanContext);
|
scanStep->process(scanContext);
|
||||||
notifyInProgress(scanContext.currentStepStats);
|
notifyInProgress(scanContext.currentStepStats);
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Completed scan step '" << scanStep->getStepName() << "'";
|
LMS_LOG(DBUPDATER, DEBUG, "Completed scan step '" << scanStep->getStepName() << "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size();
|
LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size());
|
||||||
|
|
||||||
_dbSession.analyze();
|
_dbSession.analyze();
|
||||||
|
|
||||||
@@ -290,14 +290,14 @@ namespace Scanner
|
|||||||
_currentScanStepStats.reset();
|
_currentScanStepStats.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!";
|
LMS_LOG(DBUPDATER, DEBUG, "Scan not aborted, scheduling next scan!");
|
||||||
scheduleNextScan();
|
scheduleNextScan();
|
||||||
|
|
||||||
_events.scanComplete.emit(stats);
|
_events.scanComplete.emit(stats);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!";
|
LMS_LOG(DBUPDATER, DEBUG, "Scan aborted, not scheduling next scan!");
|
||||||
|
|
||||||
std::unique_lock lock{ _statusMutex };
|
std::unique_lock lock{ _statusMutex };
|
||||||
|
|
||||||
@@ -312,9 +312,9 @@ namespace Scanner
|
|||||||
if (_settings == newSettings)
|
if (_settings == newSettings)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Scanner settings updated";
|
LMS_LOG(DBUPDATER, DEBUG, "Scanner settings updated");
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "skipDuplicateMBID = " << newSettings.skipDuplicateMBID;
|
LMS_LOG(DBUPDATER, DEBUG, "skipDuplicateMBID = " << newSettings.skipDuplicateMBID);
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Using scan settings version " << newSettings.scanVersion;
|
LMS_LOG(DBUPDATER, DEBUG, "Using scan settings version " << newSettings.scanVersion);
|
||||||
|
|
||||||
_settings = std::move(newSettings);
|
_settings = std::move(newSettings);
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ namespace Scanner
|
|||||||
|
|
||||||
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
|
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
|
||||||
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
|
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
|
||||||
[](ClusterType::pointer clusterType) { return clusterType->getName(); });
|
[](const ClusterType::pointer& clusterType) { return std::string{ clusterType->getName() }; });
|
||||||
|
|
||||||
newSettings.clusterTypeNames = std::move(clusterTypeNames);
|
newSettings.clusterTypeNames = std::move(clusterTypeNames);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "internal/InternalBackend.hpp"
|
#include "internal/InternalBackend.hpp"
|
||||||
#include "listenbrainz/ListenBrainzBackend.hpp"
|
#include "listenbrainz/ListenBrainzBackend.hpp"
|
||||||
@@ -43,15 +43,15 @@ namespace Scrobbling
|
|||||||
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
{
|
{
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
LMS_LOG(SCROBBLING, INFO, "Starting service...");
|
||||||
_scrobblingBackends.emplace(ScrobblingBackend::Internal, std::make_unique<InternalBackend>(_db));
|
_scrobblingBackends.emplace(ScrobblingBackend::Internal, std::make_unique<InternalBackend>(_db));
|
||||||
_scrobblingBackends.emplace(ScrobblingBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
|
_scrobblingBackends.emplace(ScrobblingBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
LMS_LOG(SCROBBLING, INFO, "Service started!");
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::~ScrobblingService()
|
ScrobblingService::~ScrobblingService()
|
||||||
{
|
{
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
LMS_LOG(SCROBBLING, INFO, "Service stopped!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScrobblingService::listenStarted(const Listen& listen)
|
void ScrobblingService::listenStarted(const Listen& listen)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/http/IClient.hpp"
|
#include "utils/http/IClient.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
|
|
||||||
const bool res{ duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2) };
|
const bool res{ duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2) };
|
||||||
if (!res)
|
if (!res)
|
||||||
LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s";
|
LOG(DEBUG, "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s");
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -57,12 +57,12 @@ namespace Scrobbling::ListenBrainz
|
|||||||
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
|
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
|
||||||
, _listensSynchronizer{ _ioContext, db, *_client }
|
, _listensSynchronizer{ _ioContext, db, *_client }
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Starting ListenBrainz backend... API endpoint = '" << _baseAPIUrl << "'";
|
LOG(INFO, "Starting ListenBrainz backend... API endpoint = '" << _baseAPIUrl << "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
ListenBrainzBackend::~ListenBrainzBackend()
|
ListenBrainzBackend::~ListenBrainzBackend()
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Stopped ListenBrainz backend!";
|
LOG(INFO, "Stopped ListenBrainz backend!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListenBrainzBackend::listenStarted(const Listen& listen)
|
void ListenBrainzBackend::listenStarted(const Listen& listen)
|
||||||
|
|||||||
@@ -24,86 +24,82 @@
|
|||||||
#include <Wt/Json/Value.h>
|
#include <Wt/Json/Value.h>
|
||||||
#include <Wt/Json/Parser.h>
|
#include <Wt/Json/Parser.h>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
using namespace Scrobbling::ListenBrainz;
|
|
||||||
|
|
||||||
Listen
|
|
||||||
parseListen(const Wt::Json::Object& listenObject)
|
|
||||||
{
|
|
||||||
Listen listen;
|
|
||||||
|
|
||||||
// Mandatory fields
|
|
||||||
const Wt::Json::Object& metadata = listenObject.get("track_metadata");
|
|
||||||
listen.trackName = static_cast<std::string>(metadata.get("track_name"));
|
|
||||||
listen.artistName = static_cast<std::string>(metadata.get("artist_name"));
|
|
||||||
|
|
||||||
// Optional fields
|
|
||||||
listen.releaseName = static_cast<std::string>(metadata.get("release_name").orIfNull(""));
|
|
||||||
if (listenObject.type("listened_at") == Wt::Json::Type::Number)
|
|
||||||
listen.listenedAt = Wt::WDateTime::fromTime_t(static_cast<int>(listenObject.get("listened_at")));
|
|
||||||
if (!listen.listenedAt.isValid())
|
|
||||||
LOG(ERROR) << "Invalid or missing 'listened_at' field!";
|
|
||||||
|
|
||||||
if (metadata.type("additional_info") == Wt::Json::Type::Object)
|
|
||||||
{
|
|
||||||
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
|
|
||||||
listen.trackMBID = UUID::fromString(additionalInfo.get("track_mbid").orIfNull(""));
|
|
||||||
listen.recordingMBID = UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""));
|
|
||||||
listen.releaseMBID = UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""));
|
|
||||||
|
|
||||||
// tracknumber should be an integer but some players encode as strings
|
|
||||||
int trackNumber {additionalInfo.get("tracknumber").toNumber().orIfNull(-1)};
|
|
||||||
if (trackNumber > 0)
|
|
||||||
listen.trackNumber = trackNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
return listen;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
namespace Scrobbling::ListenBrainz
|
namespace Scrobbling::ListenBrainz
|
||||||
{
|
{
|
||||||
ListensParser::Result
|
namespace
|
||||||
ListensParser::parse(std::string_view msgBody)
|
{
|
||||||
{
|
Listen parseListen(const Wt::Json::Object& listenObject)
|
||||||
Result result;
|
{
|
||||||
|
Listen listen;
|
||||||
|
|
||||||
try
|
// Mandatory fields
|
||||||
{
|
const Wt::Json::Object& metadata = listenObject.get("track_metadata");
|
||||||
Wt::Json::Object root;
|
listen.trackName = static_cast<std::string>(metadata.get("track_name"));
|
||||||
Wt::Json::parse(std::string {msgBody}, root);
|
listen.artistName = static_cast<std::string>(metadata.get("artist_name"));
|
||||||
|
|
||||||
const Wt::Json::Object& payload = root.get("payload");
|
// Optional fields
|
||||||
const Wt::Json::Array& listens = payload.get("listens");
|
listen.releaseName = static_cast<std::string>(metadata.get("release_name").orIfNull(""));
|
||||||
|
if (listenObject.type("listened_at") == Wt::Json::Type::Number)
|
||||||
|
listen.listenedAt = Wt::WDateTime::fromTime_t(static_cast<int>(listenObject.get("listened_at")));
|
||||||
|
if (!listen.listenedAt.isValid())
|
||||||
|
LOG(ERROR, "Invalid or missing 'listened_at' field!");
|
||||||
|
|
||||||
LOG(DEBUG) << "Parsing " << listens.size() << " listens...";
|
if (metadata.type("additional_info") == Wt::Json::Type::Object)
|
||||||
result.listenCount = listens.size();
|
{
|
||||||
|
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
|
||||||
|
listen.trackMBID = UUID::fromString(additionalInfo.get("track_mbid").orIfNull(""));
|
||||||
|
listen.recordingMBID = UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""));
|
||||||
|
listen.releaseMBID = UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""));
|
||||||
|
|
||||||
if (listens.empty())
|
// tracknumber should be an integer but some players encode as strings
|
||||||
return result;
|
int trackNumber{ additionalInfo.get("tracknumber").toNumber().orIfNull(-1) };
|
||||||
|
if (trackNumber > 0)
|
||||||
|
listen.trackNumber = trackNumber;
|
||||||
|
}
|
||||||
|
|
||||||
for (const Wt::Json::Value& value : listens)
|
return listen;
|
||||||
{
|
}
|
||||||
try
|
} // namespace
|
||||||
{
|
|
||||||
const Wt::Json::Object& listen = value;
|
|
||||||
result.listens.push_back(parseListen(listen));
|
|
||||||
}
|
|
||||||
catch (const Wt::WException& error)
|
|
||||||
{
|
|
||||||
LOG(ERROR) << "Cannot parse 'listen': " << error.what();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (const Wt::WException& error)
|
|
||||||
{
|
|
||||||
LOG(ERROR) << "Cannot parse 'listens': " << error.what();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
ListensParser::Result ListensParser::parse(std::string_view msgBody)
|
||||||
}
|
{
|
||||||
|
Result result;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Wt::Json::Object root;
|
||||||
|
Wt::Json::parse(std::string{ msgBody }, root);
|
||||||
|
|
||||||
|
const Wt::Json::Object& payload = root.get("payload");
|
||||||
|
const Wt::Json::Array& listens = payload.get("listens");
|
||||||
|
|
||||||
|
LOG(DEBUG, "Parsing " << listens.size() << " listens...");
|
||||||
|
result.listenCount = listens.size();
|
||||||
|
|
||||||
|
if (listens.empty())
|
||||||
|
return result;
|
||||||
|
|
||||||
|
for (const Wt::Json::Value& value : listens)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const Wt::Json::Object& listen = value;
|
||||||
|
result.listens.push_back(parseListen(listen));
|
||||||
|
}
|
||||||
|
catch (const Wt::WException& error)
|
||||||
|
{
|
||||||
|
LOG(ERROR, "Cannot parse 'listen': " << error.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const Wt::WException& error)
|
||||||
|
{
|
||||||
|
LOG(ERROR, "Cannot parse 'listens': " << error.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
} // Scrobbling::ListenBrainz
|
} // Scrobbling::ListenBrainz
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace
|
|||||||
|
|
||||||
if (artists.empty())
|
if (artists.empty())
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist";
|
LOG(DEBUG, "Track cannot be scrobbled since it does not have any artist");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ namespace
|
|||||||
}
|
}
|
||||||
catch (const Wt::WException& e)
|
catch (const Wt::WException& e)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
|
LOG(ERROR, "Cannot parse listen count response: " << e.what());
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,12 +152,12 @@ namespace
|
|||||||
// if duplicated files, do not record it (let the user correct its database)
|
// if duplicated files, do not record it (let the user correct its database)
|
||||||
if (tracks.size() == 1)
|
if (tracks.size() == 1)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Matched listen '" << listen << "' using track MBID";
|
LOG(DEBUG, "Matched listen '" << listen << "' using track MBID");
|
||||||
return tracks.front()->getId();
|
return tracks.front()->getId();
|
||||||
}
|
}
|
||||||
else if (tracks.size() > 1)
|
else if (tracks.size() > 1)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using track MBID!";
|
LOG(DEBUG, "Too many matches for listen '" << listen << "' using track MBID!");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,12 +168,12 @@ namespace
|
|||||||
// if duplicated files, do not record it (let the user correct its database)
|
// if duplicated files, do not record it (let the user correct its database)
|
||||||
if (tracks.size() == 1)
|
if (tracks.size() == 1)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Matched listen '" << listen << "' using recording MBID";
|
LOG(DEBUG, "Matched listen '" << listen << "' using recording MBID");
|
||||||
return tracks.front()->getId();
|
return tracks.front()->getId();
|
||||||
}
|
}
|
||||||
else if (tracks.size() > 1)
|
else if (tracks.size() > 1)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using recording MBID!";
|
LOG(DEBUG, "Too many matches for listen '" << listen << "' using recording MBID!");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,16 +192,16 @@ namespace
|
|||||||
// conservative behavior: in case of multiple matches: reject
|
// conservative behavior: in case of multiple matches: reject
|
||||||
if (tracks.results.size() == 1)
|
if (tracks.results.size() == 1)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Matched listen '" << listen << "' using metadata";
|
LOG(DEBUG, "Matched listen '" << listen << "' using metadata");
|
||||||
return tracks.results.front();
|
return tracks.results.front();
|
||||||
}
|
}
|
||||||
else if (tracks.results.size() > 1)
|
else if (tracks.results.size() > 1)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Too many matches for listen '" << listen << "' using metadata";
|
LOG(DEBUG, "Too many matches for listen '" << listen << "' using metadata");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(DEBUG) << "No match for listen '" << listen << "'";
|
LOG(DEBUG, "No match for listen '" << listen << "'");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +215,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
, _maxSyncListenCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-listen-count", 1000) }
|
, _maxSyncListenCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-listen-count", 1000) }
|
||||||
, _syncListensPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-listens-period-hours", 1) }
|
, _syncListensPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-listens-period-hours", 1) }
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours";
|
LOG(INFO, "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours");
|
||||||
|
|
||||||
scheduleSync(std::chrono::seconds{ 30 });
|
scheduleSync(std::chrono::seconds{ 30 });
|
||||||
}
|
}
|
||||||
@@ -267,14 +267,14 @@ namespace Scrobbling::ListenBrainz
|
|||||||
std::string bodyText{ listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now") };
|
std::string bodyText{ listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now") };
|
||||||
if (bodyText.empty())
|
if (bodyText.empty())
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Cannot convert listen to json: skipping";
|
LOG(DEBUG, "Cannot convert listen to json: skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::optional<UUID> listenBrainzToken{ Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) };
|
const std::optional<UUID> listenBrainzToken{ Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) };
|
||||||
if (!listenBrainzToken)
|
if (!listenBrainzToken)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "No listenbrainz token found: skipping";
|
LOG(DEBUG, "No listenbrainz token found: skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
dbListen = session.create<Database::Listen>(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt);
|
dbListen = session.create<Database::Listen>(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt);
|
||||||
dbListen.modify()->setSyncState(scrobblingState);
|
dbListen.modify()->setSyncState(scrobblingState);
|
||||||
|
|
||||||
LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString();
|
LOG(DEBUG, "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -347,7 +347,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(DEBUG) << "Queing " << pendingListens.size() << " pending listen";
|
LOG(DEBUG, "Queing " << pendingListens.size() << " pending listen");
|
||||||
|
|
||||||
for (const TimedListen& pendingListen : pendingListens)
|
for (const TimedListen& pendingListen : pendingListens)
|
||||||
enqueListen(pendingListen);
|
enqueListen(pendingListen);
|
||||||
@@ -379,13 +379,13 @@ namespace Scrobbling::ListenBrainz
|
|||||||
if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0)
|
if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
|
LOG(DEBUG, "Scheduled sync in " << fromNow.count() << " seconds...");
|
||||||
_syncTimer.expires_after(fromNow);
|
_syncTimer.expires_after(fromNow);
|
||||||
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
|
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
|
||||||
{
|
{
|
||||||
if (ec == boost::asio::error::operation_aborted)
|
if (ec == boost::asio::error::operation_aborted)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "getListens aborted";
|
LOG(DEBUG, "getListens aborted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (ec)
|
else if (ec)
|
||||||
@@ -399,7 +399,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
|
|
||||||
void ListensSynchronizer::startSync()
|
void ListensSynchronizer::startSync()
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Starting sync!";
|
LOG(DEBUG, "Starting sync!");
|
||||||
|
|
||||||
assert(!isSyncing());
|
assert(!isSyncing());
|
||||||
|
|
||||||
@@ -435,7 +435,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
{
|
{
|
||||||
_strand.dispatch([this, &context]
|
_strand.dispatch([this, &context]
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount;
|
LOG(INFO, "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount);
|
||||||
context.syncing = false;
|
context.syncing = false;
|
||||||
|
|
||||||
if (!isSyncing())
|
if (!isSyncing())
|
||||||
@@ -489,7 +489,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
{
|
{
|
||||||
const auto listenCount = parseListenCount(msgBody);
|
const auto listenCount = parseListenCount(msgBody);
|
||||||
if (listenCount)
|
if (listenCount)
|
||||||
LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount;
|
LOG(DEBUG, "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount);
|
||||||
|
|
||||||
bool needSync{ listenCount && (!context.listenCount || *context.listenCount != *listenCount) };
|
bool needSync{ listenCount && (!context.listenCount || *context.listenCount != *listenCount) };
|
||||||
context.listenCount = listenCount;
|
context.listenCount = listenCount;
|
||||||
@@ -551,7 +551,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
// update oldest listen for the next query
|
// update oldest listen for the next query
|
||||||
if (!parsedListen.listenedAt.isValid())
|
if (!parsedListen.listenedAt.isValid())
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Skipping entry due to invalid listenedAt";
|
LOG(DEBUG, "Skipping entry due to invalid listenedAt");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,38 +27,36 @@
|
|||||||
|
|
||||||
namespace Scrobbling::ListenBrainz::Utils
|
namespace Scrobbling::ListenBrainz::Utils
|
||||||
{
|
{
|
||||||
std::optional<UUID>
|
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId)
|
||||||
getListenBrainzToken(Database::Session& session, Database::UserId userId)
|
{
|
||||||
{
|
auto transaction{ session.createReadTransaction() };
|
||||||
auto transaction {session.createReadTransaction()};
|
|
||||||
|
|
||||||
const Database::User::pointer user {Database::User::find(session, userId)};
|
const Database::User::pointer user{ Database::User::find(session, userId) };
|
||||||
if (!user)
|
if (!user)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
return user->getListenBrainzToken();
|
return user->getListenBrainzToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string
|
std::string parseValidateToken(std::string_view msgBody)
|
||||||
parseValidateToken(std::string_view msgBody)
|
{
|
||||||
{
|
std::string listenBrainzUserName;
|
||||||
std::string listenBrainzUserName;
|
|
||||||
|
|
||||||
Wt::Json::ParseError error;
|
Wt::Json::ParseError error;
|
||||||
Wt::Json::Object root;
|
Wt::Json::Object root;
|
||||||
if (!Wt::Json::parse(std::string {msgBody}, root, error))
|
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
|
LOG(ERROR, "Cannot parse 'validate-token' result: " << error.what());
|
||||||
return listenBrainzUserName;
|
return listenBrainzUserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.get("valid").orIfNull(false))
|
if (!root.get("valid").orIfNull(false))
|
||||||
{
|
{
|
||||||
LOG(INFO) << "Invalid listenbrainz user";
|
LOG(INFO, "Invalid listenbrainz user");
|
||||||
return listenBrainzUserName;
|
return listenBrainzUserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
listenBrainzUserName = root.get("user_name").orIfNull("");
|
listenBrainzUserName = root.get("user_name").orIfNull("");
|
||||||
return listenBrainzUserName;
|
return listenBrainzUserName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "services/database/UserId.hpp"
|
#include "services/database/UserId.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/UUID.hpp"
|
#include "utils/UUID.hpp"
|
||||||
|
|
||||||
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] "
|
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, message << "[listenbrainz] ")
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,16 +19,16 @@
|
|||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/StreamLogger.hpp"
|
#include "utils/StreamLogger.hpp"
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
// log to stdout
|
// log to stdout
|
||||||
Service<Logger> logger {std::make_unique<StreamLogger>(std::cout, EnumSet<Severity> {Severity::FATAL, Severity::ERROR})};
|
Service<ILogger> logger{ std::make_unique<StreamLogger>(std::cout, EnumSet<Severity> {Severity::FATAL, Severity::ERROR}) };
|
||||||
|
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
return RUN_ALL_TESTS();
|
return RUN_ALL_TESTS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Random.hpp"
|
#include "utils/Random.hpp"
|
||||||
|
|
||||||
namespace SOM
|
namespace SOM
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include "SubsonicResponse.hpp"
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "utils/Utils.hpp"
|
#include "utils/Utils.hpp"
|
||||||
@@ -290,7 +290,7 @@ namespace API::Subsonic
|
|||||||
|
|
||||||
const std::size_t requestId{ curRequestId++ };
|
const std::size_t requestId{ curRequestId++ };
|
||||||
|
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap());
|
LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap()));
|
||||||
|
|
||||||
std::string requestPath{ request.pathInfo() };
|
std::string requestPath{ request.pathInfo() };
|
||||||
if (StringUtils::stringEndsWith(requestPath, ".view"))
|
if (StringUtils::stringEndsWith(requestPath, ".view"))
|
||||||
@@ -319,7 +319,7 @@ namespace API::Subsonic
|
|||||||
|
|
||||||
resp.write(response.out(), format);
|
resp.write(response.out(), format);
|
||||||
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
|
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
|
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -328,18 +328,18 @@ namespace API::Subsonic
|
|||||||
if (itStreamHandler != mediaRetrievalHandlers.end())
|
if (itStreamHandler != mediaRetrievalHandlers.end())
|
||||||
{
|
{
|
||||||
itStreamHandler->second(requestContext, request, response);
|
itStreamHandler->second(requestContext, request, response);
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
|
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << requestPath << "'";
|
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
|
||||||
throw UnknownEntryPointGenericError{};
|
throw UnknownEntryPointGenericError{};
|
||||||
}
|
}
|
||||||
catch (const Error& e)
|
catch (const Error& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'"
|
LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestPath << "'"
|
||||||
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
|
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
|
||||||
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
|
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'");
|
||||||
Response resp{ Response::createFailedResponse(protocolVersion, e) };
|
Response resp{ Response::createFailedResponse(protocolVersion, e) };
|
||||||
resp.write(response.out(), format);
|
resp.write(response.out(), format);
|
||||||
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
|
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Random.hpp"
|
#include "utils/Random.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "responses/Album.hpp"
|
#include "responses/Album.hpp"
|
||||||
@@ -122,7 +122,7 @@ namespace API::Subsonic
|
|||||||
// This endpoint does not scale: make sort lived transactions in order not to block the whole application
|
// This endpoint does not scale: make sort lived transactions in order not to block the whole application
|
||||||
|
|
||||||
// first pass: dispatch the artists by first letter
|
// first pass: dispatch the artists by first letter
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "GetArtists: fetching all artists...";
|
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists...");
|
||||||
std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar;
|
std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar;
|
||||||
std::size_t currentArtistOffset{ 0 };
|
std::size_t currentArtistOffset{ 0 };
|
||||||
constexpr std::size_t batchSize{ 100 };
|
constexpr std::size_t batchSize{ 100 };
|
||||||
@@ -151,7 +151,7 @@ namespace API::Subsonic
|
|||||||
}
|
}
|
||||||
|
|
||||||
// second pass: add each artist
|
// second pass: add each artist
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "GetArtists: constructing response...";
|
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: constructing response...");
|
||||||
for (const auto& [sortChar, artistIds] : artistsSortedByFirstChar)
|
for (const auto& [sortChar, artistIds] : artistsSortedByFirstChar)
|
||||||
{
|
{
|
||||||
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
|
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/IResourceHandler.hpp"
|
#include "utils/IResourceHandler.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/FileResourceHandlerCreator.hpp"
|
#include "utils/FileResourceHandlerCreator.hpp"
|
||||||
#include "utils/Utils.hpp"
|
#include "utils/Utils.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
@@ -153,7 +153,7 @@ namespace API::Subsonic
|
|||||||
|
|
||||||
if (!requestedFormat && (maxBitRate == 0 || track->getBitrate() <= maxBitRate ))
|
if (!requestedFormat && (maxBitRate == 0 || track->getBitrate() <= maxBitRate ))
|
||||||
{
|
{
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate is compatible with parameters => no transcoding";
|
LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate is compatible with parameters => no transcoding");
|
||||||
return parameters; // no transcoding needed
|
return parameters; // no transcoding needed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ namespace API::Subsonic
|
|||||||
{
|
{
|
||||||
if (maxBitRate == 0 || track->getBitrate() <= maxBitRate)
|
if (maxBitRate == 0 || track->getBitrate() <= maxBitRate)
|
||||||
{
|
{
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate and format are compatible with parameters => no transcoding";
|
LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate and format are compatible with parameters => no transcoding");
|
||||||
return parameters; // no transcoding needed
|
return parameters; // no transcoding needed
|
||||||
}
|
}
|
||||||
bitrate = maxBitRate;
|
bitrate = maxBitRate;
|
||||||
@@ -246,7 +246,7 @@ namespace API::Subsonic
|
|||||||
}
|
}
|
||||||
catch (const Av::Exception& e)
|
catch (const Av::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
|
LMS_LOG(API_SUBSONIC, ERROR, "Caught Av exception: " << e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include <archive.h>
|
#include <archive.h>
|
||||||
#include <archive_entry.h>
|
#include <archive_entry.h>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace Zip
|
namespace Zip
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,7 @@ namespace Zip
|
|||||||
{
|
{
|
||||||
const int res {::archive_write_free(arch)};
|
const int res {::archive_write_free(arch)};
|
||||||
if (res != ARCHIVE_OK)
|
if (res != ARCHIVE_OK)
|
||||||
LMS_LOG(UTILS, ERROR) << "Failure while freeing archive control struct: " << std::string {::strerror(res)};
|
LMS_LOG(UTILS, ERROR, "Failure while freeing archive control struct: " << std::string {::strerror(res)});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -176,7 +176,7 @@ namespace Zip
|
|||||||
void
|
void
|
||||||
ArchiveZipper::abort()
|
ArchiveZipper::abort()
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, DEBUG) << "Aborting zip creation";
|
LMS_LOG(UTILS, DEBUG, "Aborting zip creation");
|
||||||
if (_archive)
|
if (_archive)
|
||||||
{
|
{
|
||||||
::archive_write_fail(_archive.get());
|
::archive_write_fail(_archive.get());
|
||||||
|
|||||||
@@ -36,177 +36,172 @@
|
|||||||
#include <boost/asio/buffer.hpp>
|
#include <boost/asio/buffer.hpp>
|
||||||
|
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class SystemException : public ChildProcessException
|
class SystemException : public ChildProcessException
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SystemException(int err, const std::string& errMsg)
|
SystemException(int err, const std::string& errMsg)
|
||||||
: ChildProcessException {errMsg + ": " + ::strerror(err)}
|
: ChildProcessException{ errMsg + ": " + ::strerror(err) }
|
||||||
{}
|
{}
|
||||||
|
|
||||||
SystemException(boost::system::error_code ec, const std::string& errMsg)
|
SystemException(boost::system::error_code ec, const std::string& errMsg)
|
||||||
: ChildProcessException {errMsg + ": " + ec.message()}
|
: ChildProcessException{ errMsg + ": " + ec.message() }
|
||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args)
|
ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args)
|
||||||
: _ioContext {ioContext}
|
: _ioContext{ ioContext }
|
||||||
, _childStdout {_ioContext}
|
, _childStdout{ _ioContext }
|
||||||
|
|
||||||
{
|
{
|
||||||
// make sure only one thread is executing this part of code
|
// make sure only one thread is executing this part of code
|
||||||
static std::mutex mutex;
|
static std::mutex mutex;
|
||||||
std::unique_lock<std::mutex> lock {mutex};
|
std::unique_lock<std::mutex> lock{ mutex };
|
||||||
|
|
||||||
int pipe[2];
|
int pipe[2];
|
||||||
|
|
||||||
int res {pipe2(pipe, O_NONBLOCK | O_CLOEXEC)};
|
int res{ pipe2(pipe, O_NONBLOCK | O_CLOEXEC) };
|
||||||
if (res < 0)
|
if (res < 0)
|
||||||
throw SystemException {errno, "pipe2 failed!"};
|
throw SystemException{ errno, "pipe2 failed!" };
|
||||||
|
|
||||||
{
|
{
|
||||||
#if defined(__linux__) && defined(F_SETPIPE_SZ)
|
#if defined(__linux__) && defined(F_SETPIPE_SZ)
|
||||||
// Just a hint here to prevent the writer from writing too many bytes ahead of the reader
|
// Just a hint here to prevent the writer from writing too many bytes ahead of the reader
|
||||||
constexpr std::size_t pipeSize {65536*4};
|
constexpr std::size_t pipeSize{ 65536 * 4 };
|
||||||
|
|
||||||
if (fcntl(pipe[0], F_SETPIPE_SZ, pipeSize) == -1)
|
if (fcntl(pipe[0], F_SETPIPE_SZ, pipeSize) == -1)
|
||||||
throw SystemException {errno, "fcntl failed!"};
|
throw SystemException{ errno, "fcntl failed!" };
|
||||||
if (fcntl(pipe[1], F_SETPIPE_SZ, pipeSize) == -1)
|
if (fcntl(pipe[1], F_SETPIPE_SZ, pipeSize) == -1)
|
||||||
throw SystemException {errno, "fcntl failed!"};
|
throw SystemException{ errno, "fcntl failed!" };
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
res = fork();
|
res = fork();
|
||||||
if (res == -1)
|
if (res == -1)
|
||||||
throw SystemException {errno, "fork failed!"};
|
throw SystemException{ errno, "fork failed!" };
|
||||||
|
|
||||||
if (res == 0) // CHILD
|
if (res == 0) // CHILD
|
||||||
{
|
{
|
||||||
close(pipe[0]);
|
close(pipe[0]);
|
||||||
close(STDIN_FILENO);
|
close(STDIN_FILENO);
|
||||||
close(STDERR_FILENO);
|
close(STDERR_FILENO);
|
||||||
|
|
||||||
// Replace stdout with pipe write
|
// Replace stdout with pipe write
|
||||||
if (dup2(pipe[1], STDOUT_FILENO) == -1)
|
if (dup2(pipe[1], STDOUT_FILENO) == -1)
|
||||||
exit(-1);
|
exit(-1);
|
||||||
|
|
||||||
std::vector<const char*> execArgs;
|
std::vector<const char*> execArgs;
|
||||||
std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); });
|
std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); });
|
||||||
execArgs.push_back(nullptr);
|
execArgs.push_back(nullptr);
|
||||||
|
|
||||||
res = execv(path.string().c_str(), (char *const*)&execArgs[0]);
|
res = execv(path.string().c_str(), (char* const*)&execArgs[0]);
|
||||||
if (res == -1)
|
if (res == -1)
|
||||||
exit(-1);
|
exit(-1);
|
||||||
}
|
}
|
||||||
else // PARENT
|
else // PARENT
|
||||||
{
|
{
|
||||||
close(pipe[1]);
|
close(pipe[1]);
|
||||||
{
|
{
|
||||||
boost::system::error_code assignError;
|
boost::system::error_code assignError;
|
||||||
_childStdout.assign(pipe[0], assignError);
|
_childStdout.assign(pipe[0], assignError);
|
||||||
if (assignError)
|
if (assignError)
|
||||||
throw SystemException {assignError, "fork failed!"};
|
throw SystemException{ assignError, "fork failed!" };
|
||||||
}
|
}
|
||||||
_childPID = res;
|
_childPID = res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChildProcess::~ChildProcess()
|
ChildProcess::~ChildProcess()
|
||||||
{
|
{
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Closing child process...";
|
LMS_LOG(CHILDPROCESS, DEBUG, "Closing child process...");
|
||||||
{
|
{
|
||||||
boost::system::error_code closeError;
|
boost::system::error_code closeError;
|
||||||
_childStdout.close(closeError);
|
_childStdout.close(closeError);
|
||||||
if (closeError)
|
if (closeError)
|
||||||
LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message();
|
LMS_LOG(CHILDPROCESS, ERROR, "Closed failed: " << closeError.message());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_finished)
|
if (!_finished)
|
||||||
kill();
|
kill();
|
||||||
|
|
||||||
wait(true);
|
wait(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ChildProcess::kill()
|
||||||
ChildProcess::kill()
|
|
||||||
{
|
{
|
||||||
// process may already have finished
|
// process may already have finished
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Killing child process...";
|
LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process...");
|
||||||
if (::kill(_childPID, SIGKILL) == -1)
|
if (::kill(_childPID, SIGKILL) == -1)
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Kill failed: " << ::strerror(errno);
|
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << ::strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool ChildProcess::wait(bool block)
|
||||||
ChildProcess::wait(bool block)
|
|
||||||
{
|
{
|
||||||
assert(!_waited);
|
assert(!_waited);
|
||||||
|
|
||||||
int wstatus {};
|
int wstatus{};
|
||||||
const pid_t pid {waitpid(_childPID, &wstatus, block ? 0 : WNOHANG)};
|
const pid_t pid{ waitpid(_childPID, &wstatus, block ? 0 : WNOHANG) };
|
||||||
|
|
||||||
if (pid == -1)
|
if (pid == -1)
|
||||||
throw SystemException {errno, "waitpid failed!"};
|
throw SystemException{ errno, "waitpid failed!" };
|
||||||
else if (pid == 0)
|
else if (pid == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (WIFEXITED(wstatus))
|
if (WIFEXITED(wstatus))
|
||||||
{
|
{
|
||||||
_exitCode = WEXITSTATUS(wstatus);
|
_exitCode = WEXITSTATUS(wstatus);
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Exit code = " << *_exitCode;
|
LMS_LOG(CHILDPROCESS, DEBUG, "Exit code = " << *_exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
_waited = true;
|
_waited = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
|
||||||
ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
|
|
||||||
{
|
{
|
||||||
assert(!finished());
|
assert(!finished());
|
||||||
|
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Async read, bufferSize = " << bufferSize;
|
LMS_LOG(CHILDPROCESS, DEBUG, "Async read, bufferSize = " << bufferSize);
|
||||||
|
|
||||||
boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize),
|
boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize),
|
||||||
[this, callback {std::move(callback)}](const boost::system::error_code& error, std::size_t bytesTransferred)
|
[this, callback{ std::move(callback) }](const boost::system::error_code& error, std::size_t bytesTransferred)
|
||||||
{
|
{
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Async read cb - ec = '" << error.message() << "' (" << error.value() << "), bytesTransferred = " << bytesTransferred;
|
LMS_LOG(CHILDPROCESS, DEBUG, "Async read cb - ec = '" << error.message() << "' (" << error.value() << "), bytesTransferred = " << bytesTransferred);
|
||||||
|
|
||||||
ReadResult readResult {ReadResult::Success};
|
ReadResult readResult{ ReadResult::Success };
|
||||||
if (error)
|
if (error)
|
||||||
{
|
{
|
||||||
if (error != boost::asio::error::eof)
|
if (error != boost::asio::error::eof)
|
||||||
{
|
{
|
||||||
// forbidden to read any captured param here as the ChildProcess instance may already have been killed
|
// forbidden to read any captured param here as the ChildProcess instance may already have been killed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
readResult = ReadResult::EndOfFile;
|
readResult = ReadResult::EndOfFile;
|
||||||
_finished = true;
|
_finished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(readResult, bytesTransferred);
|
callback(readResult, bytesTransferred);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
std::size_t ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
|
||||||
ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
|
|
||||||
{
|
{
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
const std::size_t res {_childStdout.read_some(boost::asio::buffer(data, bufferSize), ec)};
|
const std::size_t res{ _childStdout.read_some(boost::asio::buffer(data, bufferSize), ec) };
|
||||||
LMS_LOG(CHILDPROCESS, DEBUG) << "read some " << res << " bytes, ec = " << ec.message();
|
LMS_LOG(CHILDPROCESS, DEBUG, "read some " << res << " bytes, ec = " << ec.message());
|
||||||
if (ec)
|
if (ec)
|
||||||
_childStdout.close(ec);
|
_childStdout.close(ec);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool ChildProcess::finished() const
|
||||||
ChildProcess::finished() const
|
|
||||||
{
|
{
|
||||||
return _finished;
|
return _finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#include "ChildProcessManager.hpp"
|
#include "ChildProcessManager.hpp"
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "ChildProcess.hpp"
|
#include "ChildProcess.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
#include "Config.hpp"
|
#include "Config.hpp"
|
||||||
|
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
std::unique_ptr<IConfig> createConfig(const std::filesystem::path& p)
|
std::unique_ptr<IConfig> createConfig(const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
std::unique_ptr<IResourceHandler>
|
std::unique_ptr<IResourceHandler>
|
||||||
createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
|
createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
|
||||||
@@ -45,7 +45,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
|||||||
{
|
{
|
||||||
if (!ifs)
|
if (!ifs)
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'";
|
LMS_LOG(UTILS, ERROR, "Cannot open file stream for '" << _path.string() << "'");
|
||||||
response.setStatus(404);
|
response.setStatus(404);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
|||||||
const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) };
|
const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) };
|
||||||
ifs.seekg(0, std::ios::beg);
|
ifs.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize;
|
LMS_LOG(UTILS, DEBUG, "File '" << _path.string() << "', fileSize = " << fileSize);
|
||||||
|
|
||||||
response.addHeader("Accept-Ranges", "bytes");
|
response.addHeader("Accept-Ranges", "bytes");
|
||||||
|
|
||||||
@@ -66,13 +66,13 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
|||||||
response.setStatus(416); // Requested range not satisfiable
|
response.setStatus(416); // Requested range not satisfiable
|
||||||
response.addHeader("Content-Range", contentRange.str());
|
response.addHeader("Content-Range", contentRange.str());
|
||||||
|
|
||||||
LMS_LOG(UTILS, DEBUG) << "Range not satisfiable";
|
LMS_LOG(UTILS, DEBUG, "Range not satisfiable");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ranges.size() == 1)
|
if (ranges.size() == 1)
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte();
|
LMS_LOG(UTILS, DEBUG, "Range requested = " << ranges[0].firstByte() << "-" << ranges[0].lastByte());
|
||||||
|
|
||||||
response.setStatus(206);
|
response.setStatus(206);
|
||||||
startByte = ranges[0].firstByte();
|
startByte = ranges[0].firstByte();
|
||||||
@@ -87,19 +87,19 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, DEBUG) << "No range requested";
|
LMS_LOG(UTILS, DEBUG, "No range requested");
|
||||||
|
|
||||||
response.setStatus(200);
|
response.setStatus(200);
|
||||||
_beyondLastByte = fileSize;
|
_beyondLastByte = fileSize;
|
||||||
response.setContentLength(_beyondLastByte);
|
response.setContentLength(_beyondLastByte);
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(UTILS, DEBUG) << "Mimetype set to '" << _mimeType << "'";
|
LMS_LOG(UTILS, DEBUG, "Mimetype set to '" << _mimeType << "'");
|
||||||
response.setMimeType(_mimeType);
|
response.setMimeType(_mimeType);
|
||||||
}
|
}
|
||||||
else if (!ifs)
|
else if (!ifs)
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'";
|
LMS_LOG(UTILS, ERROR, "Cannot reopen file stream for '" << _path.string() << "'");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,19 +113,24 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
|||||||
|
|
||||||
ifs.read(&buf[0], pieceSize);
|
ifs.read(&buf[0], pieceSize);
|
||||||
const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) };
|
const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) };
|
||||||
response.out().write(&buf[0], actualPieceSize);
|
if (actualPieceSize > 0)
|
||||||
|
{
|
||||||
|
response.out().write(&buf[0], actualPieceSize);
|
||||||
|
LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << startByte << "-" << startByte + actualPieceSize - 1 << "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LMS_LOG(UTILS, DEBUG, "Written 0 byte");
|
||||||
|
}
|
||||||
|
|
||||||
LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes";
|
|
||||||
|
|
||||||
LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize;
|
|
||||||
if (ifs.good() && actualPieceSize < restSize)
|
if (ifs.good() && actualPieceSize < restSize)
|
||||||
{
|
{
|
||||||
_offset = startByte + actualPieceSize;
|
_offset = startByte + actualPieceSize;
|
||||||
LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset;
|
LMS_LOG(UTILS, DEBUG, "Job not complete! Remaining range: " << _offset << "-" << _beyondLastByte - 1);
|
||||||
|
|
||||||
return response.createContinuation();
|
return response.createContinuation();
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(UTILS, DEBUG) << "Job complete!";
|
LMS_LOG(UTILS, DEBUG, "Job complete!");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,13 @@
|
|||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount)
|
IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount)
|
||||||
: _ioService {ioService}
|
: _ioService {ioService}
|
||||||
, _work {ioService}
|
, _work {ioService}
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, INFO) << "Starting IO context with " << threadCount << " threads...";
|
LMS_LOG(UTILS, INFO, "Starting IO context with " << threadCount << " threads...");
|
||||||
for (std::size_t i {}; i < threadCount; ++i)
|
for (std::size_t i {}; i < threadCount; ++i)
|
||||||
{
|
{
|
||||||
_threads.emplace_back([&]
|
_threads.emplace_back([&]
|
||||||
@@ -38,7 +38,7 @@ IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t
|
|||||||
}
|
}
|
||||||
catch (const std::exception& e)
|
catch (const std::exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, FATAL) << "Exception caught in IO context: " << e.what();
|
LMS_LOG(UTILS, FATAL, "Exception caught in IO context: " << e.what());
|
||||||
std::abort();
|
std::abort();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -48,10 +48,10 @@ IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t
|
|||||||
void
|
void
|
||||||
IOContextRunner::stop()
|
IOContextRunner::stop()
|
||||||
{
|
{
|
||||||
LMS_LOG(UTILS, DEBUG) << "Stopping IO context...";
|
LMS_LOG(UTILS, DEBUG, "Stopping IO context...");
|
||||||
_work.reset();
|
_work.reset();
|
||||||
_ioService.stop();
|
_ioService.stop();
|
||||||
LMS_LOG(UTILS, DEBUG) << "IO context stopped!";
|
LMS_LOG(UTILS, DEBUG, "IO context stopped!");
|
||||||
}
|
}
|
||||||
|
|
||||||
IOContextRunner::~IOContextRunner()
|
IOContextRunner::~IOContextRunner()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
const char* getModuleName(Module mod)
|
const char* getModuleName(Module mod)
|
||||||
{
|
{
|
||||||
@@ -59,20 +59,19 @@ const char* getSeverityName(Severity sev)
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::Log(Logger* logger, Module module, Severity severity)
|
Log::Log(ILogger& logger, Module module, Severity severity)
|
||||||
: _module{ module },
|
: _logger{ logger }
|
||||||
_severity{ severity },
|
, _module{ module }
|
||||||
_logger{ logger }
|
, _severity{ severity }
|
||||||
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
Log::~Log()
|
Log::~Log()
|
||||||
{
|
{
|
||||||
if (_logger)
|
_logger.processLog(*this);
|
||||||
_logger->processLog(*this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string
|
std::string Log::getMessage() const
|
||||||
Log::getMessage() const
|
|
||||||
{
|
{
|
||||||
return _oss.str();
|
return _oss.str();
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-114
@@ -30,146 +30,136 @@
|
|||||||
|
|
||||||
#include "utils/Crc32Calculator.hpp"
|
#include "utils/Crc32Calculator.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
namespace PathUtils
|
namespace PathUtils
|
||||||
{
|
{
|
||||||
|
std::uint32_t computeCrc32(const std::filesystem::path& p)
|
||||||
|
{
|
||||||
|
Utils::Crc32Calculator crc32;
|
||||||
|
|
||||||
std::uint32_t
|
std::ifstream ifs{ p.string().c_str(), std::ios_base::binary };
|
||||||
computeCrc32(const std::filesystem::path& p)
|
if (ifs)
|
||||||
{
|
{
|
||||||
Utils::Crc32Calculator crc32;
|
do
|
||||||
|
{
|
||||||
|
std::array<char, 1024> buffer;
|
||||||
|
|
||||||
std::ifstream ifs {p.string().c_str(), std::ios_base::binary};
|
ifs.read(buffer.data(), buffer.size());
|
||||||
if (ifs)
|
crc32.processBytes(reinterpret_cast<const std::byte*>(buffer.data()), ifs.gcount());
|
||||||
{
|
} while (ifs);
|
||||||
do
|
}
|
||||||
{
|
else
|
||||||
std::array<char,1024> buffer;
|
{
|
||||||
|
LMS_LOG(DBUPDATER, ERROR, "Failed to open file '" << p.string() << "'");
|
||||||
|
throw LmsException("Failed to open file '" + p.string() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
ifs.read( buffer.data(), buffer.size() );
|
return crc32.getResult();
|
||||||
crc32.processBytes( reinterpret_cast<const std::byte*>(buffer.data()), ifs.gcount() );
|
}
|
||||||
}
|
|
||||||
while (ifs);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, ERROR) << "Failed to open file '" << p.string() << "'";
|
|
||||||
throw LmsException("Failed to open file '" + p.string() + "'" );
|
|
||||||
}
|
|
||||||
|
|
||||||
return crc32.getResult();
|
bool ensureDirectory(const std::filesystem::path& dir)
|
||||||
}
|
{
|
||||||
|
if (std::filesystem::exists(dir))
|
||||||
|
return std::filesystem::is_directory(dir);
|
||||||
|
else
|
||||||
|
return std::filesystem::create_directory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file)
|
||||||
ensureDirectory(const std::filesystem::path& dir)
|
{
|
||||||
{
|
struct stat sb {};
|
||||||
if (std::filesystem::exists(dir))
|
|
||||||
return std::filesystem::is_directory(dir);
|
|
||||||
else
|
|
||||||
return std::filesystem::create_directory(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WDateTime
|
if (stat(file.string().c_str(), &sb) == -1)
|
||||||
getLastWriteTime(const std::filesystem::path& file)
|
throw LmsException("Failed to get stats on file '" + file.string() + "'");
|
||||||
{
|
|
||||||
struct stat sb {};
|
|
||||||
|
|
||||||
if (stat(file.string().c_str(), &sb) == -1)
|
return Wt::WDateTime::fromTime_t(sb.st_mtime);
|
||||||
throw LmsException("Failed to get stats on file '" + file.string() + "'" );
|
}
|
||||||
|
|
||||||
return Wt::WDateTime::fromTime_t(sb.st_mtime);
|
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName)
|
||||||
}
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::directory_iterator itPath{ directory, std::filesystem::directory_options::follow_directory_symlink, ec };
|
||||||
|
|
||||||
bool
|
if (ec)
|
||||||
exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName)
|
{
|
||||||
{
|
cb(ec, directory);
|
||||||
std::error_code ec;
|
return true; // try to continue exploring anyway
|
||||||
std::filesystem::directory_iterator itPath {directory, std::filesystem::directory_options::follow_directory_symlink, ec};
|
}
|
||||||
|
|
||||||
if (ec)
|
if (excludeDirFileName && !excludeDirFileName->empty())
|
||||||
{
|
{
|
||||||
cb(ec, directory);
|
const std::filesystem::path excludePath{ directory / *excludeDirFileName };
|
||||||
return true; // try to continue exploring anyway
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeDirFileName && !excludeDirFileName->empty())
|
if (std::filesystem::exists(excludePath, ec))
|
||||||
{
|
{
|
||||||
const std::filesystem::path excludePath {directory / *excludeDirFileName};
|
LMS_LOG(DBUPDATER, DEBUG, "Found '" << excludePath.string() << "': skipping directory");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (std::filesystem::exists(excludePath, ec))
|
std::filesystem::directory_iterator itEnd;
|
||||||
{
|
while (itPath != itEnd)
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Found '" << excludePath.string() << "': skipping directory";
|
{
|
||||||
return true;
|
bool continueExploring{ true };
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::directory_iterator itEnd;
|
if (ec)
|
||||||
while (itPath != itEnd)
|
{
|
||||||
{
|
continueExploring = cb(ec, *itPath);
|
||||||
bool continueExploring {true};
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (std::filesystem::is_regular_file(*itPath, ec))
|
||||||
|
{
|
||||||
|
continueExploring = cb(ec, *itPath);
|
||||||
|
}
|
||||||
|
else if (std::filesystem::is_directory(*itPath, ec))
|
||||||
|
{
|
||||||
|
if (!ec)
|
||||||
|
continueExploring = exploreFilesRecursive(*itPath, cb, excludeDirFileName);
|
||||||
|
else
|
||||||
|
continueExploring = cb(ec, *itPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ec)
|
if (!continueExploring)
|
||||||
{
|
return false;
|
||||||
continueExploring = cb(ec, *itPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (std::filesystem::is_regular_file(*itPath, ec))
|
|
||||||
{
|
|
||||||
continueExploring = cb(ec, *itPath);
|
|
||||||
}
|
|
||||||
else if (std::filesystem::is_directory(*itPath, ec))
|
|
||||||
{
|
|
||||||
if (!ec)
|
|
||||||
continueExploring = exploreFilesRecursive(*itPath, cb, excludeDirFileName);
|
|
||||||
else
|
|
||||||
continueExploring = cb(ec, *itPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!continueExploring)
|
itPath.increment(ec);
|
||||||
return false;
|
}
|
||||||
|
|
||||||
itPath.increment(ec);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
bool hasFileAnyExtension(const std::filesystem::path& file, const std::vector<std::filesystem::path>& supportedExtensions)
|
||||||
}
|
{
|
||||||
|
const std::filesystem::path extension{ StringUtils::stringToLower(file.extension().string()) };
|
||||||
|
|
||||||
bool
|
return (std::find(std::cbegin(supportedExtensions), std::cend(supportedExtensions), extension) != std::cend(supportedExtensions));
|
||||||
hasFileAnyExtension(const std::filesystem::path& file, const std::vector<std::filesystem::path>& supportedExtensions)
|
}
|
||||||
{
|
|
||||||
const std::filesystem::path extension {StringUtils::stringToLower(file.extension().string())};
|
|
||||||
|
|
||||||
return (std::find(std::cbegin(supportedExtensions), std::cend(supportedExtensions), extension) != std::cend(supportedExtensions));
|
bool isPathInRootPath(const std::filesystem::path& path, const std::filesystem::path& rootPath, const std::filesystem::path* excludeDirFileName)
|
||||||
}
|
{
|
||||||
|
std::filesystem::path curPath = path;
|
||||||
|
|
||||||
bool
|
while (curPath.parent_path() != curPath)
|
||||||
isPathInRootPath(const std::filesystem::path& path, const std::filesystem::path& rootPath, const std::filesystem::path* excludeDirFileName)
|
{
|
||||||
{
|
curPath = curPath.parent_path();
|
||||||
std::filesystem::path curPath = path;
|
|
||||||
|
|
||||||
while (curPath.parent_path() != curPath)
|
if (excludeDirFileName && !excludeDirFileName->empty())
|
||||||
{
|
{
|
||||||
curPath = curPath.parent_path();
|
assert(!excludeDirFileName->has_parent_path());
|
||||||
|
|
||||||
if (excludeDirFileName && !excludeDirFileName->empty())
|
std::error_code ec;
|
||||||
{
|
if (std::filesystem::exists(curPath / *excludeDirFileName, ec))
|
||||||
assert(!excludeDirFileName->has_parent_path());
|
return false;
|
||||||
|
}
|
||||||
std::error_code ec;
|
|
||||||
if (std::filesystem::exists(curPath / *excludeDirFileName, ec))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curPath == rootPath)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (curPath == rootPath)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} // ns PathUtils
|
} // ns PathUtils
|
||||||
|
|||||||
@@ -170,6 +170,22 @@ namespace StringUtils
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string joinStrings(const std::vector<std::string_view>& strings, std::string_view delimiter)
|
||||||
|
{
|
||||||
|
std::string res;
|
||||||
|
bool first{ true };
|
||||||
|
|
||||||
|
for (std::string_view str : strings)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
res += delimiter;
|
||||||
|
res += str;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
||||||
{
|
{
|
||||||
return boost::algorithm::join(strings, delimiter);
|
return boost::algorithm::join(strings, delimiter);
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <Wt/WApplication.h>
|
#include <Wt/WServer.h>
|
||||||
#include <Wt/WLogger.h>
|
#include <Wt/WLogger.h>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Exception.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -36,6 +36,30 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WtLogger::WtLogger(Severity minSeverity)
|
||||||
|
: _minSeverity{ minSeverity }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string WtLogger::computeLogConfig(Severity minSeverity)
|
||||||
|
{
|
||||||
|
switch (minSeverity)
|
||||||
|
{
|
||||||
|
case Severity::DEBUG: return "*";
|
||||||
|
case Severity::INFO: return "* -debug";
|
||||||
|
case Severity::WARNING: return "* -debug -info";
|
||||||
|
case Severity::ERROR: return "* -debug -info -warning";
|
||||||
|
case Severity::FATAL: return "* -debug -info -warning -error";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw LmsException{ "Unhandled severity" };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WtLogger::isSeverityActive(Severity severity) const
|
||||||
|
{
|
||||||
|
return static_cast<int>(severity) <= static_cast<int>(_minSeverity);
|
||||||
|
}
|
||||||
|
|
||||||
void WtLogger::processLog(const Log& log)
|
void WtLogger::processLog(const Log& log)
|
||||||
{
|
{
|
||||||
Wt::log(getSeverityName(log.getSeverity())) << Wt::WLogger::sep << to_string(std::this_thread::get_id()) << Wt::WLogger::sep << "[" << getModuleName(log.getModule()) << "]" << Wt::WLogger::sep << log.getMessage();
|
Wt::log(getSeverityName(log.getSeverity())) << Wt::WLogger::sep << to_string(std::this_thread::get_id()) << Wt::WLogger::sep << "[" << getModuleName(log.getModule()) << "]" << Wt::WLogger::sep << log.getMessage();
|
||||||
|
|||||||
@@ -23,10 +23,10 @@
|
|||||||
#include <boost/asio/bind_executor.hpp>
|
#include <boost/asio/bind_executor.hpp>
|
||||||
|
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[Http SendQueue] - "
|
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, "[Http SendQueue] - " << message)
|
||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
@@ -98,7 +98,7 @@ namespace Http
|
|||||||
|
|
||||||
for (auto& [prio, requests] : _sendQueue)
|
for (auto& [prio, requests] : _sendQueue)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Processing prio " << static_cast<int>(prio) << ", request count = " << requests.size();
|
LOG(DEBUG, "Processing prio " << static_cast<int>(prio) << ", request count = " << requests.size());
|
||||||
while (!requests.empty())
|
while (!requests.empty())
|
||||||
{
|
{
|
||||||
std::unique_ptr<ClientRequest> request {std::move(requests.front())};
|
std::unique_ptr<ClientRequest> request {std::move(requests.front())};
|
||||||
@@ -118,7 +118,7 @@ namespace Http
|
|||||||
SendQueue::sendRequest(const ClientRequest& request)
|
SendQueue::sendRequest(const ClientRequest& request)
|
||||||
{
|
{
|
||||||
std::string url {_baseUrl + request.getParameters().relativeUrl};
|
std::string url {_baseUrl + request.getParameters().relativeUrl};
|
||||||
LOG(DEBUG) << "Sending request to url '" << url << "'";
|
LOG(DEBUG, "Sending request to url '" << url << "'");
|
||||||
|
|
||||||
bool res {};
|
bool res {};
|
||||||
switch (request.getType())
|
switch (request.getType())
|
||||||
@@ -133,7 +133,7 @@ namespace Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!res)
|
if (!res)
|
||||||
LOG(ERROR) << "Send failed, bad url or unsupported scheme?";
|
LOG(ERROR, "Send failed, bad url or unsupported scheme?");
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -143,14 +143,14 @@ namespace Http
|
|||||||
{
|
{
|
||||||
if (ec == boost::asio::error::operation_aborted)
|
if (ec == boost::asio::error::operation_aborted)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Client aborted";
|
LOG(DEBUG, "Client aborted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(_currentRequest);
|
assert(_currentRequest);
|
||||||
_state = State::Idle;
|
_state = State::Idle;
|
||||||
|
|
||||||
LOG(DEBUG) << "Client done. status = " << msg.status();
|
LOG(DEBUG, "Client done. status = " << msg.status());
|
||||||
if (ec)
|
if (ec)
|
||||||
onClientDoneError(std::move(_currentRequest), ec);
|
onClientDoneError(std::move(_currentRequest), ec);
|
||||||
else
|
else
|
||||||
@@ -160,7 +160,7 @@ namespace Http
|
|||||||
void
|
void
|
||||||
SendQueue::onClientDoneError(std::unique_ptr<ClientRequest> request, Wt::AsioWrapper::error_code ec)
|
SendQueue::onClientDoneError(std::unique_ptr<ClientRequest> request, Wt::AsioWrapper::error_code ec)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Retry " << request->retryCount << ", client error: '" << ec.message() << "'";
|
LOG(ERROR, "Retry " << request->retryCount << ", client error: '" << ec.message() << "'");
|
||||||
|
|
||||||
// may be a network error, try again later
|
// may be a network error, try again later
|
||||||
throttle(_defaultRetryWaitDuration);
|
throttle(_defaultRetryWaitDuration);
|
||||||
@@ -171,7 +171,7 @@ namespace Http
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Too many retries, giving up operation and throttle";
|
LOG(ERROR, "Too many retries, giving up operation and throttle");
|
||||||
if (request->getParameters().onFailureFunc)
|
if (request->getParameters().onFailureFunc)
|
||||||
request->getParameters().onFailureFunc();
|
request->getParameters().onFailureFunc();
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ namespace Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto remainingCount {headerReadAs<std::size_t>(msg, "X-RateLimit-Remaining")};
|
const auto remainingCount {headerReadAs<std::size_t>(msg, "X-RateLimit-Remaining")};
|
||||||
LOG(DEBUG) << "Remaining messages = " << (remainingCount ? *remainingCount : 0);
|
LOG(DEBUG, "Remaining messages = " << (remainingCount ? *remainingCount : 0));
|
||||||
if (mustThrottle || (remainingCount && *remainingCount == 0))
|
if (mustThrottle || (remainingCount && *remainingCount == 0))
|
||||||
{
|
{
|
||||||
const auto waitDuration {headerReadAs<std::chrono::seconds>(msg, "X-RateLimit-Reset-In")};
|
const auto waitDuration {headerReadAs<std::chrono::seconds>(msg, "X-RateLimit-Reset-In")};
|
||||||
@@ -205,7 +205,7 @@ namespace Http
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Send error: '" << msg.body() << "'";
|
LOG(ERROR, "Send error: '" << msg.body() << "'");
|
||||||
if (requestParameters.onFailureFunc)
|
if (requestParameters.onFailureFunc)
|
||||||
requestParameters.onFailureFunc();
|
requestParameters.onFailureFunc();
|
||||||
}
|
}
|
||||||
@@ -221,14 +221,14 @@ namespace Http
|
|||||||
assert(_state == State::Idle);
|
assert(_state == State::Idle);
|
||||||
|
|
||||||
const std::chrono::seconds duration {clamp(requestedDuration, _minRetryWaitDuration, _maxRetryWaitDuration)};
|
const std::chrono::seconds duration {clamp(requestedDuration, _minRetryWaitDuration, _maxRetryWaitDuration)};
|
||||||
LOG(DEBUG) << "Throttling for " << duration.count() << " seconds";
|
LOG(DEBUG, "Throttling for " << duration.count() << " seconds");
|
||||||
|
|
||||||
_throttleTimer.expires_after(duration);
|
_throttleTimer.expires_after(duration);
|
||||||
_throttleTimer.async_wait([this](const boost::system::error_code& ec)
|
_throttleTimer.async_wait([this](const boost::system::error_code& ec)
|
||||||
{
|
{
|
||||||
if (ec == boost::asio::error::operation_aborted)
|
if (ec == boost::asio::error::operation_aborted)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Throttle aborted";
|
LOG(DEBUG, "Throttle aborted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (ec)
|
else if (ec)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "utils/String.hpp"
|
||||||
#include "Service.hpp"
|
#include "Service.hpp"
|
||||||
|
|
||||||
enum class Severity
|
enum class Severity
|
||||||
@@ -59,11 +60,11 @@ enum class Module
|
|||||||
const char* getModuleName(Module mod);
|
const char* getModuleName(Module mod);
|
||||||
const char* getSeverityName(Severity sev);
|
const char* getSeverityName(Severity sev);
|
||||||
|
|
||||||
class Logger;
|
class ILogger;
|
||||||
class Log
|
class Log
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Log(Logger* logger, Module module, Severity severity);
|
Log(ILogger& logger, Module module, Severity severity);
|
||||||
~Log();
|
~Log();
|
||||||
|
|
||||||
Module getModule() const { return _module; }
|
Module getModule() const { return _module; }
|
||||||
@@ -76,18 +77,24 @@ private:
|
|||||||
Log(const Log&) = delete;
|
Log(const Log&) = delete;
|
||||||
Log& operator=(const Log&) = delete;
|
Log& operator=(const Log&) = delete;
|
||||||
|
|
||||||
|
ILogger& _logger;
|
||||||
Module _module;
|
Module _module;
|
||||||
Severity _severity;
|
Severity _severity;
|
||||||
std::ostringstream _oss;
|
std::ostringstream _oss;
|
||||||
Logger* _logger{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Logger
|
class ILogger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~Logger() = default;
|
virtual ~ILogger() = default;
|
||||||
|
|
||||||
|
virtual bool isSeverityActive(Severity severity) const = 0;
|
||||||
virtual void processLog(const Log& log) = 0;
|
virtual void processLog(const Log& log) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define LMS_LOG(module, severity) Log{Service<Logger>::get(), Module::module, Severity::severity}.getOstream()
|
#define LMS_LOG(module, severity, message) \
|
||||||
#define LMS_LOG_EX(module, severity) Log{Service<Logger>::get(), module, severity}.getOstream()
|
do \
|
||||||
|
{ \
|
||||||
|
if (auto* logger {::Service<::ILogger>::get()}; logger && logger->isSeverityActive(::Severity::severity)) \
|
||||||
|
::Log{ *logger, ::Module::module, ::Severity::severity }.getOstream() << message; \
|
||||||
|
} while(0)
|
||||||
@@ -20,19 +20,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
class StreamLogger final : public Logger
|
class StreamLogger final : public ILogger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static constexpr EnumSet<Severity> defaultSeverities {Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO};
|
static constexpr EnumSet<Severity> defaultSeverities{ Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO };
|
||||||
|
|
||||||
StreamLogger(std::ostream& oss, EnumSet<Severity> severities = defaultSeverities);
|
StreamLogger(std::ostream& oss, EnumSet<Severity> severities = defaultSeverities);
|
||||||
|
|
||||||
void processLog(const Log& log);
|
bool isSeverityActive(Severity) const override { return true; }
|
||||||
|
void processLog(const Log& log) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::ostream& _os;
|
std::ostream& _os;
|
||||||
const EnumSet<Severity> _severities;
|
const EnumSet<Severity> _severities;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,19 +38,16 @@ namespace Wt
|
|||||||
namespace StringUtils {
|
namespace StringUtils {
|
||||||
|
|
||||||
[[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
|
[[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
|
||||||
|
|
||||||
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
|
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string joinStrings(const std::vector<std::string_view>& strings, std::string_view delimiter);
|
||||||
[[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
[[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
||||||
|
|
||||||
[[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
[[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||||
|
|
||||||
[[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
[[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||||
|
|
||||||
[[nodiscard]] std::string stringToLower(std::string_view str);
|
[[nodiscard]] std::string stringToLower(std::string_view str);
|
||||||
|
|
||||||
void stringToLower(std::string& str);
|
void stringToLower(std::string& str);
|
||||||
|
|
||||||
[[nodiscard]] std::string stringToUpper(const std::string& str);
|
[[nodiscard]] std::string stringToUpper(const std::string& str);
|
||||||
|
|
||||||
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
|
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
|
||||||
|
|||||||
@@ -19,11 +19,19 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Logger.hpp"
|
#include <string>
|
||||||
|
|
||||||
class WtLogger final : public Logger
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
|
class WtLogger final : public ILogger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void processLog(const Log& log) override;
|
WtLogger(Severity minSeverity);
|
||||||
};
|
|
||||||
|
|
||||||
|
static std::string computeLogConfig(Severity minSeverity);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool isSeverityActive(Severity severity) const override;
|
||||||
|
void processLog(const Log& log) override;
|
||||||
|
const Severity _minSeverity;
|
||||||
|
};
|
||||||
+129
-102
@@ -26,155 +26,182 @@
|
|||||||
|
|
||||||
TEST(StringUtils, splitString)
|
TEST(StringUtils, splitString)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
const std::string test{ "a" };
|
const std::string test{ "a" };
|
||||||
|
|
||||||
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "") };
|
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "") };
|
||||||
ASSERT_EQ(strings.size(), 1);
|
ASSERT_EQ(strings.size(), 1);
|
||||||
EXPECT_EQ(strings.front(), "a");
|
EXPECT_EQ(strings.front(), "a");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const std::string test{ "a b" };
|
const std::string test{ "a b" };
|
||||||
|
|
||||||
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "|") };
|
const std::vector<std::string_view> strings{ StringUtils::splitString(test, "|") };
|
||||||
ASSERT_EQ(strings.size(), 1);
|
ASSERT_EQ(strings.size(), 1);
|
||||||
EXPECT_EQ(strings.front(), "a b");
|
EXPECT_EQ(strings.front(), "a b");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const std::string test{ " a" };
|
const std::string test{ " a" };
|
||||||
|
|
||||||
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
|
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
|
||||||
ASSERT_EQ(strings.size(), 1);
|
ASSERT_EQ(strings.size(), 1);
|
||||||
EXPECT_EQ(strings.front(), "a");
|
EXPECT_EQ(strings.front(), "a");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const std::string test{ "a " };
|
const std::string test{ "a " };
|
||||||
|
|
||||||
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
|
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
|
||||||
ASSERT_EQ(strings.size(), 1);
|
ASSERT_EQ(strings.size(), 1);
|
||||||
EXPECT_EQ(strings.front(), "a");
|
EXPECT_EQ(strings.front(), "a");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const std::string test{ "a b" };
|
const std::string test{ "a b" };
|
||||||
|
|
||||||
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
|
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ") };
|
||||||
ASSERT_EQ(strings.size(), 2);
|
ASSERT_EQ(strings.size(), 2);
|
||||||
EXPECT_EQ(strings.front(), "a");
|
EXPECT_EQ(strings.front(), "a");
|
||||||
EXPECT_EQ(strings.back(), "b");
|
EXPECT_EQ(strings.back(), "b");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const std::string test{ "a b,c|defgh " };
|
const std::string test{ "a b,c|defgh " };
|
||||||
|
|
||||||
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ,|") };
|
const std::vector<std::string_view> strings{ StringUtils::splitString(test, " ,|") };
|
||||||
ASSERT_EQ(strings.size(), 4);
|
ASSERT_EQ(strings.size(), 4);
|
||||||
EXPECT_EQ(strings[0], "a");
|
EXPECT_EQ(strings[0], "a");
|
||||||
EXPECT_EQ(strings[1], "b");
|
EXPECT_EQ(strings[1], "b");
|
||||||
EXPECT_EQ(strings[2], "c");
|
EXPECT_EQ(strings[2], "c");
|
||||||
EXPECT_EQ(strings[3], "defgh");
|
EXPECT_EQ(strings[3], "defgh");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(StringUtils, splitStringCopy)
|
TEST(StringUtils, splitStringCopy)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
const std::string test{ "test=foo" };
|
const std::string test{ "test=foo" };
|
||||||
|
|
||||||
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
|
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
|
||||||
ASSERT_EQ(strings.size(), 2);
|
ASSERT_EQ(strings.size(), 2);
|
||||||
EXPECT_EQ(strings[0], "test");
|
EXPECT_EQ(strings[0], "test");
|
||||||
EXPECT_EQ(strings[1], "foo");
|
EXPECT_EQ(strings[1], "foo");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const std::string test{ "test=foo bar" };
|
const std::string test{ "test=foo bar" };
|
||||||
|
|
||||||
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
|
const std::vector<std::string> strings{ StringUtils::splitStringCopy(test, "=") };
|
||||||
ASSERT_EQ(strings.size(), 2);
|
ASSERT_EQ(strings.size(), 2);
|
||||||
EXPECT_EQ(strings[0], "test");
|
EXPECT_EQ(strings[0], "test");
|
||||||
EXPECT_EQ(strings[1], "foo bar");
|
EXPECT_EQ(strings[1], "foo bar");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StringUtils, joinStrings)
|
||||||
|
{
|
||||||
|
struct TestCase
|
||||||
|
{
|
||||||
|
std::vector<std::string_view> input;
|
||||||
|
std::string delimiter;
|
||||||
|
std::string expectedOutput;
|
||||||
|
};
|
||||||
|
|
||||||
|
TestCase tests[]
|
||||||
|
{
|
||||||
|
{{"a", "b", "c"}, "-", "a-b-c"},
|
||||||
|
{{"a", "b", "c"}, ",", "a,b,c"},
|
||||||
|
{{"a", "b", "c"}, "***", "a***b***c"},
|
||||||
|
{{"a", "", "c"}, "-", "a--c"},
|
||||||
|
{{"", "b", "c"}, "-", "-b-c"},
|
||||||
|
{{"a"}, "-", "a"},
|
||||||
|
{{"a"}, ",", "a"},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const TestCase& test : tests)
|
||||||
|
{
|
||||||
|
const std::string str{ StringUtils::joinStrings(test.input, test.delimiter) };
|
||||||
|
EXPECT_EQ(str, test.expectedOutput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(StringUtils, escapeJSString)
|
TEST(StringUtils, escapeJSString)
|
||||||
{
|
{
|
||||||
EXPECT_EQ(StringUtils::jsEscape(""), "");
|
EXPECT_EQ(StringUtils::jsEscape(""), "");
|
||||||
EXPECT_EQ(StringUtils::jsEscape(R"(Test'.mp3)"), R"(Test\'.mp3)");
|
EXPECT_EQ(StringUtils::jsEscape(R"(Test'.mp3)"), R"(Test\'.mp3)");
|
||||||
EXPECT_EQ(StringUtils::jsEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)");
|
EXPECT_EQ(StringUtils::jsEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)");
|
||||||
EXPECT_EQ(StringUtils::jsEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)");
|
EXPECT_EQ(StringUtils::jsEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(StringUtils, escapeJsonString)
|
TEST(StringUtils, escapeJsonString)
|
||||||
{
|
{
|
||||||
EXPECT_EQ(StringUtils::jsonEscape(""), "");
|
EXPECT_EQ(StringUtils::jsonEscape(""), "");
|
||||||
EXPECT_EQ(StringUtils::jsonEscape(R"(Test'.mp3)"), R"(Test'.mp3)");
|
EXPECT_EQ(StringUtils::jsonEscape(R"(Test'.mp3)"), R"(Test'.mp3)");
|
||||||
EXPECT_EQ(StringUtils::jsonEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)");
|
EXPECT_EQ(StringUtils::jsonEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)");
|
||||||
EXPECT_EQ(StringUtils::jsonEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)");
|
EXPECT_EQ(StringUtils::jsonEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(StringUtils, escapeString)
|
TEST(StringUtils, escapeString)
|
||||||
{
|
{
|
||||||
EXPECT_EQ(StringUtils::escapeString("", "*", ' '), "");
|
EXPECT_EQ(StringUtils::escapeString("", "*", ' '), "");
|
||||||
EXPECT_EQ(StringUtils::escapeString("", "", ' '), "");
|
EXPECT_EQ(StringUtils::escapeString("", "", ' '), "");
|
||||||
EXPECT_EQ(StringUtils::escapeString("a", "", ' '), "a");
|
EXPECT_EQ(StringUtils::escapeString("a", "", ' '), "a");
|
||||||
EXPECT_EQ(StringUtils::escapeString("*", "*", '_'), "_*");
|
EXPECT_EQ(StringUtils::escapeString("*", "*", '_'), "_*");
|
||||||
EXPECT_EQ(StringUtils::escapeString("*a*", "*", '_'), "_*a_*");
|
EXPECT_EQ(StringUtils::escapeString("*a*", "*", '_'), "_*a_*");
|
||||||
EXPECT_EQ(StringUtils::escapeString("*a|", "*|", '_'), "_*a_|");
|
EXPECT_EQ(StringUtils::escapeString("*a|", "*|", '_'), "_*a_|");
|
||||||
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
|
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(StringUtils, readAs)
|
TEST(StringUtils, readAs)
|
||||||
{
|
{
|
||||||
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
|
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
|
||||||
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
|
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
|
||||||
EXPECT_EQ(StringUtils::readAs<bool>("false"), false);
|
EXPECT_EQ(StringUtils::readAs<bool>("false"), false);
|
||||||
EXPECT_EQ(StringUtils::readAs<bool>("0"), false);
|
EXPECT_EQ(StringUtils::readAs<bool>("0"), false);
|
||||||
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
|
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
|
||||||
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
|
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(StringUtils, capitalize)
|
TEST(StringUtils, capitalize)
|
||||||
{
|
{
|
||||||
struct TestCase
|
struct TestCase
|
||||||
{
|
{
|
||||||
std::string input;
|
std::string input;
|
||||||
std::string expectedOutput;
|
std::string expectedOutput;
|
||||||
};
|
};
|
||||||
|
|
||||||
TestCase tests[]
|
TestCase tests[]
|
||||||
{
|
{
|
||||||
{"", ""},
|
{"", ""},
|
||||||
{"C", "C"},
|
{"C", "C"},
|
||||||
{"c", "C"},
|
{"c", "C"},
|
||||||
{" c", " C"},
|
{" c", " C"},
|
||||||
{" cc", " Cc"},
|
{" cc", " Cc"},
|
||||||
{"(c", "(c"},
|
{"(c", "(c"},
|
||||||
{"1c", "1c"},
|
{"1c", "1c"},
|
||||||
{"&c", "&c"},
|
{"&c", "&c"},
|
||||||
{"c c", "C c"}
|
{"c c", "C c"}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const TestCase& test : tests)
|
for (const TestCase& test : tests)
|
||||||
{
|
{
|
||||||
std::string str{ test.input };
|
std::string str{ test.input };
|
||||||
StringUtils::capitalize(str);
|
StringUtils::capitalize(str);
|
||||||
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
|
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Stringutils, date)
|
TEST(Stringutils, date)
|
||||||
{
|
{
|
||||||
const Wt::WDate date{ 2020, 01, 03 };
|
const Wt::WDate date{ 2020, 01, 03 };
|
||||||
EXPECT_EQ(StringUtils::toISO8601String(date), "2020-01-03");
|
EXPECT_EQ(StringUtils::toISO8601String(date), "2020-01-03");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Stringutils, dateTime)
|
TEST(Stringutils, dateTime)
|
||||||
{
|
{
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate {2020, 01, 03 }, Wt::WTime{9, 8, 11, 75} };
|
const Wt::WDateTime dateTime{ Wt::WDate {2020, 01, 03 }, Wt::WTime{9, 8, 11, 75} };
|
||||||
EXPECT_EQ(StringUtils::toISO8601String(dateTime), "2020-01-03T09:08:11.075");
|
EXPECT_EQ(StringUtils::toISO8601String(dateTime), "2020-01-03T09:08:11.075");
|
||||||
}
|
}
|
||||||
+145
-128
@@ -47,140 +47,156 @@
|
|||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "utils/WtLogger.hpp"
|
#include "utils/WtLogger.hpp"
|
||||||
|
|
||||||
static
|
namespace
|
||||||
std::size_t
|
|
||||||
getThreadCount()
|
|
||||||
{
|
{
|
||||||
const unsigned long configHttpServerThreadCount{ Service<IConfig>::get()->getULong("http-server-thread-count", 0) };
|
std::size_t getThreadCount()
|
||||||
|
|
||||||
// Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg)
|
|
||||||
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
std::vector<std::string>
|
|
||||||
generateWtConfig(std::string execPath)
|
|
||||||
{
|
|
||||||
std::vector<std::string> args;
|
|
||||||
|
|
||||||
const std::filesystem::path wtConfigPath{ Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml" };
|
|
||||||
const std::filesystem::path wtLogFilePath{ Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log") };
|
|
||||||
const std::filesystem::path wtAccessLogFilePath{ Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log") };
|
|
||||||
const std::filesystem::path wtResourcesPath{ Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources") };
|
|
||||||
|
|
||||||
args.push_back(execPath);
|
|
||||||
args.push_back("--config=" + wtConfigPath.string());
|
|
||||||
args.push_back("--docroot=" + std::string{ Service<IConfig>::get()->getString("docroot") });
|
|
||||||
args.push_back("--approot=" + std::string{ Service<IConfig>::get()->getString("approot") });
|
|
||||||
args.push_back("--deploy-path=" + std::string{ Service<IConfig>::get()->getString("deploy-path", "/") });
|
|
||||||
if (!wtResourcesPath.empty())
|
|
||||||
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
|
||||||
|
|
||||||
if (Service<IConfig>::get()->getBool("tls-enable", false))
|
|
||||||
{
|
{
|
||||||
args.push_back("--https-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
|
const unsigned long configHttpServerThreadCount{ Service<IConfig>::get()->getULong("http-server-thread-count", 0) };
|
||||||
args.push_back("--https-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
|
||||||
args.push_back("--ssl-certificate=" + std::string{ Service<IConfig>::get()->getString("tls-cert") });
|
// Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg)
|
||||||
args.push_back("--ssl-private-key=" + std::string{ Service<IConfig>::get()->getString("tls-key") });
|
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
|
||||||
args.push_back("--ssl-tmp-dh=" + std::string{ Service<IConfig>::get()->getString("tls-dh") });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
args.push_back("--http-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
|
|
||||||
args.push_back("--http-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wtAccessLogFilePath.empty())
|
Severity getLogMinSeverity()
|
||||||
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
|
|
||||||
|
|
||||||
args.push_back("--threads=" + std::to_string(getThreadCount()));
|
|
||||||
|
|
||||||
// Generate the wt_config.xml file
|
|
||||||
boost::property_tree::ptree pt;
|
|
||||||
|
|
||||||
pt.put("server.application-settings.<xmlattr>.location", "*");
|
|
||||||
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
|
||||||
pt.put("server.application-settings.log-config", Service<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
|
||||||
pt.put("server.application-settings.behind-reverse-proxy", Service<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
|
||||||
|
|
||||||
{
|
{
|
||||||
boost::property_tree::ptree viewport;
|
std::string_view minSeverity{ Service<IConfig>::get()->getString("log-min-severity", "info") };
|
||||||
viewport.put("<xmlattr>.name", "viewport");
|
|
||||||
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
if (minSeverity == "debug")
|
||||||
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
return Severity::DEBUG;
|
||||||
}
|
else if (minSeverity == "info")
|
||||||
{
|
return Severity::INFO;
|
||||||
boost::property_tree::ptree themeColor;
|
else if (minSeverity == "warning")
|
||||||
themeColor.put("<xmlattr>.name", "theme-color");
|
return Severity::WARNING;
|
||||||
themeColor.put("<xmlattr>.content", "#303030");
|
else if (minSeverity == "error")
|
||||||
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
return Severity::ERROR;
|
||||||
|
else if (minSeverity == "fatal")
|
||||||
|
return Severity::FATAL;
|
||||||
|
|
||||||
|
throw LmsException{ "Invalid config value for 'log-min-severity'" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> generateWtConfig(std::string execPath, Severity minSeverity)
|
||||||
{
|
{
|
||||||
std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out };
|
std::vector<std::string> args;
|
||||||
if (!oss)
|
|
||||||
throw LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" };
|
|
||||||
|
|
||||||
boost::property_tree::xml_parser::write_xml(oss, pt);
|
const std::filesystem::path wtConfigPath{ Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml" };
|
||||||
|
const std::filesystem::path wtLogFilePath{ Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log") };
|
||||||
|
const std::filesystem::path wtAccessLogFilePath{ Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log") };
|
||||||
|
const std::filesystem::path wtResourcesPath{ Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources") };
|
||||||
|
|
||||||
if (!oss)
|
args.push_back(execPath);
|
||||||
throw LmsException{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" };
|
args.push_back("--config=" + wtConfigPath.string());
|
||||||
}
|
args.push_back("--docroot=" + std::string{ Service<IConfig>::get()->getString("docroot") });
|
||||||
|
args.push_back("--approot=" + std::string{ Service<IConfig>::get()->getString("approot") });
|
||||||
|
args.push_back("--deploy-path=" + std::string{ Service<IConfig>::get()->getString("deploy-path", "/") });
|
||||||
|
if (!wtResourcesPath.empty())
|
||||||
|
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
||||||
|
|
||||||
return args;
|
if (Service<IConfig>::get()->getBool("tls-enable", false))
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static
|
|
||||||
void
|
|
||||||
proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server)
|
|
||||||
{
|
|
||||||
auto postAll{ [](Wt::WServer& server, std::function<void()> cb)
|
|
||||||
{
|
|
||||||
server.postAll([cb = std::move(cb)]
|
|
||||||
{
|
{
|
||||||
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
|
args.push_back("--https-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
|
||||||
if (LmsApp)
|
args.push_back("--https-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
||||||
cb();
|
args.push_back("--ssl-certificate=" + std::string{ Service<IConfig>::get()->getString("tls-cert") });
|
||||||
|
args.push_back("--ssl-private-key=" + std::string{ Service<IConfig>::get()->getString("tls-key") });
|
||||||
|
args.push_back("--ssl-tmp-dh=" + std::string{ Service<IConfig>::get()->getString("tls-dh") });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
args.push_back("--http-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
|
||||||
|
args.push_back("--http-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wtAccessLogFilePath.empty())
|
||||||
|
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
|
||||||
|
|
||||||
|
args.push_back("--threads=" + std::to_string(getThreadCount()));
|
||||||
|
|
||||||
|
// Generate the wt_config.xml file
|
||||||
|
boost::property_tree::ptree pt;
|
||||||
|
|
||||||
|
pt.put("server.application-settings.<xmlattr>.location", "*");
|
||||||
|
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
||||||
|
|
||||||
|
// log-config
|
||||||
|
pt.put("server.application-settings.log-config", WtLogger::computeLogConfig(minSeverity));
|
||||||
|
pt.put("server.application-settings.behind-reverse-proxy", Service<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
||||||
|
|
||||||
|
{
|
||||||
|
boost::property_tree::ptree viewport;
|
||||||
|
viewport.put("<xmlattr>.name", "viewport");
|
||||||
|
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
||||||
|
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
boost::property_tree::ptree themeColor;
|
||||||
|
themeColor.put("<xmlattr>.name", "theme-color");
|
||||||
|
themeColor.put("<xmlattr>.content", "#303030");
|
||||||
|
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out };
|
||||||
|
if (!oss)
|
||||||
|
throw LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" };
|
||||||
|
|
||||||
|
boost::property_tree::xml_parser::write_xml(oss, pt);
|
||||||
|
|
||||||
|
if (!oss)
|
||||||
|
throw LmsException{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
void proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server)
|
||||||
|
{
|
||||||
|
auto postAll{ [](Wt::WServer& server, std::function<void()> cb)
|
||||||
|
{
|
||||||
|
server.postAll([cb = std::move(cb)]
|
||||||
|
{
|
||||||
|
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
|
||||||
|
if (LmsApp)
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
} };
|
||||||
|
|
||||||
|
scanner.getEvents().scanStarted.connect([&]
|
||||||
|
{
|
||||||
|
postAll(server, []
|
||||||
|
{
|
||||||
|
LmsApp->getScannerEvents().scanStarted.emit();
|
||||||
|
LmsApp->triggerUpdate();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} };
|
|
||||||
|
|
||||||
scanner.getEvents().scanStarted.connect([&]
|
scanner.getEvents().scanComplete.connect([&](const Scanner::ScanStats& stats)
|
||||||
{
|
{
|
||||||
postAll(server, []
|
postAll(server, [=]
|
||||||
{
|
{
|
||||||
LmsApp->getScannerEvents().scanStarted.emit();
|
LmsApp->getScannerEvents().scanComplete.emit(stats);
|
||||||
LmsApp->triggerUpdate();
|
LmsApp->triggerUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
scanner.getEvents().scanComplete.connect([&](const Scanner::ScanStats& stats)
|
scanner.getEvents().scanInProgress.connect([&](const Scanner::ScanStepStats& stats)
|
||||||
{
|
{
|
||||||
postAll(server, [=]
|
postAll(server, [=]
|
||||||
{
|
{
|
||||||
LmsApp->getScannerEvents().scanComplete.emit(stats);
|
LmsApp->getScannerEvents().scanInProgress.emit(stats);
|
||||||
LmsApp->triggerUpdate();
|
LmsApp->triggerUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
scanner.getEvents().scanInProgress.connect([&](const Scanner::ScanStepStats& stats)
|
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime)
|
||||||
{
|
{
|
||||||
postAll(server, [=]
|
postAll(server, [=]
|
||||||
{
|
{
|
||||||
LmsApp->getScannerEvents().scanInProgress.emit(stats);
|
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
|
||||||
LmsApp->triggerUpdate();
|
LmsApp->triggerUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime)
|
|
||||||
{
|
|
||||||
postAll(server, [=]
|
|
||||||
{
|
|
||||||
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
|
|
||||||
LmsApp->triggerUpdate();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
@@ -206,20 +222,21 @@ int main(int argc, char* argv[])
|
|||||||
close(STDIN_FILENO);
|
close(STDIN_FILENO);
|
||||||
|
|
||||||
Service<IConfig> config{ createConfig(configFilePath) };
|
Service<IConfig> config{ createConfig(configFilePath) };
|
||||||
Service<Logger> logger{ std::make_unique<WtLogger>() };
|
const Severity minLogSeverity{getLogMinSeverity()};
|
||||||
|
Service<ILogger> logger{ std::make_unique<WtLogger>(minLogSeverity) };
|
||||||
|
|
||||||
// use system locale. libarchive relies on this to write filenames
|
// use system locale. libarchive relies on this to write filenames
|
||||||
if (char* locale{ ::setlocale(LC_ALL, "") })
|
if (char* locale{ ::setlocale(LC_ALL, "") })
|
||||||
LMS_LOG(MAIN, INFO) << "locale set to '" << locale << "'";
|
LMS_LOG(MAIN, INFO, "locale set to '" << locale << "'");
|
||||||
else
|
else
|
||||||
LMS_LOG(MAIN, WARNING) << "Cannot set locale from system";
|
LMS_LOG(MAIN, WARNING, "Cannot set locale from system");
|
||||||
|
|
||||||
// Make sure the working directory exists
|
// Make sure the working directory exists
|
||||||
std::filesystem::create_directories(config->getPath("working-dir"));
|
std::filesystem::create_directories(config->getPath("working-dir"));
|
||||||
std::filesystem::create_directories(config->getPath("working-dir") / "cache");
|
std::filesystem::create_directories(config->getPath("working-dir") / "cache");
|
||||||
|
|
||||||
// Construct WT configuration and get the argc/argv back
|
// Construct WT configuration and get the argc/argv back
|
||||||
const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0]) };
|
const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0], minLogSeverity) };
|
||||||
|
|
||||||
std::vector<const char*> wtArgv(wtServerArgs.size());
|
std::vector<const char*> wtArgv(wtServerArgs.size());
|
||||||
for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
|
for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
|
||||||
@@ -300,27 +317,27 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
proxyScannerEventsToApplication(*scannerService, server);
|
proxyScannerEventsToApplication(*scannerService, server);
|
||||||
|
|
||||||
LMS_LOG(MAIN, INFO) << "Starting server...";
|
LMS_LOG(MAIN, INFO, "Starting server...");
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
LMS_LOG(MAIN, INFO) << "Now running...";
|
LMS_LOG(MAIN, INFO, "Now running...");
|
||||||
Wt::WServer::waitForShutdown();
|
Wt::WServer::waitForShutdown();
|
||||||
|
|
||||||
LMS_LOG(MAIN, INFO) << "Stopping server...";
|
LMS_LOG(MAIN, INFO, "Stopping server...");
|
||||||
server.stop();
|
server.stop();
|
||||||
|
|
||||||
LMS_LOG(MAIN, INFO) << "Quitting...";
|
LMS_LOG(MAIN, INFO, "Quitting...");
|
||||||
res = EXIT_SUCCESS;
|
res = EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
catch (const Wt::WServer::Exception& e)
|
catch (const Wt::WServer::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(MAIN, FATAL) << "Caught WServer::Exception: " << e.what();
|
LMS_LOG(MAIN, FATAL, "Caught WServer::Exception: " << e.what());
|
||||||
std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl;
|
std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl;
|
||||||
res = EXIT_FAILURE;
|
res = EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
catch (const std::exception& e)
|
catch (const std::exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(MAIN, FATAL) << "Caught std::exception: " << e.what();
|
LMS_LOG(MAIN, FATAL, "Caught std::exception: " << e.what());
|
||||||
std::cerr << "Caught std::exception: " << e.what() << std::endl;
|
std::cerr << "Caught std::exception: " << e.what() << std::endl;
|
||||||
res = EXIT_FAILURE;
|
res = EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@
|
|||||||
#include "services/auth/IPasswordService.hpp"
|
#include "services/auth/IPasswordService.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
#include "common/LoginNameValidator.hpp"
|
#include "common/LoginNameValidator.hpp"
|
||||||
|
|||||||
+420
-447
@@ -37,7 +37,7 @@
|
|||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
@@ -62,534 +62,507 @@
|
|||||||
#include "PlayQueue.hpp"
|
#include "PlayQueue.hpp"
|
||||||
#include "SettingsView.hpp"
|
#include "SettingsView.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface
|
||||||
|
|
||||||
static
|
|
||||||
std::shared_ptr<Wt::WMessageResourceBundle>
|
|
||||||
createMessageResourceBundle()
|
|
||||||
{
|
{
|
||||||
const std::string appRoot {Wt::WApplication::appRoot()};
|
namespace
|
||||||
|
{
|
||||||
|
constexpr const char* defaultPath{ "/releases" };
|
||||||
|
|
||||||
auto res {std::make_shared<Wt::WMessageResourceBundle>()};
|
std::shared_ptr<Wt::WMessageResourceBundle> createMessageResourceBundle()
|
||||||
res->use(appRoot + "admin-database");
|
{
|
||||||
res->use(appRoot + "admin-initwizard");
|
const std::string appRoot{ Wt::WApplication::appRoot() };
|
||||||
res->use(appRoot + "admin-scannercontroller");
|
|
||||||
res->use(appRoot + "admin-user");
|
|
||||||
res->use(appRoot + "admin-users");
|
|
||||||
res->use(appRoot + "artist");
|
|
||||||
res->use(appRoot + "artists");
|
|
||||||
res->use(appRoot + "error");
|
|
||||||
res->use(appRoot + "explore");
|
|
||||||
res->use(appRoot + "login");
|
|
||||||
res->use(appRoot + "main");
|
|
||||||
res->use(appRoot + "mediaplayer");
|
|
||||||
res->use(appRoot + "messages");
|
|
||||||
res->use(appRoot + "misc");
|
|
||||||
res->use(appRoot + "notifications");
|
|
||||||
res->use(appRoot + "playqueue");
|
|
||||||
res->use(appRoot + "release");
|
|
||||||
res->use(appRoot + "releases");
|
|
||||||
res->use(appRoot + "search");
|
|
||||||
res->use(appRoot + "settings");
|
|
||||||
res->use(appRoot + "tracklist");
|
|
||||||
res->use(appRoot + "tracklists");
|
|
||||||
res->use(appRoot + "tracks");
|
|
||||||
|
|
||||||
return res;
|
auto res{ std::make_shared<Wt::WMessageResourceBundle>() };
|
||||||
}
|
res->use(appRoot + "admin-database");
|
||||||
|
res->use(appRoot + "admin-initwizard");
|
||||||
|
res->use(appRoot + "admin-scannercontroller");
|
||||||
|
res->use(appRoot + "admin-user");
|
||||||
|
res->use(appRoot + "admin-users");
|
||||||
|
res->use(appRoot + "artist");
|
||||||
|
res->use(appRoot + "artists");
|
||||||
|
res->use(appRoot + "error");
|
||||||
|
res->use(appRoot + "explore");
|
||||||
|
res->use(appRoot + "login");
|
||||||
|
res->use(appRoot + "main");
|
||||||
|
res->use(appRoot + "mediaplayer");
|
||||||
|
res->use(appRoot + "messages");
|
||||||
|
res->use(appRoot + "misc");
|
||||||
|
res->use(appRoot + "notifications");
|
||||||
|
res->use(appRoot + "playqueue");
|
||||||
|
res->use(appRoot + "release");
|
||||||
|
res->use(appRoot + "releases");
|
||||||
|
res->use(appRoot + "search");
|
||||||
|
res->use(appRoot + "settings");
|
||||||
|
res->use(appRoot + "tracklist");
|
||||||
|
res->use(appRoot + "tracklists");
|
||||||
|
res->use(appRoot + "tracks");
|
||||||
|
|
||||||
static
|
return res;
|
||||||
std::shared_ptr<Wt::WMessageResourceBundle>
|
}
|
||||||
getOrCreateMessageBundle()
|
|
||||||
{
|
|
||||||
static std::shared_ptr<Wt::WMessageResourceBundle> res {createMessageResourceBundle()};
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
static constexpr const char* defaultPath {"/releases"};
|
std::shared_ptr<Wt::WMessageResourceBundle> getOrCreateMessageBundle()
|
||||||
|
{
|
||||||
|
static std::shared_ptr<Wt::WMessageResourceBundle> res{ createMessageResourceBundle() };
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WApplication>
|
enum IdxRoot
|
||||||
LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager)
|
{
|
||||||
{
|
IdxExplore = 0,
|
||||||
if (auto *authEnvService {Service<::Auth::IEnvService>::get()})
|
IdxPlayQueue,
|
||||||
{
|
IdxSettings,
|
||||||
const auto checkResult {authEnvService->processEnv(env)};
|
IdxAdminDatabase,
|
||||||
if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
|
IdxAdminUsers,
|
||||||
{
|
IdxAdminUser,
|
||||||
LMS_LOG(UI, ERROR) << "Cannot authenticate user from environment!";
|
};
|
||||||
// return a blank page
|
|
||||||
return std::make_unique<Wt::WApplication>(env);
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::make_unique<LmsApplication>(env, db, appManager, checkResult.userId);
|
void handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
||||||
}
|
{
|
||||||
|
static const struct
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
int index;
|
||||||
|
bool admin;
|
||||||
|
std::optional<Wt::WString> title;
|
||||||
|
} views[] =
|
||||||
|
{
|
||||||
|
{ "/artists", IdxExplore, false, Wt::WString::tr("Lms.Explore.artists") },
|
||||||
|
{ "/artist", IdxExplore, false, std::nullopt },
|
||||||
|
{ "/releases", IdxExplore, false, Wt::WString::tr("Lms.Explore.releases") },
|
||||||
|
{ "/release", IdxExplore, false, std::nullopt },
|
||||||
|
{ "/search", IdxExplore, false, Wt::WString::tr("Lms.Explore.search") },
|
||||||
|
{ "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") },
|
||||||
|
{ "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") },
|
||||||
|
{ "/tracklist", IdxExplore, false, std::nullopt },
|
||||||
|
{ "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") },
|
||||||
|
{ "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") },
|
||||||
|
{ "/admin/database", IdxAdminDatabase, true, Wt::WString::tr("Lms.Admin.Database.database") },
|
||||||
|
{ "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") },
|
||||||
|
{ "/admin/user", IdxAdminUser, true, std::nullopt },
|
||||||
|
};
|
||||||
|
|
||||||
return std::make_unique<LmsApplication>(env, db, appManager);
|
LMS_LOG(UI, DEBUG, "Internal path changed to '" << wApp->internalPath() << "'");
|
||||||
}
|
|
||||||
|
|
||||||
LmsApplication*
|
for (const auto& view : views)
|
||||||
LmsApplication::instance()
|
{
|
||||||
{
|
if (wApp->internalPathMatches(view.path))
|
||||||
return reinterpret_cast<LmsApplication*>(Wt::WApplication::instance());
|
{
|
||||||
}
|
if (view.admin && !isAdmin)
|
||||||
|
break;
|
||||||
|
|
||||||
Database::Db&
|
stack.setCurrentIndex(view.index);
|
||||||
LmsApplication::getDb()
|
if (view.title)
|
||||||
{
|
LmsApp->setTitle(*view.title);
|
||||||
return _db;
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::Session&
|
LmsApp->doJavaScript(LmsApp->javaScriptClass() + ".updateActiveNav('" + view.path + "')");
|
||||||
LmsApplication::getDbSession()
|
return;
|
||||||
{
|
}
|
||||||
return _db.getTLSSession();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Database::User::pointer
|
wApp->setInternalPath(defaultPath, true);
|
||||||
LmsApplication::getUser()
|
}
|
||||||
{
|
}
|
||||||
if (!_authenticatedUser)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
return Database::User::find(getDbSession(), _authenticatedUser->userId);
|
std::unique_ptr<Wt::WApplication> LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager)
|
||||||
}
|
{
|
||||||
|
if (auto * authEnvService{ Service<::Auth::IEnvService>::get() })
|
||||||
|
{
|
||||||
|
const auto checkResult{ authEnvService->processEnv(env) };
|
||||||
|
if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR, "Cannot authenticate user from environment!");
|
||||||
|
// return a blank page
|
||||||
|
return std::make_unique<Wt::WApplication>(env);
|
||||||
|
}
|
||||||
|
|
||||||
Database::UserId
|
return std::make_unique<LmsApplication>(env, db, appManager, checkResult.userId);
|
||||||
LmsApplication::getUserId()
|
}
|
||||||
{
|
|
||||||
return _authenticatedUser->userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
return std::make_unique<LmsApplication>(env, db, appManager);
|
||||||
LmsApplication::isUserAuthStrong() const
|
}
|
||||||
{
|
|
||||||
return _authenticatedUser->strongAuth;
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::UserType
|
LmsApplication* LmsApplication::instance()
|
||||||
LmsApplication::getUserType()
|
{
|
||||||
{
|
return static_cast<LmsApplication*>(Wt::WApplication::instance());
|
||||||
auto transaction {getDbSession().createReadTransaction()};
|
}
|
||||||
|
|
||||||
return getUser()->getType();
|
Database::Db& LmsApplication::getDb()
|
||||||
}
|
{
|
||||||
|
return _db;
|
||||||
|
}
|
||||||
|
|
||||||
std::string
|
Database::Session& LmsApplication::getDbSession()
|
||||||
LmsApplication::getUserLoginName()
|
{
|
||||||
{
|
return _db.getTLSSession();
|
||||||
auto transaction {getDbSession().createReadTransaction()};
|
}
|
||||||
|
|
||||||
return getUser()->getLoginName();
|
Database::User::pointer LmsApplication::getUser()
|
||||||
}
|
{
|
||||||
|
if (!_authenticatedUser)
|
||||||
|
return {};
|
||||||
|
|
||||||
LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
return Database::User::find(getDbSession(), _authenticatedUser->userId);
|
||||||
Database::Db& db,
|
}
|
||||||
LmsApplicationManager& appManager,
|
|
||||||
std::optional<Database::UserId> userId)
|
|
||||||
: Wt::WApplication {env}
|
|
||||||
, _db {db}
|
|
||||||
, _appManager {appManager}
|
|
||||||
, _authenticatedUser {userId ? std::make_optional<UserAuthInfo>(UserAuthInfo {*userId, false}) : std::nullopt}
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
catch (LmsApplicationException& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, WARNING) << "Caught a LmsApplication exception: " << e.what();
|
|
||||||
handleException(e);
|
|
||||||
}
|
|
||||||
catch (std::exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Caught exception: " << e.what();
|
|
||||||
throw LmsException {"Internal error"}; // Do not put details here at it may appear on the user rendered html
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LmsApplication::~LmsApplication() = default;
|
Database::UserId LmsApplication::getUserId()
|
||||||
|
{
|
||||||
|
return _authenticatedUser->userId;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
bool LmsApplication::isUserAuthStrong() const
|
||||||
LmsApplication::init()
|
{
|
||||||
{
|
return _authenticatedUser->strongAuth;
|
||||||
setTheme(std::make_shared<LmsTheme>());
|
}
|
||||||
|
|
||||||
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
Database::UserType LmsApplication::getUserType()
|
||||||
require("js/mediaplayer.js");
|
{
|
||||||
|
auto transaction{ getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
setTitle();
|
return getUser()->getType();
|
||||||
setLocalizedStrings(getOrCreateMessageBundle());
|
}
|
||||||
|
|
||||||
// Handle Media Scanner events and other session events
|
std::string LmsApplication::getUserLoginName()
|
||||||
enableUpdates(true);
|
{
|
||||||
|
auto transaction{ getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
if (_authenticatedUser)
|
return getUser()->getLoginName();
|
||||||
onUserLoggedIn();
|
}
|
||||||
else if (Service<::Auth::IPasswordService>::exists())
|
|
||||||
processPasswordAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||||
LmsApplication::processPasswordAuth()
|
Database::Db& db,
|
||||||
{
|
LmsApplicationManager& appManager,
|
||||||
{
|
std::optional<Database::UserId> userId)
|
||||||
std::optional<Database::UserId> userId {processAuthToken(environment())};
|
: Wt::WApplication{ env }
|
||||||
if (userId)
|
, _db{ db }
|
||||||
{
|
, _appManager{ appManager }
|
||||||
LMS_LOG(UI, DEBUG) << "User authenticated using Auth token!";
|
, _authenticatedUser{ userId ? std::make_optional<UserAuthInfo>(UserAuthInfo {*userId, false}) : std::nullopt }
|
||||||
_authenticatedUser = {*userId, false};
|
{
|
||||||
onUserLoggedIn();
|
try
|
||||||
return;
|
{
|
||||||
}
|
init();
|
||||||
}
|
}
|
||||||
|
catch (LmsApplicationException& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, WARNING, "Caught a LmsApplication exception: " << e.what());
|
||||||
|
handleException(e);
|
||||||
|
}
|
||||||
|
catch (std::exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR, "Caught exception: " << e.what());
|
||||||
|
throw LmsException{ "Internal error" }; // Do not put details here at it may appear on the user rendered html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If here is no account in the database, launch the first connection wizard
|
LmsApplication::~LmsApplication() = default;
|
||||||
bool firstConnection {};
|
|
||||||
{
|
|
||||||
auto transaction {getDbSession().createReadTransaction()};
|
|
||||||
firstConnection = Database::User::getCount(getDbSession()) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection;
|
void LmsApplication::init()
|
||||||
|
{
|
||||||
|
setTheme(std::make_shared<LmsTheme>());
|
||||||
|
|
||||||
if (firstConnection && Service<::Auth::IPasswordService>::get()->canSetPasswords())
|
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||||
{
|
require("js/mediaplayer.js");
|
||||||
root()->addWidget(std::make_unique<InitWizardView>());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Auth* auth {root()->addNew<Auth>()};
|
|
||||||
auth->userLoggedIn.connect(this, [this](Database::UserId userId)
|
|
||||||
{
|
|
||||||
_authenticatedUser = {userId, true};
|
|
||||||
onUserLoggedIn();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
setTitle();
|
||||||
LmsApplication::finalize()
|
setLocalizedStrings(getOrCreateMessageBundle());
|
||||||
{
|
|
||||||
if (_authenticatedUser)
|
|
||||||
_appManager.unregisterApplication(*this);
|
|
||||||
|
|
||||||
preQuit().emit();
|
// Handle Media Scanner events and other session events
|
||||||
}
|
enableUpdates(true);
|
||||||
|
|
||||||
void
|
if (_authenticatedUser)
|
||||||
LmsApplication::handleException(LmsApplicationException& e)
|
onUserLoggedIn();
|
||||||
{
|
else if (Service<::Auth::IPasswordService>::exists())
|
||||||
root()->clear();
|
processPasswordAuth();
|
||||||
Wt::WTemplate* t {root()->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Error.template"))};
|
}
|
||||||
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
||||||
|
|
||||||
t->bindString("error", e.what(), Wt::TextFormat::Plain);
|
void LmsApplication::processPasswordAuth()
|
||||||
Wt::WPushButton* btn {t->bindNew<Wt::WPushButton>("btn-go-home", Wt::WString::tr("Lms.Error.go-home"))};
|
{
|
||||||
btn->clicked().connect([this]()
|
{
|
||||||
{
|
std::optional<Database::UserId> userId{ processAuthToken(environment()) };
|
||||||
redirect(defaultPath);
|
if (userId)
|
||||||
});
|
{
|
||||||
}
|
LMS_LOG(UI, DEBUG, "User authenticated using Auth token!");
|
||||||
|
_authenticatedUser = { *userId, false };
|
||||||
|
onUserLoggedIn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
// If here is no account in the database, launch the first connection wizard
|
||||||
LmsApplication::goHomeAndQuit()
|
bool firstConnection{};
|
||||||
{
|
{
|
||||||
WApplication::quit("");
|
auto transaction{ getDbSession().createReadTransaction() };
|
||||||
redirect(".");
|
firstConnection = Database::User::getCount(getDbSession()) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum IdxRoot
|
LMS_LOG(UI, DEBUG, "Creating root widget. First connection = " << firstConnection);
|
||||||
{
|
|
||||||
IdxExplore = 0,
|
|
||||||
IdxPlayQueue,
|
|
||||||
IdxSettings,
|
|
||||||
IdxAdminDatabase,
|
|
||||||
IdxAdminUsers,
|
|
||||||
IdxAdminUser,
|
|
||||||
};
|
|
||||||
|
|
||||||
static
|
if (firstConnection && Service<::Auth::IPasswordService>::get()->canSetPasswords())
|
||||||
void
|
{
|
||||||
handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
root()->addWidget(std::make_unique<InitWizardView>());
|
||||||
{
|
}
|
||||||
static const struct
|
else
|
||||||
{
|
{
|
||||||
std::string path;
|
Auth* auth{ root()->addNew<Auth>() };
|
||||||
int index;
|
auth->userLoggedIn.connect(this, [this](Database::UserId userId)
|
||||||
bool admin;
|
{
|
||||||
std::optional<Wt::WString> title;
|
_authenticatedUser = { userId, true };
|
||||||
} views[] =
|
onUserLoggedIn();
|
||||||
{
|
});
|
||||||
{ "/artists", IdxExplore, false, Wt::WString::tr("Lms.Explore.artists") },
|
}
|
||||||
{ "/artist", IdxExplore, false, std::nullopt },
|
}
|
||||||
{ "/releases", IdxExplore, false, Wt::WString::tr("Lms.Explore.releases") },
|
|
||||||
{ "/release", IdxExplore, false, std::nullopt },
|
|
||||||
{ "/search", IdxExplore, false, Wt::WString::tr("Lms.Explore.search") },
|
|
||||||
{ "/tracks", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracks") },
|
|
||||||
{ "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") },
|
|
||||||
{ "/tracklist", IdxExplore, false, std::nullopt },
|
|
||||||
{ "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") },
|
|
||||||
{ "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") },
|
|
||||||
{ "/admin/database", IdxAdminDatabase, true, Wt::WString::tr("Lms.Admin.Database.database") },
|
|
||||||
{ "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") },
|
|
||||||
{ "/admin/user", IdxAdminUser, true, std::nullopt },
|
|
||||||
};
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
void LmsApplication::finalize()
|
||||||
|
{
|
||||||
|
if (_authenticatedUser)
|
||||||
|
_appManager.unregisterApplication(*this);
|
||||||
|
|
||||||
for (const auto& view : views)
|
preQuit().emit();
|
||||||
{
|
}
|
||||||
if (wApp->internalPathMatches(view.path))
|
|
||||||
{
|
|
||||||
if (view.admin && !isAdmin)
|
|
||||||
break;
|
|
||||||
|
|
||||||
stack.setCurrentIndex(view.index);
|
void LmsApplication::handleException(LmsApplicationException& e)
|
||||||
if (view.title)
|
{
|
||||||
LmsApp->setTitle(*view.title);
|
root()->clear();
|
||||||
|
Wt::WTemplate* t{ root()->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Error.template")) };
|
||||||
|
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
LmsApp->doJavaScript(LmsApp->javaScriptClass() + ".updateActiveNav('" + view.path +"')");
|
t->bindString("error", e.what(), Wt::TextFormat::Plain);
|
||||||
return;
|
Wt::WPushButton* btn{ t->bindNew<Wt::WPushButton>("btn-go-home", Wt::WString::tr("Lms.Error.go-home")) };
|
||||||
}
|
btn->clicked().connect([this]()
|
||||||
}
|
{
|
||||||
|
redirect(defaultPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
wApp->setInternalPath(defaultPath, true);
|
void LmsApplication::goHomeAndQuit()
|
||||||
}
|
{
|
||||||
|
WApplication::quit("");
|
||||||
|
redirect(".");
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void LmsApplication::logoutUser()
|
||||||
LmsApplication::logoutUser()
|
{
|
||||||
{
|
{
|
||||||
{
|
auto transaction{ getDbSession().createWriteTransaction() };
|
||||||
auto transaction {getDbSession().createWriteTransaction()};
|
getUser().modify()->clearAuthTokens();
|
||||||
getUser().modify()->clearAuthTokens();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out";
|
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << " 'logged out");
|
||||||
goHomeAndQuit();
|
goHomeAndQuit();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void LmsApplication::onUserLoggedIn()
|
||||||
LmsApplication::onUserLoggedIn()
|
{
|
||||||
{
|
root()->clear();
|
||||||
root()->clear();
|
|
||||||
|
|
||||||
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent();
|
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent());
|
||||||
|
|
||||||
_appManager.registerApplication(*this);
|
_appManager.registerApplication(*this);
|
||||||
_appManager.applicationRegistered.connect(this, [this] (LmsApplication& otherApplication)
|
_appManager.applicationRegistered.connect(this, [this](LmsApplication& otherApplication)
|
||||||
{
|
{
|
||||||
// Only one active session by user
|
// Only one active session by user
|
||||||
if (otherApplication.getUserId() == getUserId())
|
if (otherApplication.getUserId() == getUserId())
|
||||||
{
|
{
|
||||||
if (LmsApp->getUserType() != Database::UserType::DEMO)
|
if (LmsApp->getUserType() != Database::UserType::DEMO)
|
||||||
{
|
{
|
||||||
quit(Wt::WString::tr("Lms.quit-other-session"));
|
quit(Wt::WString::tr("Lms.quit-other-session"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
createHome();
|
createHome();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void LmsApplication::createHome()
|
||||||
LmsApplication::createHome()
|
{
|
||||||
{
|
_coverResource = std::make_shared<CoverResource>();
|
||||||
_coverResource = std::make_shared<CoverResource>();
|
|
||||||
|
|
||||||
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
||||||
declareJavaScriptFunction("updateActiveNav",
|
declareJavaScriptFunction("updateActiveNav",
|
||||||
R"(function(current) {
|
R"(function(current) {
|
||||||
const menuItems = document.querySelectorAll('.nav-item a[href]:not([href=""])');
|
const menuItems = document.querySelectorAll('.nav-item a[href]:not([href=""])');
|
||||||
for (const menuItem of menuItems) {
|
for (const menuItem of menuItems) {
|
||||||
if (menuItem.getAttribute("href") === current) {
|
if (menuItem.getAttribute("href") === current) {
|
||||||
menuItem.classList.add('active');
|
menuItem.classList.add('active');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menuItem.classList.remove('active');
|
menuItem.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})");
|
})");
|
||||||
|
|
||||||
Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.main.template")))};
|
Wt::WTemplate* main{ root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.main.template"))) };
|
||||||
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
Template* navbar {main->bindNew<Template>("navbar", Wt::WString::tr("Lms.main.template.navbar"))};
|
Template* navbar{ main->bindNew<Template>("navbar", Wt::WString::tr("Lms.main.template.navbar")) };
|
||||||
navbar->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
navbar->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
_notificationContainer = main->bindNew<NotificationContainer>("notifications");
|
_notificationContainer = main->bindNew<NotificationContainer>("notifications");
|
||||||
_modalManager = main->bindNew<ModalManager>("modal");
|
_modalManager = main->bindNew<ModalManager>("modal");
|
||||||
|
|
||||||
// MediaPlayer
|
// MediaPlayer
|
||||||
_mediaPlayer = main->bindNew<MediaPlayer>("player");
|
_mediaPlayer = main->bindNew<MediaPlayer>("player");
|
||||||
|
|
||||||
navbar->bindNew<Wt::WAnchor>("title", Wt::WLink {Wt::LinkType::InternalPath, defaultPath}, "LMS");
|
navbar->bindNew<Wt::WAnchor>("title", Wt::WLink{ Wt::LinkType::InternalPath, defaultPath }, "LMS");
|
||||||
navbar->bindNew<Wt::WAnchor>("artists", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}, Wt::WString::tr("Lms.Explore.artists"));
|
navbar->bindNew<Wt::WAnchor>("artists", Wt::WLink{ Wt::LinkType::InternalPath, "/artists" }, Wt::WString::tr("Lms.Explore.artists"));
|
||||||
navbar->bindNew<Wt::WAnchor>("releases", Wt::WLink {Wt::LinkType::InternalPath, "/releases"}, Wt::WString::tr("Lms.Explore.releases"));
|
navbar->bindNew<Wt::WAnchor>("releases", Wt::WLink{ Wt::LinkType::InternalPath, "/releases" }, Wt::WString::tr("Lms.Explore.releases"));
|
||||||
navbar->bindNew<Wt::WAnchor>("tracks", Wt::WLink {Wt::LinkType::InternalPath, "/tracks"}, Wt::WString::tr("Lms.Explore.tracks"));
|
navbar->bindNew<Wt::WAnchor>("tracks", Wt::WLink{ Wt::LinkType::InternalPath, "/tracks" }, Wt::WString::tr("Lms.Explore.tracks"));
|
||||||
navbar->bindNew<Wt::WAnchor>("tracklists", Wt::WLink {Wt::LinkType::InternalPath, "/tracklists"}, Wt::WString::tr("Lms.Explore.tracklists"));
|
navbar->bindNew<Wt::WAnchor>("tracklists", Wt::WLink{ Wt::LinkType::InternalPath, "/tracklists" }, Wt::WString::tr("Lms.Explore.tracklists"));
|
||||||
|
|
||||||
Filters* filters {navbar->bindNew<Filters>("filters")};
|
Filters* filters{ navbar->bindNew<Filters>("filters") };
|
||||||
navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
|
navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
|
||||||
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
|
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink{ Wt::LinkType::InternalPath, "/settings" }, Wt::WString::tr("Lms.Settings.menu-settings"));
|
||||||
|
|
||||||
{
|
{
|
||||||
Wt::WAnchor* logout {navbar->bindNew<Wt::WAnchor>("logout")};
|
Wt::WAnchor* logout{ navbar->bindNew<Wt::WAnchor>("logout") };
|
||||||
logout->setText(Wt::WString::tr("Lms.logout"));
|
logout->setText(Wt::WString::tr("Lms.logout"));
|
||||||
logout->clicked().connect(this, &LmsApplication::logoutUser);
|
logout->clicked().connect(this, &LmsApplication::logoutUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WLineEdit* searchEdit {navbar->bindNew<Wt::WLineEdit>("search")};
|
Wt::WLineEdit* searchEdit{ navbar->bindNew<Wt::WLineEdit>("search") };
|
||||||
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||||
|
|
||||||
if (LmsApp->getUserType() == Database::UserType::ADMIN)
|
if (LmsApp->getUserType() == Database::UserType::ADMIN)
|
||||||
{
|
{
|
||||||
navbar->setCondition("if-is-admin", true);
|
navbar->setCondition("if-is-admin", true);
|
||||||
navbar->bindNew<Wt::WAnchor>("database", Wt::WLink {Wt::LinkType::InternalPath, "/admin/database"}, Wt::WString::tr("Lms.Admin.Database.menu-database"));
|
navbar->bindNew<Wt::WAnchor>("database", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/database" }, Wt::WString::tr("Lms.Admin.Database.menu-database"));
|
||||||
navbar->bindNew<Wt::WAnchor>("users", Wt::WLink {Wt::LinkType::InternalPath, "/admin/users"}, Wt::WString::tr("Lms.Admin.Users.menu-users"));
|
navbar->bindNew<Wt::WAnchor>("users", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/users" }, Wt::WString::tr("Lms.Admin.Users.menu-users"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contents
|
// Contents
|
||||||
// Order is important in mainStack, see IdxRoot!
|
// Order is important in mainStack, see IdxRoot!
|
||||||
Wt::WStackedWidget* mainStack {main->bindNew<Wt::WStackedWidget>("contents")};
|
Wt::WStackedWidget* mainStack{ main->bindNew<Wt::WStackedWidget>("contents") };
|
||||||
mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
||||||
|
|
||||||
std::unique_ptr<PlayQueue> playQueue {std::make_unique<PlayQueue>()};
|
std::unique_ptr<PlayQueue> playQueue{ std::make_unique<PlayQueue>() };
|
||||||
Explore* explore {mainStack->addNew<Explore>(*filters, *playQueue)};
|
Explore* explore{ mainStack->addNew<Explore>(*filters, *playQueue) };
|
||||||
_playQueue = mainStack->addWidget(std::move(playQueue));
|
_playQueue = mainStack->addWidget(std::move(playQueue));
|
||||||
mainStack->addNew<SettingsView>();
|
mainStack->addNew<SettingsView>();
|
||||||
|
|
||||||
searchEdit->enterPressed().connect([=]
|
searchEdit->enterPressed().connect([=]
|
||||||
{
|
{
|
||||||
setInternalPath("/search", true);
|
setInternalPath("/search", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
searchEdit->textInput().connect([=]
|
searchEdit->textInput().connect([=]
|
||||||
{
|
{
|
||||||
setInternalPath("/search", true);
|
setInternalPath("/search", true);
|
||||||
explore->search(searchEdit->text());
|
explore->search(searchEdit->text());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin stuff
|
// Admin stuff
|
||||||
if (getUserType() == Database::UserType::ADMIN)
|
if (getUserType() == Database::UserType::ADMIN)
|
||||||
{
|
{
|
||||||
mainStack->addNew<DatabaseSettingsView>();
|
mainStack->addNew<DatabaseSettingsView>();
|
||||||
mainStack->addNew<UsersView>();
|
mainStack->addNew<UsersView>();
|
||||||
mainStack->addNew<UserView>();
|
mainStack->addNew<UserView>();
|
||||||
}
|
}
|
||||||
|
|
||||||
explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity());
|
explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity());
|
||||||
|
|
||||||
// Events from MediaPlayer
|
// Events from MediaPlayer
|
||||||
_mediaPlayer->playNext.connect([this]
|
_mediaPlayer->playNext.connect([this]
|
||||||
{
|
{
|
||||||
_playQueue->playNext();
|
_playQueue->playNext();
|
||||||
});
|
});
|
||||||
_mediaPlayer->playPrevious.connect([this]
|
_mediaPlayer->playPrevious.connect([this]
|
||||||
{
|
{
|
||||||
_playQueue->playPrevious();
|
_playQueue->playPrevious();
|
||||||
});
|
});
|
||||||
|
|
||||||
_mediaPlayer->scrobbleListenNow.connect([this](Database::TrackId trackId)
|
_mediaPlayer->scrobbleListenNow.connect([this](Database::TrackId trackId)
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, DEBUG) << "Received ScrobbleListenNow from player for trackId = " << trackId.toString();
|
LMS_LOG(UI, DEBUG, "Received ScrobbleListenNow from player for trackId = " << trackId.toString());
|
||||||
const Scrobbling::Listen listen {getUserId(), trackId};
|
const Scrobbling::Listen listen{ getUserId(), trackId };
|
||||||
Service<Scrobbling::IScrobblingService>::get()->listenStarted(listen);
|
Service<Scrobbling::IScrobblingService>::get()->listenStarted(listen);
|
||||||
});
|
});
|
||||||
_mediaPlayer->scrobbleListenFinished.connect([this](Database::TrackId trackId, unsigned durationMs)
|
_mediaPlayer->scrobbleListenFinished.connect([this](Database::TrackId trackId, unsigned durationMs)
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, DEBUG) << "Received ScrobbleListenFinished from player for trackId = " << trackId.toString() << ", duration = " << (durationMs / 1000) << "s";
|
LMS_LOG(UI, DEBUG, "Received ScrobbleListenFinished from player for trackId = " << trackId.toString() << ", duration = " << (durationMs / 1000) << "s");
|
||||||
const std::chrono::milliseconds duration {durationMs};
|
const std::chrono::milliseconds duration{ durationMs };
|
||||||
const Scrobbling::Listen listen {getUserId(), trackId};
|
const Scrobbling::Listen listen{ getUserId(), trackId };
|
||||||
Service<Scrobbling::IScrobblingService>::get()->listenFinished(listen, std::chrono::duration_cast<std::chrono::seconds>(duration));
|
Service<Scrobbling::IScrobblingService>::get()->listenFinished(listen, std::chrono::duration_cast<std::chrono::seconds>(duration));
|
||||||
});
|
});
|
||||||
|
|
||||||
_mediaPlayer->playbackEnded.connect([this]
|
_mediaPlayer->playbackEnded.connect([this]
|
||||||
{
|
{
|
||||||
_playQueue->onPlaybackEnded();
|
_playQueue->onPlaybackEnded();
|
||||||
});
|
});
|
||||||
|
|
||||||
_playQueue->trackSelected.connect([this] (Database::TrackId trackId, bool play, float replayGain)
|
_playQueue->trackSelected.connect([this](Database::TrackId trackId, bool play, float replayGain)
|
||||||
{
|
{
|
||||||
_mediaPlayer->loadTrack(trackId, play, replayGain);
|
_mediaPlayer->loadTrack(trackId, play, replayGain);
|
||||||
});
|
});
|
||||||
|
|
||||||
_playQueue->trackUnselected.connect([this]
|
_playQueue->trackUnselected.connect([this]
|
||||||
{
|
{
|
||||||
_mediaPlayer->stop();
|
_mediaPlayer->stop();
|
||||||
});
|
});
|
||||||
_playQueue->trackCountChanged.connect([this] (std::size_t trackCount)
|
_playQueue->trackCountChanged.connect([this](std::size_t trackCount)
|
||||||
{
|
{
|
||||||
_mediaPlayer->onPlayQueueUpdated(trackCount);
|
_mediaPlayer->onPlayQueueUpdated(trackCount);
|
||||||
});
|
});
|
||||||
_mediaPlayer->onPlayQueueUpdated(_playQueue->getCount());
|
_mediaPlayer->onPlayQueueUpdated(_playQueue->getCount());
|
||||||
|
|
||||||
const bool isAdmin {getUserType() == Database::UserType::ADMIN};
|
const bool isAdmin{ getUserType() == Database::UserType::ADMIN };
|
||||||
if (isAdmin)
|
if (isAdmin)
|
||||||
{
|
{
|
||||||
_scannerEvents.scanComplete.connect([=] (const Scanner::ScanStats& stats)
|
_scannerEvents.scanComplete.connect([=](const Scanner::ScanStats& stats)
|
||||||
{
|
{
|
||||||
notifyMsg(Notification::Type::Info,
|
notifyMsg(Notification::Type::Info,
|
||||||
Wt::WString::tr("Lms.Admin.Database.database"),
|
Wt::WString::tr("Lms.Admin.Database.database"),
|
||||||
Wt::WString::tr("Lms.Admin.Database.scan-complete")
|
Wt::WString::tr("Lms.Admin.Database.scan-complete")
|
||||||
.arg(static_cast<unsigned>(stats.nbFiles()))
|
.arg(static_cast<unsigned>(stats.nbFiles()))
|
||||||
.arg(static_cast<unsigned>(stats.additions))
|
.arg(static_cast<unsigned>(stats.additions))
|
||||||
.arg(static_cast<unsigned>(stats.updates))
|
.arg(static_cast<unsigned>(stats.updates))
|
||||||
.arg(static_cast<unsigned>(stats.deletions))
|
.arg(static_cast<unsigned>(stats.deletions))
|
||||||
.arg(static_cast<unsigned>(stats.duplicates.size()))
|
.arg(static_cast<unsigned>(stats.duplicates.size()))
|
||||||
.arg(static_cast<unsigned>(stats.errors.size())));
|
.arg(static_cast<unsigned>(stats.errors.size())));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
internalPathChanged().connect(mainStack, [=]
|
internalPathChanged().connect(mainStack, [=]
|
||||||
{
|
{
|
||||||
handlePathChange(*mainStack, isAdmin);
|
handlePathChange(*mainStack, isAdmin);
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePathChange(*mainStack, isAdmin);
|
handlePathChange(*mainStack, isAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void LmsApplication::notify(const Wt::WEvent& event)
|
||||||
LmsApplication::notify(const Wt::WEvent& event)
|
{
|
||||||
{
|
try
|
||||||
try
|
{
|
||||||
{
|
WApplication::notify(event);
|
||||||
WApplication::notify(event);
|
}
|
||||||
}
|
catch (LmsApplicationException& e)
|
||||||
catch (LmsApplicationException& e)
|
{
|
||||||
{
|
LMS_LOG(UI, WARNING, "Caught a LmsApplication exception: " << e.what());
|
||||||
LMS_LOG(UI, WARNING) << "Caught a LmsApplication exception: " << e.what();
|
handleException(e);
|
||||||
handleException(e);
|
}
|
||||||
}
|
catch (std::exception& e)
|
||||||
catch (std::exception& e)
|
{
|
||||||
{
|
LMS_LOG(UI, ERROR, "Caught exception: " << e.what());
|
||||||
LMS_LOG(UI, ERROR) << "Caught exception: " << e.what();
|
throw LmsException{ "Internal error" }; // Do not put details here at it may appear on the user rendered html
|
||||||
throw LmsException {"Internal error"}; // Do not put details here at it may appear on the user rendered html
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void LmsApplication::post(std::function<void()> func)
|
||||||
LmsApplication::post(std::function<void()> func)
|
{
|
||||||
{
|
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
|
||||||
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
LmsApplication::setTitle(const Wt::WString& title)
|
|
||||||
{
|
|
||||||
if (title.empty())
|
|
||||||
WApplication::setTitle("LMS");
|
|
||||||
else
|
|
||||||
WApplication::setTitle(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "Notifying message '" << message.toUTF8() << "' for category '" << category.toUTF8() << "'";
|
|
||||||
_notificationContainer->add(type, category, message, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
|
void LmsApplication::setTitle(const Wt::WString& title)
|
||||||
|
{
|
||||||
|
if (title.empty())
|
||||||
|
WApplication::setTitle("LMS");
|
||||||
|
else
|
||||||
|
WApplication::setTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, INFO, "Notifying message '" << message.toUTF8() << "' for category '" << category.toUTF8() << "'");
|
||||||
|
_notificationContainer->add(type, category, message, duration);
|
||||||
|
}
|
||||||
|
} // namespace UserInterface
|
||||||
+16
-16
@@ -24,7 +24,7 @@
|
|||||||
#include <Wt/Json/Serializer.h>
|
#include <Wt/Json/Serializer.h>
|
||||||
#include <Wt/WPushButton.h>
|
#include <Wt/WPushButton.h>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
@@ -210,30 +210,30 @@ namespace UserInterface
|
|||||||
|
|
||||||
_settingsLoaded.connect([this](const std::string& settings)
|
_settingsLoaded.connect([this](const std::string& settings)
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'";
|
LMS_LOG(UI, DEBUG, "Settings loaded! '" << settings << "'");
|
||||||
|
|
||||||
_settings = settingsfromJSString(settings);
|
_settings = settingsfromJSString(settings);
|
||||||
|
|
||||||
settingsLoaded.emit();
|
settingsLoaded.emit();
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
Settings defaultSettings;
|
Settings defaultSettings;
|
||||||
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << "LMS.mediaplayer.init("
|
oss << "LMS.mediaplayer.init("
|
||||||
<< jsRef()
|
<< jsRef()
|
||||||
<< ", defaultSettings = " << settingsToJSString(defaultSettings)
|
<< ", defaultSettings = " << settingsToJSString(defaultSettings)
|
||||||
<< ")";
|
<< ")";
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
LMS_LOG(UI, DEBUG, "Running js = '" << oss.str() << "'");
|
||||||
doJavaScript(oss.str());
|
doJavaScript(oss.str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
|
void MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId.toString();
|
LMS_LOG(UI, DEBUG, "Playing track ID = " << trackId.toString());
|
||||||
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
{
|
{
|
||||||
@@ -305,7 +305,7 @@ namespace UserInterface
|
|||||||
_separator->setText("");
|
_separator->setText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
LMS_LOG(UI, DEBUG, "Running js = '" << oss.str() << "'");
|
||||||
doJavaScript(oss.str());
|
doJavaScript(oss.str());
|
||||||
|
|
||||||
_trackIdLoaded = trackId;
|
_trackIdLoaded = trackId;
|
||||||
@@ -325,7 +325,7 @@ namespace UserInterface
|
|||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
|
oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
LMS_LOG(UI, DEBUG, "Running js = '" << oss.str() << "'");
|
||||||
doJavaScript(oss.str());
|
doJavaScript(oss.str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-49
@@ -18,64 +18,62 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ModalManager.hpp"
|
#include "ModalManager.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
ModalManager::ModalManager()
|
ModalManager::ModalManager()
|
||||||
: _closed {this, "closed"}
|
: _closed{ this, "closed" }
|
||||||
{
|
{
|
||||||
_closed.connect([=](const std::string& id)
|
_closed.connect([=](const std::string& id)
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, DEBUG) << "Received closed for id '" << id << "'";
|
LMS_LOG(UI, DEBUG, "Received closed for id '" << id << "'");
|
||||||
for (int i {}; i < count(); ++i)
|
for (int i{}; i < count(); ++i)
|
||||||
{
|
{
|
||||||
Wt::WWidget* widget {this->widget(i)};
|
Wt::WWidget* widget{ this->widget(i) };
|
||||||
LMS_LOG(UI, DEBUG) << "Widget " << i << ", id = '" << widget->id();
|
LMS_LOG(UI, DEBUG, "Widget " << i << ", id = '" << widget->id());
|
||||||
if (widget->id() == id)
|
if (widget->id() == id)
|
||||||
{
|
{
|
||||||
removeWidget(widget);
|
removeWidget(widget);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ModalManager::show(std::unique_ptr<Wt::WWidget> modalWidget)
|
||||||
ModalManager::show(std::unique_ptr<Wt::WWidget> modalWidget)
|
{
|
||||||
{
|
LMS_LOG(UI, DEBUG, "Want to show, id = " << modalWidget->id());
|
||||||
LMS_LOG(UI, DEBUG) << "Want to show, id = " << modalWidget->id();
|
|
||||||
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss
|
oss
|
||||||
<< R"({const modalElement = )" << jsRef() << R"(.getElementsByClassName('modal')[0];)"
|
<< R"({const modalElement = )" << jsRef() << R"(.getElementsByClassName('modal')[0];)"
|
||||||
<< R"(const modal = bootstrap.Modal.getOrCreateInstance(modalElement);)"
|
<< R"(const modal = bootstrap.Modal.getOrCreateInstance(modalElement);)"
|
||||||
<< R"(modal.show();)"
|
<< R"(modal.show();)"
|
||||||
<< R"(modalElement.addEventListener('hidden.bs.modal', function () {)"
|
<< R"(modalElement.addEventListener('hidden.bs.modal', function () {)"
|
||||||
<< _closed.createCall({"'" + modalWidget->id() + "'"})
|
<< _closed.createCall({ "'" + modalWidget->id() + "'" })
|
||||||
<< R"(modal.dispose();)"
|
<< R"(modal.dispose();)"
|
||||||
<< R"(});})";
|
<< R"(});})";
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
|
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
|
||||||
|
|
||||||
doJavaScript(oss.str());
|
doJavaScript(oss.str());
|
||||||
|
|
||||||
addWidget(std::move(modalWidget));
|
addWidget(std::move(modalWidget));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ModalManager::dispose(Wt::WWidget* modalWidget)
|
||||||
ModalManager::dispose(Wt::WWidget* modalWidget)
|
{
|
||||||
{
|
std::ostringstream oss;
|
||||||
std::ostringstream oss;
|
oss
|
||||||
oss
|
<< R"({const modalElementParent = document.getElementById(')" << modalWidget->id() << R"(');)"
|
||||||
<< R"({const modalElementParent = document.getElementById(')" << modalWidget->id() << R"(');)"
|
<< R"(const modalElement = modalElementParent.getElementsByClassName('modal')[0];)"
|
||||||
<< R"(const modalElement = modalElementParent.getElementsByClassName('modal')[0];)"
|
<< R"(const modal = bootstrap.Modal.getInstance(modalElement);)"
|
||||||
<< R"(const modal = bootstrap.Modal.getInstance(modalElement);)"
|
<< R"(modal.hide();)"
|
||||||
<< R"(modal.hide();)"
|
<< R"(})";
|
||||||
<< R"(})";
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
|
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
|
||||||
|
|
||||||
doJavaScript(oss.str());
|
doJavaScript(oss.str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,65 +22,67 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
class NotificationWidget : public Wt::WTemplate
|
namespace
|
||||||
{
|
{
|
||||||
public:
|
class NotificationWidget : public Wt::WTemplate
|
||||||
NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration);
|
{
|
||||||
Wt::JSignal<> closed {this, "closed"};
|
public:
|
||||||
};
|
NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration);
|
||||||
|
Wt::JSignal<> closed{ this, "closed" };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
NotificationWidget::NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
NotificationWidget::NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||||
: Wt::WTemplate {Wt::WString::tr("Lms.notifications.template.entry")}
|
: Wt::WTemplate{ Wt::WString::tr("Lms.notifications.template.entry") }
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case Notification::Type::Info:
|
case Notification::Type::Info:
|
||||||
bindString("bg-color", "bg-primary");
|
bindString("bg-color", "bg-primary");
|
||||||
bindString("text-color", "white");
|
bindString("text-color", "white");
|
||||||
break;
|
break;
|
||||||
case Notification::Type::Warning:
|
case Notification::Type::Warning:
|
||||||
bindString("bg-color", "bg-warning");
|
bindString("bg-color", "bg-warning");
|
||||||
bindString("text-color", "dark");
|
bindString("text-color", "dark");
|
||||||
break;
|
break;
|
||||||
case Notification::Type::Danger:
|
case Notification::Type::Danger:
|
||||||
bindString("bg-color", "bg-danger");
|
bindString("bg-color", "bg-danger");
|
||||||
bindString("text-color", "white");
|
bindString("text-color", "white");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindString("category", category);
|
bindString("category", category);
|
||||||
bindString("message", message);
|
bindString("message", message);
|
||||||
bindInt("duration", duration.count());
|
bindInt("duration", duration.count());
|
||||||
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
|
|
||||||
oss
|
oss
|
||||||
<< R"({const toastElement = )" << jsRef() << R"(.getElementsByClassName('toast')[0];)"
|
<< R"({const toastElement = )" << jsRef() << R"(.getElementsByClassName('toast')[0];)"
|
||||||
<< R"(const toast = bootstrap.Toast.getOrCreateInstance(toastElement);)"
|
<< R"(const toast = bootstrap.Toast.getOrCreateInstance(toastElement);)"
|
||||||
<< R"(toast.show();)"
|
<< R"(toast.show();)"
|
||||||
<< R"(toastElement.addEventListener('hidden.bs.toast', function () {)"
|
<< R"(toastElement.addEventListener('hidden.bs.toast', function () {)"
|
||||||
<< closed.createCall({})
|
<< closed.createCall({})
|
||||||
<< R"(toast.dispose();)"
|
<< R"(toast.dispose();)"
|
||||||
<< R"(});})";
|
<< R"(});})";
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
|
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
|
||||||
|
|
||||||
doJavaScript(oss.str());
|
doJavaScript(oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void NotificationContainer::add(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||||
NotificationContainer::add(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
{
|
||||||
{
|
NotificationWidget* notification{ addNew<NotificationWidget>(type, category, message, duration) };
|
||||||
NotificationWidget* notification {addNew<NotificationWidget>(type, category, message, duration)};
|
|
||||||
|
|
||||||
notification->closed.connect([=]
|
notification->closed.connect([=]
|
||||||
{
|
{
|
||||||
removeWidget(notification);
|
removeWidget(notification);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IPlaylistGeneratorService.hpp"
|
#include "services/recommendation/IPlaylistGeneratorService.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Random.hpp"
|
#include "utils/Random.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
@@ -226,7 +226,7 @@ namespace UserInterface
|
|||||||
|
|
||||||
if (LmsApp->getUser()->isDemo())
|
if (LmsApp->getUser()->isDemo())
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, DEBUG) << "Removing queue (tracklist id " << _queueId.toString() << ")";
|
LMS_LOG(UI, DEBUG, "Removing queue (tracklist id " << _queueId.toString() << ")");
|
||||||
if (Database::TrackList::pointer queue{ getQueue() })
|
if (Database::TrackList::pointer queue{ getQueue() })
|
||||||
queue.remove();
|
queue.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
@@ -259,10 +259,7 @@ namespace UserInterface
|
|||||||
setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow));
|
setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow));
|
||||||
|
|
||||||
if (auto listenBrainzToken{ user->getListenBrainzToken() })
|
if (auto listenBrainzToken{ user->getListenBrainzToken() })
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "Read listenBrainzToken! value = " << listenBrainzToken->getAsString();
|
|
||||||
setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() }));
|
setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() }));
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz };
|
const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz };
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ namespace UserInterface::Utils
|
|||||||
auto res{ std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::TextFormat::UnsafeXHTML) };
|
auto res{ std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::TextFormat::UnsafeXHTML) };
|
||||||
|
|
||||||
res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK
|
res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK
|
||||||
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
|
res->setToolTip(std::string{ cluster->getType()->getName() }, Wt::TextFormat::Plain);
|
||||||
res->setInline(true);
|
res->setInline(true);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "services/scanner/IScannerService.hpp"
|
#include "services/scanner/IScannerService.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
@@ -112,8 +112,8 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
|||||||
auto clusterTypes {scanSettings->getClusterTypes()};
|
auto clusterTypes {scanSettings->getClusterTypes()};
|
||||||
if (!clusterTypes.empty())
|
if (!clusterTypes.empty())
|
||||||
{
|
{
|
||||||
std::vector<std::string> names;
|
std::vector<std::string_view> names;
|
||||||
std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](auto clusterType) { return clusterType->getName(); });
|
std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](const auto& clusterType) { return clusterType->getName(); });
|
||||||
setValue(ClustersField, StringUtils::joinStrings(names, " "));
|
setValue(ClustersField, StringUtils::joinStrings(names, " "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,8 +138,8 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
|||||||
if (similarityEngineTypeRow)
|
if (similarityEngineTypeRow)
|
||||||
scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow));
|
scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow));
|
||||||
|
|
||||||
auto clusterTypes {StringUtils::splitStringCopy(valueText(ClustersField).toUTF8(), " ")};
|
const std::vector<std::string_view> clusterTypes {StringUtils::splitString(valueText(ClustersField).toUTF8(), " ")};
|
||||||
scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string>(clusterTypes.begin(), clusterTypes.end()));
|
scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string_view>(clusterTypes.begin(), clusterTypes.end()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
#include "common/LoginNameValidator.hpp"
|
#include "common/LoginNameValidator.hpp"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "services/auth/IPasswordService.hpp"
|
#include "services/auth/IPasswordService.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "common/InfiniteScrollingContainer.hpp"
|
#include "common/InfiniteScrollingContainer.hpp"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "common/InfiniteScrollingContainer.hpp"
|
#include "common/InfiniteScrollingContainer.hpp"
|
||||||
#include "ArtistListHelpers.hpp"
|
#include "ArtistListHelpers.hpp"
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ Filters::showDialog()
|
|||||||
for (const ClusterTypeId clusterTypeId : clusterTypesIds.results)
|
for (const ClusterTypeId clusterTypeId : clusterTypesIds.results)
|
||||||
{
|
{
|
||||||
const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypeId)};
|
const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypeId)};
|
||||||
typeCombo->addItem(Wt::WString::fromUTF8(clusterType->getName()));
|
typeCombo->addItem(Wt::WString::fromUTF8(std::string{ clusterType->getName() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clusterTypesIds.results.empty())
|
if (!clusterTypesIds.results.empty())
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "resource/DownloadResource.hpp"
|
#include "resource/DownloadResource.hpp"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "common/InfiniteScrollingContainer.hpp"
|
#include "common/InfiniteScrollingContainer.hpp"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "common/InfiniteScrollingContainer.hpp"
|
#include "common/InfiniteScrollingContainer.hpp"
|
||||||
#include "explore/Filters.hpp"
|
#include "explore/Filters.hpp"
|
||||||
|
|||||||
@@ -26,87 +26,80 @@
|
|||||||
#include "av/RawResourceHandlerCreator.hpp"
|
#include "av/RawResourceHandlerCreator.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface
|
||||||
|
|
||||||
#define LOG(level) LMS_LOG(UI, level) << "Audio file resource: "
|
|
||||||
|
|
||||||
AudioFileResource:: ~AudioFileResource()
|
|
||||||
{
|
{
|
||||||
beingDeleted();
|
#define LOG(severity, message) LMS_LOG(UI, severity, "Audio file resource: " << message)
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
namespace
|
||||||
AudioFileResource::getUrl(Database::TrackId trackId) const
|
{
|
||||||
{
|
std::optional<std::filesystem::path> getTrackPathFromTrackId(Database::TrackId trackId)
|
||||||
return url()+ "&trackid=" + trackId.toString();
|
{
|
||||||
}
|
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
static
|
const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), trackId) };
|
||||||
std::optional<std::filesystem::path>
|
if (!track)
|
||||||
getTrackPathFromTrackId(Database::TrackId trackId)
|
{
|
||||||
{
|
LOG(ERROR, "Missing track");
|
||||||
auto transaction {LmsApp->getDbSession().createReadTransaction()};
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)};
|
return track->getPath();
|
||||||
if (!track)
|
}
|
||||||
{
|
|
||||||
LOG(ERROR) << "Missing track";
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return track->getPath();
|
std::optional<std::filesystem::path> getTrackPathFromURLArgs(const Wt::Http::Request& request)
|
||||||
}
|
{
|
||||||
|
const std::string* trackIdParameter{ request.getParameter("trackid") };
|
||||||
|
if (!trackIdParameter)
|
||||||
|
{
|
||||||
|
LOG(ERROR, "Missing trackid URL parameter!");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
static
|
const std::optional<Database::TrackId> trackId{ StringUtils::readAs<Database::TrackId::ValueType>(*trackIdParameter) };
|
||||||
std::optional<std::filesystem::path>
|
if (!trackId)
|
||||||
getTrackPathFromURLArgs(const Wt::Http::Request& request)
|
{
|
||||||
{
|
LOG(ERROR, "Bad trackid URL parameter!");
|
||||||
const std::string* trackIdParameter {request.getParameter("trackid")};
|
return std::nullopt;
|
||||||
if (!trackIdParameter)
|
}
|
||||||
{
|
|
||||||
LOG(ERROR) << "Missing trackid URL parameter!";
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::optional<Database::TrackId> trackId {StringUtils::readAs<Database::TrackId::ValueType>(*trackIdParameter)};
|
return getTrackPathFromTrackId(*trackId);
|
||||||
if (!trackId)
|
}
|
||||||
{
|
|
||||||
LOG(ERROR) << "Bad trackid URL parameter!";
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getTrackPathFromTrackId(*trackId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
AudioFileResource:: ~AudioFileResource()
|
||||||
AudioFileResource::handleRequest(const Wt::Http::Request& request,
|
{
|
||||||
Wt::Http::Response& response)
|
beingDeleted();
|
||||||
{
|
}
|
||||||
std::shared_ptr<IResourceHandler> fileResourceHandler;
|
|
||||||
|
|
||||||
if (!request.continuation())
|
std::string AudioFileResource::getUrl(Database::TrackId trackId) const
|
||||||
{
|
{
|
||||||
auto trackPath {getTrackPathFromURLArgs(request)};
|
return url() + "&trackid=" + trackId.toString();
|
||||||
if (!trackPath)
|
}
|
||||||
return;
|
|
||||||
|
|
||||||
fileResourceHandler = Av::createRawResourceHandler(*trackPath);
|
void AudioFileResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
}
|
{
|
||||||
else
|
std::shared_ptr<IResourceHandler> fileResourceHandler;
|
||||||
{
|
|
||||||
fileResourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(request.continuation()->data());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* continuation {fileResourceHandler->processRequest(request, response)};
|
if (!request.continuation())
|
||||||
if (continuation)
|
{
|
||||||
continuation->setData(fileResourceHandler);
|
auto trackPath{ getTrackPathFromURLArgs(request) };
|
||||||
}
|
if (!trackPath)
|
||||||
|
return;
|
||||||
|
|
||||||
|
fileResourceHandler = Av::createRawResourceHandler(*trackPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fileResourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(request.continuation()->data());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* continuation{ fileResourceHandler->processRequest(request, response) };
|
||||||
|
if (continuation)
|
||||||
|
continuation->setData(fileResourceHandler);
|
||||||
|
}
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,12 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
#define LOG(level) LMS_LOG(UI, level) << "Audio transcode resource: "
|
#define LOG(severity, message) LMS_LOG(UI, severity, "Audio transcode resource: " << message)
|
||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
@@ -61,7 +61,7 @@ namespace StringUtils
|
|||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(ERROR) << "Cannot determine audio format from value '" << str << "'";
|
LOG(ERROR, "Cannot determine audio format from value '" << str << "'");
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ namespace UserInterface
|
|||||||
case Database::TranscodingOutputFormat::WEBM_VORBIS: return Av::Transcoding::OutputFormat::WEBM_VORBIS;
|
case Database::TranscodingOutputFormat::WEBM_VORBIS: return Av::Transcoding::OutputFormat::WEBM_VORBIS;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(ERROR) << "Cannot convert from audio format to AV format";
|
LOG(ERROR, "Cannot convert from audio format to AV format");
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -93,13 +93,13 @@ namespace UserInterface
|
|||||||
auto paramStr{ request.getParameter(parameterName) };
|
auto paramStr{ request.getParameter(parameterName) };
|
||||||
if (!paramStr)
|
if (!paramStr)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Missing parameter '" << parameterName << "'";
|
LOG(DEBUG, "Missing parameter '" << parameterName << "'");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto res{ StringUtils::readAs<T>(*paramStr) };
|
auto res{ StringUtils::readAs<T>(*paramStr) };
|
||||||
if (!res)
|
if (!res)
|
||||||
LOG(ERROR) << "Cannot parse parameter '" << parameterName << "' from value '" << *paramStr << "'";
|
LOG(ERROR, "Cannot parse parameter '" << parameterName << "' from value '" << *paramStr << "'");
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ namespace UserInterface
|
|||||||
|
|
||||||
if (!Database::isAudioBitrateAllowed(*bitrate))
|
if (!Database::isAudioBitrateAllowed(*bitrate))
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
|
LOG(ERROR, "Bitrate '" << *bitrate << "' is not allowed");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ namespace UserInterface
|
|||||||
const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), *trackId) };
|
const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), *trackId) };
|
||||||
if (!track)
|
if (!track)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Missing track";
|
LOG(ERROR, "Missing track");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ namespace UserInterface
|
|||||||
}
|
}
|
||||||
catch (const Av::Exception& e)
|
catch (const Av::Exception& e)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Caught Av exception: " << e.what();
|
LOG(ERROR, "Caught Av exception: " << e.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,16 +25,16 @@
|
|||||||
#include "services/cover/ICoverService.hpp"
|
#include "services/cover/ICoverService.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
#define LOG(level) LMS_LOG(UI, level) << "Image resource: "
|
#define LOG(severity, message) LMS_LOG(UI, severity, "Image resource: " << message)
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
|
namespace UserInterface
|
||||||
|
{
|
||||||
CoverResource::CoverResource()
|
CoverResource::CoverResource()
|
||||||
{
|
{
|
||||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
||||||
@@ -69,14 +69,14 @@ namespace UserInterface {
|
|||||||
// Mandatory parameter size
|
// Mandatory parameter size
|
||||||
if (!sizeStr)
|
if (!sizeStr)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "no size provided!";
|
LOG(DEBUG, "no size provided!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto size{ StringUtils::readAs<std::size_t>(*sizeStr) };
|
const auto size{ StringUtils::readAs<std::size_t>(*sizeStr) };
|
||||||
if (!size || *size > maxSize)
|
if (!size || *size > maxSize)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "invalid size provided!";
|
LOG(DEBUG, "invalid size provided!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,12 +84,12 @@ namespace UserInterface {
|
|||||||
|
|
||||||
if (trackIdStr)
|
if (trackIdStr)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size;
|
LOG(DEBUG, "Requested cover for track " << *trackIdStr << ", size = " << *size);
|
||||||
|
|
||||||
const std::optional<Database::TrackId> trackId{ StringUtils::readAs<Database::TrackId::ValueType>(*trackIdStr) };
|
const std::optional<Database::TrackId> trackId{ StringUtils::readAs<Database::TrackId::ValueType>(*trackIdStr) };
|
||||||
if (!trackId)
|
if (!trackId)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "track not found";
|
LOG(DEBUG, "track not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ namespace UserInterface {
|
|||||||
}
|
}
|
||||||
else if (releaseIdStr)
|
else if (releaseIdStr)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size;
|
LOG(DEBUG, "Requested cover for release " << *releaseIdStr << ", size = " << *size);
|
||||||
|
|
||||||
const std::optional<Database::ReleaseId> releaseId{ StringUtils::readAs<Database::ReleaseId::ValueType>(*releaseIdStr) };
|
const std::optional<Database::ReleaseId> releaseId{ StringUtils::readAs<Database::ReleaseId::ValueType>(*releaseIdStr) };
|
||||||
if (!releaseId)
|
if (!releaseId)
|
||||||
@@ -111,7 +111,7 @@ namespace UserInterface {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "No track or release provided";
|
LOG(DEBUG, "No track or release provided");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,5 +119,4 @@ namespace UserInterface {
|
|||||||
|
|
||||||
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
|
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -28,15 +28,14 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
#define LOG(level) LMS_LOG(UI, level) << "Download resource: "
|
#define LOG(severity, message) LMS_LOG(UI, severity, "Download resource: " << message)
|
||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
DownloadResource::~DownloadResource()
|
DownloadResource::~DownloadResource()
|
||||||
{
|
{
|
||||||
beingDeleted();
|
beingDeleted();
|
||||||
@@ -66,7 +65,7 @@ namespace UserInterface
|
|||||||
}
|
}
|
||||||
catch (Zip::Exception& exception)
|
catch (Zip::Exception& exception)
|
||||||
{
|
{
|
||||||
LOG(ERROR) << "Zipper exception: " << exception.what();
|
LOG(ERROR, "Zipper exception: " << exception.what());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +215,7 @@ namespace UserInterface
|
|||||||
const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), _trackId) };
|
const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), _trackId) };
|
||||||
if (!track)
|
if (!track)
|
||||||
{
|
{
|
||||||
LOG(DEBUG) << "Cannot find track";
|
LOG(DEBUG, "Cannot find track");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,5 +242,4 @@ namespace UserInterface
|
|||||||
const auto tracks{ Track::find(LmsApp->getDbSession(), params) };
|
const auto tracks{ Track::find(LmsApp->getDbSession(), params) };
|
||||||
return details::createZipper(tracks.results);
|
return details::createZipper(tracks.results);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user