Mercurial > minori
view 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 source
#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