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/Body.cpp | 6 +- src/NetUtils.cpp | 16 +-- src/QuickMedia.cpp | 10 +- src/StringUtils.cpp | 20 +++ 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 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 362 insertions(+), 29 deletions(-) create mode 100644 src/plugins/MyAnimeList.cpp (limited to 'src') diff --git a/src/Body.cpp b/src/Body.cpp index c3cbcda..b7424c5 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -115,8 +115,8 @@ namespace QuickMedia { embedded_item_load_text = sf::Text("", *FontLoader::get_font(FontLoader::FontType::LATIN), body_spacing[body_theme].embedded_item_font_size); progress_text.setFillColor(get_current_theme().text_color); replies_text.setFillColor(get_current_theme().replies_text_color); - thumbnail_max_size.x = 250; - thumbnail_max_size.y = 141; + thumbnail_max_size.x = 600; + thumbnail_max_size.y = 337; sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); loading_icon.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); render_selected_item_bg = !is_touch_enabled(); @@ -923,7 +923,7 @@ namespace QuickMedia { if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0) content_size = clamp_to_size(sf::Vector2i(std::floor(item->thumbnail_size.x * get_ui_scale()), std::floor(item->thumbnail_size.y * get_ui_scale())), thumbnail_max_size_scaled); else - content_size = thumbnail_max_size_scaled; + content_size = sf::Vector2i(250 * get_ui_scale(), 141 * get_ui_scale()); return content_size; } diff --git a/src/NetUtils.cpp b/src/NetUtils.cpp index 28256cb..0f957d5 100644 --- a/src/NetUtils.cpp +++ b/src/NetUtils.cpp @@ -34,17 +34,6 @@ namespace QuickMedia { std::string unescaped_str; }; - static bool to_num(const char *str, size_t size, int &num) { - num = 0; - for(size_t i = 0; i < size; ++i) { - const char num_c = str[i] - '0'; - if(num_c < 0 || num_c > 9) - return false; - num = (num * 10) + num_c; - } - return true; - } - static void html_unescape_sequence_numbers(std::string &str) { size_t index = 0; while(true) { @@ -69,10 +58,13 @@ namespace QuickMedia { void html_unescape_sequences(std::string &str) { html_unescape_sequence_numbers(str); - const std::array unescape_sequences = { + // TODO: Use string find and find & and ; instead of string_replace_all + const std::array unescape_sequences = { HtmlUnescapeSequence { """, "\"" }, HtmlUnescapeSequence { "<", "<" }, HtmlUnescapeSequence { ">", ">" }, + HtmlUnescapeSequence { "—", "—" }, + HtmlUnescapeSequence { " ", " " }, HtmlUnescapeSequence { "&", "&" } // This should be last, to not accidentally replace a new sequence caused by replacing this }; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index d006f3d..9b982a1 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -14,6 +14,7 @@ #include "../plugins/Saucenao.hpp" #include "../plugins/Info.hpp" #include "../plugins/HotExamples.hpp" +#include "../plugins/MyAnimeList.hpp" #include "../include/Scale.hpp" #include "../include/Program.hpp" #include "../include/VideoPlayer.hpp" @@ -77,6 +78,7 @@ static const std::pair valid_plugins[] = { std::make_pair("4chan", "4chan_logo.png"), std::make_pair("nyaa.si", "nyaa_si_logo.png"), std::make_pair("matrix", "matrix_logo.png"), + std::make_pair("mal", "mal_logo.png"), std::make_pair("hotexamples", nullptr), std::make_pair("file-manager", nullptr), std::make_pair("stdin", nullptr), @@ -308,7 +310,7 @@ namespace QuickMedia { return PluginResult::OK; } - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } void add_option(Body *body, std::string title, std::string description, OptionsPageHandler handler) { assert(handler); @@ -349,7 +351,7 @@ namespace QuickMedia { static void usage() { fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--use-system-mpv-config] [--dir ] [-e ] [youtube-url]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); + fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, mal, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); fprintf(stderr, " --use-system-mpv-config Use system mpv config instead of no config. Disabled by default\n"); fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n"); @@ -1091,6 +1093,7 @@ namespace QuickMedia { pipe_body->set_items({ create_launcher_body_item("4chan", "4chan", resources_root + "icons/4chan_launcher.png"), create_launcher_body_item("Hot Examples", "hotexamples", ""), + create_launcher_body_item("MyAnimeList", "mal", resources_root + "images/mal_logo.png"), create_launcher_body_item("Manga (all)", "manga", ""), create_launcher_body_item("Mangadex", "mangadex", resources_root + "icons/mangadex_launcher.png"), create_launcher_body_item("Mangakatana", "mangakatana", resources_root + "icons/mangakatana_launcher.png"), @@ -1204,6 +1207,8 @@ namespace QuickMedia { hot_examples_front_page_fill(body_items); body->set_items(std::move(body_items)); tabs.push_back(Tab{std::move(body), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "mal") == 0) { + tabs.push_back(Tab{create_body(), std::make_unique(this), create_search_bar("Search...", 400)}); } else if(strcmp(plugin_name, "file-manager") == 0) { auto file_manager_page = std::make_unique(this, fm_mime_type, file_selection_handler); if(!file_manager_page->set_current_directory(file_manager_start_dir)) @@ -1685,7 +1690,6 @@ namespace QuickMedia { for(Tab &tab : tabs) { if(tab.body->attach_side == AttachSide::BOTTOM) tab.body->select_last_item(); - tab.body->thumbnail_max_size = tab.page->get_thumbnail_max_size(); tab.page->on_navigate_to_page(tab.body.get()); } diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 9820d29..5dfeca9 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -129,4 +129,24 @@ namespace QuickMedia { ++str2; } } + + bool to_num(const char *str, size_t size, int &num) { + size_t i = 0; + const bool is_negative = size > 0 && str[0] == '-'; + if(is_negative) + i = 1; + + num = 0; + for(; i < size; ++i) { + const char num_c = str[i] - '0'; + if(num_c < 0 || num_c > 9) + return false; + num = (num * 10) + num_c; + } + + if(is_negative) + num = -num; + + return true; + } } \ No newline at end of file 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