Mercurial > minori
diff src/anilist.cpp @ 1:1ae666fdf9e2
*: initial commit
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Tue, 08 Aug 2023 19:49:15 -0400 |
parents | |
children | 23d0d9319a00 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/anilist.cpp Tue Aug 08 19:49:15 2023 -0400 @@ -0,0 +1,235 @@ +#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 ret["data"]["User"]["id"].get<int>(); +#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) { +#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" \ +" media {\n" \ +" id\n" \ +" title {\n" \ +" userPreferred\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?? */ + int list_key = std::stoi(list.key()); + AnimeList anime_list; + anime_list.name = StringUtils::Utf8ToWstr(list.value()["name"].get<std::string>()); + for (const auto& entry : list.value()["entries"].items()) { + int entry_key = std::stoi(entry.key()); + Anime anime; + anime.score = entry.value()["score"].get<int>(); + anime.progress = entry.value()["progress"].get<int>(); + if (entry.value()["status"].is_string()) + anime.status = StringToAnimeWatchingMap[entry.value()["status"].get<std::string>()]; + if (entry.value()["notes"].is_string()) + anime.notes = StringUtils::Utf8ToWstr(entry.value()["notes"].get<std::string>()); + + if (ANILIST_DATE_IS_VALID(entry.value()["startedAt"])) + anime.started = ANILIST_DATE_TO_YMD(entry.value()["startedAt"]); + if (ANILIST_DATE_IS_VALID(entry.value()["completedAt"])) + anime.completed = ANILIST_DATE_TO_YMD(entry.value()["completedAt"]); + + anime.title = StringUtils::Utf8ToWstr(entry.value()["media"]["title"]["userPreferred"].get<std::string>()); + anime.id = entry.value()["media"]["id"].get<int>(); + if (!entry.value()["media"]["episodes"].is_null()) + anime.episodes = entry.value()["media"]["episodes"].get<int>(); + else // hasn't aired yet + anime.episodes = 0; + + if (!entry.value()["media"]["format"].is_null()) + anime.type = StringToAnimeFormatMap[entry.value()["media"]["format"].get<std::string>()]; + + anime.airing = StringToAnimeAiringMap[entry.value()["media"]["status"].get<std::string>()]; + + if (ANILIST_DATE_IS_VALID(entry.value()["media"]["startDate"])) + anime.air_date = ANILIST_DATE_TO_YMD(entry.value()["media"]["startDate"]); + + if (entry.value()["media"]["averageScore"].is_number()) + anime.audience_score = entry.value()["media"]["averageScore"].get<int>(); + + if (entry.value()["media"]["season"].is_string()) + anime.season = StringToAnimeSeasonMap[entry.value()["media"]["season"].get<std::string>()]; + + if (entry.value()["media"]["duration"].is_number()) + anime.duration = entry.value()["media"]["duration"].get<int>(); + else + anime.duration = 0; + + if (entry.value()["media"]["genres"].is_array()) + anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>(); + if (entry.value()["media"]["description"].is_string()) + anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(entry.value()["media"]["description"].get<std::string>())); + anime_list.Add(anime); + } + anime_lists->push_back(anime_list); + } + return 1; +} + +int AniList::Authorize() { + if (session.config.anilist.auth_token.empty()) { + /* 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; +}