Mercurial > minori
view src/services/kitsu.cc @ 321:8141f409d52c
services/anilist: fix getting producers
should studios and producers be stored separately?
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Wed, 12 Jun 2024 20:42:44 -0400 |
parents | 1b5c04268d6a |
children | 5d3c9b31aa6e |
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 "core/time.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 <iostream> using namespace nlohmann::literals::json_literals; static constexpr std::string_view CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"; static constexpr std::string_view CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"; static constexpr std::string_view BASE_API_PATH = "https://kitsu.io/api/edge"; static constexpr std::string_view OAUTH_PATH = "https://kitsu.io/api/oauth/token"; namespace Services { namespace Kitsu { /* This nifty little function basically handles authentication AND reauthentication. */ static bool SendAuthRequest(const nlohmann::json& data) { static const std::vector<std::string> headers = { {"Content-Type: application/json"} }; const std::string ret = Strings::ToUtf8String(HTTP::Request(std::string(OAUTH_PATH), headers, data.dump(), HTTP::Type::Post)); if (ret.empty()) { session.SetStatusBar("Kitsu: Request returned empty data!"); return false; } nlohmann::json result; try { result = nlohmann::json::parse(ret, nullptr, false); } catch (const std::exception& ex) { session.SetStatusBar(std::string("Kitsu: Failed to parse authorization data with error \"") + ex.what() + "\"!"); return false; } if (result.contains("/error"_json_pointer)) { std::string status = "Kitsu: Failed with error \""; status += result["/error"_json_pointer].get<std::string>(); if (result.contains("/error_description"_json_pointer)) { status += "\" and description \""; status += result["/error_description"_json_pointer].get<std::string>(); } status += "\"!"; session.SetStatusBar(status); return false; } const std::vector<nlohmann::json::json_pointer> required = { "/access_token"_json_pointer, "/created_at"_json_pointer, "/expires_in"_json_pointer, "/refresh_token"_json_pointer, "/scope"_json_pointer, "/token_type"_json_pointer }; for (const auto& ptr : required) { if (!result.contains(ptr)) { session.SetStatusBar("Kitsu: Authorization request returned bad data!"); return false; } } session.config.auth.kitsu.access_token = result["/access_token"_json_pointer].get<std::string>(); session.config.auth.kitsu.access_token_expiration = result["/created_at"_json_pointer].get<Time::Timestamp>(); + result["/expires_in"_json_pointer].get<Time::Timestamp>(); session.config.auth.kitsu.refresh_token = result["/refresh_token"_json_pointer].get<std::string>(); /* the next two are not that important */ return true; } static bool RefreshAccessToken(std::string& access_token, const std::string& refresh_token) { const nlohmann::json request = { {"grant_type", "refresh_token"}, {"refresh_token", refresh_token} }; if (!SendAuthRequest(request)) return false; return true; } /* ----------------------------------------------------------------------------- */ static std::optional<std::string> AccountAccessToken() { auto& auth = session.config.auth.kitsu; if (Time::GetSystemTime() >= session.config.auth.kitsu.access_token_expiration) if (!RefreshAccessToken(auth.access_token, auth.refresh_token)) return std::nullopt; return auth.access_token; } /* ----------------------------------------------------------------------------- */ static void AddAnimeFilters(std::map<std::string, std::string>& map) { static const std::vector<std::string> fields = { "abbreviatedTitles", "averageRating", "episodeCount", "episodeLength", "posterImage", "startDate", "status", "subtype", "titles", "categories", "synopsis", "animeProductions", }; static const std::string imploded = Strings::Implode(fields, ","); map["fields[anime]"] = imploded; map["fields[animeProductions]"] = "producer"; map["fields[categories]"] = "title"; map["fields[producers]"] = "name"; } static void AddLibraryEntryFilters(std::map<std::string, std::string>& map) { static const std::vector<std::string> fields = { "anime", "startedAt", "finishedAt", "notes", "progress", "ratingTwenty", "reconsumeCount", "reconsuming", "status", "updatedAt", }; static const std::string imploded = Strings::Implode(fields, ","); map["fields[libraryEntries]"] = imploded; } /* ----------------------------------------------------------------------------- */ static std::optional<nlohmann::json> SendJSONAPIRequest(const std::string& path, const std::map<std::string, std::string>& params = {}) { std::optional<std::string> token = AccountAccessToken(); if (!token) return std::nullopt; const std::vector<std::string> headers = { "Accept: application/vnd.api+json", "Authorization: Bearer " + token.value(), "Content-Type: application/vnd.api+json" }; const std::string url = HTTP::EncodeParamsList(std::string(BASE_API_PATH) + path, params); const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get)); if (response.empty()) return std::nullopt; nlohmann::json json; try { json = nlohmann::json::parse(response); } catch (const std::exception& ex) { session.SetStatusBar(std::string("Kitsu: Failed to parse response with error \"") + ex.what() + "\"!"); return std::nullopt; } if (json.contains("/errors"_json_pointer)) { for (const auto& item : json["/errors"]) std::cerr << "Kitsu: API returned error \"" << json["/errors/title"_json_pointer] << "\" with detail \"" << json["/errors/detail"] << std::endl; session.SetStatusBar("Kitsu: Request failed with errors!"); return std::nullopt; } return json; } static void ParseTitleJson(Anime::Anime& anime, const nlohmann::json& json) { static const std::map<std::string, Anime::TitleLanguage> lookup = { {"en", Anime::TitleLanguage::English}, {"en_jp", Anime::TitleLanguage::Romaji}, {"ja_jp", Anime::TitleLanguage::Native} }; for (const auto& [string, title] : lookup) if (json.contains(string)) anime.SetTitle(title, json[string].get<std::string>()); } static void ParseSubtype(Anime::Anime& anime, const std::string& str) { static const std::map<std::string, Anime::SeriesFormat> lookup = { {"ONA", Anime::SeriesFormat::Ona}, {"OVA", Anime::SeriesFormat::Ova}, {"TV", Anime::SeriesFormat::Tv}, {"movie", Anime::SeriesFormat::Movie}, {"music", Anime::SeriesFormat::Music}, {"special", Anime::SeriesFormat::Special} }; if (lookup.find(str) == lookup.end()) return; anime.SetFormat(lookup.at(str)); } static void ParseListStatus(Anime::Anime& anime, const std::string& str) { static const std::map<std::string, Anime::ListStatus> lookup = { {"completed", Anime::ListStatus::Completed}, {"current", Anime::ListStatus::Current}, {"dropped", Anime::ListStatus::Dropped}, {"on_hold", Anime::ListStatus::Paused}, {"planned", Anime::ListStatus::Planning} }; if (lookup.find(str) == lookup.end()) return; anime.SetUserStatus(lookup.at(str)); } static void ParseSeriesStatus(Anime::Anime& anime, const std::string& str) { static const std::map<std::string, Anime::SeriesStatus> lookup = { {"current", Anime::SeriesStatus::Releasing}, {"finished", Anime::SeriesStatus::Finished}, {"tba", Anime::SeriesStatus::Hiatus}, // is this right? {"unreleased", Anime::SeriesStatus::Cancelled}, {"upcoming", Anime::SeriesStatus::NotYetReleased}, }; if (lookup.find(str) == lookup.end()) return; anime.SetAiringStatus(lookup.at(str)); } static int ParseAnimeJson(const nlohmann::json& json) { static const std::string FAILED_TO_PARSE = "Kitsu: Failed to parse anime object!"; const std::string service_id = json["/id"_json_pointer].get<std::string>(); if (service_id.empty()) { session.SetStatusBar(FAILED_TO_PARSE + " (/id)"); return 0; } if (!json.contains("/attributes"_json_pointer)) { session.SetStatusBar(FAILED_TO_PARSE + " (/attributes)"); return 0; } const auto& attributes = json["/attributes"_json_pointer]; int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); Anime::Anime& anime = Anime::db.items[id]; anime.SetId(id); anime.SetServiceId(Anime::Service::Kitsu, service_id); if (attributes.contains("/synopsis"_json_pointer) && attributes["/synopsis"_json_pointer].is_string()) anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>()); if (attributes.contains("/titles"_json_pointer) && attributes["/titles"_json_pointer].is_object()) ParseTitleJson(anime, attributes["/titles"_json_pointer]); if (attributes.contains("/abbreviatedTitles"_json_pointer) && attributes["/abbreviatedTitles"_json_pointer].is_array()) for (const auto& title : attributes["/abbreviatedTitles"_json_pointer]) anime.AddTitleSynonym(title.get<std::string>()); if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_string()) anime.SetAudienceScore(Strings::ToInt<double>(attributes["/averageRating"_json_pointer].get<std::string>())); if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string()) anime.SetAirDate(attributes["/startDate"_json_pointer].get<std::string>()); // XXX endDate if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string()) ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>()); if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string()) ParseSeriesStatus(anime, attributes["/status"_json_pointer].get<std::string>()); if (attributes.contains("/posterImage/original"_json_pointer) && attributes["/posterImage/original"_json_pointer].is_string()) anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>()); if (attributes.contains("/episodeCount"_json_pointer) && attributes["/episodeCount"_json_pointer].is_number()) anime.SetEpisodes(attributes["/episodeCount"_json_pointer].get<int>()); if (attributes.contains("/episodeLength"_json_pointer) && attributes["/episodeLength"_json_pointer].is_number()) anime.SetDuration(attributes["/episodeLength"_json_pointer].get<int>()); return id; } static int ParseLibraryJson(const nlohmann::json& json) { static const std::vector<nlohmann::json::json_pointer> required = { "/id"_json_pointer, "/relationships/anime/data/id"_json_pointer, "/attributes"_json_pointer, }; for (const auto& ptr : required) { if (!json.contains(ptr)) { session.SetStatusBar(std::string("Kitsu: Failed to parse library object! (missing ") + ptr.to_string() + ")"); return 0; } } std::string service_id = json["/relationships/anime/data/id"_json_pointer].get<std::string>(); if (service_id.empty()) { session.SetStatusBar("Kitsu: Failed to parse library object!"); return 0; } int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); const auto& attributes = json["/attributes"_json_pointer]; const std::string library_id = json["/id"_json_pointer].get<std::string>(); Anime::Anime& anime = Anime::db.items[id]; anime.AddToUserList(); anime.SetId(id); anime.SetServiceId(Anime::Service::Kitsu, service_id); anime.SetUserId(library_id); if (attributes.contains("/startedAt"_json_pointer) && attributes["/startedAt"_json_pointer].is_string()) anime.SetUserDateStarted(Date(Time::ParseISO8601Time(attributes["/startedAt"_json_pointer].get<std::string>()))); if (attributes.contains("/finishedAt"_json_pointer) && attributes["/finishedAt"_json_pointer].is_string()) anime.SetUserDateCompleted(Date(Time::ParseISO8601Time(attributes["/finishedAt"_json_pointer].get<std::string>()))); if (attributes.contains("/notes"_json_pointer) && attributes["/notes"_json_pointer].is_string()) anime.SetUserNotes(attributes["/notes"_json_pointer].get<std::string>()); if (attributes.contains("/progress"_json_pointer) && attributes["/progress"_json_pointer].is_number()) anime.SetUserProgress(attributes["/progress"_json_pointer].get<int>()); if (attributes.contains("/ratingTwenty"_json_pointer) && attributes["/ratingTwenty"_json_pointer].is_number()) anime.SetUserScore(attributes["/ratingTwenty"_json_pointer].get<int>() * 5); if (attributes.contains("/private"_json_pointer) && attributes["/private"_json_pointer].is_boolean()) anime.SetUserIsPrivate(attributes["/private"_json_pointer].get<bool>()); if (attributes.contains("/reconsumeCount"_json_pointer) && attributes["/reconsumeCount"_json_pointer].is_number()) anime.SetUserRewatchedTimes(attributes["/reconsumeCount"_json_pointer].get<int>()); if (attributes.contains("/reconsuming"_json_pointer) && attributes["/reconsuming"_json_pointer].is_boolean()) anime.SetUserIsRewatching(attributes["/reconsuming"_json_pointer].get<bool>()); /* lmfao "reconsuming" */ if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string()) ParseListStatus(anime, attributes["/status"_json_pointer].get<std::string>()); if (attributes.contains("/progressedAt"_json_pointer) && attributes["/progressedAt"_json_pointer].is_string()) anime.SetUserTimeUpdated(Time::ParseISO8601Time(attributes["/progressedAt"_json_pointer].get<std::string>())); return id; } static void ParseMetadataJson(Anime::Anime& anime, const nlohmann::json& json) { std::vector<std::string> categories; std::vector<std::string> producers; for (const auto& item : json) { std::string variant; { static const nlohmann::json::json_pointer p = "/type"_json_pointer; if (!item.contains(p) || !item[p].is_string()) continue; variant = item[p].get<std::string>(); } /* now parse variants */ if (variant == "categories") { static const nlohmann::json::json_pointer p = "/attributes/title"_json_pointer; if (!item.contains(p) || !item[p].is_string()) continue; categories.push_back(item[p].get<std::string>()); } else if (variant == "producers") { static const nlohmann::json::json_pointer p = "/attributes/name"_json_pointer; if (!item.contains(p) || !item[p].is_string()) continue; producers.push_back(item[p].get<std::string>()); } } anime.SetGenres(categories); anime.SetProducers(producers); } static bool ParseAnyJson(const nlohmann::json& json) { static const nlohmann::json::json_pointer required = "/type"_json_pointer; if (!json.contains(required) && !json[required].is_string()) { session.SetStatusBar(std::string("Kitsu: Failed to parse generic object! (missing ") + required.to_string() + ")"); return 0; } std::string variant = json["/type"_json_pointer].get<std::string>(); if (variant == "anime") { return !!ParseAnimeJson(json); } else if (variant == "libraryEntries") { return !!ParseLibraryJson(json); } else if (variant == "categories" || variant == "producers") { /* do nothing */ } else { std::cerr << "Kitsu: received unknown type " << variant << std::endl; } return true; } int GetAnimeList() { static constexpr int LIBRARY_MAX_SIZE = 500; const auto& auth = session.config.auth.kitsu; if (auth.user_id.empty()) { session.SetStatusBar("Kitsu: User ID is unavailable!"); return 0; } int page = 0; bool have_next_page = true; std::map<std::string, std::string> params = { {"filter[user_id]", auth.user_id}, {"filter[kind]", "anime"}, {"include", "anime"}, {"page[offset]", Strings::ToUtf8String(page)}, {"page[limit]", Strings::ToUtf8String(LIBRARY_MAX_SIZE)} }; AddAnimeFilters(params); AddLibraryEntryFilters(params); Anime::db.RemoveAllUserData(); bool success = true; while (have_next_page) { std::optional<nlohmann::json> response = SendJSONAPIRequest("/library-entries", params); if (!response) return 0; const nlohmann::json& root = response.value(); if (root.contains("/next"_json_pointer) && root["/next"_json_pointer].is_number()) { page += root["/next"_json_pointer].get<int>(); if (page <= 0) have_next_page = false; } else have_next_page = false; for (const auto& item : root["/data"_json_pointer]) if (!ParseLibraryJson(item)) success = false; for (const auto& variant : root["/included"_json_pointer]) if (!ParseAnyJson(variant)) success = false; params["page[offset]"] = Strings::ToUtf8String(page); } if (success) session.SetStatusBar("Kitsu: Successfully received library data!"); return 1; } bool RetrieveAnimeMetadata(int id) { /* TODO: the genres should *probably* be a std::optional */ Anime::Anime& anime = Anime::db.items[id]; if (anime.GetGenres().size() > 0 && anime.GetProducers().size()) return false; std::optional<std::string> service_id = anime.GetServiceId(Anime::Service::Kitsu); if (!service_id) return false; session.SetStatusBar("Kitsu: Retrieving anime metadata..."); static const std::map<std::string, std::string> params = { {"include", Strings::Implode({ "categories", "animeProductions", "animeProductions.producer", }, ",")} }; std::optional<nlohmann::json> response = SendJSONAPIRequest("/anime/" + service_id.value(), params); if (!response) return false; const auto& json = response.value(); if (!json.contains("/included"_json_pointer) || !json["/included"_json_pointer].is_array()) { session.SetStatusBar("Kitsu: Server returned bad data when trying to retrieve anime metadata!"); return false; } ParseMetadataJson(anime, json["/included"_json_pointer]); session.SetStatusBar("Kitsu: Successfully retrieved anime metadata!"); return true; } /* unimplemented for now */ std::vector<int> Search(const std::string& search) { return {}; } std::vector<int> GetSeason(Anime::SeriesSeason season, Date::Year year) { return {}; } int UpdateAnimeEntry(int id) { return 0; } bool AuthorizeUser(const std::string& email, const std::string& password) { const nlohmann::json body = { {"grant_type", "password"}, {"username", email}, {"password", HTTP::UrlEncode(password)} }; if (!SendAuthRequest(body)) return false; static const std::map<std::string, std::string> params = { {"filter[self]", "true"} }; std::optional<nlohmann::json> response = SendJSONAPIRequest("/users", params); if (!response) return false; // whuh? const nlohmann::json& json = response.value(); if (!json.contains("/data/0/id"_json_pointer)) { session.SetStatusBar("Kitsu: Failed to retrieve user ID!"); return false; } session.SetStatusBar("Kitsu: Successfully authorized user!"); session.config.auth.kitsu.user_id = json["/data/0/id"_json_pointer].get<std::string>(); return true; } } // namespace Kitsu } // namespace Services