From 12c59fddcf1201536c3bee8db4e0d6ac8964fde6 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 18 Sep 2020 01:10:18 +0200 Subject: Initial nyaa.si support --- src/Body.cpp | 2 +- src/QuickMedia.cpp | 74 ++++++++++--- src/plugins/Manganelo.cpp | 2 +- src/plugins/NyaaSi.cpp | 272 ++++++++++++++++++++++++++++++++++++++++++++++ src/plugins/Plugin.cpp | 7 ++ 5 files changed, 342 insertions(+), 15 deletions(-) create mode 100644 src/plugins/NyaaSi.cpp (limited to 'src') diff --git a/src/Body.cpp b/src/Body.cpp index 2233f92..fd279a8 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -211,7 +211,7 @@ namespace QuickMedia { if(body_item->title_text) body_item->title_text->setString(body_item->get_title()); else - body_item->title_text = std::make_unique(body_item->get_title(), font, 14, size.x - 50 - image_padding_x * 2.0f); + body_item->title_text = std::make_unique(body_item->get_title(), font, 16, size.x - 50 - image_padding_x * 2.0f); body_item->title_text->updateGeometry(); } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index f5aaea5..01a82c6 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -6,6 +6,7 @@ #include "../plugins/Pornhub.hpp" #include "../plugins/Fourchan.hpp" #include "../plugins/Dmenu.hpp" +#include "../plugins/NyaaSi.hpp" #include "../include/Scale.hpp" #include "../include/Program.h" #include "../include/VideoPlayer.hpp" @@ -161,9 +162,9 @@ namespace QuickMedia { if (!disp) throw std::runtime_error("Failed to open display to X11 server"); - resources_root = "../../../"; - if(get_file_type("/usr/share/quickmedia/") == FileType::DIRECTORY) { - resources_root = "/usr/share/quickmedia/"; + resources_root = "/usr/share/quickmedia/"; + if(get_file_type("../../../images/manganelo_logo.png") == FileType::REGULAR) { + resources_root = "../../../"; } if(!font.loadFromFile(resources_root + "fonts/Lato-Regular.ttf")) { @@ -236,7 +237,7 @@ namespace QuickMedia { static void usage() { fprintf(stderr, "usage: QuickMedia [--tor] [--use-system-mpv-config] [-p placeholder-text]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube or dmenu\n"); + fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, nyaa.si or dmenu\n"); fprintf(stderr, " --tor Use tor. 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"); @@ -296,6 +297,9 @@ namespace QuickMedia { } else if(strcmp(argv[i], "4chan") == 0) { current_plugin = new Fourchan(resources_root); plugin_logo_path = resources_root + "images/4chan_logo.png"; + } else if(strcmp(argv[i], "nyaa.si") == 0) { + current_plugin = new NyaaSi(); + plugin_logo_path = resources_root + "images/nyaa_si_logo.png"; } else if(strcmp(argv[i], "dmenu") == 0) { current_plugin = new Dmenu(); } else { @@ -827,7 +831,7 @@ namespace QuickMedia { return false; Page next_page = current_plugin->get_page_after_search(); - bool skip_search = next_page == Page::VIDEO_CONTENT; + bool skip_search = (next_page == Page::VIDEO_CONTENT || next_page == Page::CONTENT_LIST); // TODO: This shouldn't be done if search_selected_suggestion fails if(search_selected_suggestion(input_body, output_body, current_plugin, content_title, content_url, skip_search) != SearchResult::OK) { show_notification("Search", "Search failed!", Urgency::CRITICAL); @@ -2291,15 +2295,28 @@ namespace QuickMedia { } void Program::content_list_page() { + std::string update_search_text; + bool search_running = false; + std::future search_future; + + if(!current_plugin->content_list_search_is_filter()) + search_bar->text_autosearch_delay = current_plugin->get_content_list_search_delay(); + + body->clear_items(); + body->clear_thumbnails(); if(current_plugin->get_content_list(content_list_url, body->items) != PluginResult::OK) { show_notification("Content list", "Failed to get content list for url: " + content_list_url, Urgency::CRITICAL); current_page = Page::SEARCH_SUGGESTION; return; } - search_bar->onTextUpdateCallback = [this](const std::string &text) { - body->filter_search_fuzzy(text); - body->select_first_item(); + search_bar->onTextUpdateCallback = [this, &update_search_text](const std::string &text) { + if(current_plugin->content_list_search_is_filter()) { + body->filter_search_fuzzy(text); + body->clamp_selection(); + } else { + update_search_text = text; + } }; search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { @@ -2335,11 +2352,35 @@ namespace QuickMedia { search_bar->update(); + if(!update_search_text.empty() && !search_running) { + search_future = std::async(std::launch::async, [this, update_search_text]() { + BodyItems result; + if(current_plugin->content_list_search(content_list_url, update_search_text, result) != SearchResult::OK) { + show_notification("Search", "Search failed!", Urgency::CRITICAL); + } + return result; + }); + update_search_text.clear(); + search_running = true; + } + + if(search_running && search_future.valid() && search_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + if(update_search_text.empty()) { + body->items = search_future.get(); + body->clamp_selection(); + } else { + search_future.get(); + } + search_running = false; + } + window.clear(back_color); body->draw(window, body_pos, body_size); search_bar->draw(window); window.display(); } + + search_bar->text_autosearch_delay = current_plugin->get_search_delay(); } void Program::content_details_page() { @@ -2352,15 +2393,22 @@ namespace QuickMedia { return; } - // Instead of using search bar to searching, use it for commenting. // TODO: Have an option for the search bar to be multi-line. search_bar->onTextUpdateCallback = nullptr; search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { - if(text.empty()) - return false; - - return true; + if(current_plugin->name == "nyaa.si") { + BodyItem *selected_item = body->get_selected(); + if(selected_item && strncmp(selected_item->url.c_str(), "magnet:?", 8) == 0) { + if(!is_program_executable_by_name("xdg-open")) { + show_notification("Nyaa.si", "xdg-utils which provides xdg-open needs to be installed to download torrents", Urgency::CRITICAL); + return false; + } + const char *args[] = { "xdg-open", selected_item->url.c_str(), nullptr }; + exec_program_async(args, nullptr); + } + } + return false; }; sf::Vector2f body_pos; diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index a772601..6363991 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -33,7 +33,7 @@ namespace QuickMedia { } }, &result_items); - result = quickmedia_html_find_nodes_xpath(&html_search, "//a[class='a-h']", + quickmedia_html_find_nodes_xpath(&html_search, "//a[class='a-h']", [](QuickMediaHtmlNode *node, void *userdata) { std::vector *creators = (std::vector*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); diff --git a/src/plugins/NyaaSi.cpp b/src/plugins/NyaaSi.cpp new file mode 100644 index 0000000..18dd53a --- /dev/null +++ b/src/plugins/NyaaSi.cpp @@ -0,0 +1,272 @@ +#include "../../plugins/NyaaSi.hpp" +#include "../../include/Program.h" +#include + +namespace QuickMedia { + // Returns empty string on error + static std::string get_rss_item_text(const std::string &data, size_t start, size_t end, const std::string &tag_start, const std::string &tag_end) { + size_t item_begin = data.find(tag_start, start); + if(item_begin == std::string::npos || item_begin >= end) + return ""; + + size_t item_end = data.find(tag_end, item_begin + tag_start.size()); + if(item_end == std::string::npos || item_end >= end) + return ""; + + std::string result = data.substr(item_begin + tag_start.size(), item_end - (item_begin + tag_start.size())); + html_unescape_sequences(result); + return strip(result); + } + + NyaaSi::NyaaSi() : Plugin("nyaa.si") { + + } + + NyaaSi::~NyaaSi() { + + } + + static std::unique_ptr create_front_page_item(const std::string &title, const std::string &category) { + auto body_item = std::make_unique(title); + body_item->url = category; + return body_item; + } + + PluginResult NyaaSi::get_front_page(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")); + return PluginResult::OK; + } + + SearchResult NyaaSi::content_list_search(const std::string &list_url, const std::string &text, BodyItems &result_items) { + std::string full_url = "https://nyaa.si/?page=rss&c=" + list_url + "&f=0&p=1&q="; + full_url += url_param_encode(text); + + std::string website_data; + if(download_to_string(full_url, website_data, {}, use_tor) != DownloadResult::OK) + return SearchResult::NET_ERR; + + const std::string title_tag_begin = ""; + const std::string title_tag_end = ""; + const std::string link_tag_begin = ""; + const std::string link_tag_end = ""; + const std::string pub_date_tag_begin = ""; + const std::string pub_date_tag_end = ""; + const std::string seeders_tag_begin = ""; + const std::string seeders_tag_end = ""; + const std::string leechers_tag_begin = ""; + const std::string leechers_tag_end = ""; + const std::string downloads_tag_begin = ""; + const std::string downloads_tag_end = ""; + const std::string category_id_tag_begin = ""; + const std::string category_id_tag_end = ""; + const std::string size_tag_begin = ""; + const std::string size_tag_end = ""; + + size_t index = 0; + while(index < website_data.size()) { + size_t item_start = website_data.find("", index); + if(item_start == std::string::npos) + break; + + index = item_start + 6; + + size_t item_end = website_data.find("", index); + if(item_end == std::string::npos) + return SearchResult::ERR; + + std::string title = get_rss_item_text(website_data, index, item_end, title_tag_begin, title_tag_end); + std::string link = get_rss_item_text(website_data, index, item_end, link_tag_begin, link_tag_end); + std::string pub_date = get_rss_item_text(website_data, index, item_end, pub_date_tag_begin, pub_date_tag_end); + std::string seeders = get_rss_item_text(website_data, index, item_end, seeders_tag_begin, seeders_tag_end); + std::string leechers = get_rss_item_text(website_data, index, item_end, leechers_tag_begin, leechers_tag_end); + std::string downloads = get_rss_item_text(website_data, index, item_end, downloads_tag_begin, downloads_tag_end); + std::string category_id = get_rss_item_text(website_data, index, item_end, category_id_tag_begin, category_id_tag_end); + std::string size = get_rss_item_text(website_data, index, item_end, size_tag_begin, size_tag_end); + + if(title.empty() || link.empty() || pub_date.empty() || seeders.empty() || leechers.empty() || downloads.empty() || category_id.empty() || size.empty()) { + fprintf(stderr, "Error: failed to parse nyaa.si rss items\n"); + return SearchResult::ERR; + } + + auto body_item = std::make_unique(std::move(title)); + body_item->url = std::move(link); + body_item->thumbnail_url = "https://nyaa.si/static/img/icons/nyaa/" + category_id + ".png"; + body_item->set_description("Published: " + pub_date + "\nSeeders: " + seeders + "\nLeechers: " + leechers + "\nDownloads: " + downloads + "\nSize: " + size); + result_items.push_back(std::move(body_item)); + + index = item_end + 7; + } + + return SearchResult::OK; + } + + static PluginResult search_result_to_plugin_result(SearchResult search_result) { + switch(search_result) { + case SearchResult::OK: return PluginResult::OK; + case SearchResult::ERR: return PluginResult::ERR; + case SearchResult::NET_ERR: return PluginResult::NET_ERR; + } + return PluginResult::ERR; + } + + PluginResult NyaaSi::get_content_list(const std::string &url, BodyItems &result_items) { + return search_result_to_plugin_result(content_list_search(url, "", result_items)); + } + + struct BodyItemImageContext { + BodyItems *body_items; + size_t index; + }; + + // Returns empty string on error + // static std::string view_url_get_id(const std::string &url) { + // size_t index = url.rfind('/'); + // if(index == std::string::npos) + // return ""; + // return url.substr(index); + // } + + PluginResult NyaaSi::get_content_details(const std::string &list_url, const std::string &url, BodyItems &result_items) { + size_t comments_start_index; + // std::string id = view_url_get_id(url); + // if(id.empty()) { + // fprintf(stderr, "Error: nyaa.si: failed to extract id from url %s\n", url.c_str()); + // return PluginResult::ERR; + // } + + // std::string torrent_url = "https://nyaa.si/download/" + id + ".torrent"; + // auto torrent_item = std::make_unique("Download torrent"); + // torrent_item->url = "https://nyaa.si/download/" + id + ".torrent"; + auto torrent_item = std::make_unique("Download magnet"); + std::string magnet_url; + + std::string website_data; + if(download_to_string(url, website_data, {}, use_tor, 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, "//div[class='panel-body']//a", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (BodyItems*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *title = quickmedia_html_node_get_attribute_value(node, "title"); + const char *text = quickmedia_html_node_get_text(node); + if(item_data->empty() && href && title && text && strcmp(title, "User") == 0 && strncmp(href, "/user/", 6) == 0) { + auto body_item = std::make_unique("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()) { + fprintf(stderr, "Error: nyaa.si: failed to get submitter\n"); + result = -1; + goto cleanup; + } + + 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 *title = quickmedia_html_node_get_attribute_value(node, "title"); + const char *text = quickmedia_html_node_get_text(node); + if(href && title && text && strcmp(title, "User") == 0) { + auto body_item = std::make_unique(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; + + BodyItemImageContext 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 = (BodyItemImageContext*)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->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 = (BodyItemImageContext*)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) { + result_items.clear(); + return PluginResult::ERR; + } + return PluginResult::OK; + } +} \ No newline at end of file diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp index a9adf15..8690964 100644 --- a/src/plugins/Plugin.cpp +++ b/src/plugins/Plugin.cpp @@ -16,6 +16,13 @@ namespace QuickMedia { return SuggestionResult::OK; } + SearchResult Plugin::content_list_search(const std::string &list_url, const std::string &text, BodyItems &result_items) { + (void)list_url; + (void)text; + (void)result_items; + return SearchResult::OK; + } + BodyItems Plugin::get_related_media(const std::string &url) { (void)url; return {}; -- cgit v1.2.3