#include "../../plugins/Manganelo.hpp" #include "../../include/Notification.hpp" #include "../../include/NetUtils.hpp" #include "../../include/Theme.hpp" #include "../../include/StringUtils.hpp" #include #include namespace QuickMedia { static bool string_view_contains(const QuickMediaStringView str, const char *sub) { return memmem(str.data, str.size, sub, strlen(sub)); } // Returns true if modified static bool remove_html_span(std::string &str) { size_t open_tag_start = str.find("', open_tag_start + 5); if(open_tag_end == std::string::npos) return false; str.erase(open_tag_start, open_tag_end - open_tag_start + 1); size_t close_tag = str.find(""); if(close_tag == std::string::npos) return true; str.erase(close_tag, 7); return true; } static PluginResult redirect_check(std::string &website_data) { size_t idx = website_data.find("window.location.assign(\""); if(idx == std::string::npos) return PluginResult::OK; idx += 24; size_t end = website_data.find('"', idx); if(end == std::string::npos) return PluginResult::OK; std::string url = website_data.substr(idx, end - idx); website_data.clear(); if(download_to_string(url, website_data, {CommandArg { "-H", "referer: https://manganelo.com/" }}, true) != DownloadResult::OK) return PluginResult::NET_ERR; return PluginResult::OK; } static PluginResult redirect_migrated_url(std::string &url, QuickMediaHtmlSearch &html_search) { bool page_not_found = false; quickmedia_html_find_nodes_xpath(&html_search, "//div[class='panel-not-found']", [](QuickMediaMatchNode*, void *userdata) { bool *page_not_found = (bool*)userdata; *page_not_found = true; return 0; }, &page_not_found); if(!page_not_found) return PluginResult::OK; if(!url.empty() && url.back() == '/') url.pop_back(); string_replace_all(url, "manganelo", "mangakakalot"); std::string website_data; if(download_to_string(url, website_data, {CommandArg { "-H", "referer: https://manganelo.com/" }}, true) != DownloadResult::OK) return PluginResult::NET_ERR; redirect_check(website_data); quickmedia_html_search_deinit(&html_search); memset(&html_search, 0, sizeof(html_search)); int result = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); if(result != 0) return PluginResult::ERR; return PluginResult::OK; } static PluginResult submit_manga(Page *page, const SubmitArgs &args, std::vector &result_tabs) { BodyItems chapters_items; std::vector creators; std::string url = args.url; string_replace_all(url, "mangakakalot", "manganelo"); std::string website_data; if(download_to_string(url, website_data, {CommandArg { "-H", "referer: https://manganelo.com/" }}, true) != DownloadResult::OK) return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); if(result != 0) goto cleanup; if(redirect_migrated_url(url, html_search) != PluginResult::OK) { result = -1; goto cleanup; } result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='row-content-chapter']//a", [](QuickMediaMatchNode *node, void *userdata) { auto *item_data = (BodyItems*)userdata; QuickMediaStringView href = quickmedia_html_node_get_attribute_value(node, "href"); QuickMediaStringView text = quickmedia_html_node_get_text(node); if(href.data && text.data) { auto item = BodyItem::create(std::string(text.data, text.size)); item->url.assign(href.data, href.size); item_data->push_back(std::move(item)); } return 0; }, &chapters_items); result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='chapter-list']//a", [](QuickMediaMatchNode *node, void *userdata) { auto *item_data = (BodyItems*)userdata; QuickMediaStringView href = quickmedia_html_node_get_attribute_value(node, "href"); QuickMediaStringView text = quickmedia_html_node_get_text(node); if(href.data && text.data) { auto item = BodyItem::create(std::string(text.data, text.size)); item->url.assign(href.data, href.size); item_data->push_back(std::move(item)); } return 0; }, &chapters_items); BodyItemContext body_item_context; body_item_context.body_items = &chapters_items; body_item_context.index = 0; quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='row-content-chapter']//span", [](QuickMediaMatchNode *node, void *userdata) { auto *item_data = (BodyItemContext*)userdata; QuickMediaStringView class_attr = quickmedia_html_node_get_attribute_value(node, "class"); QuickMediaStringView text = quickmedia_html_node_get_text(node); if(text.data && class_attr.data && string_view_contains(class_attr, "chapter-time") && item_data->index < item_data->body_items->size()) { std::string uploaded_date(text.data, text.size); (*item_data->body_items)[item_data->index]->set_description("Uploaded: " + std::move(uploaded_date)); (*item_data->body_items)[item_data->index]->set_description_color(get_theme().faded_text_color); item_data->index++; } return 0; }, &body_item_context); quickmedia_html_find_nodes_xpath(&html_search, "//a[class='a-h']", [](QuickMediaMatchNode *node, void *userdata) { std::vector *creators = (std::vector*)userdata; QuickMediaStringView href = quickmedia_html_node_get_attribute_value(node, "href"); QuickMediaStringView text = quickmedia_html_node_get_text(node); if(href.data && text.data && string_view_contains(href, "/author/story/")) { Creator creator; creator.name.assign(text.data, text.size); creator.url.assign(href.data, href.size); creators->push_back(std::move(creator)); } return 0; }, &creators); cleanup: quickmedia_html_search_deinit(&html_search); if(result != 0) return PluginResult::ERR; auto chapters_body = page->create_body(); chapters_body->set_items(std::move(chapters_items)); auto chapters_page = std::make_unique(page->program, args.title, url, args.thumbnail_url); result_tabs.push_back(Tab{std::move(chapters_body), std::move(chapters_page), page->create_search_bar("Search...", SEARCH_DELAY_FILTER)}); // TODO: Fix. Doesn't work because manganelo changed creator url format /*for(Creator &creator : creators) { result_tabs.push_back(Tab{page->create_body(), std::make_unique(page->program, std::move(creator)), page->create_search_bar("Search...", SEARCH_DELAY_FILTER)}); }*/ return PluginResult::OK; } SearchResult ManganeloSearchPage::search(const std::string &str, BodyItems &result_items) { std::string url = "https://manganato.com/getstorysearchjson"; std::string search_term = "searchword="; search_term += url_param_encode(str); CommandArg data_arg = { "--data-raw", std::move(search_term) }; Json::Value json_root; DownloadResult result = download_json(json_root, url, {data_arg}, true); if(result != DownloadResult::OK) return download_result_to_search_result(result); if(json_root.isNull()) return SearchResult::OK; if(!json_root.isArray()) return SearchResult::ERR; for(const Json::Value &child : json_root) { if(!child.isObject()) continue; Json::Value name = child.get("name", ""); Json::Value nameunsigned = child.get("nameunsigned", ""); Json::Value lastchapter = child.get("lastchapter", ""); if(name.isString() && name.asCString()[0] != '\0' && nameunsigned.isString() && nameunsigned.asCString()[0] != '\0') { std::string name_str = name.asString(); while(remove_html_span(name_str)) {} auto item = BodyItem::create(name_str); item->url = "https://manganelo.com/manga/" + url_param_encode(nameunsigned.asString()); if(lastchapter.isString() && lastchapter.asCString()[0] != '\0') { item->set_description("Latest chapter: " + lastchapter.asString()); item->set_description_color(get_theme().faded_text_color); } 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)); } } return SearchResult::OK; } PluginResult ManganeloSearchPage::submit(const SubmitArgs &args, std::vector &result_tabs) { return submit_manga(this, args, result_tabs); } PluginResult ManganeloChaptersPage::submit(const SubmitArgs &args, std::vector &result_tabs) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, content_title, args.title, args.url, thumbnail_url), nullptr}); return PluginResult::OK; } bool ManganeloChaptersPage::extract_id_from_url(const std::string &url, std::string &manga_id) const { bool manganelo_website = false; if(url.find("mangakakalot") != std::string::npos || url.find("manganelo") != std::string::npos) manganelo_website = true; if(manganelo_website) { size_t index = url.find("manga/"); if(index == std::string::npos) { std::string err_msg = "Url "; err_msg += url; err_msg += " doesn't contain manga id"; show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } manga_id = url.substr(index + 6); if(manga_id.empty()) { std::string err_msg = "Url "; err_msg += url; err_msg += " doesn't contain manga id"; show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } return true; } else { std::string err_msg = "Unexpected url "; err_msg += url; err_msg += " is not manganelo or mangakakalot"; show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } } PluginResult ManganeloCreatorPage::submit(const SubmitArgs &args, std::vector &result_tabs) { return submit_manga(this, args, result_tabs); } PluginResult ManganeloCreatorPage::lazy_fetch(BodyItems &result_items) { std::string website_data; if(download_to_string(creator.url, website_data, {CommandArg { "-H", "referer: https://manganelo.com/" }}, true) != DownloadResult::OK) return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); if(result != 0) goto cleanup; result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-story-item']//a[class='item-img']", [](QuickMediaMatchNode *node, void *userdata) { auto *item_data = (BodyItems*)userdata; QuickMediaStringView href = quickmedia_html_node_get_attribute_value(node, "href"); QuickMediaStringView title = quickmedia_html_node_get_attribute_value(node, "title"); if(href.data && title.data && string_view_contains(href, "/manga/")) { auto body_item = BodyItem::create(std::string(title.data, title.size)); body_item->url.assign(href.data, href.size); item_data->push_back(std::move(body_item)); } return 0; }, &result_items); if(result != 0) goto cleanup; BodyItemContext body_item_image_context; body_item_image_context.body_items = &result_items; body_item_image_context.index = 0; result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-story-item']//a[class='item-img']//img", [](QuickMediaMatchNode *node, void *userdata) { auto *item_data = (BodyItemContext*)userdata; 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; }, &body_item_image_context); cleanup: quickmedia_html_search_deinit(&html_search); if(result != 0) { result_items.clear(); return PluginResult::ERR; } return PluginResult::OK; } ImageResult ManganeloImagesPage::update_image_urls(int &num_images) { num_images = 0; std::string website_data; if(download_to_string(url, website_data, {CommandArg { "-H", "referer: https://manganelo.com/" }}, true) != DownloadResult::OK) return ImageResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); if(result != 0) goto cleanup; result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='container-chapter-reader']/img", [](QuickMediaMatchNode *node, void *userdata) { auto *urls = (std::vector*)userdata; QuickMediaStringView src = quickmedia_html_node_get_attribute_value(node, "src"); if(src.data) { std::string image_url(src.data, src.size); urls->push_back(std::move(image_url)); } return 0; }, &chapter_image_urls); cleanup: quickmedia_html_search_deinit(&html_search); if(result != 0 || chapter_image_urls.empty()) { chapter_image_urls.clear(); return ImageResult::ERR; } num_images = chapter_image_urls.size(); return ImageResult::OK; } ImageResult ManganeloImagesPage::for_each_page_in_chapter(PageCallback callback) { for(const std::string &url : chapter_image_urls) { if(!callback(url)) break; } return ImageResult::OK; } }