comparison src/services/anilist.cc @ 323:1686fac290c5

services/anilist: refactor HTTP requests...
author Paper <paper@paper.us.eu.org>
date Wed, 12 Jun 2024 22:48:16 -0400
parents 8141f409d52c
children 5d3c9b31aa6e
comparison
equal deleted inserted replaced
322:c32467cd06bb 323:1686fac290c5
64 "synonyms\n" \ 64 "synonyms\n" \
65 "description(asHtml: false)\n" 65 "description(asHtml: false)\n"
66 66
67 /* FIXME: why is this here */ 67 /* FIXME: why is this here */
68 68
69 static struct { 69 static bool AccountIsValid() {
70 int UserId() const { return session.config.auth.anilist.user_id; } 70 const auto& auth = session.config.auth.anilist;
71 void SetUserId(const int id) { session.config.auth.anilist.user_id = id; } 71 return (auth.user_id && !auth.auth_token.empty());
72 72 }
73 std::string AuthToken() const { return session.config.auth.anilist.auth_token; } 73
74 void SetAuthToken(const std::string& auth_token) { session.config.auth.anilist.auth_token = auth_token; } 74 static std::optional<nlohmann::json> SendJSONRequest(const nlohmann::json& data) {
75 75 if (!AccountIsValid()) {
76 bool IsValid() const { return UserId() && !AuthToken().empty(); } 76 session.SetStatusBar("AniList: Account isn't valid! (unauthorized?)");
77 } account; 77 return std::nullopt;
78 78 }
79 static std::string SendRequest(const std::string& data) { 79
80 std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json", 80 const auto& auth = session.config.auth.anilist;
81 "Content-Type: application/json"}; 81
82 return Strings::ToUtf8String(HTTP::Request("https://graphql.anilist.co", headers, data, HTTP::Type::Post)); 82 const std::vector<std::string> headers = {
83 } 83 "Authorization: Bearer " + auth.auth_token,
84 84 "Accept: application/json",
85 static bool SendJSONRequest(const nlohmann::json& data, nlohmann::json& out) { 85 "Content-Type: application/json",
86 std::string request = SendRequest(data.dump()); 86 };
87 if (request.empty()) { 87
88 const std::string response = Strings::ToUtf8String(HTTP::Request("https://graphql.anilist.co", headers, data.dump(), HTTP::Type::Post));
89 if (response.empty()) {
88 session.SetStatusBar("AniList: JSON request returned an empty result!"); 90 session.SetStatusBar("AniList: JSON request returned an empty result!");
89 return false; 91 return std::nullopt;
90 } 92 }
91 93
92 out = nlohmann::json::parse(request, nullptr, false); 94 nlohmann::json out;
93 if (out.is_discarded()) { 95
94 session.SetStatusBar("AniList: Failed to parse request JSON!"); 96 try {
95 return false; 97 out = nlohmann::json::parse(response);
98 } catch (const std::exception& ex) {
99 session.SetStatusBar("AniList: Failed to parse request JSON with error!");
100 return std::nullopt;
96 } 101 }
97 102
98 if (out.contains("/errors"_json_pointer) && out.at("/errors"_json_pointer).is_array()) { 103 if (out.contains("/errors"_json_pointer) && out.at("/errors"_json_pointer).is_array()) {
99 for (const auto& error : out.at("/errors"_json_pointer)) 104 for (const auto& error : out.at("/errors"_json_pointer))
100 std::cerr << "AniList: Received an error in response: " 105 std::cerr << "AniList: Received an error in response: "
101 << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl; 106 << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl;
102 107
103 session.SetStatusBar("AniList: Received an error in response!"); 108 session.SetStatusBar("AniList: Received an error in response!");
104 return false; 109 return std::nullopt;
105 } 110 }
106 111
107 return true; 112 return out;
108 } 113 }
109 114
110 static void ParseListStatus(std::string status, Anime::Anime& anime) { 115 static void ParseListStatus(std::string status, Anime::Anime& anime) {
111 static const std::unordered_map<std::string, Anime::ListStatus> map = { 116 static const std::unordered_map<std::string, Anime::ListStatus> map = {
112 {"CURRENT", Anime::ListStatus::Current }, 117 {"CURRENT", Anime::ListStatus::Current },
113 {"PLANNING", Anime::ListStatus::Planning }, 118 {"PLANNING", Anime::ListStatus::Planning },
114 {"COMPLETED", Anime::ListStatus::Completed}, 119 {"COMPLETED", Anime::ListStatus::Completed},
115 {"DROPPED", Anime::ListStatus::Dropped }, 120 {"DROPPED", Anime::ListStatus::Dropped },
116 {"PAUSED", Anime::ListStatus::Paused } 121 {"PAUSED", Anime::ListStatus::Paused }
117 }; 122 };
118 123
119 if (status == "REPEATING") { 124 if (status == "REPEATING") {
120 anime.SetUserIsRewatching(true); 125 anime.SetUserIsRewatching(true);
121 anime.SetUserStatus(Anime::ListStatus::Current); 126 anime.SetUserStatus(Anime::ListStatus::Current);
122 return; 127 return;
172 177
173 Anime::Anime& anime = Anime::db.items[id]; 178 Anime::Anime& anime = Anime::db.items[id];
174 anime.SetId(id); 179 anime.SetId(id);
175 anime.SetServiceId(Anime::Service::AniList, service_id); 180 anime.SetServiceId(Anime::Service::AniList, service_id);
176 181
177 if (json.contains("/id_mal"_json_pointer)) 182 if (json.contains("/idMal"_json_pointer) && json["/idMal"_json_pointer].is_number())
178 anime.SetServiceId(Anime::Service::MyAnimeList, json["/id_mal"_json_pointer].get<std::string>()); 183 anime.SetServiceId(Anime::Service::MyAnimeList, Strings::ToUtf8String(json["/idMal"_json_pointer].get<int>()));
179 184
180 ParseTitle(json.at("/title"_json_pointer), anime); 185 ParseTitle(json.at("/title"_json_pointer), anime);
181 186
182 anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0)); 187 anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0));
183 anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, ""))); 188 anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, "")));
184 189
185 anime.SetAiringStatus( 190 anime.SetAiringStatus(
186 Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""))); 191 Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, "")));
187 192
188 anime.SetAirDate(Date(json["/startDate"_json_pointer])); 193 anime.SetAirDate(Date(json["/startDate"_json_pointer]));
189 194
190 anime.SetPosterUrl(JSON::GetString<std::string>(json, "/coverImage/large"_json_pointer, "")); 195 anime.SetPosterUrl(JSON::GetString<std::string>(json, "/coverImage/large"_json_pointer, ""));
191 196
245 250
246 return success; 251 return success;
247 } 252 }
248 253
249 int GetAnimeList() { 254 int GetAnimeList() {
250 if (!account.IsValid()) { 255 auto& auth = session.config.auth.anilist;
251 session.SetStatusBar("AniList: Account isn't valid! (unauthorized?)");
252 return 0;
253 }
254 256
255 /* NOTE: these really ought to be in the qrc file */ 257 /* NOTE: these really ought to be in the qrc file */
256 constexpr std::string_view query = "query ($id: Int) {\n" 258 constexpr std::string_view query = "query ($id: Int) {\n"
257 " MediaListCollection (userId: $id, type: ANIME) {\n" 259 " MediaListCollection (userId: $id, type: ANIME) {\n"
258 " lists {\n" 260 " lists {\n"
259 " name\n" 261 " name\n"
260 " entries {\n" 262 " entries {\n"
261 " score\n" 263 " score\n"
262 " notes\n" 264 " notes\n"
263 " status\n" 265 " status\n"
264 " progress\n" 266 " progress\n"
265 " startedAt {\n" 267 " startedAt {\n"
266 " year\n" 268 " year\n"
267 " month\n" 269 " month\n"
268 " day\n" 270 " day\n"
269 " }\n" 271 " }\n"
270 " completedAt {\n" 272 " completedAt {\n"
271 " year\n" 273 " year\n"
272 " month\n" 274 " month\n"
273 " day\n" 275 " day\n"
274 " }\n" 276 " }\n"
275 " updatedAt\n" 277 " updatedAt\n"
276 " media {\n" 278 " media {\n"
277 MEDIA_FIELDS 279 MEDIA_FIELDS
278 " }\n" 280 " }\n"
279 " }\n" 281 " }\n"
280 " }\n" 282 " }\n"
281 " }\n" 283 " }\n"
282 "}\n"; 284 "}\n";
283 // clang-format off 285 // clang-format off
284 nlohmann::json json = { 286 nlohmann::json request = {
285 {"query", query}, 287 {"query", query},
286 {"variables", { 288 {"variables", {
287 {"id", account.UserId()} 289 {"id", auth.user_id}
288 }} 290 }}
289 }; 291 };
290 // clang-format on 292 // clang-format on
291 293
292 session.SetStatusBar("AniList: Parsing anime list..."); 294 session.SetStatusBar("AniList: Parsing anime list...");
293 295
294 nlohmann::json result; 296 const std::optional<nlohmann::json> response = SendJSONRequest(request);
295 const bool res = SendJSONRequest(json, result); 297 if (!response)
296 if (!res) 298 return 0;
297 return 0; 299
300 Anime::db.RemoveAllUserData();
301
302 const nlohmann::json& json = response.value();
298 303
299 bool success = true; 304 bool success = true;
300 305
301 Anime::db.RemoveAllUserData(); 306 for (const auto& list : json["data"]["MediaListCollection"]["lists"].items())
302
303 for (const auto& list : result["data"]["MediaListCollection"]["lists"].items())
304 if (!ParseList(list.value())) 307 if (!ParseList(list.value()))
305 success = false; 308 success = false;
306 309
307 if (success) 310 if (success)
308 session.SetStatusBar("AniList: Retrieved anime list successfully!"); 311 session.SetStatusBar("AniList: Retrieved anime list successfully!");
311 } 314 }
312 315
313 /* return is a vector of anime ids */ 316 /* return is a vector of anime ids */
314 std::vector<int> Search(const std::string& search) { 317 std::vector<int> Search(const std::string& search) {
315 constexpr std::string_view query = "query ($search: String) {\n" 318 constexpr std::string_view query = "query ($search: String) {\n"
316 " Page (page: 1, perPage: 50) {\n" 319 " Page (page: 1, perPage: 50) {\n"
317 " media (search: $search, type: ANIME) {\n" 320 " media (search: $search, type: ANIME) {\n"
318 MEDIA_FIELDS 321 MEDIA_FIELDS
319 " }\n" 322 " }\n"
320 " }\n" 323 " }\n"
321 "}\n"; 324 "}\n";
322 325
323 // clang-format off 326 // clang-format off
324 nlohmann::json json = { 327 nlohmann::json json = {
325 {"query", query}, 328 {"query", query},
326 {"variables", { 329 {"variables", {
327 {"search", search} 330 {"search", search}
328 }} 331 }}
329 }; 332 };
330 // clang-format on 333 // clang-format on
331 334
332 nlohmann::json result; 335 const std::optional<nlohmann::json> response = SendJSONRequest(json);
333 const bool res = SendJSONRequest(json, result); 336 if (!response)
334 if (!res)
335 return {}; 337 return {};
338
339 const nlohmann::json& result = response.value();
336 340
337 /* FIXME: error handling here */ 341 /* FIXME: error handling here */
338 std::vector<int> ret; 342 std::vector<int> ret;
339 ret.reserve(result["/data/Page/media"_json_pointer].size()); 343 ret.reserve(result["/data/Page/media"_json_pointer].size());
340 344
344 return ret; 348 return ret;
345 } 349 }
346 350
347 std::vector<int> GetSeason(Anime::SeriesSeason season, Date::Year year) { 351 std::vector<int> GetSeason(Anime::SeriesSeason season, Date::Year year) {
348 constexpr std::string_view query = "query ($season: MediaSeason!, $season_year: Int!, $page: Int) {\n" 352 constexpr std::string_view query = "query ($season: MediaSeason!, $season_year: Int!, $page: Int) {\n"
349 " Page(page: $page) {\n" 353 " Page(page: $page) {\n"
350 " media(season: $season, seasonYear: $season_year, type: ANIME, sort: START_DATE) {\n" 354 " media(season: $season, seasonYear: $season_year, type: ANIME, sort: START_DATE) {\n"
351 MEDIA_FIELDS 355 MEDIA_FIELDS
352 " }\n" 356 " }\n"
353 " pageInfo {\n" 357 " pageInfo {\n"
354 " total\n" 358 " total\n"
355 " perPage\n" 359 " perPage\n"
356 " currentPage\n" 360 " currentPage\n"
357 " lastPage\n" 361 " lastPage\n"
358 " hasNextPage\n" 362 " hasNextPage\n"
359 " }\n" 363 " }\n"
360 " }\n" 364 " }\n"
361 "}\n"; 365 "}\n";
362 std::vector<int> ret; 366 std::vector<int> ret;
363 367
364 int page = 0; 368 int page = 0;
365 bool has_next_page = true; 369 bool has_next_page = true;
366 while (has_next_page) { 370 while (has_next_page) {
371 {"season_year", Strings::ToUtf8String(year)}, 375 {"season_year", Strings::ToUtf8String(year)},
372 {"page", page} 376 {"page", page}
373 }} 377 }}
374 }; 378 };
375 379
376 nlohmann::json result; 380 const std::optional<nlohmann::json> res = SendJSONRequest(json);
377 const bool res = SendJSONRequest(json, result);
378 if (!res) 381 if (!res)
379 return {}; 382 return {};
383
384 const nlohmann::json& result = res.value();
380 385
381 ret.reserve(ret.capacity() + result["data"]["Page"]["media"].size()); 386 ret.reserve(ret.capacity() + result["data"]["Page"]["media"].size());
382 387
383 for (const auto& media : result["data"]["Page"]["media"].items()) 388 for (const auto& media : result["data"]["Page"]["media"].items())
384 ret.push_back(ParseMediaJson(media.value())); 389 ret.push_back(ParseMediaJson(media.value()));
420 return 0; 425 return 0;
421 426
422 session.SetStatusBar("AniList: Updating anime entry..."); 427 session.SetStatusBar("AniList: Updating anime entry...");
423 428
424 constexpr std::string_view query = 429 constexpr std::string_view query =
425 "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String, $start: " 430 "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String, $start: "
426 "FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n" 431 "FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n"
427 " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: " 432 " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: "
428 "$notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n" 433 "$notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n"
429 " id\n" 434 " id\n"
430 " }\n" 435 " }\n"
431 "}\n"; 436 "}\n";
432 // clang-format off 437 // clang-format off
433 nlohmann::json json = { 438 nlohmann::json json = {
434 {"query", query}, 439 {"query", query},
435 {"variables", { 440 {"variables", {
436 {"media_id", Strings::ToInt<int64_t>(service_id.value())}, 441 {"media_id", Strings::ToInt<int64_t>(service_id.value())},
443 {"repeat", anime.GetUserRewatchedTimes()} 448 {"repeat", anime.GetUserRewatchedTimes()}
444 }} 449 }}
445 }; 450 };
446 // clang-format on 451 // clang-format on
447 452
448 nlohmann::json result; 453 const std::optional<nlohmann::json> res = SendJSONRequest(json);
449 const bool ret = SendJSONRequest(json, result); 454 if (!res)
450 if (!ret) 455 return 0;
451 return 0; 456
457 const nlohmann::json& result = res.value();
452 458
453 session.SetStatusBar("AniList: Anime entry updated successfully!"); 459 session.SetStatusBar("AniList: Anime entry updated successfully!");
454 460
455 return JSON::GetNumber(result, "/data/SaveMediaListEntry/id"_json_pointer, 0); 461 return JSON::GetNumber(result, "/data/SaveMediaListEntry/id"_json_pointer, 0);
456 } 462 }
457 463
458 static int ParseUser(const nlohmann::json& json) { 464 static int ParseUser(const nlohmann::json& json) {
459 account.SetUserId(JSON::GetNumber(json, "/id"_json_pointer, 0)); 465 auto& auth = session.config.auth.anilist;
460 return account.UserId(); 466
467 return auth.user_id = JSON::GetNumber(json, "/id"_json_pointer, 0);
461 } 468 }
462 469
463 bool AuthorizeUser() { 470 bool AuthorizeUser() {
471 auto& auth = session.config.auth.anilist;
472
464 /* Prompt for PIN */ 473 /* Prompt for PIN */
465 QDesktopServices::openUrl(QUrl(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" + 474 QDesktopServices::openUrl(QUrl(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" +
466 std::string(CLIENT_ID) + "&response_type=token"))); 475 std::string(CLIENT_ID) + "&response_type=token")));
467 476
468 bool ok; 477 bool ok;
469 QString token = QInputDialog::getText( 478 QString token = QInputDialog::getText(
470 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, 479 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal,
471 "", &ok); 480 "", &ok);
472 481
473 if (!ok || token.isEmpty()) 482 if (!ok || token.isEmpty())
474 return false; 483 return false;
475 484
476 account.SetAuthToken(Strings::ToUtf8String(token)); 485 auth.auth_token = Strings::ToUtf8String(token);
477 486
478 session.SetStatusBar("AniList: Requesting user ID..."); 487 session.SetStatusBar("AniList: Requesting user ID...");
479 488
480 constexpr std::string_view query = "query {\n" 489 constexpr std::string_view query = "query {\n"
481 " Viewer {\n" 490 " Viewer {\n"
482 " id\n" 491 " id\n"
483 " }\n" 492 " }\n"
484 "}\n"; 493 "}\n";
485 nlohmann::json json = { 494 nlohmann::json json = {
486 {"query", query} 495 {"query", query}
487 }; 496 };
488 497
489 /* SendJSONRequest handles status errors */ 498 /* SendJSONRequest handles status errors */
490 nlohmann::json result; 499 const std::optional<nlohmann::json> ret = SendJSONRequest(json);
491 const bool ret = SendJSONRequest(json, result);
492 if (!ret) 500 if (!ret)
493 return 0; 501 return 0;
502
503 const nlohmann::json& result = ret.value();
494 504
495 if (ParseUser(result["data"]["Viewer"])) 505 if (ParseUser(result["data"]["Viewer"]))
496 session.SetStatusBar("AniList: Successfully retrieved user data!"); 506 session.SetStatusBar("AniList: Successfully retrieved user data!");
497 else 507 else
498 session.SetStatusBar("AniList: Failed to retrieve user ID!"); 508 session.SetStatusBar("AniList: Failed to retrieve user ID!");