view src/anilist.cpp @ 6:1d82f6e04d7d

Update: add first parts to the settings dialog
author Paper <mrpapersonic@gmail.com>
date Wed, 16 Aug 2023 00:49:17 -0400
parents 5af270662505
children 07a9095eaeed
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 JSON::GetInt(ret, "/data/User/id"_json_pointer);
#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 = JSON::GetString(list.value(), "/name"_json_pointer);
		for (const auto& entry : list.value()["entries"].items()) {
			Anime anime;
			anime.score = JSON::GetInt(entry.value(), "/score"_json_pointer);
			anime.progress = JSON::GetInt(entry.value(), "/progress"_json_pointer);
			anime.status = StringToAnimeWatchingMap[JSON::GetString(entry.value(), "/status"_json_pointer)];
			anime.notes = JSON::GetString(entry.value(), "/notes"_json_pointer);

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

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

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

			anime.title.native  = JSON::GetString(entry.value(), "/media/title/native"_json_pointer);
			anime.title.english = JSON::GetString(entry.value(), "/media/title/english"_json_pointer);
			anime.title.romaji  = JSON::GetString(entry.value(), "/media/title/romaji"_json_pointer);
			/* fallback to romaji if english is not available
			   note that this takes up more space in memory and is stinky */
			if (anime.title.english.empty())
				anime.title.english = anime.title.romaji;

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

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

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

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

			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() {
	/* 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;
}