From 42a9ed9b441291c94404980c8c962b973086fe2b Mon Sep 17 00:00:00 2001 From: "Scott E. Graves" Date: Wed, 19 Feb 2025 10:39:03 -0600 Subject: [PATCH] refactor --- monitarr/monitarr/include/actions.hpp | 23 +++ monitarr/monitarr/include/block_cmd.hpp | 13 ++ monitarr/monitarr/include/config_cmd.hpp | 12 ++ monitarr/monitarr/include/list_cmd.hpp | 13 ++ monitarr/monitarr/include/show_cmd.hpp | 13 ++ monitarr/monitarr/main.cpp | 199 ++--------------------- monitarr/monitarr/src/actions.cpp | 151 +++++++++++++++++ monitarr/monitarr/src/block_cmd.cpp | 38 +++++ monitarr/monitarr/src/config_cmd.cpp | 15 ++ monitarr/monitarr/src/list_cmd.cpp | 25 +++ monitarr/monitarr/src/show_cmd.cpp | 35 ++++ support/test/src/utils/error_test.cpp | 36 ++++ 12 files changed, 391 insertions(+), 182 deletions(-) create mode 100644 monitarr/monitarr/include/actions.hpp create mode 100644 monitarr/monitarr/include/block_cmd.hpp create mode 100644 monitarr/monitarr/include/config_cmd.hpp create mode 100644 monitarr/monitarr/include/list_cmd.hpp create mode 100644 monitarr/monitarr/include/show_cmd.hpp create mode 100644 monitarr/monitarr/src/actions.cpp create mode 100644 monitarr/monitarr/src/block_cmd.cpp create mode 100644 monitarr/monitarr/src/config_cmd.cpp create mode 100644 monitarr/monitarr/src/list_cmd.cpp create mode 100644 monitarr/monitarr/src/show_cmd.cpp diff --git a/monitarr/monitarr/include/actions.hpp b/monitarr/monitarr/include/actions.hpp new file mode 100644 index 0000000..b8434f0 --- /dev/null +++ b/monitarr/monitarr/include/actions.hpp @@ -0,0 +1,23 @@ +#ifndef LIBMONITARR_INCLUDE_ACTIONS_HPP_ +#define LIBMONITARR_INCLUDE_ACTIONS_HPP_ + +#include "utils/config.hpp" + +namespace monitarr { +struct server_cfg; +class data_db; + +[[nodiscard]] auto create_client(const server_cfg &server) -> httplib::Client; + +[[nodiscard]] auto get_download(std::uint64_t record_id, + const server_cfg &server) + -> std::optional; + +[[nodiscard]] auto list_queue(const server_cfg &server) -> int; + +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); +} // namespace monitarr + +#endif // LIBMONITARR_INCLUDE_ACTIONS_HPP_ diff --git a/monitarr/monitarr/include/block_cmd.hpp b/monitarr/monitarr/include/block_cmd.hpp new file mode 100644 index 0000000..8c982df --- /dev/null +++ b/monitarr/monitarr/include/block_cmd.hpp @@ -0,0 +1,13 @@ +#ifndef LIBMONITARR_INCLUDE_BLOCK_CMD_HPP_ +#define LIBMONITARR_INCLUDE_BLOCK_CMD_HPP_ + +#include "utils/config.hpp" + +namespace monitarr { +struct app_config; + +[[nodiscard]] auto block_cmd(int argc, char **argv, const app_config &cfg) + -> int; +} // namespace monitarr + +#endif // LIBMONITARR_INCLUDE_BLOCK_CMD_HPP_ diff --git a/monitarr/monitarr/include/config_cmd.hpp b/monitarr/monitarr/include/config_cmd.hpp new file mode 100644 index 0000000..23f1062 --- /dev/null +++ b/monitarr/monitarr/include/config_cmd.hpp @@ -0,0 +1,12 @@ +#ifndef LIBMONITARR_INCLUDE_CONFIG_CMD_HPP_ +#define LIBMONITARR_INCLUDE_CONFIG_CMD_HPP_ + +#include "utils/config.hpp" + +namespace monitarr { +struct app_config; + +[[nodiscard]] auto config_cmd(const app_config &cfg) -> int; +} // namespace monitarr + +#endif // LIBMONITARR_INCLUDE_CONFIG_CMD_HPP_ diff --git a/monitarr/monitarr/include/list_cmd.hpp b/monitarr/monitarr/include/list_cmd.hpp new file mode 100644 index 0000000..aabeadd --- /dev/null +++ b/monitarr/monitarr/include/list_cmd.hpp @@ -0,0 +1,13 @@ +#ifndef LIBMONITARR_INCLUDE_LIST_CMD_HPP_ +#define LIBMONITARR_INCLUDE_LIST_CMD_HPP_ + +#include "utils/config.hpp" + +namespace monitarr { +struct app_config; + +[[nodiscard]] auto list_cmd(int argc, char **argv, const app_config &cfg) + -> int; +} // namespace monitarr + +#endif // LIBMONITARR_INCLUDE_LIST_CMD_HPP_ diff --git a/monitarr/monitarr/include/show_cmd.hpp b/monitarr/monitarr/include/show_cmd.hpp new file mode 100644 index 0000000..03101c0 --- /dev/null +++ b/monitarr/monitarr/include/show_cmd.hpp @@ -0,0 +1,13 @@ +#ifndef LIBMONITARR_INCLUDE_SHOW_CMD_HPP_ +#define LIBMONITARR_INCLUDE_SHOW_CMD_HPP_ + +#include "utils/config.hpp" + +namespace monitarr { +struct app_config; + +[[nodiscard]] auto show_cmd(int argc, char **argv, const app_config &cfg) + -> int; +} // namespace monitarr + +#endif // LIBMONITARR_INCLUDE_SHOW_CMD_HPP_ diff --git a/monitarr/monitarr/main.cpp b/monitarr/monitarr/main.cpp index 4af9a52..2e57d7a 100644 --- a/monitarr/monitarr/main.cpp +++ b/monitarr/monitarr/main.cpp @@ -6,162 +6,24 @@ #include "initialize.hpp" +#include "actions.hpp" #include "args.hpp" +#include "block_cmd.hpp" +#include "config_cmd.hpp" #include "data_db.hpp" +#include "list_cmd.hpp" #include "settings.hpp" +#include "show_cmd.hpp" #include "utils/common.hpp" #include "utils/config.hpp" #include "utils/file.hpp" #include "utils/path.hpp" +#include "utils/string.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() != 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 { - 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() != 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(); - }); - 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(); @@ -299,53 +161,24 @@ auto main(int argc, char **argv) -> int { try { std::string cfg_file; auto cfg{load_config(cfg_file)}; - if (has_arg("-d", argc, argv)) { - fmt::println("{}", nlohmann::json(cfg).dump(2)); + if (has_arg("-b", argc, argv)) { + ret = block_cmd(argc, argv, cfg); + } else if (has_arg("-c", argc, argv)) { + ret = config_cmd(cfg); } else if (has_arg("-h", argc, argv)) { fmt::println("usage:"); - fmt::println("monitarr -d"); + fmt::println("monitarr -b -i -id "); + fmt::println("\tblocklist and search record id at configuration index"); + fmt::println("monitarr -c"); fmt::println("\tdisplay configuration"); fmt::println("monitarr -l -i "); fmt::println("\tdisplay server queue at configuration index"); - fmt::println("monitarr -b -i -id "); - fmt::println("\tblocklist and search record id at configuration index"); fmt::println("monitarr -s -i -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(), 0U, - entry->at("movieId").get(), server); - } - } - } + ret = list_cmd(argc, argv, cfg); } 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)); - } - } - } + ret = show_cmd(argc, argv, cfg); } } catch (const std::exception &ex) { utils::error::handle_exception(function_name, ex); @@ -368,7 +201,9 @@ auto main(int argc, char **argv) -> int { }; std::signal(SIGINT, quit_handler); +#if !defined(_WIN32) std::signal(SIGQUIT, quit_handler); +#endif // !defined(_WIN32) std::signal(SIGTERM, quit_handler); try { diff --git a/monitarr/monitarr/src/actions.cpp b/monitarr/monitarr/src/actions.cpp new file mode 100644 index 0000000..313ae5f --- /dev/null +++ b/monitarr/monitarr/src/actions.cpp @@ -0,0 +1,151 @@ +#include "actions.hpp" + +#include "data_db.hpp" +#include "settings.hpp" +#include "utils/error.hpp" +#include "utils/string.hpp" + +namespace monitarr { +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; +} + +auto get_download(std::uint64_t record_id, const server_cfg &server) + -> std::optional { + 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() != 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(); + }); + if (iter == json_data.at("records").end()) { + continue; + } + + return *iter; + } + + return std::nullopt; +} + +auto list_queue(const server_cfg &server) -> int { + 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 3; + } + + auto json_data = nlohmann::json::parse(response->body); + if (json_data.at("page").get() != page) { + break; + } + + utils::error::handle_info( + function_name, fmt::format("{}", json_data.at("records").dump(2))); + } + + return 0; +} + +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) { + 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)); + } +} + +} // namespace monitarr diff --git a/monitarr/monitarr/src/block_cmd.cpp b/monitarr/monitarr/src/block_cmd.cpp new file mode 100644 index 0000000..b4563f0 --- /dev/null +++ b/monitarr/monitarr/src/block_cmd.cpp @@ -0,0 +1,38 @@ +#include "block_cmd.hpp" + +#include "actions.hpp" +#include "args.hpp" +#include "settings.hpp" +#include "utils/error.hpp" +#include "utils/string.hpp" + +namespace monitarr { +auto block_cmd(int argc, char **argv, const app_config &cfg) -> int { + MONITARR_USES_FUNCTION_NAME(); + + auto idx = get_arg("-i", argc, argv); + if (not idx.has_value()) { + utils::error::handle_error(function_name, "'-i ' is required"); + return 3; + } + + auto &server = cfg.server_list.at(utils::string::to_uint64(*idx)); + auto record_id = get_arg("-id", argc, argv); + if (not record_id.has_value()) { + utils::error::handle_error(function_name, "'-id ' is required"); + return 3; + } + + auto entry = get_download(utils::string::to_uint64(*record_id), server); + if (not entry.has_value()) { + utils::error::handle_error(function_name, + fmt::format("record {} not found", *record_id)); + return 3; + } + + remove_stalled(fmt::format("{}/{}", server.id, *record_id), + entry->at("title").get(), 0U, + entry->at("movieId").get(), server); + return 0; +} +} // namespace monitarr diff --git a/monitarr/monitarr/src/config_cmd.cpp b/monitarr/monitarr/src/config_cmd.cpp new file mode 100644 index 0000000..a505e4a --- /dev/null +++ b/monitarr/monitarr/src/config_cmd.cpp @@ -0,0 +1,15 @@ +#include "list_cmd.hpp" + +#include "args.hpp" +#include "settings.hpp" +#include "utils/error.hpp" + +namespace monitarr { +auto config_cmd(const app_config &cfg) -> int { + MONITARR_USES_FUNCTION_NAME(); + + utils::error::handle_info(function_name, + fmt::format("{}", nlohmann::json(cfg).dump(2))); + return 0; +} +} // namespace monitarr diff --git a/monitarr/monitarr/src/list_cmd.cpp b/monitarr/monitarr/src/list_cmd.cpp new file mode 100644 index 0000000..5b2b171 --- /dev/null +++ b/monitarr/monitarr/src/list_cmd.cpp @@ -0,0 +1,25 @@ +#include "list_cmd.hpp" + +#include "actions.hpp" +#include "args.hpp" +#include "settings.hpp" +#include "utils/error.hpp" +#include "utils/string.hpp" + +namespace monitarr { +auto list_cmd(int argc, char **argv, const app_config &cfg) -> int { + MONITARR_USES_FUNCTION_NAME(); + + auto idx = get_arg("-i", argc, argv); + if (not idx.has_value()) { + utils::error::handle_error(function_name, "'-i ' is required"); + return 3; + } + + auto &server = cfg.server_list.at(utils::string::to_uint64(*idx)); + + utils::error::handle_info( + function_name, fmt::format("list queue|{}|{}", server.id, server.url)); + return list_queue(server); +} +} // namespace monitarr diff --git a/monitarr/monitarr/src/show_cmd.cpp b/monitarr/monitarr/src/show_cmd.cpp new file mode 100644 index 0000000..2e92a0d --- /dev/null +++ b/monitarr/monitarr/src/show_cmd.cpp @@ -0,0 +1,35 @@ +#include "show_cmd.hpp" + +#include "actions.hpp" +#include "args.hpp" +#include "settings.hpp" +#include "utils/error.hpp" +#include "utils/string.hpp" + +namespace monitarr { +auto show_cmd(int argc, char **argv, const app_config &cfg) -> int { + MONITARR_USES_FUNCTION_NAME(); + + auto idx = get_arg("-i", argc, argv); + if (not idx.has_value()) { + utils::error::handle_error(function_name, "'-i ' is required"); + return 3; + } + + auto &server = cfg.server_list.at(utils::string::to_uint64(*idx)); + auto record_id = get_arg("-id", argc, argv); + if (not record_id.has_value()) { + utils::error::handle_error(function_name, "'-id ' is required"); + return 3; + } + + auto entry = get_download(utils::string::to_uint64(*record_id), server); + if (not entry.has_value()) { + return 3; + } + + utils::error::handle_info(function_name, + fmt::format("", nlohmann::json(*entry).dump(2))); + return 0; +} +} // namespace monitarr diff --git a/support/test/src/utils/error_test.cpp b/support/test/src/utils/error_test.cpp index 85fdb04..3bd28e9 100644 --- a/support/test/src/utils/error_test.cpp +++ b/support/test/src/utils/error_test.cpp @@ -38,6 +38,12 @@ TEST(utils_error, check_default_exception_handler) { TEST(utils_error, can_override_exception_handler) { struct my_exc_handler final : public utils::error::i_exception_handler { +#if defined(PROJECT_ENABLE_V2_ERRORS) + MOCK_METHOD(void, handle_debug, + (std::string_view function_name, std::string_view msg), + (const, override)); +#endif // defined(PROJECT_ENABLE_V2_ERRORS) + MOCK_METHOD(void, handle_error, (std::string_view function_name, std::string_view msg), (const, override)); @@ -48,11 +54,30 @@ TEST(utils_error, can_override_exception_handler) { MOCK_METHOD(void, handle_exception, (std::string_view function_name, const std::exception &ex), (const, override)); + +#if defined(PROJECT_ENABLE_V2_ERRORS) + MOCK_METHOD(void, handle_info, + (std::string_view function_name, std::string_view msg), + (const, override)); + + MOCK_METHOD(void, handle_trace, + (std::string_view function_name, std::string_view msg), + (const, override)); + + MOCK_METHOD(void, handle_warn, + (std::string_view function_name, std::string_view msg), + (const, override)); +#endif // defined(PROJECT_ENABLE_V2_ERRORS) }; my_exc_handler handler{}; utils::error::set_exception_handler(&handler); +#if defined(PROJECT_ENABLE_V2_ERRORS) + EXPECT_CALL(handler, handle_debug("test_func", "debug")).WillOnce(Return()); + utils::error::handle_debug("test_func", "debug"); +#endif // defined(PROJECT_ENABLE_V2_ERRORS) + EXPECT_CALL(handler, handle_error("test_func", "error")).WillOnce(Return()); utils::error::handle_error("test_func", "error"); @@ -68,6 +93,17 @@ TEST(utils_error, can_override_exception_handler) { }); utils::error::handle_exception("test_func_ex", ex); +#if defined(PROJECT_ENABLE_V2_ERRORS) + EXPECT_CALL(handler, handle_info("test_func", "info")).WillOnce(Return()); + utils::error::handle_info("test_func", "info"); + + EXPECT_CALL(handler, handle_trace("test_func", "trace")).WillOnce(Return()); + utils::error::handle_trace("test_func", "trace"); + + EXPECT_CALL(handler, handle_warn("test_func", "warn")).WillOnce(Return()); + utils::error::handle_warn("test_func", "warn"); +#endif // defined(PROJECT_ENABLE_V2_ERRORS) + utils::error::set_exception_handler(&utils::error::default_exception_handler); } } // namespace monitarr