view src/anilist.cpp @ 5:51ae25154b70

Fix OS X support code
author Paper <mrpapersonic@gmail.com>
date Sat, 12 Aug 2023 13:10:34 -0400
parents 5af270662505
children 1d82f6e04d7d
line wrap: on
line source

#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) {
/* NOTE: these should be in the qrc file */
#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" \
"        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"
	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?? */
		AnimeList anime_list;
		anime_list.name = StringUtils::Utf8ToWstr(JSON::GetString(list.value(), "name"));
		for (const auto& entry : list.value()["entries"].items()) {
			Anime anime;
			anime.score = JSON::GetInt(entry.value(), "score");
			anime.progress = JSON::GetInt(entry.value(), "progress");
			anime.status = StringToAnimeWatchingMap[JSON::GetString(entry.value(), "status")];
			anime.notes = StringUtils::Utf8ToWstr(JSON::GetString(entry.value(), "notes"));

			anime.started.SetYear(JSON::GetInt(entry.value()["startedAt"], "year"));
			anime.started.SetMonth(JSON::GetInt(entry.value()["startedAt"], "month"));
			anime.started.SetDay(JSON::GetInt(entry.value()["startedAt"], "day"));

			anime.completed.SetYear(JSON::GetInt(entry.value()["completedAt"], "year"));
			anime.completed.SetMonth(JSON::GetInt(entry.value()["completedAt"], "month"));
			anime.completed.SetDay(JSON::GetInt(entry.value()["completedAt"], "day"));

			anime.updated = JSON::GetInt(entry.value(), "updatedAt");

			anime.title.native  = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "native"));
			anime.title.english = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "english"));
			anime.title.romaji  = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "romaji"));

			anime.id = JSON::GetInt(entry.value()["media"], "id");
			anime.episodes = JSON::GetInt(entry.value()["media"], "episodes");
			anime.type = StringToAnimeFormatMap[JSON::GetString(entry.value()["media"], "format")];

			anime.airing = StringToAnimeAiringMap[JSON::GetString(entry.value()["media"], "status")];

			anime.air_date.SetYear(JSON::GetInt(entry.value()["media"]["startDate"], "year"));
			anime.air_date.SetMonth(JSON::GetInt(entry.value()["media"]["startDate"], "month"));
			anime.air_date.SetDay(JSON::GetInt(entry.value()["media"]["startDate"], "day"));

			anime.audience_score = JSON::GetInt(entry.value()["media"], "averageScore");
			anime.season = StringToAnimeSeasonMap[JSON::GetString(entry.value()["media"], "season")];
			anime.duration = JSON::GetInt(entry.value()["media"], "duration");
			anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"], "duration")));

			if (entry.value()["media"]["genres"].is_array())
				anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>();
			anime_list.Add(anime);
		}
		anime_lists->push_back(anime_list);
	}
	return 1;
#undef QUERY
}

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;
}