9
|
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
|