From 5cc735b22570f1667d62958e59ce4910b529f5af Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 16 Aug 2021 21:13:24 +0200 Subject: Add MyAnimeList (wip) --- src/plugins/MyAnimeList.cpp | 311 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 src/plugins/MyAnimeList.cpp (limited to 'src/plugins/MyAnimeList.cpp') diff --git a/src/plugins/MyAnimeList.cpp b/src/plugins/MyAnimeList.cpp new file mode 100644 index 0000000..dd80297 --- /dev/null +++ b/src/plugins/MyAnimeList.cpp @@ -0,0 +1,311 @@ +#include "../../plugins/MyAnimeList.hpp" +#include "../../include/Theme.hpp" +#include "../../include/NetUtils.hpp" +#include "../../include/StringUtils.hpp" +#include + +namespace QuickMedia { + // Returns {0, 0} if unknown + static sf::Vector2i thumbnail_url_get_resolution(const std::string &url) { + const size_t index = url.find("/r/"); + if(index == std::string::npos) + return {0, 0}; + + const size_t width_index = index + 3; + const size_t x_index = url.find('x', width_index); + if(x_index == std::string::npos) + return {0, 0}; + + const size_t height_index = x_index + 1; + const size_t size_end_index = url.find('/', height_index); + if(size_end_index == std::string::npos) + return {0, 0}; + + sf::Vector2i size; + if(!to_num(url.c_str() + width_index, (x_index - width_index), size.x) || !to_num(url.c_str() + height_index, (size_end_index - height_index), size.y)) + return {0, 0}; + + return size; + } + + static std::shared_ptr search_item_to_body_item(const Json::Value &search_item_json) { + if(!search_item_json.isObject()) + return nullptr; + + const Json::Value &name_json = search_item_json["name"]; + const Json::Value &url_json = search_item_json["url"]; + const Json::Value &payload_json = search_item_json["payload"]; + if(!name_json.isString() || !url_json.isString() || !payload_json.isObject()) + return nullptr; + + std::string name = name_json.asString(); + const Json::Value &media_type_json = payload_json["media_type"]; + if(media_type_json.isString()) + name += " (" + media_type_json.asString() + ")"; + + auto body_item = BodyItem::create(""); + body_item->url = url_json.asString(); + body_item->set_author(std::move(name)); + + const Json::Value &image_url_json = search_item_json["thumbnail_url"]; + body_item->thumbnail_size = {116, 76}; + if(image_url_json.isString()) { + body_item->thumbnail_url = image_url_json.asString(); + body_item->thumbnail_size = thumbnail_url_get_resolution(body_item->thumbnail_url); + } + + std::string description; + const Json::Value &aired_json = payload_json["aired"]; + if(aired_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Aired: " + aired_json.asString(); + } + + const Json::Value &published_json = payload_json["published"]; + if(published_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Published: " + published_json.asString(); + } + + const Json::Value &score_json = payload_json["score"]; + if(score_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Score: " + score_json.asString(); + } + + const Json::Value &status_json = payload_json["status"]; + if(status_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Status: " + status_json.asString(); + } + + if(!description.empty()) { + body_item->set_description(std::move(description)); + body_item->set_description_color(get_current_theme().faded_text_color); + } + + return body_item; + } + + SearchResult MyAnimeListSearchPage::search(const std::string &str, BodyItems &result_items) { + std::string url = "https://myanimelist.net/search/prefix.json?type=all&keyword="; + url += url_param_encode(str) + "&v=1"; + + Json::Value json_root; + DownloadResult download_result = download_json(json_root, url, {}, true); + if(download_result != DownloadResult::OK) return download_result_to_search_result(download_result); + + if(!json_root.isObject()) + return SearchResult::ERR; + + const Json::Value &categories_json = json_root["categories"]; + if(!categories_json.isArray()) + return SearchResult::ERR; + + BodyItems anime_items; + BodyItems manga_items; + for(const Json::Value &category_json : categories_json) { + if(!category_json.isObject()) + continue; + + const Json::Value &type_json = category_json["type"]; + const Json::Value &items_json = category_json["items"]; + if(!type_json.isString() || !items_json.isArray()) + continue; + + if(strcmp(type_json.asCString(), "anime") == 0) { + for(const Json::Value &item_json : items_json) { + auto body_item = search_item_to_body_item(item_json); + if(body_item) + anime_items.push_back(std::move(body_item)); + } + } else if(strcmp(type_json.asCString(), "manga") == 0) { + for(const Json::Value &item_json : items_json) { + auto body_item = search_item_to_body_item(item_json); + if(body_item) + manga_items.push_back(std::move(body_item)); + } + } + } + + 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; + } + + PluginResult MyAnimeListSearchPage::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(false, true), std::make_unique(program, url), nullptr }); + return PluginResult::OK; + } + + PluginResult MyAnimeListDetailsPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + return PluginResult::OK; + } + + PluginResult MyAnimeListDetailsPage::lazy_fetch(BodyItems &result_items) { + std::string website_data; + DownloadResult download_result = download_to_string(url, website_data, {}, true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + std::string thumbnail_url; + std::string description; + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); + if(result != 0) + return PluginResult::ERR; + + quickmedia_html_find_nodes_xpath(&html_search, "//img[itemprop='image']", + [](QuickMediaMatchNode *node, void *userdata) { + std::string *thumbnail_url = (std::string*)userdata; + QuickMediaStringView data_src = quickmedia_html_node_get_attribute_value(node, "data-src"); + if(data_src.data) { + thumbnail_url->assign(data_src.data, data_src.size); + html_unescape_sequences(*thumbnail_url); + } + return 1; + }, &thumbnail_url); + + quickmedia_html_find_nodes_xpath(&html_search, "//p[itemprop='description']", + [](QuickMediaMatchNode *node, void *userdata) { + std::string *description = (std::string*)userdata; + QuickMediaStringView text = quickmedia_html_node_get_text(node); + if(text.data) { + *description = "Synopsis:\n"; + description->append(text.data, text.size); + html_unescape_sequences(*description); + } + return 1; + }, &description); + + quickmedia_html_search_deinit(&html_search); + + auto synopsis_body_item = BodyItem::create(""); + synopsis_body_item->set_description(std::move(description)); + synopsis_body_item->thumbnail_url = std::move(thumbnail_url); + synopsis_body_item->thumbnail_size = {225, 337}; + result_items.push_back(std::move(synopsis_body_item)); + + return PluginResult::OK; + } + + PluginResult MyAnimeListRecommendationsPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(false, true), std::make_unique(program, url), nullptr }); + return PluginResult::OK; + } + + static std::string img_alt_get_title(const std::string &alt) { + size_t index = alt.find(':'); + if(index == std::string::npos) + return alt; + return alt.substr(index + 2); + } + + static QuickMediaStringView quickmedia_html_node_get_attribute_value(QuickMediaHtmlNode *node, const char *attribute_name) { + QuickMediaMatchNode match_node; + match_node.node = node; + return quickmedia_html_node_get_attribute_value(&match_node, attribute_name); + } + + PluginResult MyAnimeListRecommendationsPage::lazy_fetch(BodyItems &result_items) { + std::string website_data; + DownloadResult download_result = download_to_string(url + "/userrecs", website_data, {}, true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); + if(result != 0) + return PluginResult::ERR; + + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='picSurround']/a", + [](QuickMediaMatchNode *node, void *userdata) { + BodyItems *result_items = (BodyItems*)userdata; + QuickMediaStringView href = quickmedia_html_node_get_attribute_value(node, "href"); + if(!href.data) + return 0; + + auto body_item = BodyItem::create(""); + body_item->url.assign(href.data, href.size); + html_unescape_sequences(body_item->url); + result_items->push_back(body_item); + if(!node->node->first_child || !node->node->first_child->node.name.data || node->node->first_child->node.name.size != 3 || memcmp(node->node->first_child->node.name.data, "img", 3) != 0) + return 0; + + QuickMediaStringView data_src = quickmedia_html_node_get_attribute_value(&node->node->first_child->node, "data-src"); + QuickMediaStringView alt = quickmedia_html_node_get_attribute_value(&node->node->first_child->node, "alt"); + if(data_src.data && alt.data) { + std::string title = img_alt_get_title(std::string(alt.data, alt.size)); + html_unescape_sequences(title); + body_item->set_author(std::move(title)); + + body_item->thumbnail_url.assign(data_src.data, data_src.size); + html_unescape_sequences(body_item->thumbnail_url); + + if(body_item->thumbnail_url.empty()) + body_item->thumbnail_size = {50, 70}; + else + body_item->thumbnail_size = thumbnail_url_get_resolution(body_item->thumbnail_url); + } + return 0; + }, &result_items); + + BodyItemContext body_item_image_context; + body_item_image_context.body_items = &result_items; + body_item_image_context.index = 0; + + // TODO: Fix, incorrect descriptions! + #if 0 + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='*detail-user-recs-text']", + [](QuickMediaMatchNode *node, void *userdata) { + BodyItemContext *item_data = (BodyItemContext*)userdata; + QuickMediaStringView text = quickmedia_html_node_get_text(node); + if(text.data && item_data->index < item_data->body_items->size()) { + std::string description(text.data, text.size); + html_unescape_sequences(description); + (*item_data->body_items)[item_data->index]->set_description(std::move(description)); + (*item_data->body_items)[item_data->index]->set_description_color(get_current_theme().faded_text_color); + item_data->index++; + } + return 0; + }, &body_item_image_context); + #endif + + /* + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='borderClass']//div[class='*detail-user-recs-text']", + [](QuickMediaMatchNode *node, void *userdata) { + BodyItems *result_items = (BodyItems*)userdata; + QuickMediaStringView text = quickmedia_html_node_get_text(node); + if(text.data) { + std::string description(text.data, text.size); + html_unescape_sequences(description); + + auto body_item = BodyItem::create(""); + body_item->set_description(std::move(description)); + body_item->set_description_color(get_current_theme().faded_text_color); + result_items->push_back(std::move(body_item)); + } + return 0; + }, &result_items); + */ + + quickmedia_html_search_deinit(&html_search); + return PluginResult::OK; + } +} \ No newline at end of file -- cgit v1.2.3