#include "../../plugins/MyAnimeList.hpp" #include "../../include/Theme.hpp" #include "../../include/NetUtils.hpp" #include "../../include/StringUtils.hpp" #include #include namespace QuickMedia { // Returns {0, 0} if unknown static mgl::vec2i 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}; mgl::vec2i 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_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)); } } } if(!anime_items.empty()) { auto anime_title_item = BodyItem::create("", false); 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())); } if(!manga_items.empty()) { auto manga_title_item = BodyItem::create("", false); 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 SubmitArgs &args, std::vector &result_tabs) { if(args.url.empty()) return PluginResult::OK; result_tabs.push_back({ create_body(), std::make_unique(program, args.url), nullptr }); result_tabs.push_back({ create_body(false, true), std::make_unique(program, args.url), nullptr }); return PluginResult::OK; } PluginResult MyAnimeListDetailsPage::submit(const SubmitArgs &args, 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 SubmitArgs &args, std::vector &result_tabs) { result_tabs.push_back({ create_body(), std::make_unique(program, args.url), nullptr }); result_tabs.push_back({ create_body(false, true), std::make_unique(program, args.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_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_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; } }