9
+ − 1 #include "services/anilist.h"
+ − 2 #include "core/anime.h"
10
+ − 3 #include "core/anime_db.h"
9
+ − 4 #include "core/config.h"
+ − 5 #include "core/json.h"
+ − 6 #include "core/session.h"
+ − 7 #include "core/strings.h"
+ − 8 #include <QDesktopServices>
+ − 9 #include <QInputDialog>
+ − 10 #include <QLineEdit>
+ − 11 #include <QMessageBox>
10
+ − 12 #include <QUrl>
9
+ − 13 #include <chrono>
+ − 14 #include <curl/curl.h>
+ − 15 #include <exception>
+ − 16 #include <format>
+ − 17 #define CLIENT_ID "13706"
+ − 18
11
+ − 19 using nlohmann::literals::operator "" _json_pointer;
+ − 20
9
+ − 21 namespace Services::AniList {
+ − 22
+ − 23 class Account {
+ − 24 public:
10
+ − 25 std::string Username() const { return session.config.anilist.username; }
+ − 26 void SetUsername(std::string const& username) { session.config.anilist.username = username; }
9
+ − 27
10
+ − 28 int UserId() const { return session.config.anilist.user_id; }
+ − 29 void SetUserId(const int id) { session.config.anilist.user_id = id; }
9
+ − 30
10
+ − 31 std::string AuthToken() const { return session.config.anilist.auth_token; }
+ − 32 void SetAuthToken(std::string const& auth_token) { session.config.anilist.auth_token = auth_token; }
9
+ − 33
+ − 34 bool Authenticated() const { return !AuthToken().empty(); }
10
+ − 35 };
9
+ − 36
+ − 37 static Account account;
+ − 38
+ − 39 static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userdata) {
+ − 40 ((std::string*)userdata)->append((char*)contents, size * nmemb);
+ − 41 return size * nmemb;
+ − 42 }
+ − 43
+ − 44 /* A wrapper around cURL to send requests to AniList */
+ − 45 std::string SendRequest(std::string data) {
+ − 46 struct curl_slist* list = NULL;
+ − 47 std::string userdata;
+ − 48 CURL* curl = curl_easy_init();
+ − 49 if (curl) {
+ − 50 list = curl_slist_append(list, "Accept: application/json");
+ − 51 list = curl_slist_append(list, "Content-Type: application/json");
+ − 52 std::string bearer = "Authorization: Bearer " + account.AuthToken();
+ − 53 list = curl_slist_append(list, bearer.c_str());
+ − 54 curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co");
+ − 55 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
+ − 56 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
+ − 57 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
+ − 58 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback);
+ − 59 /* Use system certs... useful on Windows. */
+ − 60 curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
+ − 61 CURLcode res = curl_easy_perform(curl);
+ − 62 curl_slist_free_all(list);
+ − 63 curl_easy_cleanup(curl);
+ − 64 if (res != CURLE_OK) {
+ − 65 QMessageBox box(QMessageBox::Icon::Critical, "",
+ − 66 QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
+ − 67 box.exec();
+ − 68 return "";
+ − 69 }
+ − 70 return userdata;
+ − 71 }
+ − 72 return "";
+ − 73 }
+ − 74
10
+ − 75 /* TODO: Move to Translate */
9
+ − 76
10
+ − 77 std::map<std::string, Anime::ListStatus> AniListStringToAnimeWatchingMap = {
+ − 78 {"CURRENT", Anime::ListStatus::CURRENT },
+ − 79 {"PLANNING", Anime::ListStatus::PLANNING },
+ − 80 {"COMPLETED", Anime::ListStatus::COMPLETED},
+ − 81 {"DROPPED", Anime::ListStatus::DROPPED },
+ − 82 {"PAUSED", Anime::ListStatus::PAUSED },
+ − 83 {"REPEATING", Anime::ListStatus::CURRENT}
9
+ − 84 };
+ − 85
10
+ − 86 std::map<Anime::ListStatus, std::string> AniListAnimeWatchingToStringMap = {
+ − 87 {Anime::ListStatus::CURRENT, "CURRENT" },
+ − 88 {Anime::ListStatus::PLANNING, "PLANNING" },
+ − 89 {Anime::ListStatus::COMPLETED, "COMPLETED"},
+ − 90 {Anime::ListStatus::DROPPED, "DROPPED" },
+ − 91 {Anime::ListStatus::PAUSED, "PAUSED" }
9
+ − 92 };
+ − 93
10
+ − 94 std::map<std::string, Anime::SeriesStatus> AniListStringToAnimeAiringMap = {
+ − 95 {"FINISHED", Anime::SeriesStatus::FINISHED },
+ − 96 {"RELEASING", Anime::SeriesStatus::RELEASING },
+ − 97 {"NOT_YET_RELEASED", Anime::SeriesStatus::NOT_YET_RELEASED},
+ − 98 {"CANCELLED", Anime::SeriesStatus::CANCELLED },
+ − 99 {"HIATUS", Anime::SeriesStatus::HIATUS }
9
+ − 100 };
+ − 101
10
+ − 102 std::map<std::string, Anime::SeriesSeason> AniListStringToAnimeSeasonMap = {
+ − 103 {"WINTER", Anime::SeriesSeason::WINTER},
+ − 104 {"SPRING", Anime::SeriesSeason::SPRING},
+ − 105 {"SUMMER", Anime::SeriesSeason::SUMMER},
+ − 106 {"FALL", Anime::SeriesSeason::FALL }
9
+ − 107 };
+ − 108
10
+ − 109 std::map<std::string, enum Anime::SeriesFormat> AniListStringToAnimeFormatMap = {
+ − 110 {"TV", Anime::SeriesFormat::TV },
+ − 111 {"TV_SHORT", Anime::SeriesFormat::TV_SHORT},
+ − 112 {"MOVIE", Anime::SeriesFormat::MOVIE },
+ − 113 {"SPECIAL", Anime::SeriesFormat::SPECIAL },
+ − 114 {"OVA", Anime::SeriesFormat::OVA },
+ − 115 {"ONA", Anime::SeriesFormat::ONA },
+ − 116 {"MUSIC", Anime::SeriesFormat::MUSIC },
+ − 117 {"MANGA", Anime::SeriesFormat::MANGA },
+ − 118 {"NOVEL", Anime::SeriesFormat::NOVEL },
+ − 119 {"ONE_SHOT", Anime::SeriesFormat::ONE_SHOT}
9
+ − 120 };
+ − 121
10
+ − 122 Date ParseDate(const nlohmann::json& json) {
+ − 123 Date date;
11
+ − 124 if (json.contains("/year"_json_pointer) && json.at("/year"_json_pointer).is_number())
9
+ − 125 date.SetYear(JSON::GetInt(json, "/year"_json_pointer));
+ − 126 else
+ − 127 date.VoidYear();
+ − 128
11
+ − 129 if (json.contains("/month"_json_pointer) && json.at("/month"_json_pointer).is_number())
9
+ − 130 date.SetMonth(JSON::GetInt(json, "/month"_json_pointer));
+ − 131 else
+ − 132 date.VoidMonth();
+ − 133
11
+ − 134 if (json.contains("/day"_json_pointer) && json.at("/day"_json_pointer).is_number())
9
+ − 135 date.SetDay(JSON::GetInt(json, "/day"_json_pointer));
+ − 136 else
+ − 137 date.VoidDay();
10
+ − 138 return date;
9
+ − 139 }
+ − 140
+ − 141 void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) {
+ − 142 anime.SetNativeTitle(JSON::GetString(json, "/native"_json_pointer));
+ − 143 anime.SetEnglishTitle(JSON::GetString(json, "/english"_json_pointer));
+ − 144 anime.SetRomajiTitle(JSON::GetString(json, "/romaji"_json_pointer));
+ − 145 }
+ − 146
+ − 147 int ParseMediaJson(const nlohmann::json& json) {
+ − 148 int id = JSON::GetInt(json, "/id"_json_pointer);
+ − 149 if (!id)
+ − 150 return 0;
+ − 151 Anime::Anime& anime = Anime::db.items[id];
+ − 152 anime.SetId(id);
+ − 153
11
+ − 154 ParseTitle(json.at("/title"_json_pointer), anime);
9
+ − 155
+ − 156 anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer));
+ − 157 anime.SetFormat(AniListStringToAnimeFormatMap[JSON::GetString(json, "/format"_json_pointer)]);
+ − 158
10
+ − 159 anime.SetAiringStatus(AniListStringToAnimeAiringMap[JSON::GetString(json, "/status"_json_pointer)]);
9
+ − 160
10
+ − 161 anime.SetAirDate(ParseDate(json["/startDate"_json_pointer]));
9
+ − 162
+ − 163 anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer));
+ − 164 anime.SetSeason(AniListStringToAnimeSeasonMap[JSON::GetString(json, "/season"_json_pointer)]);
+ − 165 anime.SetDuration(JSON::GetInt(json, "/duration"_json_pointer));
10
+ − 166 anime.SetSynopsis(Strings::TextifySynopsis(JSON::GetString(json, "/description"_json_pointer)));
9
+ − 167
+ − 168 if (json.contains("/genres"_json_pointer) && json["/genres"_json_pointer].is_array())
+ − 169 anime.SetGenres(json["/genres"_json_pointer].get<std::vector<std::string>>());
+ − 170 if (json.contains("/synonyms"_json_pointer) && json["/synonyms"_json_pointer].is_array())
10
+ − 171 anime.SetTitleSynonyms(json["/synonyms"_json_pointer].get<std::vector<std::string>>());
+ − 172 return id;
9
+ − 173 }
+ − 174
10
+ − 175 int ParseListItem(const nlohmann::json& json) {
+ − 176 int id = ParseMediaJson(json["media"]);
+ − 177
+ − 178 Anime::Anime& anime = Anime::db.items[id];
+ − 179
+ − 180 anime.AddToUserList();
9
+ − 181
10
+ − 182 anime.SetUserScore(JSON::GetInt(json, "/score"_json_pointer));
+ − 183 anime.SetUserProgress(JSON::GetInt(json, "/progress"_json_pointer));
+ − 184 anime.SetUserStatus(AniListStringToAnimeWatchingMap[JSON::GetString(json, "/status"_json_pointer)]);
+ − 185 anime.SetUserNotes(JSON::GetString(json, "/notes"_json_pointer));
9
+ − 186
10
+ − 187 anime.SetUserDateStarted(ParseDate(json["/startedAt"_json_pointer]));
+ − 188 anime.SetUserDateCompleted(ParseDate(json["/completedAt"_json_pointer]));
9
+ − 189
10
+ − 190 anime.SetUserTimeUpdated(JSON::GetInt(json, "/updatedAt"_json_pointer));
+ − 191
+ − 192 return id;
9
+ − 193 }
+ − 194
+ − 195 int ParseList(const nlohmann::json& json) {
+ − 196 for (const auto& entry : json["entries"].items()) {
+ − 197 ParseListItem(entry.value());
+ − 198 }
10
+ − 199 return 1;
9
+ − 200 }
+ − 201
10
+ − 202 int GetAnimeList() {
9
+ − 203 /* NOTE: these should be in the qrc file */
+ − 204 const std::string query = "query ($id: Int) {\n"
+ − 205 " MediaListCollection (userId: $id, type: ANIME) {\n"
+ − 206 " lists {\n"
+ − 207 " name\n"
+ − 208 " entries {\n"
+ − 209 " score\n"
+ − 210 " notes\n"
10
+ − 211 " status\n"
9
+ − 212 " progress\n"
+ − 213 " startedAt {\n"
+ − 214 " year\n"
+ − 215 " month\n"
+ − 216 " day\n"
+ − 217 " }\n"
+ − 218 " completedAt {\n"
+ − 219 " year\n"
+ − 220 " month\n"
+ − 221 " day\n"
+ − 222 " }\n"
+ − 223 " updatedAt\n"
+ − 224 " media {\n"
+ − 225 " id\n"
+ − 226 " title {\n"
+ − 227 " romaji\n"
+ − 228 " english\n"
+ − 229 " native\n"
+ − 230 " }\n"
+ − 231 " format\n"
+ − 232 " status\n"
+ − 233 " averageScore\n"
+ − 234 " season\n"
+ − 235 " startDate {\n"
+ − 236 " year\n"
+ − 237 " month\n"
+ − 238 " day\n"
+ − 239 " }\n"
+ − 240 " genres\n"
+ − 241 " episodes\n"
+ − 242 " duration\n"
+ − 243 " synonyms\n"
+ − 244 " description(asHtml: false)\n"
+ − 245 " }\n"
+ − 246 " }\n"
+ − 247 " }\n"
+ − 248 " }\n"
+ − 249 "}\n";
+ − 250 // clang-format off
+ − 251 nlohmann::json json = {
+ − 252 {"query", query},
+ − 253 {"variables", {
10
+ − 254 {"id", account.UserId()}
9
+ − 255 }}
+ − 256 };
+ − 257 // clang-format on
+ − 258 /* TODO: do a try catch here, catch any json errors and then call
+ − 259 Authorize() if needed */
+ − 260 auto res = nlohmann::json::parse(SendRequest(json.dump()));
+ − 261 /* TODO: make sure that we actually need the wstring converter and see
+ − 262 if we can just get wide strings back from nlohmann::json */
+ − 263 for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
10
+ − 264 ParseList(list.value());
9
+ − 265 }
+ − 266 return 1;
+ − 267 }
+ − 268
10
+ − 269 int UpdateAnimeEntry(const Anime::Anime& anime) {
9
+ − 270 /**
+ − 271 * possible values:
+ − 272 *
+ − 273 * int mediaId,
+ − 274 * MediaListStatus status,
+ − 275 * float score,
+ − 276 * int scoreRaw,
+ − 277 * int progress,
+ − 278 * int progressVolumes,
+ − 279 * int repeat,
+ − 280 * int priority,
+ − 281 * bool private,
+ − 282 * string notes,
+ − 283 * bool hiddenFromStatusLists,
+ − 284 * string[] customLists,
+ − 285 * float[] advancedScores,
+ − 286 * Date startedAt,
+ − 287 * Date completedAt
+ − 288 **/
+ − 289 const std::string query =
+ − 290 "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String) {\n"
+ − 291 " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: "
+ − 292 "$notes) {\n"
+ − 293 " id\n"
+ − 294 " }\n"
+ − 295 "}\n";
+ − 296 // clang-format off
+ − 297 nlohmann::json json = {
+ − 298 {"query", query},
+ − 299 {"variables", {
10
+ − 300 {"media_id", anime.GetId()},
+ − 301 {"progress", anime.GetUserProgress()},
+ − 302 {"status", AniListAnimeWatchingToStringMap[anime.GetUserStatus()]},
+ − 303 {"score", anime.GetUserScore()},
+ − 304 {"notes", anime.GetUserNotes()}
9
+ − 305 }}
+ − 306 };
+ − 307 // clang-format on
+ − 308 SendRequest(json.dump());
+ − 309 return 1;
+ − 310 }
+ − 311
+ − 312 int ParseUser(const nlohmann::json& json) {
+ − 313 account.SetUsername(JSON::GetString(json, "/name"_json_pointer));
+ − 314 account.SetUserId(JSON::GetInt(json, "/id"_json_pointer));
10
+ − 315 return account.UserId();
9
+ − 316 }
+ − 317
+ − 318 int AuthorizeUser() {
+ − 319 /* Prompt for PIN */
+ − 320 QDesktopServices::openUrl(
+ − 321 QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token"));
+ − 322 bool ok;
+ − 323 QString token = QInputDialog::getText(
+ − 324 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal,
+ − 325 "", &ok);
+ − 326 if (ok && !token.isEmpty())
+ − 327 account.SetAuthToken(token.toStdString());
+ − 328 else { // fail
+ − 329 return 0;
+ − 330 }
+ − 331 const std::string query = "query {\n"
+ − 332 " Viewer {\n"
+ − 333 " id\n"
+ − 334 " name\n"
+ − 335 " mediaListOptions {\n"
+ − 336 " scoreFormat\n"
+ − 337 " }\n"
+ − 338 " }\n"
+ − 339 "}\n";
+ − 340 nlohmann::json json = {
+ − 341 {"query", query}
+ − 342 };
+ − 343 auto ret = nlohmann::json::parse(SendRequest(json.dump()));
10
+ − 344 ParseUser(json["Viewer"]);
9
+ − 345 return 1;
+ − 346 }
+ − 347
+ − 348 } // namespace Services::AniList