Files
monitarr/monitarr/monitarr/main.cpp
2025-02-18 19:25:26 -06:00

426 lines
14 KiB
C++

#if defined(PROJECT_ENABLE_BACKWARD_CPP)
#include "backward.hpp"
#endif // defined(PROJECT_ENABLE_BACKWARD_CPP)
#include <execution>
#include "initialize.hpp"
#include "args.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 {
[[nodiscard]] static auto create_client(const server_cfg &server)
-> httplib::Client {
httplib::Client cli{server.url};
cli.set_default_headers({
{"X-Api-Key", server.api_key},
});
return cli;
}
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 = nullptr) {
MONITARR_USES_FUNCTION_NAME();
fmt::println("remove and block {}|{}|{}|{}", server.id, server.url, title,
download_id);
if (state_db != nullptr) {
state_db->remove(download_id);
}
auto cli = create_client(server);
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) {
utils::error::handle_error(
function_name, fmt::format("failed to delete download|{}|{}|{}",
server.id, server.url, response->status));
return;
}
if (utils::string::contains("radarr", server.id)) {
nlohmann::json data({
{"name", "MoviesSearch"},
{"movieIds", {movie_id}},
});
response = cli.Post(fmt::format("/api/{}/command", server.api_version),
data.dump(), "application/json");
if (response->status != httplib::StatusCode::Created_201) {
utils::error::handle_error(function_name,
fmt::format("failed to search|{}|{}|{}|{}|{}",
server.id, server.url, title,
movie_id, response->status));
}
return;
}
if (not utils::string::contains("sonarr", server.id)) {
return;
}
nlohmann::json data({
{"name", "EpisodeSearch"},
{"episodeIds", {episode_id}},
});
response = cli.Post(fmt::format("/api/{}/command", server.api_version),
data.dump(), "application/json");
if (response->status != httplib::StatusCode::Created_201) {
utils::error::handle_error(function_name,
fmt::format("failed to search|{}|{}|{}|{}|{}",
server.id, server.url, title,
episode_id, response->status));
}
}
static void display_queue(const server_cfg &server) {
MONITARR_USES_FUNCTION_NAME();
auto cli = create_client(server);
std::uint16_t page{0U};
while (++page != 0U) {
httplib::Params params;
params.emplace("page", std::to_string(page));
params.emplace("pageSize", "100");
auto response =
cli.Get(fmt::format("/api/{}/queue", server.api_version), params, {});
if (response->status != httplib::StatusCode::OK_200) {
utils::error::handle_error(
function_name, fmt::format("check server request failed|{}|{}|{}",
server.id, server.url, response->status));
break;
}
auto json_data = nlohmann::json::parse(response->body);
if (json_data.at("page").get<std::uint32_t>() != page) {
return;
}
for (const auto &record : json_data.at("records")) {
fmt::println("{}", record.dump(2));
}
}
}
[[nodiscard]] static auto get_download(std::uint64_t record_id,
const server_cfg &server)
-> std::optional<nlohmann::json> {
MONITARR_USES_FUNCTION_NAME();
auto cli = create_client(server);
std::uint16_t page{0U};
while (++page != 0U) {
httplib::Params params;
params.emplace("page", std::to_string(page));
params.emplace("pageSize", "100");
auto response =
cli.Get(fmt::format("/api/{}/queue", server.api_version), params, {});
if (response->status != httplib::StatusCode::OK_200) {
utils::error::handle_error(
function_name, fmt::format("check server request failed|{}|{}|{}",
server.id, server.url, response->status));
return std::nullopt;
}
auto json_data = nlohmann::json::parse(response->body);
if (json_data.at("page").get<std::uint32_t>() != page) {
return std::nullopt;
}
auto iter = std::ranges::find_if(
json_data.at("records"),
[&record_id](const nlohmann::json &record) -> bool {
return record_id == record.at("id").get<std::uint64_t>();
});
if (iter == json_data.at("records").end()) {
continue;
}
return *iter;
}
return std::nullopt;
}
static void check_server(const server_cfg &server, data_db &state_db) {
MONITARR_USES_FUNCTION_NAME();
fmt::println("checking server|{}|{}", server.id, server.url);
auto cli = create_client(server);
std::uint16_t page{0U};
while (++page != 0U) {
httplib::Params params;
params.emplace("page", std::to_string(page));
params.emplace("pageSize", "100");
auto response =
cli.Get(fmt::format("/api/{}/queue", server.api_version), params, {});
if (response->status != httplib::StatusCode::OK_200) {
utils::error::handle_error(
function_name, fmt::format("check server request failed|{}|{}|{}",
server.id, 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("id").get<std::uint64_t>());
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 is_downloading = utils::string::contains(
record.at("status").get<std::string>(), "downloading");
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 if (size_left == 0U || not is_downloading) {
state_db.remove(download_id);
} else {
update_entry();
}
} else if (size_left == 0U || not is_downloading) {
state_db.remove(download_id);
}
} else if (is_downloading && size_left > 0U) {
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;
}
auto ret{0};
if (argc > 1) {
try {
std::string cfg_file;
auto cfg{load_config(cfg_file)};
if (has_arg("-d", argc, argv)) {
fmt::println("{}", nlohmann::json(cfg).dump(2));
} else if (has_arg("-h", argc, argv)) {
fmt::println("usage:");
fmt::println("monitarr -d");
fmt::println("\tdisplay configuration");
fmt::println("monitarr -l -i <index>");
fmt::println("\tdisplay server queue at configuration index");
fmt::println("monitarr -b -i <index> -id <record id>");
fmt::println("\tblocklist and search record id at configuration index");
fmt::println("monitarr -s -i <index> -id <record id>");
fmt::println("\tshow record id details at configuration index");
} else if (has_arg("-l", argc, argv)) {
auto idx = get_arg("-i", argc, argv);
if (idx.has_value()) {
auto &server = cfg.server_list.at(utils::string::to_uint64(*idx));
fmt::println("queue|{}|{}", server.id, server.url);
display_queue(server);
}
} else if (has_arg("-b", argc, argv)) {
auto idx = get_arg("-i", argc, argv);
if (idx.has_value()) {
auto &server = cfg.server_list.at(utils::string::to_uint64(*idx));
auto record_id = get_arg("-id", argc, argv);
if (record_id.has_value()) {
auto entry =
get_download(utils::string::to_uint64(*record_id), server);
if (entry.has_value()) {
remove_stalled(fmt::format("{}/{}", server.id, *record_id),
entry->at("title").get<std::string>(), 0U,
entry->at("movieId").get<std::uint64_t>(), server);
}
}
}
} else if (has_arg("-s", argc, argv)) {
auto idx = get_arg("-i", argc, argv);
if (idx.has_value()) {
auto &server = cfg.server_list.at(utils::string::to_uint64(*idx));
auto record_id = get_arg("-id", argc, argv);
if (record_id.has_value()) {
auto entry =
get_download(utils::string::to_uint64(*record_id), server);
if (entry.has_value()) {
fmt::println("{}", entry->dump(2));
}
}
}
}
} catch (const std::exception &ex) {
utils::error::handle_exception(function_name, ex);
ret = 2;
} catch (...) {
utils::error::handle_exception(function_name);
ret = 2;
}
} else {
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);
try {
std::string cfg_file;
auto cfg{load_config(cfg_file)};
auto state_db{load_db()};
if (cfg.server_list.empty()) {
utils::error::handle_error(function_name,
"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](const server_cfg &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;
}