diff src/services/anilist.cc @ 202:71832ffe425a

animia: re-add kvm fd source this is all being merged from my wildly out-of-date laptop. SORRY! in other news, I edited the CI file to install the wayland client as well, so the linux CI build might finally get wayland stuff.
author Paper <paper@paper.us.eu.org>
date Tue, 02 Jan 2024 06:05:06 -0500
parents 9613d72b097e
children 7cf53145de11
line wrap: on
line diff
--- a/src/services/anilist.cc	Sun Nov 19 19:13:28 2023 -0500
+++ b/src/services/anilist.cc	Tue Jan 02 06:05:06 2024 -0500
@@ -7,21 +7,27 @@
 #include "core/session.h"
 #include "core/strings.h"
 #include "gui/translate/anilist.h"
+
+#include <QDate>
 #include <QByteArray>
 #include <QDesktopServices>
 #include <QInputDialog>
 #include <QLineEdit>
 #include <QMessageBox>
 #include <QUrl>
+
 #include <chrono>
 #include <exception>
-#define CLIENT_ID "13706"
+
+#include <iostream>
 
 using namespace nlohmann::literals::json_literals;
 
 namespace Services {
 namespace AniList {
 
+constexpr int CLIENT_ID = 13706;
+
 class Account {
 	public:
 		std::string Username() const { return session.config.auth.anilist.username; }
@@ -34,24 +40,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   }
-    };
+	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   }
+	};
 
 	if (status == "REPEATING") {
 		anime.SetUserIsRewatching(true);
@@ -64,7 +94,7 @@
 		return;
 	}
 
-	anime.SetUserStatus(map[status]);
+	anime.SetUserStatus(map.at(status));
 }
 
 std::string ListStatusToString(const Anime::Anime& anime) {
@@ -81,60 +111,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 +154,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 +175,61 @@
 }
 
 int GetAnimeList() {
-	/* 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";
+	if (!account.IsValid()) {
+		std::cerr << "AniList: Account isn't valid!" << std::endl;
+		return 0;
+	}
+
+	/* 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"
+							  "          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 +238,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());
 	}
@@ -244,9 +256,9 @@
 	 * float score,
 	 * int scoreRaw,
 	 * int progress,
-	 * int progressVolumes,
-	 * int repeat,
-	 * int priority,
+	 * int progressVolumes, // manga-specific.
+	 * int repeat, // rewatch
+	 * int priority,  
 	 * bool private,
 	 * string notes,
 	 * bool hiddenFromStatusLists,
@@ -256,13 +268,16 @@
 	 * Date completedAt
 	 **/
 	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";
+	if (!anime.IsInUserList())
+		return 0;
+
+	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";
 	// clang-format off
 	nlohmann::json json = {
 		{"query", query},
@@ -273,45 +288,53 @@
 			{"score",    anime.GetUserScore()},
 			{"notes",    anime.GetUserNotes()},
 			{"start",    anime.GetUserDateStarted().GetAsAniListJson()},
-			{"comp",     anime.GetUserDateCompleted().GetAsAniListJson()}
+			{"comp",     anime.GetUserDateCompleted().GetAsAniListJson()},
+			{"repeat",   anime.GetUserRewatchedTimes()}
 		}}
 	};
 	// 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(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" + std::to_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);
-	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;
-	const std::string query = "query {\n"
-	                          "  Viewer {\n"
-	                          "    id\n"
-	                          "    name\n"
-	                          "    mediaListOptions {\n"
-	                          "      scoreFormat\n"
-	                          "    }\n"
-	                          "  }\n"
-	                          "}\n";
+
+	account.SetAuthToken(Strings::ToUtf8String(token));
+
+	constexpr std::string_view query = "query {\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;
 }