#include "../../plugins/NyaaSi.hpp" #include "../../include/Program.hpp" #include "../../include/Storage.hpp" #include "../../include/Notification.hpp" #include "../../include/StringUtils.hpp" #include "../../include/NetUtils.hpp" #include namespace QuickMedia { // Return end of td tag, or std::string::npos static size_t find_td_get_value(const std::string &str, size_t start_index, size_t end_index, std::string &result) { size_t td_begin = str.find("= end_index) return std::string::npos; size_t td_end = str.find("", td_begin + 3); if(td_end == std::string::npos || td_end >= end_index) return std::string::npos; size_t value_begin = str.find('>', td_begin + 3); if(value_begin == std::string::npos || value_begin >= td_end) return std::string::npos; result = str.substr(value_begin + 1, td_end - (value_begin + 1)); return td_end + 5; } static bool is_only_numbers(const char *str, size_t size) { for(size_t i = 0; i < size; ++i) { if(str[i] < '0' || str[i] > '9') return false; } return true; } static std::shared_ptr create_front_page_item(const std::string &title, const std::string &category) { auto body_item = BodyItem::create(title); body_item->url = category; return body_item; } // TODO: Also show the number of comments for each torrent. TODO: Optimize? // TODO: Show each field as seperate columns instead of seperating by | static SearchResult search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items) { std::string full_url = "https://nyaa.si/?c=" + list_url + "&f=0&p=" + std::to_string(page) + "&q="; full_url += url_param_encode(text); std::string website_data; if(download_to_string(full_url, website_data, {}, true) != DownloadResult::OK) return SearchResult::NET_ERR; size_t tbody_begin = website_data.find(""); if(tbody_begin == std::string::npos) return SearchResult::OK; size_t tbody_end = website_data.find("", tbody_begin + 7); if(tbody_end == std::string::npos) return SearchResult::ERR; size_t index = tbody_begin + 7; while(index < tbody_end) { size_t tr_begin = website_data.find("= tbody_end) break; size_t tr_end = website_data.find("", tr_begin + 3); if(tr_end == std::string::npos || tr_end >= tbody_end) return SearchResult::ERR; index = tr_begin + 3; bool is_trusted = false; bool is_remake = false; size_t tr_class_begin = website_data.find("class=\"", index); if(tr_class_begin != std::string::npos && tr_class_begin < tr_end) { size_t tr_class_end = website_data.find('"', tr_class_begin + 7); size_t class_length = tr_class_end - (tr_class_begin + 7); if(strncmp(website_data.c_str() + tr_class_begin + 7, "success", class_length) == 0) is_trusted = true; else if(strncmp(website_data.c_str() + tr_class_begin + 7, "danger", class_length) == 0) is_remake = true; index = tr_class_end + 1; } size_t category_begin = website_data.find("/?c=", index); if(category_begin == std::string::npos || category_begin >= tr_end) return SearchResult::ERR; size_t category_end = website_data.find('"', category_begin + 4); if(category_end == std::string::npos || category_end >= tr_end) return SearchResult::ERR; index = category_end + 1; size_t view_begin = website_data.find("/view/", index); if(view_begin == std::string::npos || view_begin >= tr_end) return SearchResult::ERR; size_t view_end = website_data.find('"', view_begin + 6); if(view_end == std::string::npos || view_end >= tr_end) return SearchResult::ERR; std::string view_url = website_data.substr(view_begin, view_end - view_begin); // Torrents with comments have two /view/, one for comments and one for the title if(!is_only_numbers(website_data.c_str() + view_begin + 6, view_end - (view_begin + 6))) { size_t view_begin2 = website_data.find("/view/", view_end + 1); if(view_begin2 == std::string::npos || view_begin2 >= tr_end) return SearchResult::ERR; size_t view_end2 = website_data.find('"', view_begin2 + 6); if(view_end2 == std::string::npos || view_end2 >= tr_end) return SearchResult::ERR; view_end = view_end2; } size_t title_begin = website_data.find('>', view_end + 1); if(title_begin == std::string::npos || title_begin >= tr_end) return SearchResult::ERR; size_t title_end = website_data.find("", title_begin + 1); if(title_end == std::string::npos || title_end >= tr_end) return SearchResult::ERR; std::string title = website_data.substr(title_begin + 1, title_end - (title_begin + 1)); html_unescape_sequences(title); title = strip(title); index = title_end + 4; size_t magnet_begin = website_data.find("magnet:?xt", index); if(magnet_begin == std::string::npos || magnet_begin >= tr_end) return SearchResult::ERR; size_t magnet_end = website_data.find('"', magnet_begin + 10); if(magnet_end == std::string::npos || magnet_end >= tr_end) return SearchResult::ERR; index = magnet_end + 1; std::string size; index = find_td_get_value(website_data, index, tr_end, size); if(index == std::string::npos) return SearchResult::ERR; std::string timestamp; index = find_td_get_value(website_data, index, tr_end, timestamp); if(index == std::string::npos) return SearchResult::ERR; std::string seeders; index = find_td_get_value(website_data, index, tr_end, seeders); if(index == std::string::npos) return SearchResult::ERR; std::string leechers; index = find_td_get_value(website_data, index, tr_end, leechers); if(index == std::string::npos) return SearchResult::ERR; std::string completed; index = find_td_get_value(website_data, index, tr_end, completed); if(index == std::string::npos) return SearchResult::ERR; index = tr_end + 5; std::string description = "Size: " + size + " | Published: " + timestamp + " | Seeders: " + seeders + " | Leechers: " + leechers + " | Completed: " + completed; auto body_item = BodyItem::create(std::move(title)); body_item->thumbnail_url = "https://nyaa.si/static/img/icons/nyaa/" + website_data.substr(category_begin + 4, category_end - (category_begin + 4)) + ".png"; body_item->set_description(std::move(description)); body_item->url = "https://nyaa.si" + std::move(view_url); if(is_trusted) body_item->set_title_color(sf::Color(43, 255, 47)); else if(is_remake) body_item->set_title_color(sf::Color(255, 45, 47)); body_item->thumbnail_size = sf::Vector2i(80, 28); result_items.push_back(std::move(body_item)); } return SearchResult::OK; } PluginResult NyaaSiCategoryPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { BodyItems result_items; SearchResult search_result = search_page(url, "", 1, result_items); if(search_result != SearchResult::OK) return search_result_to_plugin_result(search_result); auto body = create_body(); body->items = std::move(result_items); result_tabs.push_back(Tab{std::move(body), std::make_unique(program, strip(title), url), create_search_bar("Search...", 300)}); return PluginResult::OK; } void NyaaSiCategoryPage::get_categories(BodyItems &result_items) { result_items.push_back(create_front_page_item("All categories", "0_0")); result_items.push_back(create_front_page_item("Anime", "1_0")); result_items.push_back(create_front_page_item(" Anime - Music video", "1_1")); result_items.push_back(create_front_page_item(" Anime - English translated", "1_2")); result_items.push_back(create_front_page_item(" Anime - Non-english translated", "1_3")); result_items.push_back(create_front_page_item(" Anime - Raw", "1_4")); result_items.push_back(create_front_page_item("Audio", "2_0")); result_items.push_back(create_front_page_item(" Audio - Lossless", "2_1")); result_items.push_back(create_front_page_item(" Anime - Lossy", "2_2")); result_items.push_back(create_front_page_item("Literature", "3_0")); result_items.push_back(create_front_page_item(" Literature - English translated", "3_1")); result_items.push_back(create_front_page_item(" Literature - Non-english translated", "3_1")); result_items.push_back(create_front_page_item(" Literature - Raw", "3_3")); result_items.push_back(create_front_page_item("Live Action", "4_0")); result_items.push_back(create_front_page_item(" Live Action - English translated", "4_1")); result_items.push_back(create_front_page_item(" Live Action - Non-english translated", "4_3")); result_items.push_back(create_front_page_item(" Live Action - Idol/Promotional video", "4_2")); result_items.push_back(create_front_page_item(" Live Action - Raw", "4_4")); result_items.push_back(create_front_page_item("Pictures", "5_0")); result_items.push_back(create_front_page_item(" Pictures - Graphics", "5_1")); result_items.push_back(create_front_page_item(" Pictures - Photos", "5_2")); result_items.push_back(create_front_page_item("Software", "6_0")); result_items.push_back(create_front_page_item(" Software - Applications", "6_1")); result_items.push_back(create_front_page_item(" Software - Games", "6_2")); } SearchResult NyaaSiSearchPage::search(const std::string &str, BodyItems &result_items) { return search_page(category_id, str, 1, result_items); } PluginResult NyaaSiSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { return search_result_to_plugin_result(search_page(category_id, str, 1 + page, result_items)); } PluginResult NyaaSiSearchPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { size_t comments_start_index; std::string title; BodyItems result_items; auto torrent_item = BodyItem::create("💾 Download magnet"); std::string magnet_url; std::string description; std::string website_data; if(download_to_string(url, website_data, {}, true) != DownloadResult::OK) return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; result = quickmedia_html_find_nodes_xpath(&html_search, "//h3[class='panel-title']", [](QuickMediaHtmlNode *node, void *userdata) { std::string *title = (std::string*)userdata; const char *text = quickmedia_html_node_get_text(node); if(title->empty() && text) { *title = text; } }, &title); if(result != 0) goto cleanup; if(title.empty()) { fprintf(stderr, "Error: nyaa.si: failed to get title\n"); result = -1; goto cleanup; } result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='panel-body']//div[class='row']//a", [](QuickMediaHtmlNode *node, void *userdata) { auto *item_data = (BodyItems*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *text = quickmedia_html_node_get_text(node); if(item_data->empty() && href && text && strncmp(href, "/user/", 6) == 0) { auto body_item = BodyItem::create(""); body_item->set_description("Submitter: " + strip(text)); body_item->url = "https://nyaa.si/" + std::string(href); item_data->push_back(std::move(body_item)); } }, &result_items); if(result != 0) goto cleanup; if(result_items.empty()) { auto body_item = BodyItem::create(""); body_item->set_description("Submitter: Anonymous"); result_items.push_back(std::move(body_item)); } result_items.front()->set_title(strip(title)); result = quickmedia_html_find_nodes_xpath(&html_search, "//div[id='torrent-description']", [](QuickMediaHtmlNode *node, void *userdata) { std::string *description = (std::string*)userdata; const char *text = quickmedia_html_node_get_text(node); if(description->empty() && text) { *description = strip(text); } }, &description); if(result != 0) goto cleanup; if(!description.empty()) result_items.front()->set_description(result_items.front()->get_description() + "\nDescription:\n" + description); result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='container']//a", [](QuickMediaHtmlNode *node, void *userdata) { std::string *magnet_url = (std::string*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); if(magnet_url->empty() && href && strncmp(href, "magnet:?", 8) == 0) { *magnet_url = href; } }, &magnet_url); if(result != 0) goto cleanup; if(magnet_url.empty()) { fprintf(stderr, "Error: nyaa.si: failed to get magnet link\n"); result = -1; goto cleanup; } torrent_item->url = std::move(magnet_url); result_items.push_back(std::move(torrent_item)); comments_start_index = result_items.size(); result = quickmedia_html_find_nodes_xpath(&html_search, "//div[id='comments']//a", [](QuickMediaHtmlNode *node, void *userdata) { auto *item_data = (BodyItems*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *text = quickmedia_html_node_get_text(node); if(href && text && strncmp(href, "/user/", 6) == 0) { auto body_item = BodyItem::create(strip(text)); //body_item->url = "https://nyaa.si/" + std::string(href); item_data->push_back(std::move(body_item)); } }, &result_items); if(result != 0 || result_items.size() == comments_start_index) goto cleanup; BodyItemContext body_item_image_context; body_item_image_context.body_items = &result_items; body_item_image_context.index = comments_start_index; result = quickmedia_html_find_nodes_xpath(&html_search, "//div[id='comments']//img[class='avatar']", [](QuickMediaHtmlNode *node, void *userdata) { auto *item_data = (BodyItemContext*)userdata; const char *src = quickmedia_html_node_get_attribute_value(node, "src"); if(src && item_data->index < item_data->body_items->size()) { (*item_data->body_items)[item_data->index]->thumbnail_url = src; (*item_data->body_items)[item_data->index]->thumbnail_size = sf::Vector2i(120, 120); item_data->index++; } }, &body_item_image_context); if(result != 0) goto cleanup; body_item_image_context.index = comments_start_index; result = quickmedia_html_find_nodes_xpath(&html_search, "//div[id='comments']//div[class='comment-content']", [](QuickMediaHtmlNode *node, void *userdata) { auto *item_data = (BodyItemContext*)userdata; const char *text = quickmedia_html_node_get_text(node); if(text && item_data->index < item_data->body_items->size()) { (*item_data->body_items)[item_data->index]->set_description(strip(text)); item_data->index++; } }, &body_item_image_context); cleanup: quickmedia_html_search_deinit(&html_search); if(result != 0) return PluginResult::ERR; auto body = create_body(); body->items = std::move(result_items); result_tabs.push_back(Tab{std::move(body), std::make_unique(program), nullptr}); return PluginResult::OK; } PluginResult NyaaSiTorrentPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { (void)title; (void)result_tabs; if(strncmp(url.c_str(), "magnet:?", 8) == 0) { if(!is_program_executable_by_name("xdg-open")) { show_notification("QuickMedia", "xdg-utils which provides xdg-open needs to be installed to download torrents", Urgency::CRITICAL); return PluginResult::ERR; } const char *args[] = { "xdg-open", url.c_str(), nullptr }; exec_program_async(args, nullptr); } return PluginResult::OK; } }