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/MangaGeneric.cpp | 2 + src/plugins/Mangadex.cpp | 1 + src/plugins/Manganelo.cpp | 2 + src/plugins/Matrix.cpp | 13 +- src/plugins/MediaGeneric.cpp | 10 +- src/plugins/MyAnimeList.cpp | 311 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 328 insertions(+), 11 deletions(-) create mode 100644 src/plugins/MyAnimeList.cpp (limited to 'src/plugins') diff --git a/src/plugins/MangaGeneric.cpp b/src/plugins/MangaGeneric.cpp index 90da0c2..738c6dc 100644 --- a/src/plugins/MangaGeneric.cpp +++ b/src/plugins/MangaGeneric.cpp @@ -104,8 +104,10 @@ namespace QuickMedia { && field_value.data && (!merge_userdata->field_contains || string_view_contains(field_value, merge_userdata->field_contains))) { std::string field_stripped(field_value.data, field_value.size); + html_unescape_sequences(field_stripped); if(merge_userdata->type == MergeType::THUMBNAIL) { (*body_item_image_context.body_items)[body_item_image_context.index]->thumbnail_url = std::move(field_stripped); + (*body_item_image_context.body_items)[body_item_image_context.index]->thumbnail_size = {101, 141}; } else if(merge_userdata->type == MergeType::DESCRIPTION) { const char *prefix = merge_userdata->desc_prefix ? merge_userdata->desc_prefix : ""; (*body_item_image_context.body_items)[body_item_image_context.index]->set_description(prefix + std::move(field_stripped)); diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp index 98683c1..5b8debd 100644 --- a/src/plugins/Mangadex.cpp +++ b/src/plugins/Mangadex.cpp @@ -138,6 +138,7 @@ namespace QuickMedia { continue; body_item->thumbnail_url = "https://uploads.mangadex.org/covers/" + body_item->url + "/" + filename_json.asString() + ".256.jpg"; + body_item->thumbnail_size = {101, 141}; } return PluginResult::OK; diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index d3d7bfa..90003cb 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -139,6 +139,7 @@ namespace QuickMedia { Json::Value image = child.get("image", ""); if(image.isString() && image.asCString()[0] != '\0') item->thumbnail_url = image.asString(); + item->thumbnail_size = {101, 141}; result_items.push_back(std::move(item)); } } @@ -228,6 +229,7 @@ namespace QuickMedia { QuickMediaStringView src = quickmedia_html_node_get_attribute_value(node, "src"); if(src.data && item_data->index < item_data->body_items->size()) { (*item_data->body_items)[item_data->index]->thumbnail_url.assign(src.data, src.size); + (*item_data->body_items)[item_data->index]->thumbnail_size = {101, 141}; item_data->index++; } return 0; diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index fce4135..775d172 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -1173,6 +1173,7 @@ namespace QuickMedia { assert(!this->delegate); assert(!access_token.empty()); // Need to be logged in + assert(delegate); this->delegate = delegate; Path matrix_cache_dir = get_cache_dir().join("matrix"); @@ -2033,13 +2034,11 @@ namespace QuickMedia { } } - if(delegate) { - bool cache_sync = sync_is_cache; - bool is_initial_sync = next_batch.empty(); - ui_thread_tasks.push([this, room_data, cache_sync, new_messages{std::move(new_messages)}, is_initial_sync, message_dir]{ - delegate->room_add_new_messages(room_data, new_messages, is_initial_sync, cache_sync, message_dir); - }); - } + bool cache_sync = sync_is_cache; + bool is_initial_sync = next_batch.empty(); + ui_thread_tasks.push([this, room_data, cache_sync, new_messages{std::move(new_messages)}, is_initial_sync, message_dir]{ + delegate->room_add_new_messages(room_data, new_messages, is_initial_sync, cache_sync, message_dir); + }); return num_new_messages; } diff --git a/src/plugins/MediaGeneric.cpp b/src/plugins/MediaGeneric.cpp index d536a09..bcb8dc3 100644 --- a/src/plugins/MediaGeneric.cpp +++ b/src/plugins/MediaGeneric.cpp @@ -37,7 +37,7 @@ namespace QuickMedia { } } - static PluginResult fetch_page_results(const std::string &url, const std::string &website_url, const std::vector &text_queries, const std::vector &thumbnail_queries, MediaRelatedCustomHandler *custom_handler, BodyItems &result_items, bool cloudflare_bypass) { + static PluginResult fetch_page_results(const std::string &url, const std::string &website_url, const std::vector &text_queries, const std::vector &thumbnail_queries, sf::Vector2i thumbnail_max_size, MediaRelatedCustomHandler *custom_handler, BodyItems &result_items, bool cloudflare_bypass) { std::vector args; if(!website_url.empty()) args.push_back({ "-H", "referer: " + website_url }); @@ -55,6 +55,7 @@ namespace QuickMedia { auto body_item = BodyItem::create(media_related_item.title); body_item->url = std::move(media_related_item.url); body_item->thumbnail_url = std::move(media_related_item.thumbnail_url); + body_item->thumbnail_size = thumbnail_max_size; result_items.push_back(std::move(body_item)); } body_items_prepend_website_url(result_items, website_url); @@ -92,10 +93,11 @@ namespace QuickMedia { assert(thumbnail_query.html_query && thumbnail_query.field_name); if(thumbnail_query.html_query && thumbnail_query.field_name) { size_t index = 0; - result = quickmedia_html_find_nodes_xpath(&html_search, thumbnail_query.html_query, [&thumbnail_query, &result_items, &index](QuickMediaMatchNode *node) { + result = quickmedia_html_find_nodes_xpath(&html_search, thumbnail_query.html_query, [&thumbnail_query, &result_items, &index, thumbnail_max_size](QuickMediaMatchNode *node) { QuickMediaStringView field_value = html_attr_or_inner_text(node, thumbnail_query.field_name); if(index < result_items.size() && field_value.data && (!thumbnail_query.field_contains || string_view_contains(field_value, thumbnail_query.field_contains))) { result_items[index]->thumbnail_url.assign(field_value.data, field_value.size); + result_items[index]->thumbnail_size = thumbnail_max_size; ++index; } }); @@ -133,7 +135,7 @@ namespace QuickMedia { std::string url = search_query.search_template; string_replace_all(url, "%s", url_param_encode(str)); string_replace_all(url, "%p", std::to_string(search_query.page_start + page)); - return fetch_page_results(url, website_url, text_queries, thumbnail_queries, nullptr, result_items, cloudflare_bypass); + return fetch_page_results(url, website_url, text_queries, thumbnail_queries, thumbnail_max_size, nullptr, result_items, cloudflare_bypass); } PluginResult MediaGenericSearchPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { @@ -142,7 +144,7 @@ namespace QuickMedia { } PluginResult MediaGenericSearchPage::get_related_media(const std::string &url, BodyItems &result_items) { - return fetch_page_results(url, website_url, related_media_text_queries, related_media_thumbnail_queries, &related_custom_handler, result_items, cloudflare_bypass); + return fetch_page_results(url, website_url, related_media_text_queries, related_media_thumbnail_queries, thumbnail_max_size, &related_custom_handler, result_items, cloudflare_bypass); } MediaGenericSearchPage& MediaGenericSearchPage::search_handler(const char *search_template, int page_start) { 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