1
+ − 1 #include "window.h"
+ − 2 #include "json.h"
+ − 3 #include <curl/curl.h>
+ − 4 #include <chrono>
+ − 5 #include <exception>
+ − 6 #include <format>
+ − 7 #include "anilist.h"
+ − 8 #include "anime.h"
+ − 9 #include "config.h"
+ − 10 #include "string_utils.h"
+ − 11 #define CLIENT_ID "13706"
+ − 12
+ − 13 size_t AniList::CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata) {
+ − 14 ((std::string*)userdata)->append((char*)contents, size * nmemb);
+ − 15 return size * nmemb;
+ − 16 }
+ − 17
+ − 18 std::string AniList::SendRequest(std::string data) {
+ − 19 struct curl_slist *list = NULL;
+ − 20 std::string userdata;
+ − 21 curl = curl_easy_init();
+ − 22 if (curl) {
+ − 23 list = curl_slist_append(list, "Accept: application/json");
+ − 24 list = curl_slist_append(list, "Content-Type: application/json");
+ − 25 std::string bearer = "Authorization: Bearer " + session.config.anilist.auth_token;
+ − 26 list = curl_slist_append(list, bearer.c_str());
+ − 27 curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co");
+ − 28 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
+ − 29 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
+ − 30 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
+ − 31 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback);
+ − 32 /* FIXME: This sucks. When using HTTPS, we should ALWAYS make sure that our peer
+ − 33 is actually valid. I assume the best way to go about this would be to bundle a
+ − 34 certificate file, and if it's not found we should *prompt the user* and ask them
+ − 35 if it's okay to contact AniList WITHOUT verification. If so, we're golden, and this
+ − 36 flag will be set. If not, we should abort mission.
+ − 37
+ − 38 For this program, it's probably fine to just contact AniList without
+ − 39 HTTPS verification. However it should still be in the list of things to do... */
+ − 40 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE);
+ − 41 res = curl_easy_perform(curl);
+ − 42 curl_slist_free_all(list);
+ − 43 if (res != CURLE_OK) {
+ − 44 QMessageBox box(QMessageBox::Icon::Critical, "", QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
+ − 45 box.exec();
+ − 46 curl_easy_cleanup(curl);
+ − 47 return "";
+ − 48 }
+ − 49 curl_easy_cleanup(curl);
+ − 50 return userdata;
+ − 51 }
+ − 52 return "";
+ − 53 }
+ − 54
+ − 55 int AniList::GetUserId(std::string name) {
+ − 56 #define QUERY "query ($name: String) {\n" \
+ − 57 " User (name: $name) {\n" \
+ − 58 " id\n" \
+ − 59 " }\n" \
+ − 60 "}\n"
+ − 61 nlohmann::json json = {
+ − 62 {"query", QUERY},
+ − 63 {"variables", {
+ − 64 {"name", name}
+ − 65 }}
+ − 66 };
+ − 67 auto ret = nlohmann::json::parse(SendRequest(json.dump()));
+ − 68 return ret["data"]["User"]["id"].get<int>();
+ − 69 #undef QUERY
+ − 70 }
+ − 71
+ − 72 /* Maps to convert string forms to our internal enums */
+ − 73
+ − 74 std::map<std::string, enum AnimeWatchingStatus> StringToAnimeWatchingMap = {
+ − 75 {"CURRENT", CURRENT},
+ − 76 {"PLANNING", PLANNING},
+ − 77 {"COMPLETED", COMPLETED},
+ − 78 {"DROPPED", DROPPED},
+ − 79 {"PAUSED", PAUSED},
+ − 80 {"REPEATING", REPEATING}
+ − 81 };
+ − 82
+ − 83 std::map<std::string, enum AnimeAiringStatus> StringToAnimeAiringMap = {
+ − 84 {"FINISHED", FINISHED},
+ − 85 {"RELEASING", RELEASING},
+ − 86 {"NOT_YET_RELEASED", NOT_YET_RELEASED},
+ − 87 {"CANCELLED", CANCELLED},
+ − 88 {"HIATUS", HIATUS}
+ − 89 };
+ − 90
+ − 91 std::map<std::string, enum AnimeSeason> StringToAnimeSeasonMap = {
+ − 92 {"WINTER", WINTER},
+ − 93 {"SPRING", SPRING},
+ − 94 {"SUMMER", SUMMER},
+ − 95 {"FALL", FALL}
+ − 96 };
+ − 97
+ − 98 std::map<std::string, enum AnimeFormat> StringToAnimeFormatMap = {
+ − 99 {"TV", TV},
+ − 100 {"TV_SHORT", TV_SHORT},
+ − 101 {"MOVIE", MOVIE},
+ − 102 {"SPECIAL", SPECIAL},
+ − 103 {"OVA", OVA},
+ − 104 {"ONA", ONA},
+ − 105 {"MUSIC", MUSIC},
+ − 106 {"MANGA", MANGA},
+ − 107 {"NOVEL", NOVEL},
+ − 108 {"ONE_SHOT", ONE_SHOT}
+ − 109 };
+ − 110
+ − 111 int AniList::UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id) {
+ − 112 #define QUERY "query ($id: Int) {\n" \
+ − 113 " MediaListCollection (userId: $id, type: ANIME) {\n" \
+ − 114 " lists {\n" \
+ − 115 " name\n" \
+ − 116 " entries {\n" \
+ − 117 " score\n" \
+ − 118 " notes\n" \
+ − 119 " progress\n" \
+ − 120 " startedAt {\n" \
+ − 121 " year\n" \
+ − 122 " month\n" \
+ − 123 " day\n" \
+ − 124 " }\n" \
+ − 125 " completedAt {\n" \
+ − 126 " year\n" \
+ − 127 " month\n" \
+ − 128 " day\n" \
+ − 129 " }\n" \
+ − 130 " media {\n" \
+ − 131 " id\n" \
+ − 132 " title {\n" \
+ − 133 " userPreferred\n" \
+ − 134 " }\n" \
+ − 135 " format\n" \
+ − 136 " status\n" \
+ − 137 " averageScore\n" \
+ − 138 " season\n" \
+ − 139 " startDate {\n" \
+ − 140 " year\n" \
+ − 141 " month\n" \
+ − 142 " day\n" \
+ − 143 " }\n" \
+ − 144 " genres\n" \
+ − 145 " episodes\n" \
+ − 146 " duration\n" \
+ − 147 " synonyms\n" \
+ − 148 " description(asHtml: false)\n" \
+ − 149 " }\n" \
+ − 150 " }\n" \
+ − 151 " }\n" \
+ − 152 " }\n" \
+ − 153 "}\n"
+ − 154 nlohmann::json json = {
+ − 155 {"query", QUERY},
+ − 156 {"variables", {
+ − 157 {"id", id}
+ − 158 }}
+ − 159 };
+ − 160 /* TODO: do a try catch here, catch any json errors and then call
+ − 161 Authorize() if needed */
+ − 162 auto res = nlohmann::json::parse(SendRequest(json.dump()));
+ − 163 /* TODO: make sure that we actually need the wstring converter and see
+ − 164 if we can just get wide strings back from nlohmann::json */
+ − 165 for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
+ − 166 /* why are the .key() values strings?? */
+ − 167 int list_key = std::stoi(list.key());
+ − 168 AnimeList anime_list;
+ − 169 anime_list.name = StringUtils::Utf8ToWstr(list.value()["name"].get<std::string>());
+ − 170 for (const auto& entry : list.value()["entries"].items()) {
+ − 171 int entry_key = std::stoi(entry.key());
+ − 172 Anime anime;
+ − 173 anime.score = entry.value()["score"].get<int>();
+ − 174 anime.progress = entry.value()["progress"].get<int>();
+ − 175 if (entry.value()["status"].is_string())
+ − 176 anime.status = StringToAnimeWatchingMap[entry.value()["status"].get<std::string>()];
+ − 177 if (entry.value()["notes"].is_string())
+ − 178 anime.notes = StringUtils::Utf8ToWstr(entry.value()["notes"].get<std::string>());
+ − 179
+ − 180 if (ANILIST_DATE_IS_VALID(entry.value()["startedAt"]))
+ − 181 anime.started = ANILIST_DATE_TO_YMD(entry.value()["startedAt"]);
+ − 182 if (ANILIST_DATE_IS_VALID(entry.value()["completedAt"]))
+ − 183 anime.completed = ANILIST_DATE_TO_YMD(entry.value()["completedAt"]);
+ − 184
+ − 185 anime.title = StringUtils::Utf8ToWstr(entry.value()["media"]["title"]["userPreferred"].get<std::string>());
+ − 186 anime.id = entry.value()["media"]["id"].get<int>();
+ − 187 if (!entry.value()["media"]["episodes"].is_null())
+ − 188 anime.episodes = entry.value()["media"]["episodes"].get<int>();
+ − 189 else // hasn't aired yet
+ − 190 anime.episodes = 0;
+ − 191
+ − 192 if (!entry.value()["media"]["format"].is_null())
+ − 193 anime.type = StringToAnimeFormatMap[entry.value()["media"]["format"].get<std::string>()];
+ − 194
+ − 195 anime.airing = StringToAnimeAiringMap[entry.value()["media"]["status"].get<std::string>()];
+ − 196
+ − 197 if (ANILIST_DATE_IS_VALID(entry.value()["media"]["startDate"]))
+ − 198 anime.air_date = ANILIST_DATE_TO_YMD(entry.value()["media"]["startDate"]);
+ − 199
+ − 200 if (entry.value()["media"]["averageScore"].is_number())
+ − 201 anime.audience_score = entry.value()["media"]["averageScore"].get<int>();
+ − 202
+ − 203 if (entry.value()["media"]["season"].is_string())
+ − 204 anime.season = StringToAnimeSeasonMap[entry.value()["media"]["season"].get<std::string>()];
+ − 205
+ − 206 if (entry.value()["media"]["duration"].is_number())
+ − 207 anime.duration = entry.value()["media"]["duration"].get<int>();
+ − 208 else
+ − 209 anime.duration = 0;
+ − 210
+ − 211 if (entry.value()["media"]["genres"].is_array())
+ − 212 anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>();
+ − 213 if (entry.value()["media"]["description"].is_string())
+ − 214 anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(entry.value()["media"]["description"].get<std::string>()));
+ − 215 anime_list.Add(anime);
+ − 216 }
+ − 217 anime_lists->push_back(anime_list);
+ − 218 }
+ − 219 return 1;
+ − 220 }
+ − 221
+ − 222 int AniList::Authorize() {
+ − 223 if (session.config.anilist.auth_token.empty()) {
+ − 224 /* Prompt for PIN */
+ − 225 QDesktopServices::openUrl(QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token"));
+ − 226 bool ok;
+ − 227 QString token = QInputDialog::getText(0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, "", &ok);
+ − 228 if (ok && !token.isEmpty()) {
+ − 229 session.config.anilist.auth_token = token.toStdString();
+ − 230 } else { // fail
+ − 231 return 0;
+ − 232 }
+ − 233 }
+ − 234 return 1;
+ − 235 }