comparison src/services/anilist.cc @ 258:862d0d8619f6

*: HUUUGE changes animia has been renamed to animone, so instead of thinking of a health condition, you think of a beautiful flower :) I've also edited some of the code for animone, but I have no idea if it even works or not because I don't have a mac or windows machine lying around. whoops! ... anyway, all of the changes divergent from Anisthesia are now licensed under BSD. it's possible that I could even rewrite most of the code to where I don't even have to keep the MIT license, but that's thinking too far into the future I've been slacking off on implementing the anime seasons page, mostly out of laziness. I think I'd have to create another db file specifically for the seasons anyway, this code is being pushed *primarily* because the hard drive it's on is failing! yay :)
author Paper <paper@paper.us.eu.org>
date Mon, 01 Apr 2024 02:43:44 -0400
parents c130f47f6f48
children dd211ff68b36
comparison
equal deleted inserted replaced
257:699a20c57dc8 258:862d0d8619f6
6 #include "core/json.h" 6 #include "core/json.h"
7 #include "core/session.h" 7 #include "core/session.h"
8 #include "core/strings.h" 8 #include "core/strings.h"
9 #include "gui/translate/anilist.h" 9 #include "gui/translate/anilist.h"
10 10
11 #include <QByteArray>
11 #include <QDate> 12 #include <QDate>
12 #include <QByteArray>
13 #include <QDesktopServices> 13 #include <QDesktopServices>
14 #include <QInputDialog> 14 #include <QInputDialog>
15 #include <QLineEdit> 15 #include <QLineEdit>
16 #include <QMessageBox> 16 #include <QMessageBox>
17 #include <QUrl> 17 #include <QUrl>
27 namespace AniList { 27 namespace AniList {
28 28
29 constexpr int CLIENT_ID = 13706; 29 constexpr int CLIENT_ID = 13706;
30 30
31 class Account { 31 class Account {
32 public: 32 public:
33 int UserId() const { return session.config.auth.anilist.user_id; } 33 int UserId() const { return session.config.auth.anilist.user_id; }
34 void SetUserId(const int id) { session.config.auth.anilist.user_id = id; } 34 void SetUserId(const int id) { session.config.auth.anilist.user_id = id; }
35 35
36 std::string AuthToken() const { return session.config.auth.anilist.auth_token; } 36 std::string AuthToken() const { return session.config.auth.anilist.auth_token; }
37 void SetAuthToken(std::string const& auth_token) { session.config.auth.anilist.auth_token = auth_token; } 37 void SetAuthToken(std::string const& auth_token) { session.config.auth.anilist.auth_token = auth_token; }
38 38
39 bool Authenticated() const { return !AuthToken().empty(); } 39 bool Authenticated() const { return !AuthToken().empty(); }
40 bool IsValid() const { return UserId() && Authenticated(); } 40 bool IsValid() const { return UserId() && Authenticated(); }
41 }; 41 };
42 42
43 static Account account; 43 static Account account;
44 44
45 std::string SendRequest(std::string data) { 45 std::string SendRequest(std::string data) {
46 std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json", 46 std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json",
47 "Content-Type: application/json"}; 47 "Content-Type: application/json"};
48 return Strings::ToUtf8String(HTTP::Post("https://graphql.anilist.co", data, headers)); 48 return Strings::ToUtf8String(HTTP::Post("https://graphql.anilist.co", data, headers));
49 } 49 }
50 50
51 nlohmann::json SendJSONRequest(nlohmann::json data) { 51 nlohmann::json SendJSONRequest(nlohmann::json data) {
52 std::string request = SendRequest(data.dump()); 52 std::string request = SendRequest(data.dump());
61 return {}; 61 return {};
62 } 62 }
63 63
64 if (ret.contains("/errors"_json_pointer) && ret.at("/errors"_json_pointer).is_array()) { 64 if (ret.contains("/errors"_json_pointer) && ret.at("/errors"_json_pointer).is_array()) {
65 for (const auto& error : ret.at("/errors"_json_pointer)) 65 for (const auto& error : ret.at("/errors"_json_pointer))
66 std::cerr << "[AniList] Received an error in response: " << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl; 66 std::cerr << "[AniList] Received an error in response: "
67 << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl;
67 68
68 return {}; 69 return {};
69 } 70 }
70 71
71 return ret; 72 return ret;
72 } 73 }
73 74
74 void ParseListStatus(std::string status, Anime::Anime& anime) { 75 void ParseListStatus(std::string status, Anime::Anime& anime) {
75 static const std::unordered_map<std::string, Anime::ListStatus> map = { 76 static const std::unordered_map<std::string, Anime::ListStatus> map = {
76 {"CURRENT", Anime::ListStatus::CURRENT }, 77 {"CURRENT", Anime::ListStatus::CURRENT },
77 {"PLANNING", Anime::ListStatus::PLANNING }, 78 {"PLANNING", Anime::ListStatus::PLANNING },
78 {"COMPLETED", Anime::ListStatus::COMPLETED}, 79 {"COMPLETED", Anime::ListStatus::COMPLETED},
79 {"DROPPED", Anime::ListStatus::DROPPED }, 80 {"DROPPED", Anime::ListStatus::DROPPED },
80 {"PAUSED", Anime::ListStatus::PAUSED } 81 {"PAUSED", Anime::ListStatus::PAUSED }
81 }; 82 };
82 83
83 if (status == "REPEATING") { 84 if (status == "REPEATING") {
84 anime.SetUserIsRewatching(true); 85 anime.SetUserIsRewatching(true);
85 anime.SetUserStatus(Anime::ListStatus::CURRENT); 86 anime.SetUserStatus(Anime::ListStatus::CURRENT);
86 return; 87 return;
125 ParseTitle(json.at("/title"_json_pointer), anime); 126 ParseTitle(json.at("/title"_json_pointer), anime);
126 127
127 anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0)); 128 anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0));
128 anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, ""))); 129 anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, "")));
129 130
130 anime.SetAiringStatus(Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""))); 131 anime.SetAiringStatus(
132 Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, "")));
131 133
132 anime.SetAirDate(Date(json["/startDate"_json_pointer])); 134 anime.SetAirDate(Date(json["/startDate"_json_pointer]));
133 135
134 anime.SetPosterUrl(JSON::GetString<std::string>(json, "/coverImage/large"_json_pointer, "")); 136 anime.SetPosterUrl(JSON::GetString<std::string>(json, "/coverImage/large"_json_pointer, ""));
135 137
177 return 0; 179 return 0;
178 } 180 }
179 181
180 /* NOTE: these really ought to be in the qrc file */ 182 /* NOTE: these really ought to be in the qrc file */
181 constexpr std::string_view query = "query ($id: Int) {\n" 183 constexpr std::string_view query = "query ($id: Int) {\n"
182 " MediaListCollection (userId: $id, type: ANIME) {\n" 184 " MediaListCollection (userId: $id, type: ANIME) {\n"
183 " lists {\n" 185 " lists {\n"
184 " name\n" 186 " name\n"
185 " entries {\n" 187 " entries {\n"
186 " score\n" 188 " score\n"
187 " notes\n" 189 " notes\n"
188 " status\n" 190 " status\n"
189 " progress\n" 191 " progress\n"
190 " startedAt {\n" 192 " startedAt {\n"
191 " year\n" 193 " year\n"
192 " month\n" 194 " month\n"
193 " day\n" 195 " day\n"
194 " }\n" 196 " }\n"
195 " completedAt {\n" 197 " completedAt {\n"
196 " year\n" 198 " year\n"
197 " month\n" 199 " month\n"
198 " day\n" 200 " day\n"
199 " }\n" 201 " }\n"
200 " updatedAt\n" 202 " updatedAt\n"
201 " media {\n" 203 " media {\n"
202 " coverImage {\n" 204 " coverImage {\n"
203 " large\n" 205 " large\n"
204 " }\n" 206 " }\n"
205 " id\n" 207 " id\n"
206 " title {\n" 208 " title {\n"
207 " romaji\n" 209 " romaji\n"
208 " english\n" 210 " english\n"
209 " native\n" 211 " native\n"
210 " }\n" 212 " }\n"
211 " format\n" 213 " format\n"
212 " status\n" 214 " status\n"
213 " averageScore\n" 215 " averageScore\n"
214 " season\n" 216 " season\n"
215 " startDate {\n" 217 " startDate {\n"
216 " year\n" 218 " year\n"
217 " month\n" 219 " month\n"
218 " day\n" 220 " day\n"
219 " }\n" 221 " }\n"
220 " genres\n" 222 " genres\n"
221 " episodes\n" 223 " episodes\n"
222 " duration\n" 224 " duration\n"
223 " synonyms\n" 225 " synonyms\n"
224 " description(asHtml: false)\n" 226 " description(asHtml: false)\n"
225 " }\n" 227 " }\n"
226 " }\n" 228 " }\n"
227 " }\n" 229 " }\n"
228 " }\n" 230 " }\n"
229 "}\n"; 231 "}\n";
230 // clang-format off 232 // clang-format off
231 nlohmann::json json = { 233 nlohmann::json json = {
232 {"query", query}, 234 {"query", query},
233 {"variables", { 235 {"variables", {
234 {"id", account.UserId()} 236 {"id", account.UserId()}
244 return 1; 246 return 1;
245 } 247 }
246 248
247 /* return is a vector of anime ids */ 249 /* return is a vector of anime ids */
248 std::vector<int> Search(const std::string& search) { 250 std::vector<int> Search(const std::string& search) {
249 constexpr std::string_view query = 251 constexpr std::string_view query = "query ($search: String) {\n"
250 "query ($search: String) {\n" 252 " Page (page: 1, perPage: 50) {\n"
251 " Page (page: 1, perPage: 50) {\n" 253 " media (search: $search, type: ANIME) {\n"
252 " media (search: $search, type: ANIME) {\n" 254 " coverImage {\n"
253 " coverImage {\n" 255 " large\n"
254 " large\n" 256 " }\n"
255 " }\n" 257 " id\n"
256 " id\n" 258 " title {\n"
257 " title {\n" 259 " romaji\n"
258 " romaji\n" 260 " english\n"
259 " english\n" 261 " native\n"
260 " native\n" 262 " }\n"
261 " }\n" 263 " format\n"
262 " format\n" 264 " status\n"
263 " status\n" 265 " averageScore\n"
264 " averageScore\n" 266 " season\n"
265 " season\n" 267 " startDate {\n"
266 " startDate {\n" 268 " year\n"
267 " year\n" 269 " month\n"
268 " month\n" 270 " day\n"
269 " day\n" 271 " }\n"
270 " }\n" 272 " genres\n"
271 " genres\n" 273 " episodes\n"
272 " episodes\n" 274 " duration\n"
273 " duration\n" 275 " synonyms\n"
274 " synonyms\n" 276 " description(asHtml: false)\n"
275 " description(asHtml: false)\n" 277 " }\n"
276 " }\n" 278 " }\n"
277 " }\n" 279 "}\n";
278 "}\n";
279 280
280 // clang-format off 281 // clang-format off
281 nlohmann::json json = { 282 nlohmann::json json = {
282 {"query", query}, 283 {"query", query},
283 {"variables", { 284 {"variables", {
306 * float score, 307 * float score,
307 * int scoreRaw, 308 * int scoreRaw,
308 * int progress, 309 * int progress,
309 * int progressVolumes, // manga-specific. 310 * int progressVolumes, // manga-specific.
310 * int repeat, // rewatch 311 * int repeat, // rewatch
311 * int priority, 312 * int priority,
312 * bool private, 313 * bool private,
313 * string notes, 314 * string notes,
314 * bool hiddenFromStatusLists, 315 * bool hiddenFromStatusLists,
315 * string[] customLists, 316 * string[] customLists,
316 * float[] advancedScores, 317 * float[] advancedScores,
317 * Date startedAt, 318 * Date startedAt,
318 * Date completedAt 319 * Date completedAt
319 **/ 320 **/
320 Anime::Anime& anime = Anime::db.items[id]; 321 Anime::Anime& anime = Anime::db.items[id];
321 if (!anime.IsInUserList()) 322 if (!anime.IsInUserList())
322 return 0; 323 return 0;
323 324
324 constexpr std::string_view query = 325 constexpr std::string_view query =
325 "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String, $start: FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n" 326 "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String, $start: "
326 " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: $notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n" 327 "FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n"
327 " id\n" 328 " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: "
328 " }\n" 329 "$notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n"
329 "}\n"; 330 " id\n"
331 " }\n"
332 "}\n";
330 // clang-format off 333 // clang-format off
331 nlohmann::json json = { 334 nlohmann::json json = {
332 {"query", query}, 335 {"query", query},
333 {"variables", { 336 {"variables", {
334 {"media_id", anime.GetId()}, 337 {"media_id", anime.GetId()},
353 return account.UserId(); 356 return account.UserId();
354 } 357 }
355 358
356 bool AuthorizeUser() { 359 bool AuthorizeUser() {
357 /* Prompt for PIN */ 360 /* Prompt for PIN */
358 QDesktopServices::openUrl( 361 QDesktopServices::openUrl(QUrl(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" +
359 QUrl(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" + Strings::ToUtf8String(CLIENT_ID) + "&response_type=token"))); 362 Strings::ToUtf8String(CLIENT_ID) + "&response_type=token")));
360 363
361 bool ok; 364 bool ok;
362 QString token = QInputDialog::getText( 365 QString token = QInputDialog::getText(
363 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, 366 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal,
364 "", &ok); 367 "", &ok);
365 368
366 if (!ok || token.isEmpty()) 369 if (!ok || token.isEmpty())
367 return false; 370 return false;
368 371
369 account.SetAuthToken(Strings::ToUtf8String(token)); 372 account.SetAuthToken(Strings::ToUtf8String(token));
370 373
371 constexpr std::string_view query = "query {\n" 374 constexpr std::string_view query = "query {\n"
372 " Viewer {\n" 375 " Viewer {\n"
373 " id\n" 376 " id\n"
374 " name\n" 377 " name\n"
375 " mediaListOptions {\n" 378 " mediaListOptions {\n"
376 " scoreFormat\n" // this will be used... eventually 379 " scoreFormat\n" // this will be used... eventually
377 " }\n" 380 " }\n"
378 " }\n" 381 " }\n"
379 "}\n"; 382 "}\n";
380 nlohmann::json json = { 383 nlohmann::json json = {
381 {"query", query} 384 {"query", query}
382 }; 385 };
383 386
384 auto ret = SendJSONRequest(json); 387 auto ret = SendJSONRequest(json);
385 388
386 ParseUser(ret["data"]["Viewer"]); 389 ParseUser(ret["data"]["Viewer"]);
387 return true; 390 return true;