Mercurial > minori
diff src/services/kitsu.cc @ 319:d928ec7b6a0d
services/kitsu: implement GetAnimeList()
it finally works!
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Wed, 12 Jun 2024 17:52:26 -0400 |
parents | b1f4d1867ab1 |
children | 1b5c04268d6a |
line wrap: on
line diff
--- a/src/services/kitsu.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/services/kitsu.cc Wed Jun 12 17:52:26 2024 -0400 @@ -123,7 +123,7 @@ /* ----------------------------------------------------------------------------- */ -static std::optional<std::string> SendRequest(const std::string& path, const std::map<std::string, std::string>& params) { +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; @@ -136,7 +136,29 @@ const std::string url = HTTP::EncodeParamsList(std::string(BASE_API_PATH) + path, params); - return Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get)); + const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get)); + if (response.empty()) + return std::nullopt; + + std::optional<nlohmann::json> result; + try { + result = 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; + } + + const nlohmann::json& json = result.value(); + + 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 result; } static void ParseTitleJson(Anime::Anime& anime, const nlohmann::json& json) { @@ -167,17 +189,32 @@ anime.SetFormat(lookup.at(str)); } -static const std::string FAILED_TO_PARSE = "Kitsu: Failed to parse anime object!"; +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 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); + session.SetStatusBar(FAILED_TO_PARSE + " (/id)"); return 0; } if (!json.contains("/attributes"_json_pointer)) { - session.SetStatusBar(FAILED_TO_PARSE); + session.SetStatusBar(FAILED_TO_PARSE + " (/attributes)"); return 0; } @@ -185,7 +222,7 @@ int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); if (!id) { - session.SetStatusBar(FAILED_TO_PARSE); + session.SetStatusBar(FAILED_TO_PARSE + " getting unused ID"); return 0; } @@ -193,39 +230,59 @@ anime.SetId(id); anime.SetServiceId(Anime::Service::Kitsu, service_id); - anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>()); - ParseTitleJson(anime, attributes["/titles"_json_pointer]); + + 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]); // FIXME: parse abbreviatedTitles for synonyms?? - anime.SetAudienceScore(JSON::GetNumber<double>(attributes, "/averageRating"_json_pointer)); + if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_number()) + anime.SetAudienceScore(JSON::GetNumber<double>(attributes, "/averageRating"_json_pointer)); - if (attributes.contains("/startDate"_json_pointer)) + if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string()) anime.SetAirDate(attributes["/startDate"_json_pointer].get<std::string>()); // TODO: endDate - if (attributes.contains("/subtype"_json_pointer)) + if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string()) ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>()); - anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>()); - anime.SetEpisodes(attributes["/episodeCount"_json_pointer].get<int>()); - anime.SetDuration(attributes["/episodeLength"_json_pointer].get<int>()); + 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) { - if (!json.contains("/relationships/anime/data"_json_pointer) - || !json.contains("/attributes"_json_pointer) - || !json.contains("/id"_json_pointer)) { + 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 = ParseAnimeJson(json["/relationships/anime/data"_json_pointer]); - if (!id) - return 0; + int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); const auto& attributes = json["/attributes"_json_pointer]; @@ -235,23 +292,141 @@ anime.AddToUserList(); + anime.SetId(id); + anime.SetServiceId(Anime::Service::Kitsu, service_id); + anime.SetUserId(library_id); - anime.SetUserDateStarted(Date(attributes["/startedAt"_json_pointer].get<std::string>())); - anime.SetUserDateCompleted(Date(attributes["/finishedAt"_json_pointer].get<std::string>())); - anime.SetUserNotes(attributes["/notes"_json_pointer].get<std::string>()); - anime.SetUserProgress(attributes["/progress"_json_pointer].get<int>()); - anime.SetUserScore(attributes["/ratingTwenty"_json_pointer].get<int>() * 5); - anime.SetUserIsPrivate(attributes["/private"_json_pointer].get<bool>()); - anime.SetUserRewatchedTimes(attributes["/reconsumeCount"_json_pointer].get<int>()); - anime.SetUserIsRewatching(attributes["/reconsuming"_json_pointer].get<bool>()); /* "reconsuming". really? */ - // anime.SetUserStatus(); - // anime.SetUserLastUpdated(); + + 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 bool ParseAnyJson(const nlohmann::json& json) { + enum class Variant { + Unknown, + Anime, + LibraryEntry, + Category, + Producer, + }; + + static const std::map<std::string, Variant> lookup = { + {"anime", Variant::Anime}, + {"libraryEntries", Variant::LibraryEntry}, + {"category", Variant::Category}, + {"producers", Variant::Producer} + }; + + 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; + } + + Variant variant = Variant::Unknown; + + std::string json_type = json["/type"_json_pointer].get<std::string>(); + + if (lookup.find(json_type) != lookup.end()) + variant = lookup.at(json_type); + + switch (variant) { + case Variant::Anime: + return !!ParseAnimeJson(json); + case Variant::LibraryEntry: + return !!ParseLibraryJson(json); + /* ... */ + case Variant::Category: + case Variant::Producer: + return true; + default: + std::cerr << "Kitsu: received unknown type " << json_type << std::endl; + return true; + } +} + int GetAnimeList() { - return 0; + 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)} + }; + + 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; } /* unimplemented for now */ @@ -281,24 +456,18 @@ {"filter[self]", "true"} }; - std::optional<std::string> response = SendRequest("/users", params); + std::optional<nlohmann::json> response = SendJSONAPIRequest("/users", params); if (!response) return false; // whuh? - nlohmann::json json; - try { - json = nlohmann::json::parse(response.value()); - } catch (const std::exception& ex) { - session.SetStatusBar(std::string("Kitsu: Failed to parse user data with error \"") + ex.what() + "\"!"); - return false; - } + 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 retrieved user data!"); + session.SetStatusBar("Kitsu: Successfully authorized user!"); session.config.auth.kitsu.user_id = json["/data/0/id"_json_pointer].get<std::string>(); return true;