Mercurial > minori
changeset 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 | 3b355fa948c7 |
children | 1b5c04268d6a |
files | include/core/anime_db.h include/core/date.h include/core/session.h include/core/time.h include/services/services.h src/core/anime_db.cc src/core/date.cc src/core/http.cc src/core/session.cc src/core/time.cc src/gui/dialog/settings/services.cc src/gui/window.cc src/library/library.cc src/services/anilist.cc src/services/kitsu.cc |
diffstat | 15 files changed, 303 insertions(+), 99 deletions(-) [+] |
line wrap: on
line diff
--- a/include/core/anime_db.h Wed Jun 12 05:25:41 2024 -0400 +++ b/include/core/anime_db.h Wed Jun 12 17:52:26 2024 -0400 @@ -11,26 +11,29 @@ class Database { public: std::unordered_map<int, Anime> items; - size_t GetTotalAnimeAmount(); - size_t GetTotalEpisodeAmount(); - size_t GetTotalWatchedAmount(); - size_t GetTotalPlannedAmount(); - double GetAverageScore(); - double GetScoreDeviation(); - size_t GetListsAnimeAmount(ListStatus status); - int GetAnimeFromTitle(const std::string& title); + size_t GetTotalAnimeAmount() const; + size_t GetTotalEpisodeAmount() const; + size_t GetTotalWatchedAmount() const; + size_t GetTotalPlannedAmount() const; + double GetAverageScore() const; + double GetScoreDeviation() const; + size_t GetListsAnimeAmount(ListStatus status) const; + int LookupAnimeTitle(const std::string& title) const; - bool GetDatabaseAsJSON(nlohmann::json& json); - bool SaveDatabaseToDisk(); + bool GetDatabaseAsJSON(nlohmann::json& json) const; + bool SaveDatabaseToDisk() const; bool ParseDatabaseJSON(const nlohmann::json& json); bool LoadDatabaseFromDisk(); /* These are here to make sure that our service IDs don't collide * and make the whole thing go boom. */ - int GetUnusedId(); - int LookupServiceId(Service service, const std::string& id_to_find); - int LookupServiceIdOrUnused(Service service, const std::string& id_to_find); + int GetUnusedId() const; + int LookupServiceId(Service service, const std::string& id_to_find) const; + int LookupServiceIdOrUnused(Service service, const std::string& id_to_find) const; + + /* when syncing we don't want to keep deleted anime */ + void RemoveAllUserData(); }; extern Database db;
--- a/include/core/date.h Wed Jun 12 05:25:41 2024 -0400 +++ b/include/core/date.h Wed Jun 12 17:52:26 2024 -0400 @@ -3,6 +3,8 @@ #include "json/json_fwd.hpp" +#include "core/time.h" + #include <optional> #include <string> @@ -37,6 +39,7 @@ Date(const std::string& str); Date(const QDate& date); Date(const nlohmann::json& json); + Date(Time::Timestamp timestamp); bool IsValid() const; void SetYear(Year y); void SetMonth(Month m);
--- a/include/core/session.h Wed Jun 12 05:25:41 2024 -0400 +++ b/include/core/session.h Wed Jun 12 17:52:26 2024 -0400 @@ -1,6 +1,7 @@ #ifndef MINORI_CORE_SESSION_H_ #define MINORI_CORE_SESSION_H_ +#include "core/time.h" #include "core/config.h" #include "gui/locale.h" @@ -11,6 +12,7 @@ #include <atomic> #include <string> +#include <random> class MainWindow; @@ -31,6 +33,7 @@ Config config; static constexpr semver::version version{PACKAGE_VERSION}; + std::mt19937 gen; signals: void StatusBarChange(const std::string& message);
--- a/include/core/time.h Wed Jun 12 05:25:41 2024 -0400 +++ b/include/core/time.h Wed Jun 12 17:52:26 2024 -0400 @@ -19,6 +19,8 @@ /* in UTC */ Timestamp GetSystemTime(); +Timestamp ParseISO8601Time(const std::string& str); + }; // namespace Time #endif // MINORI_CORE_TIME_H_ \ No newline at end of file
--- a/include/services/services.h Wed Jun 12 05:25:41 2024 -0400 +++ b/include/services/services.h Wed Jun 12 17:52:26 2024 -0400 @@ -9,6 +9,8 @@ namespace Services { +/* TODO: need to limit these to one thread (or put a mutex on the anime db) */ + void Synchronize(); std::vector<int> Search(const std::string& search); std::vector<int> GetSeason(Anime::SeriesSeason season, Date::Year year);
--- a/src/core/anime_db.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/core/anime_db.cc Wed Jun 12 17:52:26 2024 -0400 @@ -2,6 +2,7 @@ #include "core/anime.h" #include "core/filesystem.h" #include "core/json.h" +#include "core/session.h" #include "core/strings.h" #include "gui/translate/anilist.h" @@ -18,7 +19,7 @@ namespace Anime { -size_t Database::GetTotalAnimeAmount() { +size_t Database::GetTotalAnimeAmount() const { size_t total = 0; for (const auto& [id, anime] : items) @@ -28,7 +29,7 @@ return total; } -size_t Database::GetListsAnimeAmount(ListStatus status) { +size_t Database::GetListsAnimeAmount(ListStatus status) const { if (status == ListStatus::NotInList) return 0; @@ -41,7 +42,7 @@ return total; } -size_t Database::GetTotalEpisodeAmount() { +size_t Database::GetTotalEpisodeAmount() const { size_t total = 0; for (const auto& [id, anime] : items) @@ -52,7 +53,7 @@ } /* Returns the total watched amount in minutes. */ -size_t Database::GetTotalWatchedAmount() { +size_t Database::GetTotalWatchedAmount() const { size_t total = 0; for (const auto& [id, anime] : items) @@ -68,7 +69,7 @@ amount of episodes, as AniList will let you set episode counts up to 32768. But that should rather be handled elsewhere. */ -size_t Database::GetTotalPlannedAmount() { +size_t Database::GetTotalPlannedAmount() const { size_t total = 0; for (const auto& [id, anime] : items) @@ -81,7 +82,7 @@ /* In Taiga this is called the mean, but "average" is what's primarily used in conversation, at least in the U.S. */ -double Database::GetAverageScore() { +double Database::GetAverageScore() const { double avg = 0; size_t amt = 0; @@ -94,7 +95,7 @@ return avg / amt; } -double Database::GetScoreDeviation() { +double Database::GetScoreDeviation() const { double squares_sum = 0, avg = GetAverageScore(); size_t amt = 0; @@ -108,11 +109,7 @@ return (amt > 0) ? std::sqrt(squares_sum / amt) : 0; } -/* - * TODO: separate this from the anime DB, - * provide *some* sort of normalization - */ -int Database::GetAnimeFromTitle(const std::string& title) { +int Database::LookupAnimeTitle(const std::string& title) const { if (title.empty()) return 0; @@ -194,7 +191,7 @@ return true; } -bool Database::GetDatabaseAsJSON(nlohmann::json& json) { +bool Database::GetDatabaseAsJSON(nlohmann::json& json) const { for (const auto& [id, anime] : items) { nlohmann::json anime_json = {}; GetAnimeAsJSON(anime, anime_json); @@ -204,7 +201,7 @@ return true; } -bool Database::SaveDatabaseToDisk() { +bool Database::SaveDatabaseToDisk() const { std::filesystem::path db_path = Filesystem::GetAnimeDBPath(); Filesystem::CreateDirectories(db_path); @@ -302,22 +299,18 @@ return true; } -int Database::GetUnusedId() { - /* TODO: move these out of here */ - - std::random_device rd; - std::mt19937 gen(rd()); +int Database::GetUnusedId() const { std::uniform_int_distribution<int> distrib(1, INT_MAX); int res; do { - res = distrib(gen); - } while (items.count(res)); + res = distrib(session.gen); + } while (items.count(res) && !res); return res; } -int Database::LookupServiceId(Service service, const std::string& id_to_find) { +int Database::LookupServiceId(Service service, const std::string& id_to_find) const { for (const auto& [id, anime] : items) { std::optional<std::string> service_id = anime.GetServiceId(service); if (!service_id) @@ -330,7 +323,7 @@ return 0; } -int Database::LookupServiceIdOrUnused(Service service, const std::string& id_to_find) { +int Database::LookupServiceIdOrUnused(Service service, const std::string& id_to_find) const { int id = LookupServiceId(service, id_to_find); if (id) return id; @@ -338,6 +331,13 @@ return GetUnusedId(); } +void Database::RemoveAllUserData() { + for (auto& [id, anime] : items) { + if (anime.IsInUserList()) + anime.RemoveFromUserList(); + } +} + Database db; } // namespace Anime
--- a/src/core/date.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/core/date.cc Wed Jun 12 17:52:26 2024 -0400 @@ -1,4 +1,5 @@ #include "core/date.h" +#include "core/time.h" #include "core/json.h" #include <QDate> @@ -64,6 +65,10 @@ SetDay(json.at("/day"_json_pointer).get<unsigned char>()); } +Date::Date(Time::Timestamp timestamp) { + Date(QDateTime::fromSecsSinceEpoch(timestamp).date()); +} + void Date::VoidYear() { year.reset(); }
--- a/src/core/http.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/core/http.cc Wed Jun 12 17:52:26 2024 -0400 @@ -123,10 +123,7 @@ } RequestThread::~RequestThread() { - /* block until the function can safely exit. - * - * this sucks. find out a better way to do this, which will probably - * be to put all of the threads in a pool */ + /* block until the function can safely exit */ Stop(); wait(); }
--- a/src/core/session.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/core/session.cc Wed Jun 12 17:52:26 2024 -0400 @@ -13,7 +13,7 @@ Session session; -Session::Session() { +Session::Session() : gen(Time::GetSystemTime()) { timer_.start(); }
--- a/src/core/time.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/core/time.cc Wed Jun 12 17:52:26 2024 -0400 @@ -98,4 +98,8 @@ return QDateTime::currentDateTime().toUTC().toSecsSinceEpoch(); } +Timestamp ParseISO8601Time(const std::string& str) { + return QDateTime::fromString(Strings::ToQString(str), Qt::ISODateWithMs).toUTC().toSecsSinceEpoch(); +} + } // namespace Time
--- a/src/gui/dialog/settings/services.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/gui/dialog/settings/services.cc Wed Jun 12 17:52:26 2024 -0400 @@ -93,8 +93,11 @@ { QPushButton* auth_button = new QPushButton(credentials_grid); - connect(auth_button, &QPushButton::clicked, this, [email, password] { - Services::Kitsu::AuthorizeUser(Strings::ToUtf8String(email->text()), Strings::ToUtf8String(password->text())); + connect(auth_button, &QPushButton::clicked, this, [auth_button, email, password] { + if (Services::Kitsu::AuthorizeUser(Strings::ToUtf8String(email->text()), Strings::ToUtf8String(password->text()))) + auth_button->setText(tr("Re-authorize...")); + else + auth_button->setText(tr("Authorize...")); }); auth_button->setText(session.config.auth.kitsu.access_token.empty() ? tr("Authorize...") : tr("Re-authorize..."));
--- a/src/gui/window.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/gui/window.cc Wed Jun 12 17:52:26 2024 -0400 @@ -111,7 +111,7 @@ const auto& elements = anitomy.elements(); const std::string title = Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle)); - int id = Anime::db.GetAnimeFromTitle(title); + int id = Anime::db.LookupAnimeTitle(title); if (id <= 0) continue;
--- a/src/library/library.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/library/library.cc Wed Jun 12 17:52:26 2024 -0400 @@ -34,7 +34,7 @@ const std::string title = Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle)); - const int id = Anime::db.GetAnimeFromTitle(title); + const int id = Anime::db.LookupAnimeTitle(title); if (id <= 0) continue;
--- a/src/services/anilist.cc Wed Jun 12 05:25:41 2024 -0400 +++ b/src/services/anilist.cc Wed Jun 12 17:52:26 2024 -0400 @@ -150,12 +150,18 @@ } static int ParseMediaJson(const nlohmann::json& json) { - if (!json.contains("/id"_json_pointer) || !json["/id"_json_pointer].is_number()) + if (!json.contains("/id"_json_pointer) || !json["/id"_json_pointer].is_number()) { + session.SetStatusBar("AniList: Failed to parse anime object!"); return 0; + } std::string service_id = Strings::ToUtf8String(json["/id"_json_pointer].get<int>()); int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::AniList, service_id); + if (!id) { + session.SetStatusBar("AniList: Failed to parse anime object!"); + return 0; + } Anime::Anime& anime = Anime::db.items[id]; anime.SetId(id); @@ -191,7 +197,7 @@ } static int ParseListItem(const nlohmann::json& json) { - int id = ParseMediaJson(json); + int id = ParseMediaJson(json["/media"_json_pointer]); if (!id) return 0; @@ -212,11 +218,14 @@ return id; } -static int ParseList(const nlohmann::json& json) { +static bool ParseList(const nlohmann::json& json) { + bool success = true; + for (const auto& entry : json["entries"].items()) - ParseListItem(entry.value()); + if (!ParseListItem(entry.value())) + success = false; - return 1; + return success; } int GetAnimeList() { @@ -271,10 +280,16 @@ if (!res) return 0; + bool success = true; + + Anime::db.RemoveAllUserData(); + for (const auto& list : result["data"]["MediaListCollection"]["lists"].items()) - ParseList(list.value()); + if (!ParseList(list.value())) + success = false; - session.SetStatusBar("AniList: Retrieved anime list successfully!"); + if (success) + session.SetStatusBar("AniList: Retrieved anime list successfully!"); return 1; } @@ -449,10 +464,6 @@ constexpr std::string_view query = "query {\n" " Viewer {\n" " id\n" - " name\n" - " mediaListOptions {\n" - " scoreFormat\n" // this will be used... eventually - " }\n" " }\n" "}\n"; nlohmann::json json = { @@ -465,9 +476,11 @@ if (!ret) return 0; - session.SetStatusBar("AniList: Successfully retrieved user data!"); - - ParseUser(ret["data"]["Viewer"]); + if (ParseUser(result["data"]["Viewer"])) + session.SetStatusBar("AniList: Successfully retrieved user data!"); + else + session.SetStatusBar("AniList: Failed to retrieve user ID!"); + return true; }
--- 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;