Mercurial > minori
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!"); |