Mercurial > minori
comparison src/anilist.cpp @ 1:1ae666fdf9e2
*: initial commit
| author | Paper <mrpapersonic@gmail.com> |
|---|---|
| date | Tue, 08 Aug 2023 19:49:15 -0400 |
| parents | |
| children | 23d0d9319a00 |
comparison
equal
deleted
inserted
replaced
| 0:5a76e1b94163 | 1:1ae666fdf9e2 |
|---|---|
| 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 } |
