Mercurial > minori
diff src/services/kitsu.cc @ 320:1b5c04268d6a
services/kitsu: ACTUALLY finish GetAnimeList
there are some things the API just... doesn't provide. therefore
we have to request the genres separately any time a new anime info
box is opened...
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Wed, 12 Jun 2024 19:49:19 -0400 |
parents | d928ec7b6a0d |
children | 8141f409d52c |
line wrap: on
line diff
--- a/src/services/kitsu.cc Wed Jun 12 17:52:26 2024 -0400 +++ b/src/services/kitsu.cc Wed Jun 12 19:49:19 2024 -0400 @@ -123,7 +123,50 @@ /* ----------------------------------------------------------------------------- */ -static std::optional<nlohmann::json> SendJSONAPIRequest(const std::string& path, const std::map<std::string, std::string>& params) { +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; @@ -140,16 +183,14 @@ if (response.empty()) return std::nullopt; - std::optional<nlohmann::json> result; + nlohmann::json json; try { - result = nlohmann::json::parse(response); + 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; } - 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; @@ -158,7 +199,7 @@ return std::nullopt; } - return result; + return json; } static void ParseTitleJson(Anime::Anime& anime, const nlohmann::json& json) { @@ -204,6 +245,21 @@ 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!"; @@ -221,10 +277,6 @@ const auto& attributes = json["/attributes"_json_pointer]; int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); - if (!id) { - session.SetStatusBar(FAILED_TO_PARSE + " getting unused ID"); - return 0; - } Anime::Anime& anime = Anime::db.items[id]; @@ -237,19 +289,24 @@ if (attributes.contains("/titles"_json_pointer) && attributes["/titles"_json_pointer].is_object()) ParseTitleJson(anime, attributes["/titles"_json_pointer]); - // FIXME: parse abbreviatedTitles for synonyms?? + 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_number()) - anime.SetAudienceScore(JSON::GetNumber<double>(attributes, "/averageRating"_json_pointer)); + 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>()); - // TODO: endDate + // 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>()); @@ -330,48 +387,63 @@ 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) { - 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); + std::string variant = json["/type"_json_pointer].get<std::string>(); - 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; + 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() { @@ -394,6 +466,8 @@ {"page[offset]", Strings::ToUtf8String(page)}, {"page[limit]", Strings::ToUtf8String(LIBRARY_MAX_SIZE)} }; + AddAnimeFilters(params); + AddLibraryEntryFilters(params); Anime::db.RemoveAllUserData(); @@ -429,6 +503,44 @@ 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) + 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 {};