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