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 {};