comparison src/services/anilist.cpp @ 9:5c0397762b53

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