From 7c40643d8e652adf47cd0ad66fd98b4d808dfade Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 17 Aug 2021 01:53:55 +0200 Subject: Add AniList (WIP) --- src/plugins/AniList.cpp | 514 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 src/plugins/AniList.cpp (limited to 'src/plugins') diff --git a/src/plugins/AniList.cpp b/src/plugins/AniList.cpp new file mode 100644 index 0000000..5bf069c --- /dev/null +++ b/src/plugins/AniList.cpp @@ -0,0 +1,514 @@ +#include "../../plugins/AniList.hpp" +#include "../../include/Theme.hpp" +#include "../../include/StringUtils.hpp" +#include "../../include/Notification.hpp" +#include + +// TODO: Use get_scale() to fetch correctly sized thumbnail + +namespace QuickMedia { + static const std::string search_query_graphql = R"END( +query ($page: Int, $perPage: Int, $search: String) { + Page (page: $page, perPage: $perPage) { + media (search: $search) { + id + type + format + status (version: 2) + startDate { + year + month + day + } + endDate { + year + month + day + } + averageScore + episodes + chapters + genres + coverImage { + medium + } + title { + romaji + } + synonyms + } + } +} + )END"; + + // TODO: description asHtml seems to be broken, so for now we manually remove html from the description text. + // Remove that part when anilist api is fixed. + static const std::string details_query_graphql = R"END( +query ($id: Int) { + Media (id: $id) { + type + format + status (version: 2) + startDate { + year + month + day + } + endDate { + year + month + day + } + averageScore + episodes + chapters + genres + description (asHtml: false) + coverImage { + large + } + title { + romaji + } + synonyms + } +} + )END"; + + enum class AniListMediaType { + ANIME, + MANGA + }; + + static const char *month_names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + static bool in_range(int value, int min, int max) { + return value >= min && value <= max; + } + + static std::string fuzzy_date_to_str(const Json::Value &fuzzy_date_json) { + std::string result; + if(!fuzzy_date_json.isObject()) + return result; + + const Json::Value &year_json = fuzzy_date_json["year"]; + const Json::Value &month_json = fuzzy_date_json["month"]; + const Json::Value &day_json = fuzzy_date_json["day"]; + if(!year_json.isInt()) + return result; + + if(!month_json.isInt() || !in_range(month_json.asInt(), 1, 12)) { + result = std::to_string(year_json.asInt()); + return result; + } + + result += month_names[month_json.asInt() - 1]; + if(day_json.isInt()) { + result += " "; + result += std::to_string(day_json.asInt()); + } + result += ", "; + result += std::to_string(year_json.asInt()); + return result; + } + + static const char* media_status_to_readable(const char *media_status, AniListMediaType media_type) { + if(strcmp(media_status, "FINISHED") == 0) + return media_type == AniListMediaType::ANIME ? "Finished airing" : "Finished"; + if(strcmp(media_status, "RELEASING") == 0) + return media_type == AniListMediaType::ANIME ? "Publishing" : "Airing"; + if(strcmp(media_status, "NOT_YET_RELEASED") == 0) + return "Not yet released"; + if(strcmp(media_status, "CANCELLED") == 0) + return "Cancelled"; + if(strcmp(media_status, "HIATUS") == 0) + return "Hiatus"; + return media_status; + } + + static const char* media_format_to_readable(const char *media_format) { + if(strcmp(media_format, "TV") == 0) + return "TV"; + if(strcmp(media_format, "TV_SHORT") == 0) + return "TV short"; + if(strcmp(media_format, "MOVIE") == 0) + return "Movie"; + if(strcmp(media_format, "SPECIAL") == 0) + return "Special"; + if(strcmp(media_format, "OVA") == 0) + return "OVA"; + if(strcmp(media_format, "ONA") == 0) + return "ONA"; + if(strcmp(media_format, "MUSIC") == 0) + return "Music"; + if(strcmp(media_format, "MANGA") == 0) + return "Manga"; + if(strcmp(media_format, "NOVEL") == 0) + return "Novel"; + if(strcmp(media_format, "ONE_SHOT") == 0) + return "One shot"; + return media_format; + } + + static std::string json_string_array_to_string(const Json::Value &json_arr) { + std::string result; + if(!json_arr.isArray()) + return result; + + for(const Json::Value &json_item : json_arr) { + if(!json_item.isString()) + continue; + if(!result.empty()) + result += ", "; + result += json_item.asString(); + } + + return result; + } + + static void description_remove_html(std::string &description) { + string_replace_all(description, "", ""); + string_replace_all(description, "", ""); + string_replace_all(description, "", ""); + string_replace_all(description, "", ""); + string_replace_all(description, "
", ""); + html_unescape_sequences(description); + } + + enum class ThumbnailSize { + MEDIUM, + LARGE + }; + + static const char* thumbnail_size_to_cover_image_type(ThumbnailSize thumbnail_size) { + switch(thumbnail_size) { + case ThumbnailSize::MEDIUM: return "medium"; + case ThumbnailSize::LARGE: return "large"; + } + return ""; + } + + // TODO: Somehow get the correct thumbnail size? + static sf::Vector2i thumbnail_size_get_prediced_size(ThumbnailSize thumbnail_size) { + switch(thumbnail_size) { + case ThumbnailSize::MEDIUM: return {100, 158}; + case ThumbnailSize::LARGE: return {215, 304}; + } + return {0, 0}; + } + + // Returns nullptr on error + static std::shared_ptr media_json_to_body_item(const Json::Value &media_json, ThumbnailSize thumbnail_size, AniListMediaType &media_type) { + if(!media_json.isObject()) + return nullptr; + + const Json::Value &type_json = media_json["type"]; + const Json::Value &title_json = media_json["title"]; + if(!type_json.isString() || !title_json.isObject()) + return nullptr; + + const Json::Value &id_json = media_json["id"]; + const Json::Value &cover_image_json = media_json["coverImage"]; + const Json::Value &synonyms_json = media_json["synonyms"]; + const Json::Value &start_date_json = media_json["startDate"]; + const Json::Value &end_date_json = media_json["endDate"]; + const Json::Value &average_score_json = media_json["averageScore"]; + const Json::Value &format_json = media_json["format"]; + const Json::Value &status_json = media_json["status"]; + const Json::Value &episodes_json = media_json["episodes"]; + const Json::Value &chapters_json = media_json["chapters"]; + const Json::Value &genres_json = media_json["genres"]; + const Json::Value &description_json = media_json["description"]; + + const Json::Value &romaji_title_json = title_json["romaji"]; + if(!romaji_title_json.isString()) + return nullptr; + + if(strcmp(type_json.asCString(), "ANIME") == 0) + media_type = AniListMediaType::ANIME; + else if(strcmp(type_json.asCString(), "MANGA") == 0) + media_type = AniListMediaType::MANGA; + else + return nullptr; + + std::string title = romaji_title_json.asString(); + if(format_json.isString()) { + title += " ("; + title += media_format_to_readable(format_json.asCString()); + title += ")"; + } + + std::string description; + if(synonyms_json.isArray() && synonyms_json.size() > 0) + description += "Synonyms: " + json_string_array_to_string(synonyms_json); + + std::string start_date_str = fuzzy_date_to_str(start_date_json); + std::string end_date_str = fuzzy_date_to_str(end_date_json); + const char *aired_prefix = nullptr; + if(media_type == AniListMediaType::ANIME) + aired_prefix = "Aired"; + else if(media_type == AniListMediaType::MANGA) + aired_prefix = "Published"; + + if(!start_date_str.empty()) { + if(!description.empty()) + description += '\n'; + description += aired_prefix; + description += ": " + std::move(start_date_str); + + if(end_date_str.empty()) + end_date_str = "?"; + + description += " to " + std::move(end_date_str); + } + + if(average_score_json.isInt()) { + if(!description.empty()) + description += '\n'; + description += "Score: "; + + char buffer[32]; + int len = snprintf(buffer, sizeof(buffer), "%.2f", (double)average_score_json.asInt() / 10.0); + if(len > 0) + description.append(buffer, len); + } + + if(episodes_json.isInt()) { + if(!description.empty()) + description += '\n'; + description += "Episodes: " + std::to_string(episodes_json.asInt()); + } + + if(chapters_json.isInt()) { + if(!description.empty()) + description += '\n'; + description += "Chapters: " + std::to_string(chapters_json.asInt()); + } + + if(status_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Status: "; + description += media_status_to_readable(status_json.asCString(), media_type); + } + + if(genres_json.isArray() && genres_json.size() > 0) { + if(!description.empty()) + description += '\n'; + description += "Genres: " + json_string_array_to_string(genres_json); + } + + if(description_json.isString()) { + if(!description.empty()) + description += "\n\n"; + description += "Synopsis: "; + + std::string synopsis = description_json.asString(); + description_remove_html(synopsis); + description += std::move(synopsis); + } + + auto body_item = BodyItem::create(""); + if(id_json.isInt()) + body_item->url = std::to_string(id_json.asInt()); + body_item->set_author(std::move(title)); + if(!description.empty()) { + body_item->set_description(std::move(description)); + body_item->set_description_color(get_current_theme().faded_text_color); + } + if(cover_image_json.isObject()) { + const Json::Value &cover_img_sized_json = cover_image_json[thumbnail_size_to_cover_image_type(thumbnail_size)]; + if(cover_img_sized_json.isString()) + body_item->thumbnail_url = cover_img_sized_json.asString(); + } + body_item->thumbnail_size = thumbnail_size_get_prediced_size(thumbnail_size); + + return body_item; + } + + SearchResult AniListSearchPage::search_page(const std::string &str, int page, BodyItems &result_items) { + Json::Value variables_json(Json::objectValue); + variables_json["page"] = page; + variables_json["perPage"] = 20; + variables_json["search"] = str; + + Json::Value request_json(Json::objectValue); + request_json["query"] = search_query_graphql; + request_json["variables"] = std::move(variables_json); + + Json::StreamWriterBuilder json_builder; + json_builder["commentStyle"] = "None"; + json_builder["indentation"] = ""; + + std::vector additional_args = { + { "-X", "POST" }, + { "-H", "content-type: application/json" }, + { "--data-binary", Json::writeString(json_builder, request_json) } + }; + + Json::Value json_root; + std::string err_msg; + DownloadResult download_result = download_json(json_root, "https://graphql.anilist.co", std::move(additional_args), true, &err_msg); + if(download_result != DownloadResult::OK) return download_result_to_search_result(download_result); + + if(!json_root.isObject()) + return SearchResult::ERR; + + const Json::Value &errors_json = json_root["errors"]; + if(errors_json.isArray() && errors_json.size() > 0) { + const Json::Value &error_json = errors_json[0]; + if(error_json.isObject()) { + const Json::Value &error_message_json = error_json["message"]; + if(error_message_json.isString()) { + show_notification("QuickMedia", "AniList search failed, error: " + error_message_json.asString(), Urgency::CRITICAL); + return SearchResult::ERR; + } + } + } + + const Json::Value &data_json = json_root["data"]; + if(!data_json.isObject()) + return SearchResult::ERR; + + const Json::Value &page_json = data_json["Page"]; + if(!page_json.isObject()) + return SearchResult::ERR; + + const Json::Value &media_list_json = page_json["media"]; + if(!media_list_json.isArray()) + return SearchResult::ERR; + + BodyItems anime_items; + BodyItems manga_items; + for(const Json::Value &media_json : media_list_json) { + AniListMediaType media_type; + auto body_item = media_json_to_body_item(media_json, ThumbnailSize::MEDIUM, media_type); + if(!body_item) + continue; + + if(media_type == AniListMediaType::ANIME) + anime_items.push_back(std::move(body_item)); + else if(media_type == AniListMediaType::MANGA) + manga_items.push_back(std::move(body_item)); + } + + if(anime_items.empty() && manga_items.empty()) + return SearchResult::OK; + + auto anime_title_item = BodyItem::create(""); + anime_title_item->set_author("------------------------ Anime ------------------------"); + result_items.push_back(std::move(anime_title_item)); + result_items.insert(result_items.end(), std::move_iterator(anime_items.begin()), std::move_iterator(anime_items.end())); + + auto manga_title_item = BodyItem::create(""); + manga_title_item->set_author("------------------------ Manga ------------------------"); + result_items.push_back(std::move(manga_title_item)); + result_items.insert(result_items.end(), std::move_iterator(manga_items.begin()), std::move_iterator(manga_items.end())); + + return SearchResult::OK; + } + + SearchResult AniListSearchPage::search(const std::string &str, BodyItems &result_items) { + return search_page(str, 1, result_items); + } + + PluginResult AniListSearchPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + if(url.empty()) + return PluginResult::OK; + + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + return PluginResult::OK; + } + + PluginResult AniListSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { + return search_result_to_plugin_result(search_page(str, 1 + page, result_items)); + } + + PluginResult AniListRelatedPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + if(url.empty()) + return PluginResult::OK; + + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + return PluginResult::OK; + } + + PluginResult AniListRelatedPage::lazy_fetch(BodyItems &result_items) { + return PluginResult::OK; + } + + PluginResult AniListDetailsPage::submit(const std::string&, const std::string&, std::vector&) { + return PluginResult::OK; + } + + PluginResult AniListDetailsPage::lazy_fetch(BodyItems &result_items) { + Json::Value variables_json(Json::objectValue); + variables_json["id"] = atoi(id.c_str()); + + Json::Value request_json(Json::objectValue); + request_json["query"] = details_query_graphql; + request_json["variables"] = std::move(variables_json); + + Json::StreamWriterBuilder json_builder; + json_builder["commentStyle"] = "None"; + json_builder["indentation"] = ""; + + std::vector additional_args = { + { "-X", "POST" }, + { "-H", "content-type: application/json" }, + { "--data-binary", Json::writeString(json_builder, request_json) } + }; + + Json::Value json_root; + std::string err_msg; + DownloadResult download_result = download_json(json_root, "https://graphql.anilist.co", std::move(additional_args), true, &err_msg); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + if(!json_root.isObject()) + return PluginResult::ERR; + + const Json::Value &errors_json = json_root["errors"]; + if(errors_json.isArray() && errors_json.size() > 0) { + const Json::Value &error_json = errors_json[0]; + if(error_json.isObject()) { + const Json::Value &error_message_json = error_json["message"]; + if(error_message_json.isString()) { + show_notification("QuickMedia", "AniList search failed, error: " + error_message_json.asString(), Urgency::CRITICAL); + return PluginResult::ERR; + } + } + } + + const Json::Value &data_json = json_root["data"]; + if(!data_json.isObject()) + return PluginResult::ERR; + + const Json::Value &media_json = data_json["Media"]; + if(!media_json.isObject()) + return PluginResult::ERR; + + AniListMediaType media_type; + auto body_item = media_json_to_body_item(media_json, ThumbnailSize::LARGE, media_type); + if(!body_item) + return PluginResult::ERR; + + result_items.push_back(std::move(body_item)); + return PluginResult::OK; + } + + PluginResult AniListRecommendationsPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + if(url.empty()) + return PluginResult::OK; + + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + return PluginResult::OK; + } + + PluginResult AniListRecommendationsPage::lazy_fetch(BodyItems &result_items) { + return PluginResult::OK; + } +} \ No newline at end of file -- cgit v1.2.3