diff src/anilist.cpp @ 1:1ae666fdf9e2

*: initial commit
author Paper <mrpapersonic@gmail.com>
date Tue, 08 Aug 2023 19:49:15 -0400
parents
children 23d0d9319a00
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/anilist.cpp	Tue Aug 08 19:49:15 2023 -0400
@@ -0,0 +1,235 @@
+#include "window.h"
+#include "json.h"
+#include <curl/curl.h>
+#include <chrono>
+#include <exception>
+#include <format>
+#include "anilist.h"
+#include "anime.h"
+#include "config.h"
+#include "string_utils.h"
+#define CLIENT_ID "13706"
+
+size_t AniList::CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata) {
+    ((std::string*)userdata)->append((char*)contents, size * nmemb);
+    return size * nmemb;
+}
+
+std::string AniList::SendRequest(std::string data) {
+	struct curl_slist *list = NULL;
+	std::string userdata;
+	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 " + session.config.anilist.auth_token;
+		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);
+		/* FIXME: This sucks. When using HTTPS, we should ALWAYS make sure that our peer
+		   is actually valid. I assume the best way to go about this would be to bundle a
+		   certificate file, and if it's not found we should *prompt the user* and ask them
+		   if it's okay to contact AniList WITHOUT verification. If so, we're golden, and this
+		   flag will be set. If not, we should abort mission.
+
+		   For this program, it's probably fine to just contact AniList without
+		   HTTPS verification. However it should still be in the list of things to do... */
+		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE);
+		res = curl_easy_perform(curl);
+		curl_slist_free_all(list);
+		if (res != CURLE_OK) {
+			QMessageBox box(QMessageBox::Icon::Critical, "", QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
+			box.exec();
+			curl_easy_cleanup(curl);
+			return "";
+		}
+		curl_easy_cleanup(curl);
+		return userdata;
+	}
+	return "";
+}
+
+int AniList::GetUserId(std::string name) {
+#define QUERY "query ($name: String) {\n" \
+			  "  User (name: $name) {\n" \
+			  "    id\n" \
+			  "  }\n" \
+			  "}\n"
+	nlohmann::json json = {
+		{"query", QUERY},
+		{"variables", {
+			{"name", name}
+		}}
+	};
+	auto ret = nlohmann::json::parse(SendRequest(json.dump()));
+	return ret["data"]["User"]["id"].get<int>();
+#undef QUERY
+}
+
+/* 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<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}
+};
+
+int AniList::UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id) {
+#define 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" \
+"        media {\n" \
+"          id\n" \
+"          title {\n" \
+"            userPreferred\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"
+	nlohmann::json json = {
+		{"query", QUERY},
+		{"variables", {
+			{"id", id}
+		}}
+	};
+	/* 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()) {
+		/* why are the .key() values strings?? */
+		int list_key = std::stoi(list.key());
+		AnimeList anime_list;
+		anime_list.name = StringUtils::Utf8ToWstr(list.value()["name"].get<std::string>());
+		for (const auto& entry : list.value()["entries"].items()) {
+			int entry_key = std::stoi(entry.key());
+			Anime anime;
+			anime.score = entry.value()["score"].get<int>();
+			anime.progress = entry.value()["progress"].get<int>();
+			if (entry.value()["status"].is_string())
+				anime.status = StringToAnimeWatchingMap[entry.value()["status"].get<std::string>()];
+			if (entry.value()["notes"].is_string())
+				anime.notes = StringUtils::Utf8ToWstr(entry.value()["notes"].get<std::string>());
+
+			if (ANILIST_DATE_IS_VALID(entry.value()["startedAt"]))
+				anime.started = ANILIST_DATE_TO_YMD(entry.value()["startedAt"]);
+			if (ANILIST_DATE_IS_VALID(entry.value()["completedAt"]))
+				anime.completed = ANILIST_DATE_TO_YMD(entry.value()["completedAt"]);
+
+			anime.title = StringUtils::Utf8ToWstr(entry.value()["media"]["title"]["userPreferred"].get<std::string>());
+			anime.id = entry.value()["media"]["id"].get<int>();
+			if (!entry.value()["media"]["episodes"].is_null())
+				anime.episodes = entry.value()["media"]["episodes"].get<int>();
+			else // hasn't aired yet
+				anime.episodes = 0;
+
+			if (!entry.value()["media"]["format"].is_null())
+				anime.type = StringToAnimeFormatMap[entry.value()["media"]["format"].get<std::string>()];
+
+			anime.airing = StringToAnimeAiringMap[entry.value()["media"]["status"].get<std::string>()];
+
+			if (ANILIST_DATE_IS_VALID(entry.value()["media"]["startDate"]))
+				anime.air_date = ANILIST_DATE_TO_YMD(entry.value()["media"]["startDate"]);
+
+			if (entry.value()["media"]["averageScore"].is_number())
+				anime.audience_score = entry.value()["media"]["averageScore"].get<int>();
+
+			if (entry.value()["media"]["season"].is_string())
+				anime.season = StringToAnimeSeasonMap[entry.value()["media"]["season"].get<std::string>()];
+
+			if (entry.value()["media"]["duration"].is_number())
+				anime.duration = entry.value()["media"]["duration"].get<int>();
+			else
+				anime.duration = 0;
+
+			if (entry.value()["media"]["genres"].is_array())
+				anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>();
+			if (entry.value()["media"]["description"].is_string())
+				anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(entry.value()["media"]["description"].get<std::string>()));
+			anime_list.Add(anime);
+		}
+		anime_lists->push_back(anime_list);
+	}
+	return 1;
+}
+
+int AniList::Authorize() {
+	if (session.config.anilist.auth_token.empty()) {
+		/* 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()) {
+			session.config.anilist.auth_token = token.toStdString();
+		} else { // fail
+			return 0;
+		}
+	}
+	return 1;
+}