changeset 389:1e5d922fe82b default tip

kitsu: implement UpdateAnimeEntry yay... i guess
author Paper <paper@tflc.us>
date Thu, 06 Nov 2025 12:21:35 -0500
parents 83aa0ddd1a46
children
files include/core/date.h include/core/http.h include/gui/widgets/anime_info.h src/core/anime_db.cc src/core/date.cc src/core/http.cc src/gui/widgets/anime_info.cc src/services/kitsu.cc
diffstat 8 files changed, 122 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/include/core/date.h	Thu Nov 06 09:53:06 2025 -0500
+++ b/include/core/date.h	Thu Nov 06 12:21:35 2025 -0500
@@ -56,6 +56,7 @@
 	std::optional<Day> GetDay() const;
 	QDate GetAsQDate() const;
 	nlohmann::json GetAsAniListJson() const;
+	std::string GetAsISO8601() const;
 
 private:
 	std::optional<Year> year;
--- a/include/core/http.h	Thu Nov 06 09:53:06 2025 -0500
+++ b/include/core/http.h	Thu Nov 06 12:21:35 2025 -0500
@@ -18,7 +18,8 @@
 
 enum class Type {
 	Get,
-	Post
+	Post,
+	Patch
 };
 
 QByteArray Request(const std::string &url, const std::vector<std::string> &headers = {}, const std::string &data = "",
--- a/include/gui/widgets/anime_info.h	Thu Nov 06 09:53:06 2025 -0500
+++ b/include/gui/widgets/anime_info.h	Thu Nov 06 12:21:35 2025 -0500
@@ -37,6 +37,7 @@
 public:
 	AnimeInfoWidget(QWidget* parent = nullptr);
 	AnimeInfoWidget(const Anime::Anime& anime, QWidget* parent = nullptr);
+	~AnimeInfoWidget();
 	void SetAnime(const Anime::Anime& anime);
 
 protected:
--- a/src/core/anime_db.cc	Thu Nov 06 09:53:06 2025 -0500
+++ b/src/core/anime_db.cc	Thu Nov 06 12:21:35 2025 -0500
@@ -146,6 +146,7 @@
 
 	// clang-format off
 	json = {
+		{"id", anime.GetUserId()},
 		{"status", Translate::ToString(anime.GetUserStatus())},
 		{"progress", anime.GetUserProgress()},
 		{"score", anime.GetUserScore()},
@@ -236,6 +237,7 @@
 	if (!anime.IsInUserList())
 		anime.AddToUserList();
 
+	anime.SetUserId(JSON::GetString<std::string>(json, "/id"_json_pointer, ""));
 	anime.SetUserStatus(Translate::ToListStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, "")));
 	anime.SetUserProgress(JSON::GetNumber(json, "/progress"_json_pointer, 0));
 	anime.SetUserScore(JSON::GetNumber(json, "/score"_json_pointer, 0));
--- a/src/core/date.cc	Thu Nov 06 09:53:06 2025 -0500
+++ b/src/core/date.cc	Thu Nov 06 12:21:35 2025 -0500
@@ -154,3 +154,19 @@
 
 	return json;
 }
+
+std::string Date::GetAsISO8601() const
+{
+	std::stringstream res;
+
+	res << year.value_or(2000);
+	res << '-';
+	res << (static_cast<int>(month.value_or(Date::Month::Jan)) + 1);
+	res << '-';
+	res << day.value_or(1);
+
+	/* fake the rest... */
+	res << "T00:00:00.000Z";
+
+	return res.str();
+}
--- a/src/core/http.cc	Thu Nov 06 09:53:06 2025 -0500
+++ b/src/core/http.cc	Thu Nov 06 12:21:35 2025 -0500
@@ -82,8 +82,10 @@
 			list = curl_slist_append(list, h.c_str());
 
 		curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
-		if (type == Type::Post)
+		if (type == Type::Post || type == Type::Patch)
 			curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
+		if (type == Type::Patch)
+			curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
 		curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
 		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
 		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteCallback);
@@ -165,7 +167,7 @@
 	if (curl) {
 		curl_easy_setopt(curl, CURLOPT_URL, url_.c_str());
 
-		if (type_ == Type::Post)
+		if (type_ == Type::Post || type_ == Type::Patch)
 			curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data_.c_str());
 
 		for (const std::string &h : headers_)
--- a/src/gui/widgets/anime_info.cc	Thu Nov 06 09:53:06 2025 -0500
+++ b/src/gui/widgets/anime_info.cc	Thu Nov 06 12:21:35 2025 -0500
@@ -84,6 +84,12 @@
 	SetAnime(anime);
 }
 
+AnimeInfoWidget::~AnimeInfoWidget()
+{
+	disconnect(&get_metadata_thread, nullptr, this, nullptr);
+	get_metadata_thread.wait();
+}
+
 void AnimeInfoWidget::SetAnime(const Anime::Anime &anime)
 {
 	setUpdatesEnabled(false);
--- a/src/services/kitsu.cc	Thu Nov 06 09:53:06 2025 -0500
+++ b/src/services/kitsu.cc	Thu Nov 06 12:21:35 2025 -0500
@@ -139,7 +139,9 @@
 /* ----------------------------------------------------------------------------- */
 
 static std::optional<nlohmann::json> SendJSONAPIRequest(const std::string &path,
-                                                        const std::map<std::string, std::string> &params = {})
+                                                        const std::map<std::string, std::string> &params = {},
+                                                        const std::string &data = "",
+                                                        HTTP::Type type = HTTP::Type::Get)
 {
 	std::optional<std::string> token = AccountAccessToken();
 	if (!token)
@@ -151,7 +153,7 @@
 
 	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));
+	const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, data, type));
 	if (response.empty())
 		return std::nullopt;
 
@@ -165,9 +167,12 @@
 	}
 
 	if (json.contains("/errors"_json_pointer)) {
+		std::cout << json["/errors"_json_pointer] << '\n';
+#if 0
 		for (const auto &item : json["/errors"])
 			std::cerr << "Kitsu: API returned error \"" << json["/errors/title"_json_pointer] << "\" with detail \""
 			          << json["/errors/detail"] << std::endl;
+#endif
 
 		session.SetStatusBar(Strings::Translate("Kitsu: Request failed with errors!"));
 		return std::nullopt;
@@ -608,9 +613,91 @@
 	return true;
 }
 
+static std::string UserStatusToString(Anime::ListStatus status)
+{
+	switch (status) {
+		case Anime::ListStatus::Planning: return "planned";
+		case Anime::ListStatus::Completed: return "completed";
+		case Anime::ListStatus::Dropped: return "dropped";
+		case Anime::ListStatus::Paused: return "on_hold";
+		default: break;
+	}
+
+	return "current";
+}
+
 int UpdateAnimeEntry(int id)
 {
-	return 0;
+	const Anime::Anime &anime = Anime::db.items[id];
+	int score;
+
+	if (!anime.IsInUserList())
+		return 0; /* WTF */
+
+	nlohmann::json json = {
+		{"data", {
+			{"type", "libraryEntries"},
+			{"attributes", {
+				{"status", UserStatusToString(anime.GetUserStatus())},
+				{"progress", anime.GetUserProgress()},
+				{"reconsuming", anime.GetUserIsRewatching()},
+				{"reconsumeCount", anime.GetUserRewatchedTimes()},
+				{"notes", anime.GetUserNotes()},
+				{"private", anime.GetUserIsPrivate()},
+				// WTF is reactionSkipped?
+				{"startedAt", anime.GetUserDateStarted().GetAsISO8601()},
+				{"finishedAt", anime.GetUserDateCompleted().GetAsISO8601()},
+			}},
+			{"relationships", {
+				{"anime", {
+					{"data", {
+						{"type", "anime"},
+						{"id", anime.GetServiceId(Anime::Service::Kitsu)},
+					}}
+				}},
+				{"user", {
+					{"data", {
+						{"type", "users"},
+						{"id", session.config.auth.kitsu.user_id},
+					}}
+				}}
+			}}
+		}}
+	};
+
+	nlohmann::json &attributes = json["data"]["attributes"];
+
+	score = anime.GetUserScore() / 5;
+	if (score > 0) {
+		attributes["ratingTwenty"] = score;
+	} else {
+		attributes["ratingTwenty"] = nullptr;
+	}
+
+	/* I really don't like this */
+	std::string uid = anime.GetUserId();
+
+	std::string path = "/library-entries";
+	HTTP::Type type;
+
+	if (!uid.empty()) {
+		json["data"]["id"] = uid;
+		path = path + "/" + uid;
+		type = HTTP::Type::Patch;
+	} else {
+		type = HTTP::Type::Post;
+	}
+
+	std::optional<nlohmann::json> res = SendJSONAPIRequest(path, {}, json.dump(), type);
+	if (!res)
+		return 0;
+
+	/* TODO parse result; can reduces races */
+
+	session.SetStatusBar(Strings::Translate("Kitsu: Anime entry updated successfully!"));
+
+	/* I guess? */
+	return Strings::ToInt<int>(anime.GetUserId());
 }
 
 bool AuthorizeUser(const std::string &email, const std::string &password)