Mercurial > minori
view src/services/anilist.cc @ 279:657fda1b9cac
*: clean up enums
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Fri, 19 Apr 2024 13:24:06 -0400 |
parents | dd211ff68b36 |
children | e66ffc338d82 |
line wrap: on
line source
#include "services/anilist.h" #include "core/anime.h" #include "core/anime_db.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 <iostream> using namespace nlohmann::literals::json_literals; namespace Services { namespace AniList { constexpr int CLIENT_ID = 13706; class Account { public: int UserId() const { return session.config.auth.anilist.user_id; } void SetUserId(const int id) { session.config.auth.anilist.user_id = id; } std::string AuthToken() const { return session.config.auth.anilist.auth_token; } void SetAuthToken(std::string const& auth_token) { session.config.auth.anilist.auth_token = auth_token; } bool Authenticated() const { return !AuthToken().empty(); } bool IsValid() const { return UserId() && Authenticated(); } }; static Account account; std::string SendRequest(std::string data) { std::vector<std::string> headers = {"Authorization: Bearer " + account.AuthToken(), "Accept: application/json", "Content-Type: application/json"}; return Strings::ToUtf8String(HTTP::Post("https://graphql.anilist.co", data, headers)); } nlohmann::json SendJSONRequest(nlohmann::json data) { std::string request = SendRequest(data.dump()); if (request.empty()) { std::cerr << "[AniList] JSON Request returned an empty result!" << std::endl; return {}; } auto ret = nlohmann::json::parse(request, nullptr, false); if (ret.is_discarded()) { std::cerr << "[AniList] Failed to parse request JSON!" << std::endl; return {}; } if (ret.contains("/errors"_json_pointer) && ret.at("/errors"_json_pointer).is_array()) { for (const auto& error : ret.at("/errors"_json_pointer)) std::cerr << "[AniList] Received an error in response: " << JSON::GetString<std::string>(error, "/message"_json_pointer, "") << std::endl; return {}; } return ret; } 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)); } 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"; } void ParseTitle(const nlohmann::json& json, Anime::Anime& anime) { anime.SetNativeTitle(JSON::GetString<std::string>(json, "/native"_json_pointer, "")); anime.SetEnglishTitle(JSON::GetString<std::string>(json, "/english"_json_pointer, "")); anime.SetRomajiTitle(JSON::GetString<std::string>(json, "/romaji"_json_pointer, "")); } int ParseMediaJson(const nlohmann::json& json) { int id = JSON::GetNumber(json, "/id"_json_pointer); if (!id) return 0; Anime::Anime& anime = Anime::db.items[id]; anime.SetId(id); 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, ""))); anime.SetAirDate(Date(json["/startDate"_json_pointer])); 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, {})); return id; } int ParseListItem(const nlohmann::json& json) { int id = ParseMediaJson(json["media"]); 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; } int ParseList(const nlohmann::json& json) { for (const auto& entry : json["entries"].items()) { ParseListItem(entry.value()); } return 1; } int GetAnimeList() { if (!account.IsValid()) { std::cerr << "AniList: Account isn't valid!" << std::endl; return 0; } /* NOTE: these really ought to be in the qrc file */ 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" " 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" " genres\n" " episodes\n" " duration\n" " synonyms\n" " description(asHtml: false)\n" " }\n" " }\n" " }\n" " }\n" "}\n"; // clang-format off nlohmann::json json = { {"query", query}, {"variables", { {"id", account.UserId()} }} }; // clang-format on auto res = SendJSONRequest(json); for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) { ParseList(list.value()); } return 1; } /* return is a vector of anime ids */ std::vector<int> Search(const std::string& search) { constexpr std::string_view query = "query ($search: String) {\n" " Page (page: 1, perPage: 50) {\n" " media (search: $search, type: ANIME) {\n" " 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" " genres\n" " episodes\n" " duration\n" " synonyms\n" " description(asHtml: false)\n" " }\n" " }\n" "}\n"; // clang-format off nlohmann::json json = { {"query", query}, {"variables", { {"search", search} }} }; // clang-format on auto res = SendJSONRequest(json); std::vector<int> ret; ret.reserve(res["data"]["Page"]["media"].size()); for (const auto& media : res["data"]["Page"]["media"].items()) ret.push_back(ParseMediaJson(media.value())); return ret; } int UpdateAnimeEntry(int id) { /** * possible values: * * int mediaId, * MediaListStatus status, * float score, * int scoreRaw, * int progress, * int progressVolumes, // manga-specific. * int repeat, // rewatch * int priority, * bool private, * string notes, * bool hiddenFromStatusLists, * string[] customLists, * float[] advancedScores, * Date startedAt, * Date completedAt **/ Anime::Anime& anime = Anime::db.items[id]; if (!anime.IsInUserList()) return 0; 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", anime.GetId()}, {"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 auto ret = SendJSONRequest(json); return JSON::GetNumber(ret, "/data/SaveMediaListEntry/id"_json_pointer, 0); } int ParseUser(const nlohmann::json& json) { account.SetUserId(JSON::GetNumber(json, "/id"_json_pointer, 0)); return account.UserId(); } bool AuthorizeUser() { /* Prompt for PIN */ QDesktopServices::openUrl(QUrl(Strings::ToQString("https://anilist.co/api/v2/oauth/authorize?client_id=" + Strings::ToUtf8String(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; account.SetAuthToken(Strings::ToUtf8String(token)); constexpr std::string_view query = "query {\n" " Viewer {\n" " id\n" " name\n" " mediaListOptions {\n" " scoreFormat\n" // this will be used... eventually " }\n" " }\n" "}\n"; nlohmann::json json = { {"query", query} }; auto ret = SendJSONRequest(json); ParseUser(ret["data"]["Viewer"]); return true; } } // namespace AniList } // namespace Services