diff src/services/anilist.cpp @ 9:5c0397762b53

INCOMPLETE: megacommit :)
author Paper <mrpapersonic@gmail.com>
date Sun, 10 Sep 2023 03:59:16 -0400
parents
children 4b198a111713
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/services/anilist.cpp	Sun Sep 10 03:59:16 2023 -0400
@@ -0,0 +1,337 @@
+#include "services/anilist.h"
+#include "core/anime.h"
+#include "core/config.h"
+#include "core/json.h"
+#include "core/session.h"
+#include "core/strings.h"
+#include <QDesktopServices>
+#include <QInputDialog>
+#include <QLineEdit>
+#include <QMessageBox>
+#include <chrono>
+#include <curl/curl.h>
+#include <exception>
+#include <format>
+#define CLIENT_ID "13706"
+
+namespace Services::AniList {
+
+class Account {
+	public:
+		std::string Username() const { return session.anilist.username; }
+		void SetUsername(std::string const& username) { session.anilist.username = username; }
+
+		int UserId() const { return session.anilist.user_id; }
+		void SetUserId(const int id) { session.anilist.user_id = id; }
+
+		std::string AuthToken() const { return session.anilist.auth_token; }
+		void SetAuthToken(std::string const& auth_token) { session.anilist.auth_token = auth_token; }
+
+		bool Authenticated() const { return !AuthToken().empty(); }
+}
+
+static Account account;
+
+static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userdata) {
+	((std::string*)userdata)->append((char*)contents, size * nmemb);
+	return size * nmemb;
+}
+
+/* A wrapper around cURL to send requests to AniList */
+std::string SendRequest(std::string data) {
+	struct curl_slist* list = NULL;
+	std::string userdata;
+	CURL* curl = curl_easy_init();
+	if (curl) {
+		list = curl_slist_append(list, "Accept: application/json");
+		list = curl_slist_append(list, "Content-Type: application/json");
+		std::string bearer = "Authorization: Bearer " + account.AuthToken();
+		list = curl_slist_append(list, bearer.c_str());
+		curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co");
+		curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
+		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
+		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
+		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback);
+		/* Use system certs... useful on Windows. */
+		curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
+		CURLcode res = curl_easy_perform(curl);
+		curl_slist_free_all(list);
+		curl_easy_cleanup(curl);
+		if (res != CURLE_OK) {
+			QMessageBox box(QMessageBox::Icon::Critical, "",
+							QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
+			box.exec();
+			return "";
+		}
+		return userdata;
+	}
+	return "";
+}
+
+/* Maps to convert string forms to our internal enums */
+
+std::map<std::string, enum AnimeWatchingStatus> StringToAnimeWatchingMap = {
+	{"CURRENT",	CURRENT  },
+	  {"PLANNING",  PLANNING },
+	  {"COMPLETED", COMPLETED},
+	{"DROPPED",	DROPPED  },
+	  {"PAUSED",	 PAUSED   },
+	  {"REPEATING", REPEATING}
+};
+
+std::map<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = {
+	{CURRENT,	  "CURRENT"  },
+	  {PLANNING,	 "PLANNING" },
+	  {COMPLETED, "COMPLETED"},
+	{DROPPED,	  "DROPPED"  },
+	  {PAUSED,	   "PAUSED"   },
+	  {REPEATING, "REPEATING"}
+};
+
+std::map<std::string, enum AnimeAiringStatus> StringToAnimeAiringMap = {
+	{"FINISHED",		 FINISHED		 },
+	{"RELEASING",		  RELEASING	   },
+	{"NOT_YET_RELEASED", NOT_YET_RELEASED},
+	{"CANCELLED",		  CANCELLED	   },
+	{"HIATUS",		   HIATUS			 }
+};
+
+std::map<std::string, enum AnimeSeason> StringToAnimeSeasonMap = {
+	{"WINTER", WINTER},
+	{"SPRING", SPRING},
+	{"SUMMER", SUMMER},
+	{"FALL",	 FALL	 }
+};
+
+std::map<std::string, enum AnimeFormat> StringToAnimeFormatMap = {
+	{"TV",	   TV		 },
+	  {"TV_SHORT", TV_SHORT},
+	  {"MOVIE",	MOVIE	 },
+	{"SPECIAL",	SPECIAL },
+	  {"OVA",	  OVA	 },
+	{"ONA",		ONA	   },
+	  {"MUSIC",	MUSIC	 },
+	  {"MANGA",	MANGA	 },
+	{"NOVEL",	  NOVEL   },
+	  {"ONE_SHOT", ONE_SHOT}
+};
+
+void ParseDate(const nlohmann::json& json, Date& date) {
+	if (json.contains("/year"_json_pointer) && json["/year"_json_pointer].is_number())
+		date.SetYear(JSON::GetInt(json, "/year"_json_pointer));
+	else
+		date.VoidYear();
+
+	if (json.contains("/month"_json_pointer) && json["/month"_json_pointer].is_number())
+		date.SetMonth(JSON::GetInt(json, "/month"_json_pointer));
+	else
+		date.VoidMonth();
+
+	if (json.contains("/day"_json_pointer) && json["/day"_json_pointer].is_number())
+		date.SetDay(JSON::GetInt(json, "/day"_json_pointer));
+	else
+		date.VoidDay();
+}
+
+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["/title"_json_pointer], anime);
+
+	anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer));
+	anime.SetFormat(AniListStringToAnimeFormatMap[JSON::GetString(json, "/format"_json_pointer)]);
+
+	anime.SetListStatus(AniListStringToAnimeAiringMap[JSON::GetString(json, "/status"_json_pointer)]);
+
+	ParseDate(json["/startDate"_json_pointer], anime.air_date);
+
+	anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer));
+	anime.SetSeason(AniListStringToAnimeSeasonMap[JSON::GetString(json, "/season"_json_pointer)]);
+	anime.SetDuration(JSON::GetInt(json, "/duration"_json_pointer));
+	anime.SetSynopsis(StringUtils::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.SetSynonyms(json["/synonyms"_json_pointer].get<std::vector<std::string>>());
+	return 1;
+}
+
+int ParseListItem(const nlohmann::json& json, Anime::Anime& anime) {
+	anime.SetScore(JSON::GetInt(entry.value(), "/score"_json_pointer));
+	anime.SetProgress(JSON::GetInt(entry.value(), "/progress"_json_pointer));
+	anime.SetStatus(AniListStringToAnimeWatchingMap[JSON::GetString(entry.value(), "/status"_json_pointer)]);
+	anime.SetNotes(JSON::GetString(entry.value(), "/notes"_json_pointer));
+
+	ParseDate(json["/startedAt"_json_pointer], anime.started);
+	ParseDate(json["/completedAt"_json_pointer], anime.completed);
+
+	anime.SetUpdated(JSON::GetInt(entry.value(), "/updatedAt"_json_pointer));
+
+	return ParseMediaJson(json["media"], anime);
+}
+
+int ParseList(const nlohmann::json& json) {
+	for (const auto& entry : json["entries"].items()) {
+		ParseListItem(entry.value());
+	}
+}
+
+int GetAnimeList(int id) {
+	/* 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"
+							  "        progress\n"
+							  "        startedAt {\n"
+							  "          year\n"
+							  "          month\n"
+							  "          day\n"
+							  "        }\n"
+							  "        completedAt {\n"
+							  "          year\n"
+							  "          month\n"
+							  "          day\n"
+							  "        }\n"
+							  "        updatedAt\n"
+							  "        media {\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", id}
+		}}
+	};
+	// 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.entry());
+	}
+	return 1;
+}
+
+int UpdateAnimeEntry(const Anime& anime) {
+	/**
+	 * 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
+	**/
+	const std::string query =
+		"mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String) {\n"
+		"  SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: "
+		"$notes) {\n"
+		"    id\n"
+		"  }\n"
+		"}\n";
+	// clang-format off
+	nlohmann::json json = {
+		{"query", query},
+		{"variables", {
+			{"media_id", anime.id},
+			{"progress", anime.progress},
+			{"status",   AnimeWatchingToStringMap[anime.status]},
+			{"score",    anime.score},
+			{"notes",    anime.notes}
+		}}
+	};
+	// clang-format on
+	SendRequest(json.dump());
+	return 1;
+}
+
+int ParseUser(const nlohmann::json& json) {
+	account.SetUsername(JSON::GetString(json, "/name"_json_pointer));
+	account.SetUserId(JSON::GetInt(json, "/id"_json_pointer));
+	account.SetAuthenticated(true);
+}
+
+int 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(token.toStdString());
+	else { // fail
+		account.SetAuthenticated(false);
+		return 0;
+	}
+	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(json["Viewer"]) account.SetAuthenticated(true);
+	return 1;
+}
+
+} // namespace Services::AniList