# HG changeset patch # User Paper # Date 1718236159 14400 # Node ID 1b5c04268d6a9567afcf6f0ad21c9ba563debecf # Parent d928ec7b6a0d01192ee1b8cb6096a260abc5f50c 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... diff -r d928ec7b6a0d -r 1b5c04268d6a include/core/anime.h --- a/include/core/anime.h Wed Jun 12 17:52:26 2024 -0400 +++ b/include/core/anime.h Wed Jun 12 19:49:19 2024 -0400 @@ -161,13 +161,13 @@ void SetUserStatus(ListStatus status); void SetUserScore(int score); void SetUserProgress(int progress); - void SetUserDateStarted(Date const& started); - void SetUserDateCompleted(Date const& completed); + void SetUserDateStarted(const Date& started); + void SetUserDateCompleted(const Date& completed); void SetUserIsPrivate(bool is_private); void SetUserRewatchedTimes(int rewatched); void SetUserIsRewatching(bool rewatching); void SetUserTimeUpdated(uint64_t updated); - void SetUserNotes(std::string const& notes); + void SetUserNotes(const std::string& notes); /* Series data */ int GetId() const; @@ -190,13 +190,13 @@ void SetId(int id); void SetServiceId(Service service, const std::string& id); void SetTitle(TitleLanguage language, const std::string& title); - void SetTitleSynonyms(std::vector const& synonyms); - void AddTitleSynonym(std::string const& synonym); + void SetTitleSynonyms(const std::vector& synonyms); + void AddTitleSynonym(const std::string& synonym); void SetEpisodes(int episodes); void SetAiringStatus(SeriesStatus status); - void SetAirDate(Date const& date); - void SetGenres(std::vector const& genres); - void SetProducers(std::vector const& producers); + void SetAirDate(const Date& date); + void SetGenres(const std::vector& genres); + void SetProducers(const std::vector& producers); void SetFormat(SeriesFormat format); void SetAudienceScore(double audience_score); void SetSynopsis(std::string synopsis); diff -r d928ec7b6a0d -r 1b5c04268d6a include/core/strings.h --- a/include/core/strings.h Wed Jun 12 17:52:26 2024 -0400 +++ b/include/core/strings.h Wed Jun 12 19:49:19 2024 -0400 @@ -17,7 +17,6 @@ * into a string, separated by delimiters. */ std::string Implode(const std::vector& vector, const std::string& delimiter); -std::string Implode(const std::set& set, const std::string& delimiter); std::vector Split(const std::string& text, const std::string& delimiter); /* Substring removal functions */ @@ -48,7 +47,7 @@ QString ToQString(const std::wstring& wstring); /* not really an "int"... but who cares? */ -template::value, bool> = true> +template::value, bool> = true> T ToInt(const std::string& str, T def = 0) { std::istringstream s(str); s >> std::noboolalpha >> def; diff -r d928ec7b6a0d -r 1b5c04268d6a include/gui/pages/anime_list.h --- a/include/gui/pages/anime_list.h Wed Jun 12 17:52:26 2024 -0400 +++ b/include/gui/pages/anime_list.h Wed Jun 12 19:49:19 2024 -0400 @@ -18,7 +18,7 @@ Q_OBJECT public: - AnimeListPageUpdateEntryThread(AnimeListPage* parent); + AnimeListPageUpdateEntryThread(QObject* parent = nullptr); void AddToQueue(int id); @@ -29,8 +29,7 @@ void run() override; private: - AnimeListPage* page_ = nullptr; - std::mutex _queue_mutex; + std::mutex queue_mutex_; std::queue queue_; }; diff -r d928ec7b6a0d -r 1b5c04268d6a include/gui/widgets/anime_info.h --- a/include/gui/widgets/anime_info.h Wed Jun 12 17:52:26 2024 -0400 +++ b/include/gui/widgets/anime_info.h Wed Jun 12 19:49:19 2024 -0400 @@ -2,12 +2,35 @@ #define MINORI_GUI_WIDGETS_ANIME_INFO_H_ #include +#include #include "gui/widgets/text.h" +#include +#include + namespace Anime { class Anime; } +class AnimeInfoWidgetGetMetadataThread final : public QThread { + Q_OBJECT + +public: + AnimeInfoWidgetGetMetadataThread(QObject* parent = nullptr); + + void AddToQueue(int id); + +signals: + void NeedRefresh(int id); + +protected: + void run() override; + +private: + std::mutex queue_mutex_; + std::queue queue_; +}; + class AnimeInfoWidget final : public QWidget { Q_OBJECT @@ -16,10 +39,15 @@ AnimeInfoWidget(const Anime::Anime& anime, QWidget* parent = nullptr); void SetAnime(const Anime::Anime& anime); +protected: + void RefreshGenres(const Anime::Anime& anime); + private: TextWidgets::OneLineSection _title; TextWidgets::LabelledSection _details; TextWidgets::SelectableSection _synopsis; + + int id_ = 0; }; #endif // MINORI_GUI_WIDGETS_ANIME_INFO_H_ diff -r d928ec7b6a0d -r 1b5c04268d6a include/services/kitsu.h --- a/include/services/kitsu.h Wed Jun 12 17:52:26 2024 -0400 +++ b/include/services/kitsu.h Wed Jun 12 19:49:19 2024 -0400 @@ -13,6 +13,8 @@ /* neither of these are stored in the config and only held temporarily */ bool AuthorizeUser(const std::string& email, const std::string& password); +bool RetrieveAnimeMetadata(int id); + int GetAnimeList(); std::vector Search(const std::string& search); std::vector GetSeason(Anime::SeriesSeason season, Date::Year year); diff -r d928ec7b6a0d -r 1b5c04268d6a include/services/services.h --- a/include/services/services.h Wed Jun 12 17:52:26 2024 -0400 +++ b/include/services/services.h Wed Jun 12 19:49:19 2024 -0400 @@ -9,9 +9,13 @@ namespace Services { -/* TODO: need to limit these to one thread (or put a mutex on the anime db) */ +void Synchronize(); -void Synchronize(); +/* true = metadata was retrieved + * false = metadata failed to be retrieved OR + * no metadata to be retrieved */ +bool RetrieveAnimeMetadata(int id); + std::vector Search(const std::string& search); std::vector GetSeason(Anime::SeriesSeason season, Date::Year year); void UpdateAnimeEntry(int id); diff -r d928ec7b6a0d -r 1b5c04268d6a src/core/anime_db.cc --- a/src/core/anime_db.cc Wed Jun 12 17:52:26 2024 -0400 +++ b/src/core/anime_db.cc Wed Jun 12 19:49:19 2024 -0400 @@ -243,6 +243,13 @@ Anime& anime = database.items[id]; anime.SetId(id); + for (const auto& service : Services) { + nlohmann::json::json_pointer p("/ids/" + Strings::ToLower(Translate::ToString(service))); + + if (json.contains(p) && json[p].is_string()) + anime.SetServiceId(service, json[p].get()); + } + for (const auto& lang : TitleLanguages) { nlohmann::json::json_pointer p("/title/" + Strings::ToLower(Translate::ToString(lang))); diff -r d928ec7b6a0d -r 1b5c04268d6a src/core/config.cc --- a/src/core/config.cc Wed Jun 12 17:52:26 2024 -0400 +++ b/src/core/config.cc Wed Jun 12 19:49:19 2024 -0400 @@ -48,19 +48,19 @@ anime_list.score_format = Translate::ToScoreFormat(toml::find_or(data, "Anime List", "Score format", "100-point")); anime_list.language = Translate::ToLanguage(toml::find_or(data, "Anime List", "Title language", "Romaji")); - anime_list.display_aired_episodes = toml::find_or(data, "Anime List", "Display only aired episodes", true); - anime_list.display_available_episodes = toml::find_or(data, "Anime List", "Display only available episodes in library", true); - anime_list.highlight_anime_if_available = toml::find_or(data, "Anime List", "Highlight anime if available", true); + anime_list.display_aired_episodes = toml::find_or(data, "Anime List", "Display only aired episodes", true); + anime_list.display_available_episodes = toml::find_or(data, "Anime List", "Display only available episodes in library", true); + anime_list.highlight_anime_if_available = toml::find_or(data, "Anime List", "Highlight anime if available", true); anime_list.highlighted_anime_above_others = (anime_list.highlight_anime_if_available) - ? toml::find_or(data, "Anime List", "Display highlighted anime above others", false) + ? toml::find_or(data, "Anime List", "Display highlighted anime above others", false) : false; auth.anilist.auth_token = toml::find_or(data, "Authentication/AniList", "Auth Token", ""); - auth.anilist.user_id = toml::find_or(data, "Authentication/AniList", "User ID", 0); + auth.anilist.user_id = toml::find_or(data, "Authentication/AniList", "User ID", 0); auth.kitsu.access_token = toml::find_or(data, "Authentication/Kitsu", "Access Token", ""); - auth.kitsu.access_token_expiration = toml::find_or(data, "Authentication/Kitsu", "Access Token Expiration", 0LL); + auth.kitsu.access_token_expiration = toml::find_or(data, "Authentication/Kitsu", "Access Token Expiration", static_cast(0)); auth.kitsu.refresh_token = toml::find_or(data, "Authentication/Kitsu", "Refresh Token", ""); auth.kitsu.user_id = toml::find_or(data, "Authentication/Kitsu", "User ID", ""); @@ -90,10 +90,10 @@ switch (player.type) { default: case animone::PlayerType::Default: - enabled = toml::find_or(data, "Recognition/Players", player.name, true); + enabled = toml::find_or(data, "Recognition/Players", player.name, true); break; case animone::PlayerType::WebBrowser: - enabled = toml::find_or(data, "Recognition/Browsers", player.name, true); + enabled = toml::find_or(data, "Recognition/Browsers", player.name, true); break; } } diff -r d928ec7b6a0d -r 1b5c04268d6a src/core/strings.cc --- a/src/core/strings.cc Wed Jun 12 17:52:26 2024 -0400 +++ b/src/core/strings.cc Wed Jun 12 19:49:19 2024 -0400 @@ -40,21 +40,6 @@ return out; } -std::string Implode(const std::set& set, const std::string& delimiter) { - if (set.size() < 1) - return ""; - - std::string out; - - for (auto it = set.cbegin(); it != set.cend(); it++) { - out.append(*it); - if (it != std::prev(set.cend(), 1)) - out.append(delimiter); - } - - return out; -} - std::vector Split(const std::string& text, const std::string& delimiter) { if (text.length() < 1) return {}; diff -r d928ec7b6a0d -r 1b5c04268d6a src/gui/pages/anime_list.cc --- a/src/gui/pages/anime_list.cc Wed Jun 12 17:52:26 2024 -0400 +++ b/src/gui/pages/anime_list.cc Wed Jun 12 19:49:19 2024 -0400 @@ -33,24 +33,29 @@ #include -AnimeListPageUpdateEntryThread::AnimeListPageUpdateEntryThread(AnimeListPage* parent) : QThread(parent) { - page_ = parent; -} +AnimeListPageUpdateEntryThread::AnimeListPageUpdateEntryThread(QObject* parent) : QThread(parent) {} void AnimeListPageUpdateEntryThread::AddToQueue(int id) { - const std::lock_guard guard(_queue_mutex); + const std::lock_guard guard(queue_mutex_); queue_.push(id); } /* processes the queue... */ void AnimeListPageUpdateEntryThread::run() { - { - const std::lock_guard guard(_queue_mutex); - while (!queue_.empty() && !isInterruptionRequested()) { - Services::UpdateAnimeEntry(queue_.front()); - queue_.pop(); - } + queue_mutex_.lock(); + while (!queue_.empty() && !isInterruptionRequested()) { + int id = queue_.front(); + + /* unlock the mutex for a long blocking operation, so items + * can be added without worry */ + queue_mutex_.unlock(); + Services::UpdateAnimeEntry(id); + queue_mutex_.lock(); + + queue_.pop(); } + queue_mutex_.unlock(); + emit NeedRefresh(); } @@ -447,7 +452,7 @@ /* --------- QTabWidget replication end ---------- */ -AnimeListPage::AnimeListPage(QWidget* parent) : QWidget(parent), update_entry_thread_(this) { +AnimeListPage::AnimeListPage(QWidget* parent) : QWidget(parent) { /* Tab bar */ tab_bar = new QTabBar(this); tab_bar->setExpanding(false); diff -r d928ec7b6a0d -r 1b5c04268d6a src/gui/widgets/anime_info.cc --- a/src/gui/widgets/anime_info.cc Wed Jun 12 17:52:26 2024 -0400 +++ b/src/gui/widgets/anime_info.cc Wed Jun 12 19:49:19 2024 -0400 @@ -1,21 +1,61 @@ #include "gui/widgets/anime_info.h" #include "core/anime.h" +#include "core/anime_db.h" #include "core/strings.h" #include "gui/translate/anime.h" #include "gui/widgets/text.h" +#include "services/services.h" #include #include +AnimeInfoWidgetGetMetadataThread::AnimeInfoWidgetGetMetadataThread(QObject* parent) : QThread(parent) {} + +void AnimeInfoWidgetGetMetadataThread::AddToQueue(int id) { + const std::lock_guard guard(queue_mutex_); + queue_.push(id); +} + +/* processes the queue... */ +void AnimeInfoWidgetGetMetadataThread::run() { + queue_mutex_.lock(); + while (!queue_.empty() && !isInterruptionRequested()) { + int id = queue_.front(); + + queue_mutex_.unlock(); + + if (Services::RetrieveAnimeMetadata(id)) + emit NeedRefresh(id); + + queue_mutex_.lock(); + + queue_.pop(); + } + queue_mutex_.unlock(); +} + +/* all widgets share this thread */ +static AnimeInfoWidgetGetMetadataThread get_metadata_thread; + AnimeInfoWidget::AnimeInfoWidget(QWidget* parent) : QWidget(parent) , _title(tr("Alternative titles"), "") - , _details(tr("Details"), tr("Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nScore:"), "") + , _details(tr("Details"), tr("Type:\nEpisodes:\nStatus:\nSeason:\nGenres:\nProducers:\nScore:"), "") , _synopsis(tr("Synopsis"), "") { QVBoxLayout* layout = new QVBoxLayout(this); layout->addWidget(&_title); layout->addWidget(&_details); layout->addWidget(&_synopsis); + + /* ... */ + connect(&get_metadata_thread, &AnimeInfoWidgetGetMetadataThread::NeedRefresh, this, [this](int id) { + setUpdatesEnabled(false); + + if (id == id_) + RefreshGenres(Anime::db.items[id]); + + setUpdatesEnabled(true); + }); } AnimeInfoWidget::AnimeInfoWidget(const Anime::Anime& anime, QWidget* parent) : AnimeInfoWidget(parent) { @@ -23,27 +63,45 @@ } void AnimeInfoWidget::SetAnime(const Anime::Anime& anime) { + setUpdatesEnabled(false); + + id_ = anime.GetId(); + + get_metadata_thread.AddToQueue(id_); + if (!get_metadata_thread.isRunning()) + get_metadata_thread.start(); + /* alt titles */ _title.GetLine()->SetText(Strings::ToQString(Strings::Implode(anime.GetTitleSynonyms(), ", "))); + RefreshGenres(anime); + + _synopsis.GetParagraph()->SetText(Strings::ToQString(anime.GetSynopsis())); + + setUpdatesEnabled(true); + + updateGeometry(); +} + +void AnimeInfoWidget::RefreshGenres(const Anime::Anime& anime) { /* details */ QString details_data; QTextStream details_data_s(&details_data); /* we have to convert ALL of these strings to * QString because QTextStream sucks and assumes - * Latin1 (on Windows?) */ + * Latin-1 (on Windows?) */ const auto genres = anime.GetGenres(); + const auto producers = anime.GetProducers(); + details_data_s << Strings::ToQString(Translate::ToLocalString(anime.GetFormat())) << "\n" << anime.GetEpisodes() << "\n" << Strings::ToQString(Translate::ToLocalString(anime.GetAiringStatus())) << "\n" << Strings::ToQString(Translate::ToLocalString(anime.GetSeason())) << " " << anime.GetAirDate().GetYear().value_or(2000) << "\n" - << Strings::ToQString((genres.size() > 1) ? Strings::Implode(genres, ", ") : "-") << "\n" + << Strings::ToQString((genres.size() > 1) ? Strings::Implode(genres, ", ") : "-") << "\n" + << Strings::ToQString((producers.size() > 1) ? Strings::Implode(producers, ", ") : "-") << "\n" << anime.GetAudienceScore() << "%"; - _details.GetData()->setText(details_data); - _synopsis.GetParagraph()->SetText(Strings::ToQString(anime.GetSynopsis())); - - updateGeometry(); + _details.GetData()->setText(details_data); } diff -r d928ec7b6a0d -r 1b5c04268d6a src/services/anilist.cc --- a/src/services/anilist.cc Wed Jun 12 17:52:26 2024 -0400 +++ b/src/services/anilist.cc Wed Jun 12 19:49:19 2024 -0400 @@ -234,8 +234,6 @@ return 0; } - session.SetStatusBar("AniList: Retrieving anime list..."); - /* NOTE: these really ought to be in the qrc file */ constexpr std::string_view query = "query ($id: Int) {\n" " MediaListCollection (userId: $id, type: ANIME) {\n" diff -r d928ec7b6a0d -r 1b5c04268d6a src/services/kitsu.cc --- 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 SendJSONAPIRequest(const std::string& path, const std::map& params) { +static void AddAnimeFilters(std::map& map) { + static const std::vector 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& map) { + static const std::vector 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 SendJSONAPIRequest(const std::string& path, const std::map& params = {}) { std::optional token = AccountAccessToken(); if (!token) return std::nullopt; @@ -140,16 +183,14 @@ if (response.empty()) return std::nullopt; - std::optional 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 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()); - if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_number()) - anime.SetAudienceScore(JSON::GetNumber(attributes, "/averageRating"_json_pointer)); + if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_string()) + anime.SetAudienceScore(Strings::ToInt(attributes["/averageRating"_json_pointer].get())); if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string()) anime.SetAirDate(attributes["/startDate"_json_pointer].get()); - // TODO: endDate + // XXX endDate if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string()) ParseSubtype(anime, attributes["/subtype"_json_pointer].get()); + if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string()) + ParseSeriesStatus(anime, attributes["/status"_json_pointer].get()); + if (attributes.contains("/posterImage/original"_json_pointer) && attributes["/posterImage/original"_json_pointer].is_string()) anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get()); @@ -330,48 +387,63 @@ return id; } +static void ParseMetadataJson(Anime::Anime& anime, const nlohmann::json& json) { + std::vector categories; + std::vector 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(); + } + + /* 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()); + } 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()); + } + } + + 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 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(); - - if (lookup.find(json_type) != lookup.end()) - variant = lookup.at(json_type); + std::string variant = json["/type"_json_pointer].get(); - 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 service_id = anime.GetServiceId(Anime::Service::Kitsu); + if (!service_id) + return false; + + session.SetStatusBar("Kitsu: Retrieving anime metadata..."); + + static const std::map params = { + {"include", Strings::Implode({ + "categories", + "animeProductions", + "animeProductions.producer", + }, ",")} + }; + + std::optional 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 Search(const std::string& search) { return {}; diff -r d928ec7b6a0d -r 1b5c04268d6a src/services/services.cc --- a/src/services/services.cc Wed Jun 12 17:52:26 2024 -0400 +++ b/src/services/services.cc Wed Jun 12 19:49:19 2024 -0400 @@ -1,11 +1,14 @@ #include "services/services.h" #include "core/session.h" +#include "gui/translate/anime.h" #include "services/anilist.h" #include "services/kitsu.h" namespace Services { void Synchronize() { + session.SetStatusBar(Translate::ToString(session.config.service) + ": Retrieving anime list..."); + switch (session.config.service) { case Anime::Service::AniList: AniList::GetAnimeList(); break; case Anime::Service::Kitsu: Kitsu::GetAnimeList(); break; @@ -13,7 +16,16 @@ } } +bool RetrieveAnimeMetadata(int id) { + switch (session.config.service) { + case Anime::Service::Kitsu: return Kitsu::RetrieveAnimeMetadata(id); + default: return false; + } +} + std::vector Search(const std::string& search) { + session.SetStatusBar(Translate::ToString(session.config.service) + ": Requesting search query..."); + switch (session.config.service) { case Anime::Service::AniList: return AniList::Search(search); case Anime::Service::Kitsu: return Kitsu::Search(search); @@ -22,6 +34,8 @@ } std::vector GetSeason(Anime::SeriesSeason season, Date::Year year) { + session.SetStatusBar(Translate::ToString(session.config.service) + ": Retrieving anime season data..."); + switch (session.config.service) { case Anime::Service::AniList: return AniList::GetSeason(season, year); case Anime::Service::Kitsu: return Kitsu::GetSeason(season, year); @@ -30,6 +44,8 @@ } void UpdateAnimeEntry(int id) { + session.SetStatusBar(Translate::ToString(session.config.service) + ": Updating remote anime entry..."); + switch (session.config.service) { case Anime::Service::AniList: AniList::UpdateAnimeEntry(id); break; case Anime::Service::Kitsu: Kitsu::UpdateAnimeEntry(id); break;