Mercurial > minori
diff src/anilist.cpp @ 2:23d0d9319a00
Update
Also converted everything to LF from CRLF
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Sat, 12 Aug 2023 03:16:26 -0400 |
parents | 1ae666fdf9e2 |
children | 190ded9438c0 |
line wrap: on
line diff
--- a/src/anilist.cpp Tue Aug 08 19:49:15 2023 -0400 +++ b/src/anilist.cpp Sat Aug 12 03:16:26 2023 -0400 @@ -1,235 +1,234 @@ -#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; -} +#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) { +/* NOTE: these should be in the qrc file */ +#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" \ +" 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" + 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(JSON::GetString(list.value(), "name")); + for (const auto& entry : list.value()["entries"].items()) { + int entry_key = std::stoi(entry.key()); + Anime anime; + anime.score = JSON::GetInt(entry.value(), "score"); + anime.progress = JSON::GetInt(entry.value(), "progress"); + anime.status = StringToAnimeWatchingMap[JSON::GetString(entry.value(), "status")]; + anime.notes = StringUtils::Utf8ToWstr(JSON::GetString(entry.value(), "notes")); + + anime.started.SetYear(JSON::GetInt(entry.value()["startedAt"], "year")); + anime.started.SetMonth(JSON::GetInt(entry.value()["startedAt"], "month")); + anime.started.SetDay(JSON::GetInt(entry.value()["startedAt"], "day")); + + anime.completed.SetYear(JSON::GetInt(entry.value()["completedAt"], "year")); + anime.completed.SetMonth(JSON::GetInt(entry.value()["completedAt"], "month")); + anime.completed.SetDay(JSON::GetInt(entry.value()["completedAt"], "day")); + + anime.updated = JSON::GetInt(entry.value(), "updatedAt"); + + anime.title.native = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "native")); + anime.title.english = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "english")); + anime.title.romaji = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "romaji")); + + anime.id = JSON::GetInt(entry.value()["media"], "id"); + anime.episodes = JSON::GetInt(entry.value()["media"], "episodes"); + anime.type = StringToAnimeFormatMap[JSON::GetString(entry.value()["media"], "format")]; + + anime.airing = StringToAnimeAiringMap[JSON::GetString(entry.value()["media"], "status")]; + + anime.air_date.SetYear(JSON::GetInt(entry.value()["media"]["startDate"], "year")); + anime.air_date.SetMonth(JSON::GetInt(entry.value()["media"]["startDate"], "month")); + anime.air_date.SetDay(JSON::GetInt(entry.value()["media"]["startDate"], "day")); + + anime.audience_score = JSON::GetInt(entry.value()["media"], "averageScore"); + anime.season = StringToAnimeSeasonMap[JSON::GetString(entry.value()["media"], "season")]; + anime.duration = JSON::GetInt(entry.value()["media"], "duration"); + anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"], "duration"))); + + if (entry.value()["media"]["genres"].is_array()) + anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>(); + anime_list.Add(anime); + } + anime_lists->push_back(anime_list); + } + return 1; +#undef QUERY +} + +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; +}