view src/services/anilist.cc @ 101:c537996cf67b

*: multitude of config changes 1. theme is now configurable from the settings menu (but you have to restart for it to apply) 2. config is now stored in an INI file, with no method of conversion from json (this repo is private-ish anyway)
author Paper <mrpapersonic@gmail.com>
date Fri, 03 Nov 2023 14:06:02 -0400
parents 9b2b41f83a5e
children 275da698697d
line wrap: on
line source

#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