Mercurial > minori
view src/services/anilist.cc @ 365:f81bed4e04ac
*: megacommit that probably breaks things
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Wed, 02 Oct 2024 23:06:43 -0400 |
parents | 948955c3ba81 |
children |
line wrap: on
line source
#include "services/anilist.h" #include "core/anime.h" #include "core/anime_db.h" #include "core/date.h" #include "core/config.h" #include "core/http.h" #include "core/json.h" #include "core/session.h" #include "core/strings.h" #include "gui/translate/anilist.h" #include <QByteArray> #include <QDate> #include <QDesktopServices> #include <QInputDialog> #include <QLineEdit> #include <QMessageBox> #include <QUrl> #include <chrono> #include <exception> #include <string_view> #include <fmt/core.h> #include <iostream> namespace Services { namespace AniList { static constexpr std::string_view CLIENT_ID = "13706"; /* This is used in multiple queries, so just put it here I guess. */ #define MEDIA_FIELDS \ "coverImage {\n" \ " large\n" \ "}\n" \ "id\n" \ "title {\n" \ " romaji\n" \ " english\n" \ " native\n" \ "}\n" \ "format\n" \ "status\n" \ "averageScore\n" \ "season\n" \ "startDate {\n" \ " year\n" \ " month\n" \ " day\n" \ "}\n" \ "endDate {\n" \ " year\n" \ " month\n" \ " day\n" \ "}\n" \ "studios {\n" \ " edges {\n" \ " node {\n" \ " name\n" \ " }\n" \ " }\n" \ "}\n" \ "genres\n" \ "episodes\n" \ "duration\n" \ "synonyms\n" \ "description(asHtml: false)\n" /* FIXME: why is this here */ static std::optional<nlohmann::json> SendJSONRequest(const nlohmann::json& data) { std::vector<std::string> headers = { "Accept: application/json", "Content-Type: application/json", }; if (!session.config.auth.anilist.auth_token.empty()) headers.push_back("Authorization: Bearer " + session.config.auth.anilist.auth_token); const std::string response = Strings::ToUtf8String(HTTP::Request("https://graphql.anilist.co", headers, data.dump(), HTTP::Type::Post)); if (response.empty()) { session.SetStatusBar(Strings::Translate("AniList: JSON request returned an empty result!")); return std::nullopt; } nlohmann::json out; try { out = nlohmann::json::parse(response); } catch (const std::exception& ex) { session.SetStatusBar(fmt::format(Strings::Translate("AniList: Failed to parse request JSON with error \"{}\"!"), ex.what())); return std::nullopt; } if (out.contains("/errors"_json_pointer) && out.at("/errors"_json_pointer).is_array()) { for (const auto& error : out.at("/errors"_json_pointer)) std::cerr << "AniList: Received an error in response: " << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl; session.SetStatusBar(Strings::Translate("AniList: Received an error in response!")); return std::nullopt; } return out; } static void ParseListStatus(std::string status, Anime::Anime& anime) { static const std::unordered_map<std::string, Anime::ListStatus> map = { {"CURRENT", Anime::ListStatus::Current }, {"PLANNING", Anime::ListStatus::Planning }, {"COMPLETED", Anime::ListStatus::Completed}, {"DROPPED", Anime::ListStatus::Dropped }, {"PAUSED", Anime::ListStatus::Paused } }; if (status == "REPEATING") { anime.SetUserIsRewatching(true); anime.SetUserStatus(Anime::ListStatus::Current); return; } if (map.find(status) == map.end()) { anime.SetUserStatus(Anime::ListStatus::NotInList); return; } anime.SetUserStatus(map.at(status)); } static std::string ListStatusToString(const Anime::Anime& anime) { if (anime.GetUserIsRewatching() && anime.GetUserStatus() == Anime::ListStatus::Current) return "REWATCHING"; switch (anime.GetUserStatus()) { case Anime::ListStatus::Planning: return "PLANNING"; case Anime::ListStatus::Completed: return "COMPLETED"; case Anime::ListStatus::Dropped: return "DROPPED"; case Anime::ListStatus::Paused: return "PAUSED"; default: break; } return "CURRENT"; } static void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) { static const std::unordered_map<Anime::TitleLanguage, nlohmann::json::json_pointer> map = { {Anime::TitleLanguage::Native, "/native"_json_pointer}, {Anime::TitleLanguage::English, "/english"_json_pointer}, {Anime::TitleLanguage::Romaji, "/romaji"_json_pointer}, }; for (const auto& [language, ptr] : map) if (json.contains(ptr) && json[ptr].is_string()) anime.SetTitle(language, json[ptr]); } static int ParseMediaJson(const nlohmann::json& json) { if (!json.contains("/id"_json_pointer) || !json["/id"_json_pointer].is_number()) { session.SetStatusBar(Strings::Translate("AniList: Failed to parse anime object!")); return 0; } std::string service_id = Strings::ToUtf8String(json["/id"_json_pointer].get<int>()); int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::AniList, service_id); if (!id) { session.SetStatusBar(Strings::Translate("AniList: Failed to parse anime object!")); return 0; } Anime::Anime& anime = Anime::db.items[id]; anime.SetId(id); anime.SetServiceId(Anime::Service::AniList, service_id); if (json.contains("/idMal"_json_pointer) && json["/idMal"_json_pointer].is_number()) anime.SetServiceId(Anime::Service::MyAnimeList, Strings::ToUtf8String(json["/idMal"_json_pointer].get<int>())); ParseTitle(json.at("/title"_json_pointer), anime); anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0)); anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, ""))); anime.SetAiringStatus( Translate::AniList::ToSeriesStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""))); if (json.contains("/startDate"_json_pointer) && json["/startDate"_json_pointer].is_object()) anime.SetStartedDate(Date(json["/startDate"_json_pointer])); anime.SetCompletedDate(json.contains("/endDate"_json_pointer) && json["/endDate"_json_pointer].is_object() ? Date(json["/endDate"_json_pointer]) : anime.GetStartedDate()); anime.SetPosterUrl(JSON::GetString<std::string>(json, "/coverImage/large"_json_pointer, "")); anime.SetAudienceScore(JSON::GetNumber(json, "/averageScore"_json_pointer, 0)); // anime.SetSeason(Translate::AniList::ToSeriesSeason(JSON::GetString<std::string>(json, "/season"_json_pointer, ""))); anime.SetDuration(JSON::GetNumber(json, "/duration"_json_pointer, 0)); std::string synopsis = JSON::GetString<std::string>(json, "/description"_json_pointer, ""); Strings::TextifySynopsis(synopsis); anime.SetSynopsis(synopsis); anime.SetGenres(JSON::GetArray<std::vector<std::string>>(json, "/genres"_json_pointer, {})); anime.SetTitleSynonyms(JSON::GetArray<std::vector<std::string>>(json, "/synonyms"_json_pointer, {})); { std::vector<std::string> producers; if (json.contains("/studios/edges"_json_pointer) && json["/studios/edges"_json_pointer].is_array()) for (const auto& edge : json["/studios/edges"_json_pointer]) if (edge.contains("/node/name"_json_pointer) && edge["/node/name"_json_pointer].is_string()) producers.push_back(edge["/node/name"_json_pointer].get<std::string>()); anime.SetProducers(producers); } return id; } static int ParseListItem(const nlohmann::json& json) { int id = ParseMediaJson(json["/media"_json_pointer]); if (!id) return 0; Anime::Anime& anime = Anime::db.items[id]; anime.AddToUserList(); anime.SetUserScore(JSON::GetNumber(json, "/score"_json_pointer, 0)); anime.SetUserProgress(JSON::GetNumber(json, "/progress"_json_pointer, 0)); ParseListStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""), anime); anime.SetUserNotes(JSON::GetString<std::string>(json, "/notes"_json_pointer, "")); anime.SetUserDateStarted(Date(json["/startedAt"_json_pointer])); anime.SetUserDateCompleted(Date(json["/completedAt"_json_pointer])); anime.SetUserTimeUpdated(JSON::GetNumber(json, "/updatedAt"_json_pointer, 0)); return id; } static bool ParseList(const nlohmann::json& json) { bool success = true; for (const auto& entry : json["entries"].items()) if (!ParseListItem(entry.value())) success = false; return success; } int GetAnimeList() { auto& auth = session.config.auth.anilist; static constexpr std::string_view query = "query ($id: Int) {\n" " MediaListCollection (userId: $id, type: ANIME) {\n" " lists {\n" " name\n" " entries {\n" " score\n" " notes\n" " status\n" " progress\n" " startedAt {\n" " year\n" " month\n" " day\n" " }\n" " completedAt {\n" " year\n" " month\n" " day\n" " }\n" " updatedAt\n" " media {\n" MEDIA_FIELDS " }\n" " }\n" " }\n" " }\n" "}\n"; // clang-format off nlohmann::json request = { {"query", query}, {"variables", { {"id", auth.user_id} }} }; // clang-format on session.SetStatusBar(Strings::Translate("AniList: Parsing anime list...")); const std::optional<nlohmann::json> response = SendJSONRequest(request); if (!response) return 0; Anime::db.RemoveAllUserData(); const nlohmann::json& json = response.value(); bool success = true; for (const auto& list : json["data"]["MediaListCollection"]["lists"].items()) if (!ParseList(list.value())) success = false; if (success) session.SetStatusBar(Strings::Translate("AniList: Retrieved anime list successfully!")); return 1; } /* return is a vector of anime ids */ std::vector<int> Search(const std::string& search) { static constexpr std::string_view query = "query ($search: String) {\n" " Page (page: 1, perPage: 50) {\n" " media (search: $search, type: ANIME) {\n" MEDIA_FIELDS " }\n" " }\n" "}\n"; // clang-format off nlohmann::json json = { {"query", query}, {"variables", { {"search", search} }} }; // clang-format on const std::optional<nlohmann::json> response = SendJSONRequest(json); if (!response) return {}; const nlohmann::json& result = response.value(); /* FIXME: error handling here */ std::vector<int> ret; ret.reserve(result["/data/Page/media"_json_pointer].size()); for (const auto& media : result["/data/Page/media"_json_pointer].items()) ret.push_back(ParseMediaJson(media.value())); return ret; } bool GetSeason(Anime::Season season) { static constexpr std::string_view query = "query ($season: MediaSeason!, $season_year: Int!, $page: Int) {\n" " Page(page: $page) {\n" " media(season: $season, seasonYear: $season_year, type: ANIME, sort: START_DATE) {\n" MEDIA_FIELDS " }\n" " pageInfo {\n" " total\n" " perPage\n" " currentPage\n" " lastPage\n" " hasNextPage\n" " }\n" " }\n" "}\n"; int page = 0; bool has_next_page = true; while (has_next_page) { nlohmann::json json = { {"query", query}, {"variables", { {"season", Translate::AniList::ToString(season.season)}, {"season_year", Strings::ToUtf8String(season.year)}, {"page", page}, }}, }; const std::optional<nlohmann::json> res = SendJSONRequest(json); if (!res) return false; const nlohmann::json& result = res.value(); for (const auto& media : result["/data/Page/media"_json_pointer].items()) ParseMediaJson(media.value()); has_next_page = JSON::GetBoolean(result, "/data/Page/pageInfo/hasNextPage"_json_pointer, false); if (has_next_page) page++; } return true; } int UpdateAnimeEntry(int id) { Anime::Anime& anime = Anime::db.items[id]; if (!anime.IsInUserList()) return 0; std::optional<std::string> service_id = anime.GetServiceId(Anime::Service::AniList); if (!service_id) return 0; static constexpr std::string_view query = "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String," " $start: FuzzyDateInput, $comp: FuzzyDateInput, $repeat: Int) {\n" " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score," " notes: $notes, startedAt: $start, completedAt: $comp, repeat: $repeat) {\n" " id\n" " }\n" "}\n"; // clang-format off nlohmann::json json = { {"query", query}, {"variables", { {"media_id", Strings::ToInt<int64_t>(service_id.value())}, {"progress", anime.GetUserProgress()}, {"status", ListStatusToString(anime)}, {"score", anime.GetUserScore()}, {"notes", anime.GetUserNotes()}, {"start", anime.GetUserDateStarted().GetAsAniListJson()}, {"comp", anime.GetUserDateCompleted().GetAsAniListJson()}, {"repeat", anime.GetUserRewatchedTimes()} }} }; // clang-format on const std::optional<nlohmann::json> res = SendJSONRequest(json); if (!res) return 0; const nlohmann::json& result = res.value(); session.SetStatusBar(Strings::Translate("AniList: Anime entry updated successfully!")); return JSON::GetNumber(result, "/data/SaveMediaListEntry/id"_json_pointer, 0); } static int ParseUser(const nlohmann::json& json) { auto& auth = session.config.auth.anilist; return auth.user_id = JSON::GetNumber(json, "/id"_json_pointer, 0); } bool AuthorizeUser() { auto& auth = session.config.auth.anilist; /* Prompt for PIN */ QDesktopServices::openUrl(QUrl(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" + std::string(CLIENT_ID) + "&response_type=token"))); bool ok; QString token = QInputDialog::getText( 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, "", &ok); if (!ok || token.isEmpty()) return false; auth.auth_token = Strings::ToUtf8String(token); session.SetStatusBar(Strings::Translate("AniList: Requesting user ID...")); static constexpr std::string_view query = "query {\n" " Viewer {\n" " id\n" " }\n" "}\n"; nlohmann::json json = { {"query", query} }; /* SendJSONRequest handles status errors */ const std::optional<nlohmann::json> ret = SendJSONRequest(json); if (!ret) return 0; const nlohmann::json& result = ret.value(); if (ParseUser(result["data"]["Viewer"])) session.SetStatusBar(Strings::Translate("AniList: Successfully retrieved user data!")); else session.SetStatusBar(Strings::Translate("AniList: Failed to retrieve user ID!")); return true; } } // namespace AniList } // namespace Services