From cd4d2e98c34ed413ca164dbad61ba19636377f77 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 19 Jul 2021 19:28:45 +0200 Subject: Add hotexamples --- README.md | 4 +- depends/html-parser | 2 +- depends/html-search | 2 +- plugins/HotExamples.hpp | 36 +++++++++++ src/NetUtils.cpp | 39 ++++++++++-- src/QuickMedia.cpp | 9 ++- src/plugins/Fourchan.cpp | 23 +++---- src/plugins/HotExamples.cpp | 139 +++++++++++++++++++++++++++++++++++++++++++ src/plugins/MangaGeneric.cpp | 5 +- src/plugins/Saucenao.cpp | 5 +- 10 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 plugins/HotExamples.hpp create mode 100644 src/plugins/HotExamples.cpp diff --git a/README.md b/README.md index 63f8610..6322cac 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # QuickMedia A dmenu-inspired native client for web services. -Currently supported web services: `youtube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao` and _others_.\ +Currently supported web services: `youtube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples` and _others_.\ Config data, including manga progress is stored under `$HOME/.config/quickmedia`.\ Cache is stored under `$HOME/.cache/quickmedia`. ## Usage ``` usage: quickmedia [--use-system-mpv-config] [--dir ] [-e ] [youtube-url] OPTIONS: - plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, file-manager or stdin + 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 or stdin --no-video Only play audio when playing a video. Disabled by default --use-system-mpv-config Use system mpv config instead of no config. Disabled by default --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default diff --git a/depends/html-parser b/depends/html-parser index 11d3632..199ca92 160000 --- a/depends/html-parser +++ b/depends/html-parser @@ -1 +1 @@ -Subproject commit 11d3632fe4508bfd2f668b7b1c4d75a88cd6449d +Subproject commit 199ca9297d2ef4ff58db4b0c948eb384deceb610 diff --git a/depends/html-search b/depends/html-search index cc37a6a..b62b5aa 160000 --- a/depends/html-search +++ b/depends/html-search @@ -1 +1 @@ -Subproject commit cc37a6af5283b4e4c052427fd0d2940ebce5fc85 +Subproject commit b62b5aa8a262d3f4e2c53c5fd5b67410106bca82 diff --git a/plugins/HotExamples.hpp b/plugins/HotExamples.hpp new file mode 100644 index 0000000..154f1ab --- /dev/null +++ b/plugins/HotExamples.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "Page.hpp" + +namespace QuickMedia { + void hot_examples_front_page_fill(BodyItems &body_items); + + class HotExamplesLanguageSelectPage : public Page { + public: + HotExamplesLanguageSelectPage(Program *program) : Page(program) {} + const char* get_title() const override { return "Select language"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; + bool submit_is_async() override { return false; } + }; + + class HotExamplesSearchPage : public Page { + public: + HotExamplesSearchPage(Program *program, const std::string &language) : Page(program), language(language) {} + const char* get_title() const override { return "Select result"; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; + private: + std::string language; + }; + + class HotExamplesCodeExamplesPage : public Page { + public: + HotExamplesCodeExamplesPage(Program *program, std::string title) : Page(program), title(std::move(title)) {} + const char* get_title() const override { return title.c_str(); } + PluginResult submit(const std::string&, const std::string&, std::vector&) override { return PluginResult::OK; } + bool submit_is_async() override { return false; } + private: + std::string title; + }; +} \ No newline at end of file diff --git a/src/NetUtils.cpp b/src/NetUtils.cpp index cc19094..28256cb 100644 --- a/src/NetUtils.cpp +++ b/src/NetUtils.cpp @@ -34,12 +34,43 @@ 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) { + index = str.find("&#", index); + if(index == std::string::npos) + break; + + index += 2; + size_t end_index = str.find(';', index); + if(end_index != std::string::npos && end_index - index <= 3) { + const size_t num_length = end_index - index; + int num; + if(to_num(str.c_str() + index, num_length, num)) { + const char num_c = (char)num; + str.replace(index - 2, 2 + num_length + 1, &num_c, 1); + index += (-2 + 1); + } + } + } + } + void html_unescape_sequences(std::string &str) { - const std::array unescape_sequences = { + html_unescape_sequence_numbers(str); + + const std::array unescape_sequences = { HtmlUnescapeSequence { """, "\"" }, - HtmlUnescapeSequence { "'", "'" }, - HtmlUnescapeSequence { "'", "'" }, - HtmlUnescapeSequence { " ", "\n" }, 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 fb6b425..1ec2c53 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -13,6 +13,7 @@ #include "../plugins/Pipe.hpp" #include "../plugins/Saucenao.hpp" #include "../plugins/Info.hpp" +#include "../plugins/HotExamples.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("hotexamples", nullptr), std::make_pair("file-manager", nullptr), std::make_pair("stdin", nullptr), std::make_pair("saucenao", nullptr), @@ -348,7 +350,7 @@ namespace QuickMedia { static void usage() { fprintf(stderr, "usage: quickmedia [--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, 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, 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"); @@ -1084,6 +1086,7 @@ namespace QuickMedia { if(strcmp(plugin_name, "launcher") == 0) { auto pipe_body = create_body(true); pipe_body->items.push_back(create_launcher_body_item("4chan", "4chan", resources_root + "icons/4chan_launcher.png")); + pipe_body->items.push_back(create_launcher_body_item("Hot Examples", "hotexamples", "")); pipe_body->items.push_back(create_launcher_body_item("Manga (all)", "manga", "")); pipe_body->items.push_back(create_launcher_body_item("Mangadex", "mangadex", resources_root + "icons/mangadex_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("Mangakatana", "mangakatana", resources_root + "icons/mangakatana_launcher.png")); @@ -1185,6 +1188,10 @@ namespace QuickMedia { auto boards_body = create_body(); boards_page->get_boards(boards_body->items); tabs.push_back(Tab{std::move(boards_body), std::move(boards_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "hotexamples") == 0) { + auto body = create_body(); + hot_examples_front_page_fill(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, "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)) diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index 4b2ca61..d4fb726 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -462,9 +462,9 @@ namespace QuickMedia { } std::vector additional_args = { - CommandArg{"-F", "id=" + token}, - CommandArg{"-F", "pin=" + pin}, - CommandArg{"-F", "xhr=1"}, + CommandArg{"--form-string", "id=" + token}, + CommandArg{"--form-string", "pin=" + pin}, + CommandArg{"--form-string", "xhr=1"}, CommandArg{"-c", cookies_filepath.data} }; @@ -502,29 +502,22 @@ namespace QuickMedia { PostResult FourchanThreadPage::post_comment(const std::string &captcha_id, const std::string &comment, const std::string &filepath) { std::string url = "https://sys.4chan.org/" + board_id + "/post"; - std::string comment_fixed = comment; - if(comment_fixed[0] == '@') - comment_fixed = "\\" + comment_fixed; std::vector additional_args = { CommandArg{"-H", "Referer: https://boards.4chan.org/"}, CommandArg{"-H", "Origin: https://boards.4chan.org"}, - CommandArg{"-F", "resto=" + thread_id}, - CommandArg{"-F", "com=" + comment_fixed}, - CommandArg{"-F", "mode=regist"} + CommandArg{"--form-string", "resto=" + thread_id}, + CommandArg{"--form-string", "com=" + comment}, + CommandArg{"--form-string", "mode=regist"} }; if(!filepath.empty()) { - std::string filename = file_get_filename(filepath); - if(filename[0] == '@') - filename = "\\" + filename; - additional_args.push_back({ "-F", "upfile=@" + filepath }); - additional_args.push_back({ "-F", "filename=" + filename }); + additional_args.push_back({ "--form-string", "filename=" + file_get_filename(filepath) }); } if(pass_id.empty()) { - additional_args.push_back(CommandArg{"-F", "g-recaptcha-response=" + captcha_id}); + additional_args.push_back(CommandArg{"--form-string", "g-recaptcha-response=" + captcha_id}); } else { Path cookies_filepath; if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) { diff --git a/src/plugins/HotExamples.cpp b/src/plugins/HotExamples.cpp new file mode 100644 index 0000000..02f1217 --- /dev/null +++ b/src/plugins/HotExamples.cpp @@ -0,0 +1,139 @@ +#include "../../plugins/HotExamples.hpp" +#include "../../include/Theme.hpp" +#include "../../include/StringUtils.hpp" +#include + +namespace QuickMedia { + static std::shared_ptr create_body_item_with_url(const std::string &title, const std::string &url) { + auto body_item = BodyItem::create(title); + body_item->url = url; + return body_item; + } + + void hot_examples_front_page_fill(BodyItems &body_items) { + body_items.push_back(create_body_item_with_url("C++", "cpp")); + body_items.push_back(create_body_item_with_url("C#", "csharp")); + body_items.push_back(create_body_item_with_url("Go", "go")); + body_items.push_back(create_body_item_with_url("Java", "java")); + body_items.push_back(create_body_item_with_url("JavaScript", "javascript")); + body_items.push_back(create_body_item_with_url("PHP", "php")); + body_items.push_back(create_body_item_with_url("Python", "python")); + body_items.push_back(create_body_item_with_url("TypeScript", "typescript")); + } + + PluginResult HotExamplesLanguageSelectPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back({ create_body(), std::make_unique(program, url), create_search_bar("Search...", 500) }); + return PluginResult::OK; + } + + SearchResult HotExamplesSearchPage::search(const std::string &str, BodyItems &result_items) { + std::vector additional_args = { + { "-H", "content-type: application/x-www-form-urlencoded" }, + { "--data-raw", "SearchForm[lang]=" + language + "&SearchForm[search]=" + url_param_encode(str) } + }; + + std::string website_data; + DownloadResult download_result = download_to_string("https://hotexamples.com/search", website_data, additional_args, true); + if(download_result != DownloadResult::OK) return download_result_to_search_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 SearchResult::ERR; + + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-result row']//div[class='header']//a", + [](QuickMediaMatchNode *node, void *userdata) { + auto *item_data = (BodyItems*)userdata; + QuickMediaStringView href = quickmedia_html_node_get_attribute_value(node, "href"); + if(href.data && memmem(href.data, href.size, "/examples/", 10)) { + QuickMediaStringView text = quickmedia_html_node_get_text(node); + if(text.data) { + std::string title(text.data, text.size); + html_unescape_sequences(title); + + auto item = BodyItem::create(std::move(title)); + item->url.assign(href.data, href.size); + item_data->push_back(std::move(item)); + } + } + return 0; + }, &result_items); + + BodyItemContext body_item_context; + body_item_context.body_items = &result_items; + body_item_context.index = 0; + + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-result row']//span[class='count']", + [](QuickMediaMatchNode *node, void *userdata) { + auto *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 desc(text.data, text.size); + html_unescape_sequences(desc); + + (*item_data->body_items)[item_data->index]->set_description(std::move(desc)); + (*item_data->body_items)[item_data->index]->set_description_color(get_current_theme().faded_text_color); + item_data->index++; + } + return 0; + }, &body_item_context); + + quickmedia_html_search_deinit(&html_search); + return SearchResult::OK; + } + + PluginResult HotExamplesSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + 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); + + 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='example-item']//div[class='example-project-info']", + [](QuickMediaMatchNode *node, void *userdata) { + auto *item_data = (BodyItems*)userdata; + QuickMediaStringView text = quickmedia_html_node_get_text(node); + if(text.data) { + std::string title(text.data, text.size); + html_unescape_sequences(title); + string_replace_all(title, "Project:", " Project:"); + + auto item = BodyItem::create(std::move(title)); + //item->url.assign(href.data, href.size); + item_data->push_back(std::move(item)); + } + return 0; + }, &result_items); + + BodyItemContext body_item_context; + body_item_context.body_items = &result_items; + body_item_context.index = 0; + + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='example-item']//div[class='example']", + [](QuickMediaMatchNode *node, void *userdata) { + auto *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 desc(text.data, text.size); + html_unescape_sequences(desc); + + (*item_data->body_items)[item_data->index]->set_description(std::move(desc)); + (*item_data->body_items)[item_data->index]->set_description_color(get_current_theme().text_color); + // TODO: Use monospace + item_data->index++; + } + return 0; + }, &body_item_context); + + quickmedia_html_search_deinit(&html_search); + + auto body = create_body(); + body->items = std::move(result_items); + result_tabs.push_back({ std::move(body), std::make_unique(program, title + " code examples"), create_search_bar("Search...", SEARCH_DELAY_FILTER) }); + return PluginResult::OK; + } +} \ No newline at end of file diff --git a/src/plugins/MangaGeneric.cpp b/src/plugins/MangaGeneric.cpp index a2608ab..29480da 100644 --- a/src/plugins/MangaGeneric.cpp +++ b/src/plugins/MangaGeneric.cpp @@ -151,10 +151,7 @@ namespace QuickMedia { args.push_back({ "-X", "POST" }); args.push_back({ "-H", "x-requested-with: XMLHttpRequest" }); for(const MangaFormDataStr &form : form_data) { - std::string form_value = form.value; - if(form_value[0] == '@') - form_value = "\\" + form_value; - args.push_back({ "-F", std::string(form.key) + "=" + form_value }); + args.push_back({ "--form-string", std::string(form.key) + "=" + form.value }); } assert(search_query.json_handler); diff --git a/src/plugins/Saucenao.cpp b/src/plugins/Saucenao.cpp index e8d8357..fd752c5 100644 --- a/src/plugins/Saucenao.cpp +++ b/src/plugins/Saucenao.cpp @@ -7,10 +7,7 @@ namespace QuickMedia { if(is_local) { additional_args.push_back({ "-F", "file=@" + path }); } else { - std::string url = path; - if(url[0] == '@') - url = "\\" + url; - additional_args.push_back({ "-F", "url=" + url }); + additional_args.push_back({ "--form-string", "url=" + path }); } std::string website_data; -- cgit v1.2.3