diff src/services/anilist.cc @ 81:9b2b41f83a5e

boring: mass rename to cc because this is a very unix-y project, it makes more sense to use the 'cc' extension
author Paper <mrpapersonic@gmail.com>
date Mon, 23 Oct 2023 12:07:27 -0400
parents src/services/anilist.cpp@6f7385bd334c
children 275da698697d
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/services/anilist.cc	Mon Oct 23 12:07:27 2023 -0400
@@ -0,0 +1,320 @@
+#include "services/anilist.h"
+#include "core/anime.h"
+#include "core/anime_db.h"
+#include "core/config.h"
+#include "core/http.h"
+#include "core/json.h"
+#include "core/session.h"
+#include "core/strings.h"
+#include "gui/translate/anilist.h"
+#include <QByteArray>
+#include <QDesktopServices>
+#include <QInputDialog>
+#include <QLineEdit>
+#include <QMessageBox>
+#include <QUrl>
+#include <chrono>
+#include <exception>
+#define CLIENT_ID "13706"
+
+using namespace nlohmann::literals::json_literals;
+
+namespace Services {
+namespace AniList {
+
+class Account {
+	public:
+		std::string Username() const { return session.config.anilist.username; }
+		void SetUsername(std::string const& username) { session.config.anilist.username = username; }
+
+		int UserId() const { return session.config.anilist.user_id; }
+		void SetUserId(const int id) { session.config.anilist.user_id = id; }
+
+		std::string AuthToken() const { return session.config.anilist.auth_token; }
+		void SetAuthToken(std::string const& auth_token) { session.config.anilist.auth_token = auth_token; }
+
+		bool Authenticated() const { return !AuthToken().empty(); }
+};
+
+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"};
+	return Strings::ToUtf8String(HTTP::Post("https://graphql.anilist.co", data, headers));
+}
+
+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   }
+    };
+
+	if (status == "REPEATING") {
+		anime.SetUserIsRewatching(true);
+		anime.SetUserStatus(Anime::ListStatus::CURRENT);
+		return;
+	}
+
+	if (map.find(status) == map.end()) {
+		anime.SetUserStatus(Anime::ListStatus::NOT_IN_LIST);
+		return;
+	}
+
+	anime.SetUserStatus(map[status]);
+}
+
+std::string ListStatusToString(const Anime::Anime& anime) {
+	if (anime.GetUserIsRewatching())
+		return "REWATCHING";
+
+	switch (anime.GetUserStatus()) {
+		case Anime::ListStatus::PLANNING: return "PLANNING";
+		case Anime::ListStatus::COMPLETED: return "COMPLETED";
+		case Anime::ListStatus::DROPPED: return "DROPPED";
+		case Anime::ListStatus::PAUSED: return "PAUSED";
+		default: break;
+	}
+	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));
+}
+
+int ParseMediaJson(const nlohmann::json& json) {
+	int id = JSON::GetInt(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.SetAiringStatus(Translate::AniList::ToSeriesStatus(JSON::GetString(json, "/status"_json_pointer)));
+
+	anime.SetAirDate(ParseDate(json["/startDate"_json_pointer]));
+
+	anime.SetPosterUrl(JSON::GetString(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)));
+
+	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>>());
+	return id;
+}
+
+int ParseListItem(const nlohmann::json& json) {
+	int id = ParseMediaJson(json["media"]);
+
+	Anime::Anime& anime = Anime::db.items[id];
+
+	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.SetUserDateStarted(ParseDate(json["/startedAt"_json_pointer]));
+	anime.SetUserDateCompleted(ParseDate(json["/completedAt"_json_pointer]));
+
+	anime.SetUserTimeUpdated(JSON::GetInt(json, "/updatedAt"_json_pointer));
+
+	return id;
+}
+
+int ParseList(const nlohmann::json& json) {
+	for (const auto& entry : json["entries"].items()) {
+		ParseListItem(entry.value());
+	}
+	return 1;
+}
+
+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";
+	// clang-format off
+	nlohmann::json json = {
+		{"query", query},
+		{"variables", {
+			{"id", account.UserId()}
+		}}
+	};
+	// 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 */
+	for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
+		ParseList(list.value());
+	}
+	return 1;
+}
+
+int UpdateAnimeEntry(int id) {
+	/**
+	 * possible values:
+	 *
+	 * int mediaId,
+	 * MediaListStatus status,
+	 * float score,
+	 * int scoreRaw,
+	 * int progress,
+	 * int progressVolumes,
+	 * int repeat,
+	 * int priority,
+	 * bool private,
+	 * string notes,
+	 * bool hiddenFromStatusLists,
+	 * string[] customLists,
+	 * float[] advancedScores,
+	 * Date startedAt,
+	 * 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";
+	// clang-format off
+	nlohmann::json json = {
+		{"query", query},
+		{"variables", {
+			{"media_id", anime.GetId()},
+			{"progress", anime.GetUserProgress()},
+			{"status",   ListStatusToString(anime)},
+			{"score",    anime.GetUserScore()},
+			{"notes",    anime.GetUserNotes()},
+			{"start",    anime.GetUserDateStarted().GetAsAniListJson()},
+			{"comp",     anime.GetUserDateCompleted().GetAsAniListJson()}
+		}}
+	};
+	// clang-format on
+	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
+	return JSON::GetInt(ret, "/data/SaveMediaListEntry/id"_json_pointer);
+}
+
+int ParseUser(const nlohmann::json& json) {
+	account.SetUsername(JSON::GetString(json, "/name"_json_pointer));
+	account.SetUserId(JSON::GetInt(json, "/id"_json_pointer));
+	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"));
+	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
+		return false;
+	const std::string query = "query {\n"
+	                          "  Viewer {\n"
+	                          "    id\n"
+	                          "    name\n"
+	                          "    mediaListOptions {\n"
+	                          "      scoreFormat\n"
+	                          "    }\n"
+	                          "  }\n"
+	                          "}\n";
+	nlohmann::json json = {
+	    {"query", query}
+    };
+	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
+	ParseUser(ret["data"]["Viewer"]);
+	return true;
+}
+
+} // namespace AniList
+} // namespace Services