view src/services/anilist.cc @ 118:39521c47c7a3

*: another huge megacommit, SORRY The torrents page works a lot better now Added the edit option to the anime list right click menu Vectorized currently playing files Available player and extensions are now loaded at runtime from files in (dotpath)/players.json and (dotpath)/extensions.json These paths are not permanent and will likely be moved to (dotpath)/recognition ... ... ...
author Paper <mrpapersonic@gmail.com>
date Tue, 07 Nov 2023 23:40:54 -0500
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