From 69977696d296fd4104e6be8b60b5fecfb838d658 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 09:17:14 +0500 Subject: [PATCH 01/12] rework settings initialization --- cpp/server/deadbeef/plugin.cpp | 27 +-- cpp/server/project_info.hpp.in | 2 +- cpp/server/settings.cpp | 366 +++++++++++++++++++-------------- cpp/server/settings.hpp | 59 +++--- 4 files changed, 253 insertions(+), 201 deletions(-) diff --git a/cpp/server/deadbeef/plugin.cpp b/cpp/server/deadbeef/plugin.cpp index d3225f57..847e2318 100644 --- a/cpp/server/deadbeef/plugin.cpp +++ b/cpp/server/deadbeef/plugin.cpp @@ -49,7 +49,7 @@ void Plugin::handlePluginsLoaded() pluginsLoaded_ = true; #ifndef MSRV_OS_MAC - SettingsData::migrate(MSRV_PLAYER_DEADBEEF, getProfileDir()); + migrateSettings(MSRV_PLAYER_DEADBEEF, getProfileDir()); #endif refreshSettings(); @@ -59,18 +59,19 @@ void Plugin::handlePluginsLoaded() void Plugin::reconfigure() { tryCatchLog([&] { - auto settings = std::make_shared(); - - settings->port = port_; - settings->allowRemote = allowRemote_; - settings->musicDirsOrig = parseValueList(musicDirs_, ';'); - settings->authRequired = authRequired_; - settings->authUser = authUser_; - settings->authPassword = authPassword_; - settings->permissions = permissions_; - - settings->initialize(getThisModuleDir(), getProfileDir()); - + SettingsBuilder builder; + + builder.resourceDir = getThisModuleDir(); + builder.profileDir = getProfileDir(); + builder.port = port_; + builder.allowRemote = allowRemote_; + builder.musicDirs = parseValueList(musicDirs_, ';'); + builder.authRequired = authRequired_; + builder.authUser = authUser_; + builder.authPassword = authPassword_; + builder.permissions = permissions_; + + auto settings = builder.build(); host_.reconfigure(std::move(settings)); }); } diff --git a/cpp/server/project_info.hpp.in b/cpp/server/project_info.hpp.in index 11603dbb..68b2a160 100644 --- a/cpp/server/project_info.hpp.in +++ b/cpp/server/project_info.hpp.in @@ -18,7 +18,7 @@ #define MSRV_DEFAULT_TEST_PORT 8882 #define MSRV_CONFIG_FILE "config.json" #define MSRV_CONFIG_FILE_OLD "beefweb.config.json" -#define MSRV_CONFIG_FILE_ENV "BEEFWEB_CONFIG_FILE" +#define MSRV_PROFILE_DIR_ENV "BEEFWEB_PROFILE_DIR" #define MSRV_CLIENT_CONFIG_DIR "clientconfig" #define MSRV_DONATE_URL "https://hyperblast.org/donate/" #define MSRV_API_DOCS_URL "https://hyperblast.org/beefweb/api/" diff --git a/cpp/server/settings.cpp b/cpp/server/settings.cpp index a102d52d..d8b17248 100644 --- a/cpp/server/settings.cpp +++ b/cpp/server/settings.cpp @@ -26,160 +26,132 @@ const PermissionDef permissionDefs[] = { {ApiPermissions::NONE, nullptr}, }; -template -void loadValue(const Json& json, T* value, const char* name) -{ - try - { - auto it = json.find(name); - if (it != json.end()) - *value = it->get(); - } - catch (std::exception& ex) - { - logError("failed to parse property '%s': %s", name, ex.what()); - } -} - -#ifndef MSRV_OS_MAC - -void tryCopyFile(const Path& from, const Path& to) +Path resolvePath(const Path& baseDir, const Path& path) { - boost::system::error_code ec; - - if (fs::is_regular_file(from, ec) && !fs::exists(to, ec)) - { - logInfo("migrating config file: %s -> %s", pathToUtf8(from).c_str(), pathToUtf8(to).c_str()); - - fs::copy_file(from, to, ec); - - if (ec.failed()) - logError("copying failed: %s", ec.message().c_str()); - } + return path.empty() || path.is_absolute() ? path : (baseDir / path).lexically_normal(); } -void tryCopyDirectory(const Path& from, const Path& to, const Path& ext) +std::vector resolveMusicDirs(const Path& baseDir, const std::vector& musicDirs) { - boost::system::error_code ec; + std::vector result; + result.reserve(musicDirs.size()); - if (!fs::is_directory(from, ec)) - return; - - for (auto& entry : fs::directory_iterator(from, ec)) + auto index = 0; + for (const auto& dir : musicDirs) { - if (entry.path().extension() == ext) + if (dir.empty()) { - tryCopyFile(entry.path(), to / entry.path().filename()); + logError("skipping empty music directory at index %d", index); + continue; } - } -} -#endif + result.emplace_back(resolvePath(baseDir, pathFromUtf8(dir))); + index++; + } + return result; } -SettingsData::SettingsData() = default; -SettingsData::~SettingsData() = default; - -#ifndef MSRV_OS_MAC - -void SettingsData::migrate(const char* appName, const Path& profileDir) +Json readJsonFile(const Path& path) { - tryCatchLog([&] { - boost::system::error_code ec; - - auto newConfigDir = profileDir / MSRV_PATH_LITERAL(MSRV_PROJECT_ID); - auto newConfigFile = newConfigDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE); - auto newClientConfigDir = newConfigDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); + Json result; - if (fs::exists(newClientConfigDir, ec)) + tryCatchLog([&] { + auto file = file_io::open(path); + if (!file) return; - fs::create_directories(newClientConfigDir, ec); - - auto userConfigDir = getUserConfigDir(); - if (!userConfigDir.empty()) - { - auto oldConfigDir = userConfigDir / MSRV_PATH_LITERAL(MSRV_PROJECT_ID) / pathFromUtf8(appName); - auto oldConfigFile = oldConfigDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE_OLD); - auto oldClientConfigDir = oldConfigDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); + logInfo("loading config file: %s", pathToUtf8(path).c_str()); + auto data = file_io::readToEnd(file.get()); + result = Json::parse(data); + }); - tryCopyFile(oldConfigFile, newConfigFile); - tryCopyDirectory(oldClientConfigDir, newClientConfigDir, MSRV_PATH_LITERAL(".json")); - } + if (!result.is_null() && !result.is_object()) + { + result = Json(); + logError("invalid config: expected json object"); + } - tryCopyFile(getThisModuleDir() / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE_OLD), newConfigFile); - }); + return result; } -#endif -bool SettingsData::isAllowedPath(const Path& path) const +template +bool parseValue(const Json& json, const char* name, T* result) { - for (const auto& root : musicDirs) + try { - if (isSubpath(root, path)) - return true; + auto it = json.find(name); + if (it == json.end()) + return false; + *result = it->get(); + return true; + } + catch (std::exception& ex) + { + logError("failed to parse property '%s': %s", name, ex.what()); + return false; } - - return false; } -void SettingsData::initialize(const Path& resourceDir, const Path& profileDir) +void parsePath(const Json& json, const char* name, const Path& baseDir, Path* result) { - logDebug("init settings: resourceDir = %s, profileDir = %s", resourceDir.c_str(), profileDir.c_str()); + std::string webRoot; - assert(!resourceDir.empty()); - assert(!profileDir.empty()); + if (parseValue(json, name, &webRoot)) + *result = resolvePath(baseDir, webRoot); +} - baseDir = profileDir / MSRV_PATH_LITERAL(MSRV_PROJECT_ID); +void parseMusicDirs(const Json& json, const Path& baseDir, std::vector* result) +{ + std::vector musicDirs; - loadFromFile(baseDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE)); + if (parseValue(json, "musicDirs", &musicDirs)) + *result = resolveMusicDirs(baseDir, musicDirs); +} - auto envConfigFile = getEnvAsPath(MSRV_CONFIG_FILE_ENV); - if (!envConfigFile.empty()) +void parsePermission(const Json& json, const char* name, ApiPermissions value, ApiPermissions* result) +{ + auto it = json.find(name); + if (it == json.end()) + return; + + if (!it->is_boolean()) { - if (envConfigFile.is_absolute()) - loadFromFile(envConfigFile); - else - logError("ignoring non-absolute config file path: %s", envConfigFile.c_str()); + logError("failed to parse permission '%s': expected boolean value", name); + return; } - webRoot = resolvePath(pathFromUtf8(webRootOrig)).lexically_normal(); + *result = setFlags(*result, value, it->get()); +} - if (webRoot.empty()) - { - webRoot = resourceDir / MSRV_PATH_LITERAL(MSRV_WEBUI_ROOT); - } +void parsePermissions(const Json& jsonRoot, ApiPermissions* result) +{ + auto it = jsonRoot.find("permissions"); + if (it == jsonRoot.end()) + return; - clientConfigDir = resolvePath(pathFromUtf8(clientConfigDirOrig)).lexically_normal(); + const Json& json = *it; - if (clientConfigDir.empty()) + if (!json.is_object()) { - clientConfigDir = baseDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); + logError("failed to parse property 'permissions': expected json object"); + return; } - musicDirs.clear(); - musicDirs.reserve(musicDirsOrig.size()); - - auto index = 0; - for (const auto& dir : musicDirsOrig) - { - if (dir.empty()) - { - logError("skipping empty music directory at index %d", index); - continue; - } + for (int i = 0; permissionDefs[i]; i++) + parsePermission(json, permissionDefs[i].id, permissionDefs[i].value, result); +} - auto path = resolvePath(pathFromUtf8(dir)).lexically_normal(); - musicDirs.emplace_back(std::move(path)); - index++; - } +void parseUrlMappings(const Json& json, const Path& baseDir, std::unordered_map* result) +{ + std::unordered_map urlMappings; + if (!parseValue(json, "urlMappings", &urlMappings)) + return; - urlMappings.clear(); - urlMappings.reserve(urlMappingsOrig.size()); + std::unordered_map urlMappingsResult; - for (const auto& kv : urlMappingsOrig) + for (const auto& kv : urlMappings) { if (kv.first.find(':') != std::string::npos) { @@ -207,85 +179,161 @@ void SettingsData::initialize(const Path& resourceDir, const Path& profileDir) if (prefix.back() != '/') prefix.push_back('/'); - auto path = resolvePath(pathFromUtf8(kv.second)).lexically_normal(); + auto path = resolvePath(baseDir, pathFromUtf8(kv.second)); logInfo("using url mapping '%s' -> '%s'", kv.first.c_str(), kv.second.c_str()); - urlMappings[std::move(prefix)] = std::move(path); + urlMappingsResult[std::move(prefix)] = std::move(path); } + + *result = std::move(urlMappingsResult); } -void SettingsData::loadFromFile(const Path& path) +void processFile(const Path& baseDir, const Path& file, SettingsData* settings) { - tryCatchLog([&] { - auto file = file_io::open(path); - if (!file) - return; - - logInfo("loading config file: %s", pathToUtf8(path).c_str()); - auto data = file_io::readToEnd(file.get()); - loadFromJson(Json::parse(data)); - }); + const auto json = readJsonFile(file); + + parseValue(json, "port", &settings->port); + parseValue(json, "allowRemote", &settings->allowRemote); + parseValue(json, "authRequired", &settings->authRequired); + parseValue(json, "authUser", &settings->authUser); + parseValue(json, "authPassword", &settings->authPassword); + parseValue(json, "responseHeaders", &settings->responseHeaders); + parsePath(json, "webRoot", baseDir, &settings->webRoot); + parsePath(json, "clientConfigDir", baseDir, &settings->clientConfigDir); + parseMusicDirs(json, baseDir, &settings->musicDirs); + parseUrlMappings(json, baseDir, &settings->urlMappings); + parsePermissions(json, &settings->permissions); } -void SettingsData::loadFromJson(const Json& json) +#ifndef MSRV_OS_MAC + +void tryCopyFile(const Path& from, const Path& to) { - if (!json.is_object()) + boost::system::error_code ec; + + if (fs::is_regular_file(from, ec) && !fs::exists(to, ec)) { - logError("invalid config: expected json object"); - return; - } + logInfo("migrating config file: %s -> %s", pathToUtf8(from).c_str(), pathToUtf8(to).c_str()); - loadValue(json, &port, "port"); - loadValue(json, &allowRemote, "allowRemote"); - loadValue(json, &musicDirsOrig, "musicDirs"); - loadValue(json, &webRootOrig, "webRoot"); - loadValue(json, &authRequired, "authRequired"); - loadValue(json, &authUser, "authUser"); - loadValue(json, &authPassword, "authPassword"); - loadValue(json, &responseHeaders, "responseHeaders"); - loadValue(json, &urlMappingsOrig, "urlMappings"); - loadValue(json, &clientConfigDirOrig, "clientConfigDir"); - loadPermissions(json); + fs::copy_file(from, to, ec); + + if (ec.failed()) + logError("copying failed: %s", ec.message().c_str()); + } } -void SettingsData::loadPermissions(const Json& jsonRoot) +void tryCopyDirectory(const Path& from, const Path& to, const Path& ext) { - auto it = jsonRoot.find("permissions"); - if (it == jsonRoot.end()) - return; + boost::system::error_code ec; - const Json& json = *it; + if (!fs::is_directory(from, ec)) + return; - if (!json.is_object()) + for (auto& entry : fs::directory_iterator(from, ec)) { - logError("failed to parse property 'permissions': expected json object"); - return; + if (entry.path().extension() == ext) + { + tryCopyFile(entry.path(), to / entry.path().filename()); + } } +} +#endif + +} + +void to_json(Json& json, const ApiPermissions& value) +{ for (int i = 0; permissionDefs[i]; i++) - loadPermission(json, permissionDefs[i].id, permissionDefs[i].value); + json[permissionDefs[i].id] = hasFlags(value, permissionDefs[i].value); } -void SettingsData::loadPermission(const Json& json, const char* name, ApiPermissions value) +bool SettingsData::isAllowedPath(const Path& path) const { - auto it = json.find(name); - if (it == json.end()) - return; + for (const auto& root : musicDirs) + { + if (isSubpath(root, path)) + return true; + } - if (!it->is_boolean()) + return false; +} + +SettingsDataPtr SettingsBuilder::build() const +{ + logDebug( + "build settings: resourceDir = %s, profileDir = %s", + pathToUtf8(resourceDir).c_str(), + pathToUtf8(profileDir).c_str()); + + assert(!resourceDir.empty()); + assert(!profileDir.empty()); + + Path pluginProfileDir = getEnvAsPath(MSRV_PROFILE_DIR_ENV); + + if (!pluginProfileDir.empty()) { - logError("failed to parse permission '%s': expected boolean value", name); - return; + if (pluginProfileDir.is_absolute()) + { + logInfo("using custom profile dir: %s", pathToUtf8(pluginProfileDir).c_str()); + } + else + { + logError("ignoring non-absolute profile dir: %s", pathToUtf8(pluginProfileDir).c_str()); + pluginProfileDir = Path(); + } } - permissions = setFlags(permissions, value, it->get()); + if (pluginProfileDir.empty()) + pluginProfileDir = profileDir / MSRV_PATH_LITERAL(MSRV_PROJECT_ID); + + auto settings = std::make_shared(); + settings->port = port; + settings->allowRemote = allowRemote; + settings->permissions = permissions; + settings->authRequired = authRequired; + settings->authUser = authUser; + settings->authPassword = authPassword; + settings->webRoot = resourceDir / MSRV_PATH_LITERAL(MSRV_WEBUI_ROOT); + settings->clientConfigDir = pluginProfileDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); + settings->musicDirs = resolveMusicDirs(pluginProfileDir, musicDirs); + + processFile(pluginProfileDir, pluginProfileDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE), settings.get()); + return settings; } -void to_json(Json& json, const ApiPermissions& value) +#ifndef MSRV_OS_MAC + +void migrateSettings(const char* appName, const Path& profileDir) { - for (int i = 0; permissionDefs[i]; i++) - json[permissionDefs[i].id] = hasFlags(value, permissionDefs[i].value); + tryCatchLog([&] { + boost::system::error_code ec; + + auto newConfigDir = profileDir / MSRV_PATH_LITERAL(MSRV_PROJECT_ID); + auto newConfigFile = newConfigDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE); + auto newClientConfigDir = newConfigDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); + + if (fs::exists(newClientConfigDir, ec)) + return; + + fs::create_directories(newClientConfigDir, ec); + + auto userConfigDir = getUserConfigDir(); + if (!userConfigDir.empty()) + { + auto oldConfigDir = userConfigDir / MSRV_PATH_LITERAL(MSRV_PROJECT_ID) / pathFromUtf8(appName); + auto oldConfigFile = oldConfigDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE_OLD); + auto oldClientConfigDir = oldConfigDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); + + tryCopyFile(oldConfigFile, newConfigFile); + tryCopyDirectory(oldClientConfigDir, newClientConfigDir, MSRV_PATH_LITERAL(".json")); + } + + tryCopyFile(getThisModuleDir() / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE_OLD), newConfigFile); + }); } +#endif + } diff --git a/cpp/server/settings.hpp b/cpp/server/settings.hpp index a018c60b..0c59c1a0 100644 --- a/cpp/server/settings.hpp +++ b/cpp/server/settings.hpp @@ -24,33 +24,30 @@ enum class ApiPermissions : uint32_t MSRV_ENUM_FLAGS(ApiPermissions, uint32_t) +void to_json(Json& json, const ApiPermissions& permissions); + +#ifndef MSRV_OS_MAC +void migrateSettings(const char* appName, const Path& profileDir); +#endif + class SettingsData { public: - SettingsData(); - ~SettingsData(); + SettingsData() = default; + ~SettingsData() = default; - Path baseDir; int port = MSRV_DEFAULT_PORT; bool allowRemote = true; - std::vector musicDirsOrig; std::vector musicDirs; - Path webRoot; - std::string webRootOrig; - std::string clientConfigDirOrig; - Path clientConfigDir; - ApiPermissions permissions = ApiPermissions::ALL; - bool authRequired = false; std::string authUser; std::string authPassword; + ApiPermissions permissions = ApiPermissions::ALL; + + Path webRoot; + Path clientConfigDir; std::unordered_map responseHeaders; std::unordered_map urlMappings; - std::unordered_map urlMappingsOrig; - -#ifndef MSRV_OS_MAC - static void migrate(const char* appName, const Path& profileDir); -#endif void ensurePermissions(ApiPermissions p) const { @@ -59,23 +56,29 @@ class SettingsData } bool isAllowedPath(const Path& path) const; - void initialize(const Path& resourceDir, const Path& profileDir); +}; - Path resolvePath(const Path& path) const - { - return path.empty() || path.is_absolute() ? path : baseDir / path; - } +using SettingsDataPtr = std::shared_ptr; -private: - void loadFromJson(const Json& json); - void loadFromFile(const Path& path); +class SettingsBuilder +{ +public: + SettingsBuilder() = default; + ~SettingsBuilder() = default; - void loadPermissions(const Json& jsonRoot); - void loadPermission(const Json& json, const char* name, ApiPermissions value); -}; + SettingsDataPtr build() const; -using SettingsDataPtr = std::shared_ptr; + Path profileDir; + Path resourceDir; -void to_json(Json& json, const ApiPermissions& permissions); + int port = MSRV_DEFAULT_PORT; + bool allowRemote = true; + std::vector musicDirs; + ApiPermissions permissions = ApiPermissions::ALL; + + bool authRequired = false; + std::string authUser; + std::string authPassword; +}; } From 0c388662564de2d231cb0bc01cf2b140d22dc9c9 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 09:27:30 +0500 Subject: [PATCH 02/12] fixes --- cpp/server/foobar2000/plugin.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/cpp/server/foobar2000/plugin.cpp b/cpp/server/foobar2000/plugin.cpp index 4d22b110..96a91b12 100644 --- a/cpp/server/foobar2000/plugin.cpp +++ b/cpp/server/foobar2000/plugin.cpp @@ -35,23 +35,24 @@ Path Plugin::getProfileDir() void Plugin::reconfigure() { tryCatchLog([&] { - auto settings = std::make_shared(); - - settings->port = settings_store::port; - settings->allowRemote = settings_store::allowRemote; - settings->musicDirsOrig = settings_store::getMusicDirs(); - settings->authRequired = settings_store::authRequired; - settings->authUser = settings_store::authUser.get(); - settings->authPassword = settings_store::authPassword.get(); - settings->permissions = settings_store::getPermissions(); + SettingsBuilder builder; #ifdef MSRV_OS_MAC - auto resourceDir = getThisModuleDir().parent_path() / Path("Resources"); + builder.resourceDir = getThisModuleDir().parent_path() / Path("Resources"); #else - const auto& resourceDir = getThisModuleDir(); + builder.resourceDir = getThisModuleDir(); #endif - settings->initialize(resourceDir, getProfileDir()); + builder.profileDir = getProfileDir(); + builder.port = settings_store::port; + builder.allowRemote = settings_store::allowRemote; + builder.musicDirs = settings_store::getMusicDirs(); + builder.authRequired = settings_store::authRequired; + builder.authUser = settings_store::authUser.get(); + builder.authPassword = settings_store::authPassword.get(); + builder.permissions = settings_store::getPermissions(); + + auto settings = builder.build(); host_.reconfigure(std::move(settings)); }); @@ -68,7 +69,7 @@ class InitQuit : public initquit { Logger::setCurrent(&logger_); #ifndef MSRV_OS_MAC - SettingsData::migrate(MSRV_PLAYER_FOOBAR2000, Plugin::getProfileDir()); + migrateSettings(MSRV_PLAYER_FOOBAR2000, Plugin::getProfileDir()); #endif tryCatchLog([this] { plugin_ = std::make_unique(); }); } From 85387fdfabd7e8569f9b1e3526a188b2d3a44cd4 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 09:40:50 +0500 Subject: [PATCH 03/12] add altWebRoot setting, move some definitions to settings.cpp --- cpp/server/project_info.hpp.in | 2 -- cpp/server/settings.cpp | 6 ++++++ cpp/server/settings.hpp | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cpp/server/project_info.hpp.in b/cpp/server/project_info.hpp.in index 68b2a160..fa94c005 100644 --- a/cpp/server/project_info.hpp.in +++ b/cpp/server/project_info.hpp.in @@ -16,10 +16,8 @@ #define MSRV_PROJECT_DESC "Provides web UI and HTTP API for controlling player remotely" #define MSRV_DEFAULT_PORT 8880 #define MSRV_DEFAULT_TEST_PORT 8882 -#define MSRV_CONFIG_FILE "config.json" #define MSRV_CONFIG_FILE_OLD "beefweb.config.json" #define MSRV_PROFILE_DIR_ENV "BEEFWEB_PROFILE_DIR" -#define MSRV_CLIENT_CONFIG_DIR "clientconfig" #define MSRV_DONATE_URL "https://hyperblast.org/donate/" #define MSRV_API_DOCS_URL "https://hyperblast.org/beefweb/api/" #define MSRV_PLAYER_DEADBEEF "deadbeef" diff --git a/cpp/server/settings.cpp b/cpp/server/settings.cpp index d8b17248..1c59bb0e 100644 --- a/cpp/server/settings.cpp +++ b/cpp/server/settings.cpp @@ -4,6 +4,10 @@ #include +#define MSRV_CONFIG_FILE "config.json" +#define MSRV_CLIENT_CONFIG_DIR "clientconfig" +#define MSRV_ALT_WEB_ROOT "webroot" + namespace msrv { namespace { @@ -200,6 +204,7 @@ void processFile(const Path& baseDir, const Path& file, SettingsData* settings) parseValue(json, "authPassword", &settings->authPassword); parseValue(json, "responseHeaders", &settings->responseHeaders); parsePath(json, "webRoot", baseDir, &settings->webRoot); + parsePath(json, "altWebRoot", baseDir, &settings->altWebRoot); parsePath(json, "clientConfigDir", baseDir, &settings->clientConfigDir); parseMusicDirs(json, baseDir, &settings->musicDirs); parseUrlMappings(json, baseDir, &settings->urlMappings); @@ -296,6 +301,7 @@ SettingsDataPtr SettingsBuilder::build() const settings->authUser = authUser; settings->authPassword = authPassword; settings->webRoot = resourceDir / MSRV_PATH_LITERAL(MSRV_WEBUI_ROOT); + settings->altWebRoot = pluginProfileDir / MSRV_PATH_LITERAL(MSRV_ALT_WEB_ROOT); settings->clientConfigDir = pluginProfileDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); settings->musicDirs = resolveMusicDirs(pluginProfileDir, musicDirs); diff --git a/cpp/server/settings.hpp b/cpp/server/settings.hpp index 0c59c1a0..827e0517 100644 --- a/cpp/server/settings.hpp +++ b/cpp/server/settings.hpp @@ -45,6 +45,7 @@ class SettingsData ApiPermissions permissions = ApiPermissions::ALL; Path webRoot; + Path altWebRoot; Path clientConfigDir; std::unordered_map responseHeaders; std::unordered_map urlMappings; From 2fd35c7988929ae56cf9ce709b987b4cf14359a9 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 10:12:54 +0500 Subject: [PATCH 04/12] static_controller: add support for altWebRoot --- cpp/server/file_system.hpp | 3 + cpp/server/static_controller.cpp | 98 +++++++++++++++++++------------- cpp/server/static_controller.hpp | 8 +-- 3 files changed, 66 insertions(+), 43 deletions(-) diff --git a/cpp/server/file_system.hpp b/cpp/server/file_system.hpp index 197f4feb..5be97f23 100644 --- a/cpp/server/file_system.hpp +++ b/cpp/server/file_system.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,8 @@ namespace fs = boost::filesystem; using Path = fs::path; +using PathVectorPtr = std::shared_ptr>; + #ifdef MSRV_OS_POSIX #define MSRV_PATH_LITERAL(str) Path(str) #endif diff --git a/cpp/server/static_controller.cpp b/cpp/server/static_controller.cpp index 077ff277..f301a2ca 100644 --- a/cpp/server/static_controller.cpp +++ b/cpp/server/static_controller.cpp @@ -1,4 +1,7 @@ #include "static_controller.hpp" + +#include + #include "file_system.hpp" #include "content_type_map.hpp" #include "router.hpp" @@ -8,8 +11,12 @@ namespace msrv { StaticController::StaticController( - Request* request, const Path& targetDir, const ContentTypeMap& contentTypes) - : ControllerBase(request), targetDir_(targetDir), contentTypes_(contentTypes) + Request* request, + PathVectorPtr targetDirs, + const ContentTypeMap& contentTypes) + : ControllerBase(request), + targetDirs_(std::move(targetDirs)), + contentTypes_(contentTypes) { } @@ -25,20 +32,13 @@ std::string StaticController::getNormalizedPath() auto isDirectory = fullPath.back() == '/'; auto path = optionalParam("path"); - if (!path || path->empty()) - { - if (isDirectory) - return "index.html"; - else - return std::string(); - } - else - { - if (isDirectory) - return *path + "/index.html"; - else - return *path; - } + return path && !path->empty() + ? isDirectory // subpath of target dir + ? *path + "/index.html" + : *path + : isDirectory // exactly target dir + ? std::string("index.html") + : std::string(); } ResponsePtr StaticController::redirectToDirectory() @@ -52,33 +52,37 @@ ResponsePtr StaticController::getFile() if (requestPath.empty()) return redirectToDirectory(); - auto filePath = (targetDir_ / pathFromUtf8(requestPath)).lexically_normal(); - - if (!isSubpath(targetDir_, filePath)) - return Response::notFound(); + for (const auto& targetDir : *targetDirs_) + { + auto filePath = (targetDir / pathFromUtf8(requestPath)).lexically_normal(); - auto info = file_io::tryQueryInfo(filePath); - if (!info) - return Response::notFound(); + if (!isSubpath(targetDir, filePath)) + throw InvalidRequestException("invalid request path"); - switch (info->type) - { - case FileType::REGULAR: - { auto handle = file_io::open(filePath); if (!handle) - return Response::notFound(); + continue; - const auto& contentType = contentTypes_.byFilePath(filePath); - return Response::file(std::move(filePath), std::move(handle), *info, contentType); - } + auto info = file_io::queryInfo(handle.get()); - case FileType::DIRECTORY: - return redirectToDirectory(); + switch (info.type) + { + case FileType::REGULAR: + return Response::file( + std::move(filePath), + std::move(handle), + info, + contentTypes_.byFilePath(filePath)); + + case FileType::DIRECTORY: + return redirectToDirectory(); - default: - return Response::notFound(); + default: + break; + } } + + return Response::notFound(); } void StaticController::defineRoutes( @@ -89,23 +93,39 @@ void StaticController::defineRoutes( { for (auto& kv : settings->urlMappings) { - defineRoutes(router, workQueue, kv.first, kv.second, contentTypes); + auto dirs = std::make_shared>(1, kv.second); + defineRoutes(router, workQueue, kv.first, std::move(dirs), contentTypes); } + if (settings->webRoot.empty() && settings->altWebRoot.empty()) + return; + + auto targetDirs = std::make_shared>(); + if (!settings->webRoot.empty()) - defineRoutes(router, workQueue, "/", settings->webRoot, contentTypes); + targetDirs->emplace_back(settings->webRoot); + + if (!settings->altWebRoot.empty()) + targetDirs->emplace_back(settings->altWebRoot); + + defineRoutes(router, workQueue, "/", std::move(targetDirs), contentTypes); } void StaticController::defineRoutes( Router* router, WorkQueue* workQueue, const std::string& urlPrefix, - const Path& targetDir, + PathVectorPtr targetDirs, const ContentTypeMap& contentTypes) { + assert(!targetDirs->empty()); + auto routes = router->defineRoutes(); - routes.createWith([=](Request* request) { return new StaticController(request, targetDir, contentTypes); }); + routes.createWith([=](Request* request) { + return new StaticController(request, targetDirs, contentTypes); + }); + routes.useWorkQueue(workQueue); routes.get(urlPrefix, &StaticController::getFile); diff --git a/cpp/server/static_controller.hpp b/cpp/server/static_controller.hpp index ed4eebbd..fad2a8dd 100644 --- a/cpp/server/static_controller.hpp +++ b/cpp/server/static_controller.hpp @@ -16,7 +16,7 @@ class StaticController : public ControllerBase public: StaticController( Request* request, - const Path& targetDir, + PathVectorPtr targetDirs, const ContentTypeMap& contentTypes); ~StaticController(); @@ -28,18 +28,18 @@ class StaticController : public ControllerBase SettingsDataPtr settings, const ContentTypeMap& contentTypes); +private: static void defineRoutes( Router* router, WorkQueue* workQueue, const std::string& urlPrefix, - const Path& targetDir, + PathVectorPtr targetDirs, const ContentTypeMap& contentTypes); -private: std::string getNormalizedPath(); ResponsePtr redirectToDirectory(); - const Path& targetDir_; + PathVectorPtr targetDirs_; const ContentTypeMap& contentTypes_; }; From 49c820f71d2c6f459fa0778b524a47525dc0e793 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 10:38:27 +0500 Subject: [PATCH 05/12] api_tests: fixes & improvements --- js/api_tests/src/static_files_tests.js | 35 ++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/js/api_tests/src/static_files_tests.js b/js/api_tests/src/static_files_tests.js index 38b5248c..042e8f85 100644 --- a/js/api_tests/src/static_files_tests.js +++ b/js/api_tests/src/static_files_tests.js @@ -27,18 +27,18 @@ function assertRedirect(assert, result, location) assert.equal(result.headers["location"], location); } -describe('static files', () => { - setupPlayer({ pluginSettings, axiosConfig }); +function getFile(url, config) +{ + return client.handler.axios.get(url, config); +} - function getFile(url, config) - { - return client.handler.axios.get(url, config); - } +function getFileData(url) +{ + return readFile(path.join(config.webRootDir, url), 'utf8'); +} - function getFileData(url) - { - return readFile(path.join(config.webRootDir, url), 'utf8'); - } +describe('static files', () => { + setupPlayer({ pluginSettings, axiosConfig }); test('get index of root', async () => { const result = await getFile('/'); @@ -204,22 +204,25 @@ describe('static files', () => { }); test('escape root dir', async () => { + const result0 = await getFile('/../../../../../../../etc/passwd', ignoreStatus); + assert.equal(result0.status, 400); + const result1 = await getFile('/../package.json', ignoreStatus); - assert.equal(result1.status, 404); + assert.equal(result1.status, 400); const result2 = await getFile('/%2E%2E/package.json', ignoreStatus); - assert.equal(result2.status, 404); + assert.equal(result2.status, 400); const result3 = await getFile('/prefix/../package.json', ignoreStatus); - assert.equal(result3.status, 404); + assert.equal(result3.status, 400); const result4 = await getFile('/prefix/%2E%2E/package.json', ignoreStatus); - assert.equal(result4.status, 404); + assert.equal(result4.status, 400); const result5 = await getFile('/prefix/nested/../package.json', ignoreStatus); - assert.equal(result5.status, 404); + assert.equal(result5.status, 400); const result6 = await getFile('/prefix/nested/%2E%2E/package.json', ignoreStatus); - assert.equal(result6.status, 404); + assert.equal(result6.status, 400); }); }); From 3b2f98fd5cdcae7542b7815ee01b8fd33bfeaeac Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 10:49:36 +0500 Subject: [PATCH 06/12] settings: create client config dir and webroot dir when building settings --- cpp/server/settings.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/server/settings.cpp b/cpp/server/settings.cpp index 1c59bb0e..f057f64a 100644 --- a/cpp/server/settings.cpp +++ b/cpp/server/settings.cpp @@ -306,6 +306,10 @@ SettingsDataPtr SettingsBuilder::build() const settings->musicDirs = resolveMusicDirs(pluginProfileDir, musicDirs); processFile(pluginProfileDir, pluginProfileDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE), settings.get()); + + tryCatchLog([&] { fs::create_directories(settings->altWebRoot); }); + tryCatchLog([&] { fs::create_directories(settings->clientConfigDir); }); + return settings; } From 4d3090ef754127c8ef375df83af01e72105f5b01 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 11:03:13 +0500 Subject: [PATCH 07/12] api_tests: improve diagnostics --- js/api_tests/src/static_files_tests.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/api_tests/src/static_files_tests.js b/js/api_tests/src/static_files_tests.js index 042e8f85..c88d0ac3 100644 --- a/js/api_tests/src/static_files_tests.js +++ b/js/api_tests/src/static_files_tests.js @@ -141,7 +141,8 @@ describe('static files', () => { for (let file of Object.keys(contentTypes)) { const result = await getFile(file); - assert.equal(result.headers['content-type'], contentTypes[file]); + assert.equal(result.status, 200, 'invalid http status for file ' + file); + assert.equal(result.headers['content-type'], contentTypes[file], 'invalid content type for file ' + file); } }); From 24af23179d2d2a59a5ad1c7298732f0309f967ed Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 11:25:11 +0500 Subject: [PATCH 08/12] fixes --- cpp/server/static_controller.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/cpp/server/static_controller.cpp b/cpp/server/static_controller.cpp index f301a2ca..411671dd 100644 --- a/cpp/server/static_controller.cpp +++ b/cpp/server/static_controller.cpp @@ -59,25 +59,46 @@ ResponsePtr StaticController::getFile() if (!isSubpath(targetDir, filePath)) throw InvalidRequestException("invalid request path"); +#ifdef MSRV_OS_WINDOWS + // Windows can't open directories as files + + auto infoResult = file_io::tryQueryInfo(filePath); + if (!infoResult) + continue; + + auto info = *infoResult; + FileHandle handle; + + if (info.type == FileType::REGULAR) + { + handle = file_io::open(filePath); + if (!handle) + continue; + } +#else auto handle = file_io::open(filePath); if (!handle) continue; auto info = file_io::queryInfo(handle.get()); +#endif switch (info.type) { case FileType::REGULAR: + { + auto contentType = contentTypes_.byFilePath(filePath); return Response::file( std::move(filePath), std::move(handle), info, - contentTypes_.byFilePath(filePath)); + std::move(contentType)); + } case FileType::DIRECTORY: return redirectToDirectory(); - default: + case FileType::UNKNOWN: break; } } From 6aa4f446ac22393673f46c7c46b3c106e5ffde0b Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 11:37:30 +0500 Subject: [PATCH 09/12] api_tests: add test for altWebRoot --- js/api_tests/src/static_files_tests.js | 26 +++++++++++++++++++--- js/api_tests/src/test_context.js | 3 +++ js/api_tests/webroot2/altsubdir/index.html | 1 + js/api_tests/webroot2/extra.html | 1 + js/api_tests/webroot2/index.html | 1 + 5 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 js/api_tests/webroot2/altsubdir/index.html create mode 100644 js/api_tests/webroot2/extra.html create mode 100644 js/api_tests/webroot2/index.html diff --git a/js/api_tests/src/static_files_tests.js b/js/api_tests/src/static_files_tests.js index c88d0ac3..e3f7b8e0 100644 --- a/js/api_tests/src/static_files_tests.js +++ b/js/api_tests/src/static_files_tests.js @@ -125,6 +125,26 @@ describe('static files', () => { assert.equal(result.data, 'subdir/file.html\n'); }); + test('get file of alt root', async () => { + const result = await getFile('/extra.html'); + assert.equal(result.data, 'extra.html\n'); + }); + + test('get subdir index of alt root', async () => { + const result = await getFile('/altsubdir/'); + assert.equal(result.data, 'altsubdir/index.html\n'); + }); + + test('get subdir file of alt root', async () => { + const result = await getFile('/altsubdir/index.html'); + assert.equal(result.data, 'altsubdir/index.html\n'); + }); + + test('redirect subdir index of alt root', async () => { + const result = await getFile('/altsubdir'); + assertRedirect(assert, result, '/altsubdir/'); + }); + test('provide content type', async () => { const contentTypes = { 'file.html': 'text/html; charset=utf-8', @@ -138,11 +158,11 @@ describe('static files', () => { 'file.txt': 'text/plain; charset=utf-8', }; - for (let file of Object.keys(contentTypes)) + for (let file in contentTypes) { const result = await getFile(file); - assert.equal(result.status, 200, 'invalid http status for file ' + file); - assert.equal(result.headers['content-type'], contentTypes[file], 'invalid content type for file ' + file); + assert.equal(result.status, 200, 'invalid http status for ' + file); + assert.equal(result.headers['content-type'], contentTypes[file], 'invalid content type for ' + file); } }); diff --git a/js/api_tests/src/test_context.js b/js/api_tests/src/test_context.js index b93e4c10..36521f89 100644 --- a/js/api_tests/src/test_context.js +++ b/js/api_tests/src/test_context.js @@ -155,6 +155,7 @@ export class TestContextFactory const serverUrl = `http://127.0.0.1:${port}`; const webRootDir = path.join(testsRootDir, 'webroot'); + const altWebRootDir = path.join(testsRootDir, 'webroot2'); const musicDir = path.join(testsRootDir, 'tracks'); const pluginSettings = { @@ -162,6 +163,7 @@ export class TestContextFactory allowRemote: false, musicDirs: [musicDir], webRoot: webRootDir, + altWebRoot: altWebRootDir, }; return { @@ -171,6 +173,7 @@ export class TestContextFactory serverUrl, pluginBuildDir, webRootDir, + altWebRootDir, musicDir, pluginSettings, }; diff --git a/js/api_tests/webroot2/altsubdir/index.html b/js/api_tests/webroot2/altsubdir/index.html new file mode 100644 index 00000000..1ce42c51 --- /dev/null +++ b/js/api_tests/webroot2/altsubdir/index.html @@ -0,0 +1 @@ +altsubdir/index.html diff --git a/js/api_tests/webroot2/extra.html b/js/api_tests/webroot2/extra.html new file mode 100644 index 00000000..f5e4a63f --- /dev/null +++ b/js/api_tests/webroot2/extra.html @@ -0,0 +1 @@ +extra.html diff --git a/js/api_tests/webroot2/index.html b/js/api_tests/webroot2/index.html new file mode 100644 index 00000000..b9bc98cb --- /dev/null +++ b/js/api_tests/webroot2/index.html @@ -0,0 +1 @@ +Should not use this From 30f670e9ceced971aae6992eb07646b264556dbe Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 11:48:20 +0500 Subject: [PATCH 10/12] api_tests: clean up --- js/api_tests/src/static_files_tests.js | 74 +++++++++++------------ js/api_tests/webroot2/altfile.html | 1 + js/api_tests/webroot2/altsubdir/file.html | 1 + js/api_tests/webroot2/extra.html | 1 - 4 files changed, 39 insertions(+), 38 deletions(-) create mode 100644 js/api_tests/webroot2/altfile.html create mode 100644 js/api_tests/webroot2/altsubdir/file.html delete mode 100644 js/api_tests/webroot2/extra.html diff --git a/js/api_tests/src/static_files_tests.js b/js/api_tests/src/static_files_tests.js index e3f7b8e0..63a1a74e 100644 --- a/js/api_tests/src/static_files_tests.js +++ b/js/api_tests/src/static_files_tests.js @@ -55,21 +55,16 @@ describe('static files', () => { assert.equal(result.data, 'index.html\n'); }); - test('redirect index of prefix', async () => { - const result = await getFile('/prefix'); - assert.equal(result.headers["location"], '/prefix/'); - }); - - test('redirect index of nested prefix', async () => { - const result = await getFile('/prefix/nested'); - assertRedirect(assert, result, '/prefix/nested/'); - }); - test('get file of root', async () => { const result = await getFile('/file.html'); assert.equal(result.data, 'file.html\n'); }); + test('get file of alt root', async () => { + const result = await getFile('/altfile.html'); + assert.equal(result.data, 'altfile.html\n'); + }); + test('get file of prefix', async () => { const result = await getFile('/prefix/file.html'); assert.equal(result.data, 'file.html\n'); @@ -85,6 +80,11 @@ describe('static files', () => { assert.equal(result.data, 'subdir/index.html\n'); }); + test('get subdir index of alt root', async () => { + const result = await getFile('/altsubdir/'); + assert.equal(result.data, 'altsubdir/index.html\n'); + }); + test('get subdir index of prefix', async () => { const result = await getFile('/prefix/subdir/'); assert.equal(result.data, 'subdir/index.html\n'); @@ -95,26 +95,16 @@ describe('static files', () => { assert.equal(result.data, 'subdir/index.html\n'); }); - test('redirect subdir index of root', async () => { - const result = await getFile('/subdir'); - assertRedirect(assert, result, '/subdir/'); - }); - - test('redirect subdir index of prefix', async () => { - const result = await getFile('/prefix/subdir'); - assertRedirect(assert, result, '/prefix/subdir/'); - }); - - test('redirect subdir index of nested prefix', async () => { - const result = await getFile('/prefix/nested/subdir'); - assertRedirect(assert, result, '/prefix/nested/subdir/'); - }); - test('get subdir file of root', async () => { const result = await getFile('/subdir/file.html'); assert.equal(result.data, 'subdir/file.html\n'); }); + test('get subdir file of alt root', async () => { + const result = await getFile('/altsubdir/file.html'); + assert.equal(result.data, 'altsubdir/file.html\n'); + }); + test('get subdir file of prefix', async () => { const result = await getFile('/prefix/subdir/file.html'); assert.equal(result.data, 'subdir/file.html\n'); @@ -125,24 +115,34 @@ describe('static files', () => { assert.equal(result.data, 'subdir/file.html\n'); }); - test('get file of alt root', async () => { - const result = await getFile('/extra.html'); - assert.equal(result.data, 'extra.html\n'); + test('redirect subdir index of alt root', async () => { + const result = await getFile('/altsubdir'); + assertRedirect(assert, result, '/altsubdir/'); }); - test('get subdir index of alt root', async () => { - const result = await getFile('/altsubdir/'); - assert.equal(result.data, 'altsubdir/index.html\n'); + test('redirect subdir index of root', async () => { + const result = await getFile('/subdir'); + assertRedirect(assert, result, '/subdir/'); }); - test('get subdir file of alt root', async () => { - const result = await getFile('/altsubdir/index.html'); - assert.equal(result.data, 'altsubdir/index.html\n'); + test('redirect subdir index of prefix', async () => { + const result = await getFile('/prefix/subdir'); + assertRedirect(assert, result, '/prefix/subdir/'); }); - test('redirect subdir index of alt root', async () => { - const result = await getFile('/altsubdir'); - assertRedirect(assert, result, '/altsubdir/'); + test('redirect subdir index of nested prefix', async () => { + const result = await getFile('/prefix/nested/subdir'); + assertRedirect(assert, result, '/prefix/nested/subdir/'); + }); + + test('redirect index of prefix', async () => { + const result = await getFile('/prefix'); + assert.equal(result.headers["location"], '/prefix/'); + }); + + test('redirect index of nested prefix', async () => { + const result = await getFile('/prefix/nested'); + assertRedirect(assert, result, '/prefix/nested/'); }); test('provide content type', async () => { diff --git a/js/api_tests/webroot2/altfile.html b/js/api_tests/webroot2/altfile.html new file mode 100644 index 00000000..79b42be9 --- /dev/null +++ b/js/api_tests/webroot2/altfile.html @@ -0,0 +1 @@ +altfile.html diff --git a/js/api_tests/webroot2/altsubdir/file.html b/js/api_tests/webroot2/altsubdir/file.html new file mode 100644 index 00000000..d03af182 --- /dev/null +++ b/js/api_tests/webroot2/altsubdir/file.html @@ -0,0 +1 @@ +altsubdir/file.html diff --git a/js/api_tests/webroot2/extra.html b/js/api_tests/webroot2/extra.html deleted file mode 100644 index f5e4a63f..00000000 --- a/js/api_tests/webroot2/extra.html +++ /dev/null @@ -1 +0,0 @@ -extra.html From d8c3ef5b6a35f56a13577513684bd97f490d5676 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 12:10:28 +0500 Subject: [PATCH 11/12] documentation & change log updates --- .github/workflows/build.yml | 2 ++ ChangeLog.md | 4 +++- docs/advanced-config.md | 45 ++++++++++++++++++++++++------------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f3a3a144..84fade63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ on: - master paths-ignore: - README.md + - ChangeLog.md - docs/** pull_request: @@ -14,6 +15,7 @@ on: - master paths-ignore: - README.md + - ChangeLog.md - docs/** jobs: diff --git a/ChangeLog.md b/ChangeLog.md index a16ef09c..0f8ee0a8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,7 +2,9 @@ - Add macOS support - Rework mobile layout: make elements larger and better positioned, support swiping between views - Rework column editor: add support for subrows and basic formatting, each layout is now configured separately -- Allow non-absolute paths in config (resolved relative to {player profile directory}/beefweb/) +- Allow non-absolute paths in config (resolved relative to `{player_profile_dir}/beefweb/`) +- Serve custom web content from `{player_profile_dir}/beefweb/webroot/` +- Replace `BEEFWEB_CONFIG_FILE` environment variable with `BEEFWEB_PROFILE_DIR` (fully overrides `{player_profile_dir}/beefweb`) - Allow setting arbitrary UI scale - Preserve scroll position in file browser - Increase icon sizes in all layouts diff --git a/docs/advanced-config.md b/docs/advanced-config.md index 3b74db42..73b4f96b 100644 --- a/docs/advanced-config.md +++ b/docs/advanced-config.md @@ -1,24 +1,36 @@ # Advanced configuration -Advanced configuration is performed by editing configuration file. +## Profile directory -The following configuration sources are considered when loading configuration (in the order of increase of preference): +Beefweb keeps all settings in `{player_profile_dir}/beefweb/` directory: +- foobar2000 on Windows: `%APPDATA%\foobar2000-v2\beefweb\` +- foobar2000 on macOS: `$HOME/Library/foobar2000-v2/beefweb/` +- DeaDBeeF on Linux/*BSD: `$XDG_CONFIG_HOME/deadbeef/beefweb/` (or `$HOME/.config/deadbeef/beefweb/`) +- DeaDBeeF on macOS: `$HOME/Library/Preferences/deadbeef/beefweb/` -* Settings in UI -* `{player_profile_dir}/beefweb/config.json` - - foobar2000 on Windows: `%APPDATA%\foobar2000-v2\beefweb\config.json` - - foobar2000 on macOS: `$HOME/Library/foobar2000-v2/beefweb/config.json` - - DeaDBeeF on Linux/*BSD: `$XDG_CONFIG_HOME/deadbeef/beefweb/config.json` or `$HOME/.config/deadbeef/beefweb/config.json` - - DeaDBeeF on macOS: `$HOME/Library/Preferences/deadbeef/beefweb/config.json` -* File specified by `BEEFWEB_CONFIG_FILE` environment variable (must be absolute) +### Overriding profile directory -If setting is specified in more preferred source it overrides values defined in less preferred. +If environment variable `BEEFWEB_PROFILE_DIR` is specified, it overrides default beefweb profile directory. + +This path must be absolute. + +### Serving custom web content + +It is possible to serve custom web content (e.g. custom UI) using built-in web server. + +If certain file does not exist in bundled web resources corresponding file inside `{beefweb_profile_dir}/webroot/` will be used. + +## Configuration file + +Advanced configuration is performed by editing configuration file stored in `{beefweb_profile_dir}/config.json`. + +If setting is specified in configuration file, it overrides setting in UI. All values are optional, you can specify only those you want to override. The following options are available: -```js +```json { "port": 8880, "allowRemote": true, @@ -26,18 +38,17 @@ The following options are available: "authRequired": false, "authUser": "", "authPassword": "", - "webRoot": ".../beefweb.root", // path inside installation directory, directory layout is different depending on OS/player + "webRoot": "{beefweb_binary_dir}/beefweb.root/", + "altWebRoot": "{beefweb_profile_dir}/webroot/", "urlMappings": {}, "responseHeaders": {}, - "clientConfigDir": "{player profile directory}/beefweb/clientconfig" + "clientConfigDir": "{beefweb_profile_dir}/clientconfig/" } ``` ### Non-absolute paths -Unless specified otherwise any path in configuration could be non-absolute. - -Such paths are resolved relative to `{player_profile_dir}/beefweb` directory. +Non-absolute paths in configuration file are resolved relative to Beefweb profile directory. ### Network settings @@ -61,6 +72,8 @@ Such paths are resolved relative to `{player_profile_dir}/beefweb` directory. `webRoot: string` - Root directory where static web content is located. +`altWebRoot: string` - Alternative web root directory, if file is not found in `webRoot` corresponding file in `altWebRoot` is also tried. + `urlMappings: {string: string}` - Alternative web directories defined by URL prefix The following configuration file uses `C:\MyWebPage` directory to serve requests starting with `/mywebpage`: From f03f55da5460afa1864fe483786c0b6431c6cfef Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Sat, 31 Jan 2026 12:13:24 +0500 Subject: [PATCH 12/12] minor fix --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 0f8ee0a8..36398866 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,7 +4,7 @@ - Rework column editor: add support for subrows and basic formatting, each layout is now configured separately - Allow non-absolute paths in config (resolved relative to `{player_profile_dir}/beefweb/`) - Serve custom web content from `{player_profile_dir}/beefweb/webroot/` -- Replace `BEEFWEB_CONFIG_FILE` environment variable with `BEEFWEB_PROFILE_DIR` (fully overrides `{player_profile_dir}/beefweb`) +- Replace `BEEFWEB_CONFIG_FILE` environment variable with `BEEFWEB_PROFILE_DIR` (fully overrides `{player_profile_dir}/beefweb/`) - Allow setting arbitrary UI scale - Preserve scroll position in file browser - Increase icon sizes in all layouts