Mercurial > minori
changeset 323:1686fac290c5
services/anilist: refactor HTTP requests...
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Wed, 12 Jun 2024 22:48:16 -0400 |
parents | c32467cd06bb |
children | 5d3c9b31aa6e |
files | include/core/json.h src/services/anilist.cc |
diffstat | 2 files changed, 132 insertions(+), 123 deletions(-) [+] |
line wrap: on
line diff
--- a/include/core/json.h Wed Jun 12 22:15:53 2024 -0400 +++ b/include/core/json.h Wed Jun 12 22:48:16 2024 -0400 @@ -24,7 +24,6 @@ namespace JSON { -/* TODO: refactor these to return a std::optional... */ template<typename T = std::string> T GetString(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, T def) { if (json.contains(ptr) && json[ptr].is_string())
--- a/src/services/anilist.cc Wed Jun 12 22:15:53 2024 -0400 +++ b/src/services/anilist.cc Wed Jun 12 22:48:16 2024 -0400 @@ -66,55 +66,60 @@ /* FIXME: why is this here */ -static struct { - int UserId() const { return session.config.auth.anilist.user_id; } - void SetUserId(const int id) { session.config.auth.anilist.user_id = id; } - - std::string AuthToken() const { return session.config.auth.anilist.auth_token; } - void SetAuthToken(const std::string& auth_token) { session.config.auth.anilist.auth_token = auth_token; } - - bool IsValid() const { return UserId() && !AuthToken().empty(); } -} account; - -static std::string SendRequest(const std::string& data) { - std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json", - "Content-Type: application/json"}; - return Strings::ToUtf8String(HTTP::Request("https://graphql.anilist.co", headers, data, HTTP::Type::Post)); +static bool AccountIsValid() { + const auto& auth = session.config.auth.anilist; + return (auth.user_id && !auth.auth_token.empty()); } -static bool SendJSONRequest(const nlohmann::json& data, nlohmann::json& out) { - std::string request = SendRequest(data.dump()); - if (request.empty()) { - session.SetStatusBar("AniList: JSON request returned an empty result!"); - return false; +static std::optional<nlohmann::json> SendJSONRequest(const nlohmann::json& data) { + if (!AccountIsValid()) { + session.SetStatusBar("AniList: Account isn't valid! (unauthorized?)"); + return std::nullopt; } - out = nlohmann::json::parse(request, nullptr, false); - if (out.is_discarded()) { - session.SetStatusBar("AniList: Failed to parse request JSON!"); - return false; + const auto& auth = session.config.auth.anilist; + + const std::vector<std::string> headers = { + "Authorization: Bearer " + auth.auth_token, + "Accept: application/json", + "Content-Type: application/json", + }; + + const std::string response = Strings::ToUtf8String(HTTP::Request("https://graphql.anilist.co", headers, data.dump(), HTTP::Type::Post)); + if (response.empty()) { + session.SetStatusBar("AniList: JSON request returned an empty result!"); + return std::nullopt; + } + + nlohmann::json out; + + try { + out = nlohmann::json::parse(response); + } catch (const std::exception& ex) { + session.SetStatusBar("AniList: Failed to parse request JSON with error!"); + return std::nullopt; } if (out.contains("/errors"_json_pointer) && out.at("/errors"_json_pointer).is_array()) { for (const auto& error : out.at("/errors"_json_pointer)) std::cerr << "AniList: Received an error in response: " - << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl; + << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl; session.SetStatusBar("AniList: Received an error in response!"); - return false; + return std::nullopt; } - return true; + return out; } static void ParseListStatus(std::string status, Anime::Anime& anime) { static const std::unordered_map<std::string, Anime::ListStatus> map = { - {"CURRENT", Anime::ListStatus::Current }, - {"PLANNING", Anime::ListStatus::Planning }, - {"COMPLETED", Anime::ListStatus::Completed}, - {"DROPPED", Anime::ListStatus::Dropped }, - {"PAUSED", Anime::ListStatus::Paused } - }; + {"CURRENT", Anime::ListStatus::Current }, + {"PLANNING", Anime::ListStatus::Planning }, + {"COMPLETED", Anime::ListStatus::Completed}, + {"DROPPED", Anime::ListStatus::Dropped }, + {"PAUSED", Anime::ListStatus::Paused } + }; if (status == "REPEATING") { anime.SetUserIsRewatching(true); @@ -174,8 +179,8 @@ anime.SetId(id); anime.SetServiceId(Anime::Service::AniList, service_id); - if (json.contains("/id_mal"_json_pointer)) - anime.SetServiceId(Anime::Service::MyAnimeList, json["/id_mal"_json_pointer].get<std::string>()); + if (json.contains("/idMal"_json_pointer) && json["/idMal"_json_pointer].is_number()) + anime.SetServiceId(Anime::Service::MyAnimeList, Strings::ToUtf8String(json["/idMal"_json_pointer].get<int>())); ParseTitle(json.at("/title"_json_pointer), anime); @@ -183,7 +188,7 @@ anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, ""))); anime.SetAiringStatus( - Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""))); + Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""))); anime.SetAirDate(Date(json["/startDate"_json_pointer])); @@ -247,60 +252,58 @@ } int GetAnimeList() { - if (!account.IsValid()) { - session.SetStatusBar("AniList: Account isn't valid! (unauthorized?)"); - return 0; - } + auto& auth = session.config.auth.anilist; /* NOTE: these really ought to be in the qrc file */ constexpr std::string_view query = "query ($id: Int) {\n" - " MediaListCollection (userId: $id, type: ANIME) {\n" - " lists {\n" - " name\n" - " entries {\n" - " score\n" - " notes\n" - " status\n" - " progress\n" - " startedAt {\n" - " year\n" - " month\n" - " day\n" - " }\n" - " completedAt {\n" - " year\n" - " month\n" - " day\n" - " }\n" - " updatedAt\n" - " media {\n" - MEDIA_FIELDS - " }\n" - " }\n" - " }\n" - " }\n" - "}\n"; + " MediaListCollection (userId: $id, type: ANIME) {\n" + " lists {\n" + " name\n" + " entries {\n" + " score\n" + " notes\n" + " status\n" + " progress\n" + " startedAt {\n" + " year\n" + " month\n" + " day\n" + " }\n" + " completedAt {\n" + " year\n" + " month\n" + " day\n" + " }\n" + " updatedAt\n" + " media {\n" + MEDIA_FIELDS + " }\n" + " }\n" + " }\n" + " }\n" + "}\n"; // clang-format off - nlohmann::json json = { + nlohmann::json request = { {"query", query}, {"variables", { - {"id", account.UserId()} + {"id", auth.user_id} }} }; // clang-format on session.SetStatusBar("AniList: Parsing anime list..."); - nlohmann::json result; - const bool res = SendJSONRequest(json, result); - if (!res) + const std::optional<nlohmann::json> response = SendJSONRequest(request); + if (!response) return 0; + Anime::db.RemoveAllUserData(); + + const nlohmann::json& json = response.value(); + bool success = true; - Anime::db.RemoveAllUserData(); - - for (const auto& list : result["data"]["MediaListCollection"]["lists"].items()) + for (const auto& list : json["data"]["MediaListCollection"]["lists"].items()) if (!ParseList(list.value())) success = false; @@ -313,12 +316,12 @@ /* return is a vector of anime ids */ std::vector<int> Search(const std::string& search) { constexpr std::string_view query = "query ($search: String) {\n" - " Page (page: 1, perPage: 50) {\n" - " media (search: $search, type: ANIME) {\n" - MEDIA_FIELDS - " }\n" - " }\n" - "}\n"; + " Page (page: 1, perPage: 50) {\n" + " media (search: $search, type: ANIME) {\n" + MEDIA_FIELDS + " }\n" + " }\n" + "}\n"; // clang-format off nlohmann::json json = { @@ -329,11 +332,12 @@ }; // clang-format on - nlohmann::json result; - const bool res = SendJSONRequest(json, result); - if (!res) + const std::optional<nlohmann::json> response = SendJSONRequest(json); + if (!response) return {}; + const nlohmann::json& result = response.value(); + /* FIXME: error handling here */ std::vector<int> ret; ret.reserve(result["/data/Page/media"_json_pointer].size()); @@ -346,19 +350,19 @@ std::vector<int> GetSeason(Anime::SeriesSeason season, Date::Year year) { constexpr std::string_view query = "query ($season: MediaSeason!, $season_year: Int!, $page: Int) {\n" - " Page(page: $page) {\n" - " media(season: $season, seasonYear: $season_year, type: ANIME, sort: START_DATE) {\n" - MEDIA_FIELDS - " }\n" - " pageInfo {\n" - " total\n" - " perPage\n" - " currentPage\n" - " lastPage\n" - " hasNextPage\n" - " }\n" - " }\n" - "}\n"; + " Page(page: $page) {\n" + " media(season: $season, seasonYear: $season_year, type: ANIME, sort: START_DATE) {\n" + MEDIA_FIELDS + " }\n" + " pageInfo {\n" + " total\n" + " perPage\n" + " currentPage\n" + " lastPage\n" + " hasNextPage\n" + " }\n" + " }\n" + "}\n"; std::vector<int> ret; int page = 0; @@ -373,11 +377,12 @@ }} }; - nlohmann::json result; - const bool res = SendJSONRequest(json, result); + const std::optional<nlohmann::json> res = SendJSONRequest(json); if (!res) return {}; + const nlohmann::json& result = res.value(); + ret.reserve(ret.capacity() + result["data"]["Page"]["media"].size()); for (const auto& media : result["data"]["Page"]["media"].items()) @@ -422,13 +427,13 @@ session.SetStatusBar("AniList: Updating anime entry..."); constexpr std::string_view query = - "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String, $start: " - "FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n" - " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: " - "$notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n" - " id\n" - " }\n" - "}\n"; + "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String, $start: " + "FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n" + " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: " + "$notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n" + " id\n" + " }\n" + "}\n"; // clang-format off nlohmann::json json = { {"query", query}, @@ -445,53 +450,58 @@ }; // clang-format on - nlohmann::json result; - const bool ret = SendJSONRequest(json, result); - if (!ret) + const std::optional<nlohmann::json> res = SendJSONRequest(json); + if (!res) return 0; + const nlohmann::json& result = res.value(); + session.SetStatusBar("AniList: Anime entry updated successfully!"); return JSON::GetNumber(result, "/data/SaveMediaListEntry/id"_json_pointer, 0); } static int ParseUser(const nlohmann::json& json) { - account.SetUserId(JSON::GetNumber(json, "/id"_json_pointer, 0)); - return account.UserId(); + auto& auth = session.config.auth.anilist; + + return auth.user_id = JSON::GetNumber(json, "/id"_json_pointer, 0); } bool AuthorizeUser() { + auto& auth = session.config.auth.anilist; + /* Prompt for PIN */ QDesktopServices::openUrl(QUrl(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" + - std::string(CLIENT_ID) + "&response_type=token"))); + std::string(CLIENT_ID) + "&response_type=token"))); bool ok; QString token = QInputDialog::getText( - 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, - "", &ok); + 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, + "", &ok); if (!ok || token.isEmpty()) return false; - account.SetAuthToken(Strings::ToUtf8String(token)); + auth.auth_token = Strings::ToUtf8String(token); session.SetStatusBar("AniList: Requesting user ID..."); constexpr std::string_view query = "query {\n" - " Viewer {\n" - " id\n" - " }\n" - "}\n"; + " Viewer {\n" + " id\n" + " }\n" + "}\n"; nlohmann::json json = { - {"query", query} - }; + {"query", query} + }; - /* SendJSONRequest handles status errors */ - nlohmann::json result; - const bool ret = SendJSONRequest(json, result); + /* SendJSONRequest handles status errors */ + const std::optional<nlohmann::json> ret = SendJSONRequest(json); if (!ret) return 0; + const nlohmann::json& result = ret.value(); + if (ParseUser(result["data"]["Viewer"])) session.SetStatusBar("AniList: Successfully retrieved user data!"); else