diff src/services/anilist.cc @ 175:9b10175be389

dep/json: update to v3.11.3 anime/db: save anime list to database, very much untested and likely won't work as intended
author Paper <mrpapersonic@gmail.com>
date Thu, 30 Nov 2023 13:52:26 -0500
parents 275da698697d
children 01d259b9c89f
line wrap: on
line diff
--- a/src/services/anilist.cc	Wed Nov 29 13:53:56 2023 -0500
+++ b/src/services/anilist.cc	Thu Nov 30 13:52:26 2023 -0500
@@ -15,6 +15,9 @@
 #include <QUrl>
 #include <chrono>
 #include <exception>
+
+#include <iostream>
+
 #define CLIENT_ID "13706"
 
 using namespace nlohmann::literals::json_literals;
@@ -34,24 +37,48 @@
 		void SetAuthToken(std::string const& auth_token) { session.config.auth.anilist.auth_token = auth_token; }
 
 		bool Authenticated() const { return !AuthToken().empty(); }
+		bool IsValid() const { return UserId() && Authenticated(); }
 };
 
 static Account account;
 
 std::string SendRequest(std::string data) {
 	std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json",
-	                                    "Content-Type: application/json"};
+										"Content-Type: application/json"};
 	return Strings::ToUtf8String(HTTP::Post("https://graphql.anilist.co", data, headers));
 }
 
+nlohmann::json SendJSONRequest(nlohmann::json data) {
+	std::string request = SendRequest(data.dump());
+	if (request.empty()) {
+		std::cerr << "[AniList] JSON Request returned an empty result!" << std::endl;
+		return {};
+	}
+
+	auto ret = nlohmann::json::parse(request, nullptr, false);
+	if (ret.is_discarded()) {
+		std::cerr << "[AniList] Failed to parse request JSON!" << std::endl;
+		return {};
+	}
+
+	if (ret.contains("/errors"_json_pointer) && ret.at("/errors"_json_pointer).is_array()) {
+		for (const auto& error : ret.at("/errors"_json_pointer))
+			std::cerr << "[AniList] Received an error in response: " << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl;
+
+		return {};
+	}
+
+	return ret;
+}
+
 void ParseListStatus(std::string status, Anime::Anime& anime) {
 	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);
@@ -81,60 +108,39 @@
 	return "CURRENT";
 }
 
-Date ParseDate(const nlohmann::json& json) {
-	Date date;
-	/* JSON for Modern C++ warns here. I'm not too sure why, this code works when I set the
-	   standard to C++17 :/ */
-	if (json.contains("/year"_json_pointer) && json.at("/year"_json_pointer).is_number())
-		date.SetYear(JSON::GetInt(json, "/year"_json_pointer));
-	else
-		date.VoidYear();
-
-	if (json.contains("/month"_json_pointer) && json.at("/month"_json_pointer).is_number())
-		date.SetMonth(JSON::GetInt(json, "/month"_json_pointer));
-	else
-		date.VoidMonth();
-
-	if (json.contains("/day"_json_pointer) && json.at("/day"_json_pointer).is_number())
-		date.SetDay(JSON::GetInt(json, "/day"_json_pointer));
-	else
-		date.VoidDay();
-	return date;
-}
-
 void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) {
-	anime.SetNativeTitle(JSON::GetString(json, "/native"_json_pointer));
-	anime.SetEnglishTitle(JSON::GetString(json, "/english"_json_pointer));
-	anime.SetRomajiTitle(JSON::GetString(json, "/romaji"_json_pointer));
+	anime.SetNativeTitle(JSON::GetString<std::string>(json, "/native"_json_pointer, ""));
+	anime.SetEnglishTitle(JSON::GetString<std::string>(json, "/english"_json_pointer, ""));
+	anime.SetRomajiTitle(JSON::GetString<std::string>(json, "/romaji"_json_pointer, ""));
 }
 
 int ParseMediaJson(const nlohmann::json& json) {
-	int id = JSON::GetInt(json, "/id"_json_pointer);
+	int id = JSON::GetNumber(json, "/id"_json_pointer);
 	if (!id)
 		return 0;
+
 	Anime::Anime& anime = Anime::db.items[id];
 	anime.SetId(id);
 
 	ParseTitle(json.at("/title"_json_pointer), anime);
 
-	anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer));
-	anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString(json, "/format"_json_pointer)));
+	anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0));
+	anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, "")));
 
-	anime.SetAiringStatus(Translate::AniList::ToSeriesStatus(JSON::GetString(json, "/status"_json_pointer)));
+	anime.SetAiringStatus(Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, "")));
 
-	anime.SetAirDate(ParseDate(json["/startDate"_json_pointer]));
+	anime.SetAirDate(Date(json["/startDate"_json_pointer]));
 
-	anime.SetPosterUrl(JSON::GetString(json, "/coverImage/large"_json_pointer));
+	anime.SetPosterUrl(JSON::GetString<std::string>(json, "/coverImage/large"_json_pointer, ""));
 
-	anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer));
-	anime.SetSeason(Translate::AniList::ToSeriesSeason(JSON::GetString(json, "/season"_json_pointer)));
-	anime.SetDuration(JSON::GetInt(json, "/duration"_json_pointer));
-	anime.SetSynopsis(Strings::TextifySynopsis(JSON::GetString(json, "/description"_json_pointer)));
+	anime.SetAudienceScore(JSON::GetNumber(json, "/averageScore"_json_pointer, 0));
+	anime.SetSeason(Translate::AniList::ToSeriesSeason(JSON::GetString<std::string>(json, "/season"_json_pointer, "")));
+	anime.SetDuration(JSON::GetNumber(json, "/duration"_json_pointer, 0));
+	anime.SetSynopsis(Strings::TextifySynopsis(JSON::GetString<std::string>(json, "/description"_json_pointer, "")));
 
-	if (json.contains("/genres"_json_pointer) && json["/genres"_json_pointer].is_array())
-		anime.SetGenres(json["/genres"_json_pointer].get<std::vector<std::string>>());
-	if (json.contains("/synonyms"_json_pointer) && json["/synonyms"_json_pointer].is_array())
-		anime.SetTitleSynonyms(json["/synonyms"_json_pointer].get<std::vector<std::string>>());
+	anime.SetGenres(JSON::GetArray<std::vector<std::string>>(json, "/genres"_json_pointer, {}));
+	anime.SetTitleSynonyms(JSON::GetArray<std::vector<std::string>>(json, "/synonyms"_json_pointer, {}));
+
 	return id;
 }
 
@@ -145,15 +151,15 @@
 
 	anime.AddToUserList();
 
-	anime.SetUserScore(JSON::GetInt(json, "/score"_json_pointer));
-	anime.SetUserProgress(JSON::GetInt(json, "/progress"_json_pointer));
-	ParseListStatus(JSON::GetString(json, "/status"_json_pointer), anime);
-	anime.SetUserNotes(JSON::GetString(json, "/notes"_json_pointer));
+	anime.SetUserScore(JSON::GetNumber(json, "/score"_json_pointer, 0));
+	anime.SetUserProgress(JSON::GetNumber(json, "/progress"_json_pointer, 0));
+	ParseListStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""), anime);
+	anime.SetUserNotes(JSON::GetString<std::string>(json, "/notes"_json_pointer, ""));
 
-	anime.SetUserDateStarted(ParseDate(json["/startedAt"_json_pointer]));
-	anime.SetUserDateCompleted(ParseDate(json["/completedAt"_json_pointer]));
+	anime.SetUserDateStarted(Date(json["/startedAt"_json_pointer]));
+	anime.SetUserDateCompleted(Date(json["/completedAt"_json_pointer]));
 
-	anime.SetUserTimeUpdated(JSON::GetInt(json, "/updatedAt"_json_pointer));
+	anime.SetUserTimeUpdated(JSON::GetNumber(json, "/updatedAt"_json_pointer, 0));
 
 	return id;
 }
@@ -166,56 +172,61 @@
 }
 
 int GetAnimeList() {
+	if (!account.IsValid()) {
+		std::cerr << "AniList: Account isn't valid!" << std::endl;
+		return 0;
+	}
+
 	/* NOTE: these should be in the qrc file */
 	const std::string 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"
-	                          "          coverImage {\n"
-	                          "            large\n"
-	                          "          }\n"
-	                          "          id\n"
-	                          "          title {\n"
-	                          "            romaji\n"
-	                          "            english\n"
-	                          "            native\n"
-	                          "          }\n"
-	                          "          format\n"
-	                          "          status\n"
-	                          "          averageScore\n"
-	                          "          season\n"
-	                          "          startDate {\n"
-	                          "            year\n"
-	                          "            month\n"
-	                          "            day\n"
-	                          "          }\n"
-	                          "          genres\n"
-	                          "          episodes\n"
-	                          "          duration\n"
-	                          "          synonyms\n"
-	                          "          description(asHtml: false)\n"
-	                          "        }\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"
+							  "          coverImage {\n"
+							  "            large\n"
+							  "          }\n"
+							  "          id\n"
+							  "          title {\n"
+							  "            romaji\n"
+							  "            english\n"
+							  "            native\n"
+							  "          }\n"
+							  "          format\n"
+							  "          status\n"
+							  "          averageScore\n"
+							  "          season\n"
+							  "          startDate {\n"
+							  "            year\n"
+							  "            month\n"
+							  "            day\n"
+							  "          }\n"
+							  "          genres\n"
+							  "          episodes\n"
+							  "          duration\n"
+							  "          synonyms\n"
+							  "          description(asHtml: false)\n"
+							  "        }\n"
+							  "      }\n"
+							  "    }\n"
+							  "  }\n"
+							  "}\n";
 	// clang-format off
 	nlohmann::json json = {
 		{"query", query},
@@ -224,11 +235,9 @@
 		}}
 	};
 	// clang-format on
-	/* TODO: do a try catch here, catch any json errors and then call
-	   Authorize() if needed */
-	auto res = nlohmann::json::parse(SendRequest(json.dump()));
-	/* TODO: make sure that we actually need the wstring converter and see
-	   if we can just get wide strings back from nlohmann::json */
+
+	auto res = SendJSONRequest(json);
+
 	for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
 		ParseList(list.value());
 	}
@@ -257,12 +266,12 @@
 	 **/
 	Anime::Anime& anime = Anime::db.items[id];
 	const std::string query = "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, "
-	                          "$notes: String, $start: FuzzyDateInput, $comp: FuzzyDateInput) {\n"
-	                          "  SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, "
-	                          "scoreRaw: $score, notes: $notes, startedAt: $start, completedAt: $comp) {\n"
-	                          "    id\n"
-	                          "  }\n"
-	                          "}\n";
+							  "$notes: String, $start: FuzzyDateInput, $comp: FuzzyDateInput) {\n"
+							  "  SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, "
+							  "scoreRaw: $score, notes: $notes, startedAt: $start, completedAt: $comp) {\n"
+							  "    id\n"
+							  "  }\n"
+							  "}\n";
 	// clang-format off
 	nlohmann::json json = {
 		{"query", query},
@@ -277,41 +286,48 @@
 		}}
 	};
 	// clang-format on
-	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
-	return JSON::GetInt(ret, "/data/SaveMediaListEntry/id"_json_pointer);
+
+	auto ret = SendJSONRequest(json);
+
+	return JSON::GetNumber(ret, "/data/SaveMediaListEntry/id"_json_pointer, 0);
 }
 
 int ParseUser(const nlohmann::json& json) {
-	account.SetUsername(JSON::GetString(json, "/name"_json_pointer));
-	account.SetUserId(JSON::GetInt(json, "/id"_json_pointer));
+	account.SetUsername(JSON::GetString<std::string>(json, "/name"_json_pointer, ""));
+	account.SetUserId(JSON::GetNumber(json, "/id"_json_pointer, 0));
 	return account.UserId();
 }
 
 bool AuthorizeUser() {
 	/* Prompt for PIN */
 	QDesktopServices::openUrl(
-	    QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token"));
+		QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" 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);
-	if (ok && !token.isEmpty())
-		account.SetAuthToken(Strings::ToUtf8String(token));
-	else // fail
+		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));
+
 	const std::string query = "query {\n"
-	                          "  Viewer {\n"
-	                          "    id\n"
-	                          "    name\n"
-	                          "    mediaListOptions {\n"
-	                          "      scoreFormat\n"
-	                          "    }\n"
-	                          "  }\n"
-	                          "}\n";
+							  "  Viewer {\n"
+							  "    id\n"
+							  "    name\n"
+							  "    mediaListOptions {\n"
+							  "      scoreFormat\n" // this will be used... eventually
+							  "    }\n"
+							  "  }\n"
+							  "}\n";
 	nlohmann::json json = {
-	    {"query", query}
-    };
-	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
+		{"query", query}
+	};
+
+	auto ret = SendJSONRequest(json);
+
 	ParseUser(ret["data"]["Viewer"]);
 	return true;
 }