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