Mercurial > minori
diff src/services/anilist.cpp @ 9:5c0397762b53
INCOMPLETE: megacommit :)
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Sun, 10 Sep 2023 03:59:16 -0400 |
parents | |
children | 4b198a111713 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/services/anilist.cpp Sun Sep 10 03:59:16 2023 -0400 @@ -0,0 +1,337 @@ +#include "services/anilist.h" +#include "core/anime.h" +#include "core/config.h" +#include "core/json.h" +#include "core/session.h" +#include "core/strings.h" +#include <QDesktopServices> +#include <QInputDialog> +#include <QLineEdit> +#include <QMessageBox> +#include <chrono> +#include <curl/curl.h> +#include <exception> +#include <format> +#define CLIENT_ID "13706" + +namespace Services::AniList { + +class Account { + public: + std::string Username() const { return session.anilist.username; } + void SetUsername(std::string const& username) { session.anilist.username = username; } + + int UserId() const { return session.anilist.user_id; } + void SetUserId(const int id) { session.anilist.user_id = id; } + + std::string AuthToken() const { return session.anilist.auth_token; } + void SetAuthToken(std::string const& auth_token) { session.anilist.auth_token = auth_token; } + + bool Authenticated() const { return !AuthToken().empty(); } +} + +static Account account; + +static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userdata) { + ((std::string*)userdata)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +/* A wrapper around cURL to send requests to AniList */ +std::string SendRequest(std::string data) { + struct curl_slist* list = NULL; + std::string userdata; + CURL* 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 " + account.AuthToken(); + 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); + /* Use system certs... useful on Windows. */ + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); + CURLcode res = curl_easy_perform(curl); + curl_slist_free_all(list); + curl_easy_cleanup(curl); + if (res != CURLE_OK) { + QMessageBox box(QMessageBox::Icon::Critical, "", + QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res))); + box.exec(); + return ""; + } + return userdata; + } + return ""; +} + +/* 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<enum AnimeWatchingStatus, std::string> AnimeWatchingToStringMap = { + {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} +}; + +void ParseDate(const nlohmann::json& json, Date& date) { + if (json.contains("/year"_json_pointer) && json["/year"_json_pointer].is_number()) + date.SetYear(JSON::GetInt(json, "/year"_json_pointer)); + else + date.VoidYear(); + + if (json.contains("/month"_json_pointer) && json["/month"_json_pointer].is_number()) + date.SetMonth(JSON::GetInt(json, "/month"_json_pointer)); + else + date.VoidMonth(); + + if (json.contains("/day"_json_pointer) && json["/day"_json_pointer].is_number()) + date.SetDay(JSON::GetInt(json, "/day"_json_pointer)); + else + date.VoidDay(); +} + +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["/title"_json_pointer], anime); + + anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer)); + anime.SetFormat(AniListStringToAnimeFormatMap[JSON::GetString(json, "/format"_json_pointer)]); + + anime.SetListStatus(AniListStringToAnimeAiringMap[JSON::GetString(json, "/status"_json_pointer)]); + + ParseDate(json["/startDate"_json_pointer], anime.air_date); + + anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer)); + anime.SetSeason(AniListStringToAnimeSeasonMap[JSON::GetString(json, "/season"_json_pointer)]); + anime.SetDuration(JSON::GetInt(json, "/duration"_json_pointer)); + anime.SetSynopsis(StringUtils::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.SetSynonyms(json["/synonyms"_json_pointer].get<std::vector<std::string>>()); + return 1; +} + +int ParseListItem(const nlohmann::json& json, Anime::Anime& anime) { + anime.SetScore(JSON::GetInt(entry.value(), "/score"_json_pointer)); + anime.SetProgress(JSON::GetInt(entry.value(), "/progress"_json_pointer)); + anime.SetStatus(AniListStringToAnimeWatchingMap[JSON::GetString(entry.value(), "/status"_json_pointer)]); + anime.SetNotes(JSON::GetString(entry.value(), "/notes"_json_pointer)); + + ParseDate(json["/startedAt"_json_pointer], anime.started); + ParseDate(json["/completedAt"_json_pointer], anime.completed); + + anime.SetUpdated(JSON::GetInt(entry.value(), "/updatedAt"_json_pointer)); + + return ParseMediaJson(json["media"], anime); +} + +int ParseList(const nlohmann::json& json) { + for (const auto& entry : json["entries"].items()) { + ParseListItem(entry.value()); + } +} + +int GetAnimeList(int id) { + /* 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" + " 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"; + // clang-format off + nlohmann::json json = { + {"query", query}, + {"variables", { + {"id", id} + }} + }; + // 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.entry()); + } + return 1; +} + +int UpdateAnimeEntry(const Anime& anime) { + /** + * 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 + **/ + const std::string query = + "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String) {\n" + " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: " + "$notes) {\n" + " id\n" + " }\n" + "}\n"; + // clang-format off + nlohmann::json json = { + {"query", query}, + {"variables", { + {"media_id", anime.id}, + {"progress", anime.progress}, + {"status", AnimeWatchingToStringMap[anime.status]}, + {"score", anime.score}, + {"notes", anime.notes} + }} + }; + // clang-format on + SendRequest(json.dump()); + return 1; +} + +int ParseUser(const nlohmann::json& json) { + account.SetUsername(JSON::GetString(json, "/name"_json_pointer)); + account.SetUserId(JSON::GetInt(json, "/id"_json_pointer)); + account.SetAuthenticated(true); +} + +int 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(token.toStdString()); + else { // fail + account.SetAuthenticated(false); + return 0; + } + 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(json["Viewer"]) account.SetAuthenticated(true); + return 1; +} + +} // namespace Services::AniList