initial changes

This commit is contained in:
2025-02-18 14:31:57 -06:00
parent 7a3028c7fc
commit 474e3c43a7
157 changed files with 23776 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
#ifndef LIBMONITARR_INCLUDE_DATA_DB_HPP_
#define LIBMONITARR_INCLUDE_DATA_DB_HPP_
#include "utils/config.hpp"
namespace monitarr {
struct data_entry final {
std::string download_id;
std::uint64_t last_check{};
std::uint64_t size_left{};
};
class data_db final {
public:
data_db() noexcept = default;
data_db(const data_db &) noexcept = default;
data_db(data_db &&) noexcept = default;
auto operator=(const data_db &) noexcept -> data_db & = default;
auto operator=(data_db &&) noexcept -> data_db & = default;
~data_db() noexcept;
private:
std::shared_ptr<rocksdb::TransactionDB> db_;
private:
[[nodiscard]] auto
perform_action(std::string_view function_name,
std::function<rocksdb::Status(rocksdb::TransactionDB *tx_db)>
action) const -> bool;
[[nodiscard]] auto perform_action(
std::string_view function_name,
std::function<rocksdb::Status(rocksdb::Transaction *txn)> action) -> bool;
public:
void close();
[[nodiscard]] auto get(std::string_view download_id) const
-> std::optional<data_entry>;
void open(std::string_view data_dir);
void remove(std::string_view download_id);
void set(const data_entry &entry);
};
} // namespace monitarr
NLOHMANN_JSON_NAMESPACE_BEGIN template <>
struct adl_serializer<monitarr::data_entry> {
static void to_json(json &data, const monitarr::data_entry &value) {
data["download_id"] = value.download_id;
data["last_check"] = value.last_check;
data["size_left"] = value.size_left;
}
static void from_json(const json &data, monitarr::data_entry &value) {
data.at("download_id").get_to(value.download_id);
data.at("last_check").get_to(value.last_check);
data.at("size_left").get_to(value.size_left);
}
};
NLOHMANN_JSON_NAMESPACE_END
#endif // LIBMONITARR_INCLUDE_DATA_DB_HPP_

View File

@@ -0,0 +1,10 @@
#ifndef LIBMONITARR_INCLUDE_INITIALIZE_HPP_
#define LIBMONITARR_INCLUDE_INITIALIZE_HPP_
namespace monitarr {
void project_cleanup();
[[nodiscard]] auto project_initialize() -> bool;
} // namespace monitarr
#endif // LIBMONITARR_INCLUDE_INITIALIZE_HPP_

View File

@@ -0,0 +1,89 @@
#ifndef LIBMONITARR_INCLUDE_SETTINGS_HPP_
#define LIBMONITARR_INCLUDE_SETTINGS_HPP_
#include "utils/config.hpp"
namespace monitarr {
inline constexpr const auto default_interval{
std::chrono::minutes{5U},
};
inline constexpr const auto default_timeout{
std::chrono::minutes{24U * 60U},
};
struct server_cfg final {
std::string id;
std::string api_key;
std::string api_version;
std::chrono::minutes timeout{default_timeout};
std::string url;
};
struct app_config final {
std::chrono::minutes check_interval{default_interval};
std::vector<server_cfg> server_list{
server_cfg{
"lidarr",
"7228e4739091469db81acfe2b97aa973",
"v1",
std::chrono::minutes(1440),
"http://192.168.1.60:8686",
},
server_cfg{
"radarr",
"affa8c547cee48e0b8c49994c0aaa931",
"v3",
std::chrono::minutes(1440),
"http://192.168.1.60:7878",
},
server_cfg{
"sonarr",
"ead2cdf34f774ad1b8185271c733c5ad",
"v3",
std::chrono::minutes(1440),
"http://192.168.1.60:8989",
},
};
void load(std::string_view file_path);
void save(std::string_view file_path) const;
};
} // namespace monitarr
NLOHMANN_JSON_NAMESPACE_BEGIN
template <> struct adl_serializer<monitarr::server_cfg> {
static void to_json(json &data, const monitarr::server_cfg &value) {
data["id"] = value.id;
data["api_key"] = value.api_key;
data["api_version"] = value.api_version;
data["timeout_minutes"] = value.timeout.count();
data["url"] = value.url;
}
static void from_json(const json &data, monitarr::server_cfg &value) {
data.at("id").get_to(value.id);
data.at("api_key").get_to(value.api_key);
data.at("api_version").get_to(value.api_version);
value.timeout =
std::chrono::minutes(data.at("timeout_minutes").get<std::int32_t>());
data.at("url").get_to(value.url);
}
};
template <> struct adl_serializer<monitarr::app_config> {
static void to_json(json &data, const monitarr::app_config &value) {
data["check_interval_minutes"] = value.check_interval.count();
data["server_list"] = value.server_list;
}
static void from_json(const json &data, monitarr::app_config &value) {
value.check_interval = std::chrono::minutes(
data.at("check_interval_minutes").get<std::int32_t>());
data.at("server_list").get_to(value.server_list);
}
};
NLOHMANN_JSON_NAMESPACE_END
#endif // LIBMONITARR_INCLUDE_SETTINGS_HPP_

View File

@@ -0,0 +1,12 @@
#ifndef LIBMONITARR_INCLUDE_VERSION_HPP_
#define LIBMONITARR_INCLUDE_VERSION_HPP_
#include <string_view>
namespace monitarr {
[[nodiscard]] auto project_get_git_rev() -> std::string_view;
[[nodiscard]] auto project_get_version() -> std::string_view;
} // namespace monitarr
#endif // LIBMONITARR_INCLUDE_VERSION_HPP_

View File

@@ -0,0 +1,131 @@
#include "data_db.hpp"
#include "utils/error.hpp"
#include "utils/path.hpp"
namespace monitarr {
data_db::~data_db() noexcept { close(); }
void data_db::close() {
if (db_) {
db_->Close();
db_.reset();
}
}
auto data_db::get(std::string_view download_id) const
-> std::optional<data_entry> {
MONITARR_USES_FUNCTION_NAME();
std::optional<data_entry> ret;
if (not perform_action(
function_name,
[&download_id, &ret](rocksdb::TransactionDB *txn_db) -> auto {
std::string value;
auto res = txn_db->Get(rocksdb::ReadOptions{}, download_id, &value);
if (res.ok()) {
ret = nlohmann::json::parse(value).get<data_entry>();
}
return res.IsNotFound() ? rocksdb::Status{} : res;
})) {
fmt::println("failed to get|{}", download_id);
}
return ret;
}
void data_db::open(std::string_view data_dir) {
auto db_path = utils::path::combine(data_dir, {"state_db"});
fmt::println("opening database|{}", db_path);
rocksdb::Options options{};
options.create_if_missing = true;
options.create_missing_column_families = true;
options.db_log_dir = data_dir;
options.keep_log_file_num = 10;
rocksdb::TransactionDB *ptr{};
auto status = rocksdb::TransactionDB::Open(
options, rocksdb::TransactionDBOptions{}, db_path, &ptr);
if (not status.ok()) {
throw std::runtime_error(fmt::format("failed to open database|{}|{}",
db_path, status.ToString()));
}
db_ = std::shared_ptr<rocksdb::TransactionDB>(ptr);
}
auto data_db::perform_action(
std::string_view function_name,
std::function<rocksdb::Status(rocksdb::TransactionDB *tx_db)> action) const
-> bool {
try {
auto res = action(db_.get());
if (not res.ok()) {
utils::error::handle_error(function_name, res.ToString());
}
return res.ok();
} catch (const std::exception &ex) {
utils::error::handle_exception(function_name, ex);
}
return false;
}
auto data_db::perform_action(
std::string_view function_name,
std::function<rocksdb::Status(rocksdb::Transaction *txn)> action) -> bool {
std::unique_ptr<rocksdb::Transaction> txn{
db_->BeginTransaction(rocksdb::WriteOptions{},
rocksdb::TransactionOptions{}),
};
try {
auto res = action(txn.get());
if (res.ok()) {
auto commit_res = txn->Commit();
if (commit_res.ok()) {
return true;
}
utils::error::handle_error(function_name,
"rocksdb commit failed|" + res.ToString());
return false;
}
utils::error::handle_error(function_name,
"rocksdb action failed|" + res.ToString());
} catch (const std::exception &ex) {
utils::error::handle_exception(function_name, ex);
}
auto rollback_res = txn->Rollback();
utils::error::handle_error(function_name, "rocksdb rollback failed|" +
rollback_res.ToString());
return false;
}
void data_db::remove(std::string_view download_id) {
MONITARR_USES_FUNCTION_NAME();
if (not perform_action(function_name,
[&download_id](rocksdb::Transaction *txn) -> auto {
return txn->Delete(download_id);
})) {
fmt::println("failed to remove|{}", download_id);
}
}
void data_db::set(const data_entry &entry) {
MONITARR_USES_FUNCTION_NAME();
if (not perform_action(
function_name, [&entry](rocksdb::Transaction *txn) -> auto {
return txn->Put(entry.download_id, nlohmann::json(entry).dump());
})) {
fmt::println("failed to set|{}", nlohmann::json(entry).dump(2));
}
}
} // namespace monitarr

View File

@@ -0,0 +1,90 @@
#if defined(PROJECT_ENABLE_CURL)
#include "curl/curl.h"
#endif // defined(PROJECT_ENABLE_CURL)
#if defined(PROJECT_ENABLE_OPENSSL)
#include "openssl/ssl.h"
#endif // defined(PROJECT_ENABLE_OPENSSL)
#if defined(PROJECT_REQUIRE_ALPINE) && !defined(PROJECT_IS_MINGW)
#include <filesystem>
#include <stdlib.h>
#include <pthread.h>
#endif // defined(PROJECT_REQUIRE_ALPINE) && !defined(PROJECT_IS_MINGW)
#if defined(PROJECT_ENABLE_LIBSODIUM)
#include "sodium.h"
#endif // defined(PROJECT_ENABLE_LIBSODIUM)
#if defined(PROJECT_ENABLE_SQLITE)
#include "sqlite3.h"
#endif // defined(PROJECT_ENABLE_SQLITE)
#include "initialize.hpp"
#if defined(PROJECT_REQUIRE_ALPINE) && !defined(PROJECT_IS_MINGW)
#include "utils/path.hpp"
#endif // defined(PROJECT_REQUIRE_ALPINE) && !defined (PROJECT_IS_MINGW)
namespace monitarr {
auto project_initialize() -> bool {
#if defined(PROJECT_REQUIRE_ALPINE) && !defined(PROJECT_IS_MINGW)
{
static constexpr const auto guard_size{4096U};
static constexpr const auto stack_size{8U * 1024U * 1024U};
pthread_attr_t attr{};
pthread_attr_setstacksize(&attr, stack_size);
pthread_attr_setguardsize(&attr, guard_size);
pthread_setattr_default_np(&attr);
setenv("ICU_DATA", utils::path::combine(".", {"/icu"}).c_str(), 1);
}
#endif // defined(PROJECT_REQUIRE_ALPINE) && !defined(PROJECT_IS_MINGW)
#if defined(PROJECT_ENABLE_LIBSODIUM)
{
if (sodium_init() == -1) {
return false;
}
}
#endif // defined(PROJECT_ENABLE_LIBSODIUM)
#if defined(PROJECT_ENABLE_OPENSSL)
{ SSL_library_init(); }
#endif // defined(PROJECT_ENABLE_OPENSSL)
#if defined(PROJECT_ENABLE_CURL)
{
auto res = curl_global_init(CURL_GLOBAL_ALL);
if (res != 0) {
return false;
}
}
#endif // defined(PROJECT_ENABLE_CURL)
#if defined(PROJECT_ENABLE_SQLITE)
{
auto res = sqlite3_initialize();
if (res != SQLITE_OK) {
#if defined(PROJECT_ENABLE_CURL)
curl_global_cleanup();
#endif // defined(PROJECT_ENABLE_CURL)
return false;
}
}
#endif // defined(PROJECT_ENABLE_SQLITE)
return true;
}
void project_cleanup() {
#if defined(PROJECT_ENABLE_CURL)
curl_global_cleanup();
#endif // defined(PROJECT_ENABLE_CURL)
#if defined(PROJECT_ENABLE_SQLITE)
sqlite3_shutdown();
#endif // defined(PROJECT_ENABLE_SQLITE)
}
} // namespace monitarr

View File

@@ -0,0 +1,18 @@
#include "settings.hpp"
#include "utils/file.hpp"
namespace monitarr {
void app_config::load(std::string_view file_path) {
nlohmann::json data;
if (utils::file::read_json_file(file_path, data)) {
*this = data.get<app_config>();
}
}
void app_config::save(std::string_view file_path) const {
if (utils::file::write_json_file(file_path, nlohmann::json(*this))) {
return;
}
}
} // namespace monitarr

260
monitarr/monitarr/main.cpp Normal file
View File

@@ -0,0 +1,260 @@
#if defined(PROJECT_ENABLE_BACKWARD_CPP)
#include "backward.hpp"
#endif // defined(PROJECT_ENABLE_BACKWARD_CPP)
#include <execution>
#include "initialize.hpp"
#include "data_db.hpp"
#include "settings.hpp"
#include "utils/common.hpp"
#include "utils/config.hpp"
#include "utils/file.hpp"
#include "utils/path.hpp"
#include "utils/time.hpp"
#include "utils/unix.hpp"
#include "utils/windows.hpp"
namespace monitarr {
static void remove_stalled(std::string_view download_id, std::string_view title,
std::uint64_t episode_id, std::uint64_t movie_id,
const server_cfg &server, data_db &state_db) {
fmt::println("remove and block {}|{}", download_id, title);
state_db.remove(download_id);
httplib::Client cli{server.url};
cli.set_default_headers({
{"X-Api-Key", server.api_key},
});
auto response = cli.Delete(
fmt::format("/api/{}/queue/{}?blocklist=true&skipRedownload=false",
server.api_version,
utils::string::split(download_id, '/', false).at(1U)));
if (response->status != httplib::StatusCode::OK_200) {
fmt::println("remove and block result|{}|{}", server.url, response->status);
return;
}
if (utils::string::contains("radarr", server.id)) {
nlohmann::json data({
{"name", "MoviesSearch"},
{"movieIds", {movie_id}},
});
response = cli.Post("/api/{}/command", data.dump(), "application/json");
if (response->status != httplib::StatusCode::OK_200) {
fmt::println("failed to search radarr|{}|{}", server.url,
response->status);
}
return;
}
if (utils::string::contains("sonarr", server.id)) {
nlohmann::json data({
{"name", "EpisodeSearch"},
{"episodeIds", {episode_id}},
});
response = cli.Post("/api/{}/command", data.dump(), "application/json");
if (response->status != httplib::StatusCode::OK_200) {
fmt::println("failed to search sonarr|{}|{}", server.url,
response->status);
}
return;
}
}
static void check_server(const server_cfg &server, data_db &state_db) {
httplib::Client cli{server.url};
cli.set_default_headers({
{"X-Api-Key", server.api_key},
});
std::uint16_t page{0U};
while (++page != 0U) {
httplib::Params params;
params.emplace("page", std::to_string(page));
params.emplace("pageSize", "50");
auto response =
cli.Get(fmt::format("/api/{}/queue", server.api_version), params, {});
if (response->status != httplib::StatusCode::OK_200) {
fmt::println("check server request failed|{}|{}", server.url,
response->status);
break;
}
auto json_data = nlohmann::json::parse(response->body);
if (json_data.at("page").get<std::uint32_t>() != page) {
break;
}
auto now = utils::time::get_time_now();
for (const auto &record : json_data.at("records")) {
auto download_id = fmt::format(
"{}/{}", server.id, record.at("downloadId").get<std::string>());
auto episode_id = record.contains("episodeId")
? record["episodeId"].get<std::uint64_t>()
: std::uint64_t{0U};
auto movie_id = record.contains("movieId")
? record["movieId"].get<std::uint64_t>()
: std::uint64_t{0U};
auto size_left = record.at("sizeleft").get<std::uint64_t>();
auto title = record.at("title").get<std::string>();
auto data = state_db.get(download_id);
const auto update_entry = [&download_id, &now, &size_left, &state_db,
&title, url = server.url]() {
if (size_left == 0U) {
state_db.remove(download_id);
return;
}
fmt::println("updating {}|{}|{}|{}", download_id, title, now,
size_left);
state_db.set(data_entry{
download_id,
now,
size_left,
});
};
if (data.has_value()) {
if (std::chrono::nanoseconds(now - data->last_check) >=
server.timeout) {
if (size_left == data->size_left) {
remove_stalled(download_id, title, episode_id, movie_id, server,
state_db);
} else {
update_entry();
}
} else if (size_left == 0U) {
state_db.remove(download_id);
}
} else {
update_entry();
}
}
}
}
[[nodiscard]] static auto load_config(std::string &cfg_file) -> app_config {
auto cfg_dir = utils::get_environment_variable("MONITARR_CFG_DIR");
if (cfg_dir.empty()) {
cfg_dir = utils::path::combine(".", {"config"});
}
if (not utils::file::directory{cfg_dir}.create_directory()) {
throw std::runtime_error(fmt::format("failed to create config dir|{}",
cfg_dir,
utils::get_last_error_code()));
}
cfg_file = utils::path::combine(cfg_dir, {"monitarr.json"});
fmt::println("loading config|{}", cfg_file);
app_config cfg{};
cfg.load(cfg_file);
return cfg;
}
[[nodiscard]] static auto load_db() -> data_db {
auto data_dir = utils::get_environment_variable("MONITARR_DATA_DIR");
if (data_dir.empty()) {
data_dir = utils::path::combine(".", {"data"});
}
if (not utils::file::directory{data_dir}.create_directory()) {
throw std::runtime_error(fmt::format("failed to create data dir|{}",
data_dir,
utils::get_last_error_code()));
}
data_db state_db{};
state_db.open(data_dir);
return state_db;
}
} // namespace monitarr
using namespace monitarr;
auto main(int /* argc */, char ** /* argv */) -> int {
MONITARR_USES_FUNCTION_NAME();
#if defined(PROJECT_ENABLE_BACKWARD_CPP)
static backward::SignalHandling sh;
#endif // defined(PROJECT_ENABLE_BACKWARD_CPP)
if (not monitarr::project_initialize()) {
return -1;
}
static std::mutex mtx;
static std::condition_variable notify;
static stop_type stop_requested{false};
static const auto quit_handler = [](int sig) {
fmt::println("stop requested|{}", sig);
stop_requested = true;
mutex_lock lock(mtx);
notify.notify_all();
};
std::signal(SIGINT, quit_handler);
std::signal(SIGQUIT, quit_handler);
std::signal(SIGTERM, quit_handler);
auto ret{0};
try {
std::string cfg_file;
auto cfg{load_config(cfg_file)};
auto state_db{load_db()};
if (cfg.server_list.empty()) {
fmt::println("no servers have been configured");
ret = 3;
} else {
while (not stop_requested) {
std::for_each(std::execution::par, cfg.server_list.begin(),
cfg.server_list.end(), [&state_db](auto &&server) {
if (stop_requested) {
return;
}
try {
check_server(server, state_db);
} catch (const std::exception &ex) {
utils::error::handle_exception(function_name, ex);
} catch (...) {
utils::error::handle_exception(function_name);
}
});
unique_mutex_lock lock(mtx);
if (stop_requested) {
continue;
}
fmt::println("waiting for next check|{}", cfg.check_interval);
notify.wait_for(lock, cfg.check_interval);
}
}
cfg.save(cfg_file);
state_db.close();
} catch (const std::exception &ex) {
utils::error::handle_exception(function_name, ex);
ret = 2;
} catch (...) {
utils::error::handle_exception(function_name);
ret = 2;
}
fmt::println("terminating application|{}", ret);
monitarr::project_cleanup();
return ret;
}

View File

@@ -0,0 +1,29 @@
#if defined(PROJECT_ENABLE_BACKWARD_CPP)
#include "backward.hpp"
#endif // defined(PROJECT_ENABLE_BACKWARD_CPP)
#include "gtest/gtest.h"
#include "initialize.hpp"
#include "utils/config.hpp"
using namespace monitarr;
int PROJECT_TEST_RESULT{0};
auto main(int argc, char **argv) -> int {
#if defined(PROJECT_ENABLE_BACKWARD_CPP)
static backward::SignalHandling sh;
#endif
if (not monitarr::project_initialize()) {
return -1;
}
::testing::InitGoogleTest(&argc, argv);
PROJECT_TEST_RESULT = RUN_ALL_TESTS();
monitarr::project_cleanup();
return PROJECT_TEST_RESULT;
}

14
monitarr/version.cpp.in Normal file
View File

@@ -0,0 +1,14 @@
#include "version.hpp"
namespace {
static constexpr const std::string_view git_rev = "@PROJECT_GIT_REV@";
static constexpr const std::string_view version =
"@PROJECT_MAJOR_VERSION@.@PROJECT_MINOR_VERSION@.@PROJECT_REVISION_VERSION@"
"-@PROJECT_RELEASE_ITER@";
} // namespace
namespace monitarr {
auto project_get_git_rev() -> std::string_view { return git_rev; }
auto project_get_version() -> std::string_view { return version; }
} // namespace monitarr

64
monitarr/version.rc.in Normal file
View File

@@ -0,0 +1,64 @@
#include <windows.h>
#define VER_FILEVERSION @PROJECT_MAJOR_VERSION@,@PROJECT_MINOR_VERSION@,@PROJECT_REVISION_VERSION@,@PROJECT_RELEASE_NUM@
#define VER_FILEVERSION_STR "@PROJECT_MAJOR_VERSION@.@PROJECT_MINOR_VERSION@.@PROJECT_REVISION_VERSION@-@PROJECT_RELEASE_ITER@_@PROJECT_GIT_REV@\0"
#define VER_PRODUCTVERSION @PROJECT_MAJOR_VERSION@,@PROJECT_MINOR_VERSION@,@PROJECT_REVISION_VERSION@,@PROJECT_RELEASE_NUM@
#define VER_PRODUCTVERSION_STR "@PROJECT_MAJOR_VERSION@.@PROJECT_MINOR_VERSION@.@PROJECT_REVISION_VERSION@-@PROJECT_RELEASE_ITER@_@PROJECT_GIT_REV@\0"
#define VER_COMPANYNAME_STR "@PROJECT_COMPANY_NAME@\0"
#define VER_INTERNALNAME_STR "@PROJECT_NAME@ @PROJECT_MAJOR_VERSION@.@PROJECT_MINOR_VERSION@.@PROJECT_REVISION_VERSION@-@PROJECT_RELEASE_ITER@_@PROJECT_GIT_REV@\0"
#define VER_LEGALCOPYRIGHT_STR "@PROJECT_COPYRIGHT@\0"
#define VER_ORIGINALFILENAME_STR "@PROJECT_NAME@.exe\0"
#define VER_LEGALTRADEMARKS1_STR "\0"
#define VER_LEGALTRADEMARKS2_STR "\0"
#define VER_FILEDESCRIPTION_STR "@PROJECT_DESC@\0"
#define VER_PRODUCTNAME_STR "@PROJECT_NAME@ @PROJECT_MAJOR_VERSION@.@PROJECT_MINOR_VERSION@.@PROJECT_REVISION_VERSION@-@PROJECT_RELEASE_ITER@_@PROJECT_GIT_REV@\0"
#ifdef DEBUG
#define VER_DEBUG VS_FF_DEBUG
#else
#define VER_DEBUG 0
#endif
#define VER_PRERELEASE @PROJECT_PRERELEASE@
VS_VERSION_INFO VERSIONINFO
FILEVERSION VER_FILEVERSION
PRODUCTVERSION VER_PRODUCTVERSION
FILEFLAGSMASK (VS_FF_DEBUG|VS_FF_PRERELEASE)
FILEFLAGS (VER_DEBUG|VER_PRERELEASE)
FILEOS VOS__WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "CompanyName", VER_COMPANYNAME_STR
VALUE "FileDescription", VER_FILEDESCRIPTION_STR
VALUE "FileVersion", VER_FILEVERSION_STR
VALUE "InternalName", VER_INTERNALNAME_STR
VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR
VALUE "LegalTrademarks1", VER_LEGALTRADEMARKS1_STR
VALUE "LegalTrademarks2", VER_LEGALTRADEMARKS2_STR
VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR
VALUE "ProductName", VER_PRODUCTNAME_STR
VALUE "ProductVersion", VER_PRODUCTVERSION_STR
END
END
BLOCK "VarFileInfo"
BEGIN
/* The following line should only be modified for localized versions. */
/* It consists of any number of WORD,WORD pairs, with each pair */
/* describing a language,codepage combination supported by the file. */
/* */
/* For example, a file might have values "0x409,1252" indicating that it */
/* supports English language (0x409) in the Windows ANSI codepage (1252). */
VALUE "Translation", 0x409, 1252
END
END