aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
m---------depends/html-parser0
m---------depends/html-search0
-rw-r--r--plugins/HotExamples.hpp36
-rw-r--r--src/NetUtils.cpp39
-rw-r--r--src/QuickMedia.cpp9
-rw-r--r--src/plugins/Fourchan.cpp23
-rw-r--r--src/plugins/HotExamples.cpp139
-rw-r--r--src/plugins/MangaGeneric.cpp5
-rw-r--r--src/plugins/Saucenao.cpp5
10 files changed, 230 insertions, 30 deletions
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 <plugin> [--use-system-mpv-config] [--dir <directory>] [-e <window>] [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
-Subproject 11d3632fe4508bfd2f668b7b1c4d75a88cd6449
+Subproject 199ca9297d2ef4ff58db4b0c948eb384deceb61
diff --git a/depends/html-search b/depends/html-search
-Subproject cc37a6af5283b4e4c052427fd0d2940ebce5fc8
+Subproject b62b5aa8a262d3f4e2c53c5fd5b67410106bca8
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<Tab> &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<Tab> &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<Tab>&) 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<HtmlUnescapeSequence, 7> unescape_sequences = {
+ html_unescape_sequence_numbers(str);
+
+ const std::array<HtmlUnescapeSequence, 4> unescape_sequences = {
HtmlUnescapeSequence { "&quot;", "\"" },
- HtmlUnescapeSequence { "&#039;", "'" },
- HtmlUnescapeSequence { "&#39;", "'" },
- HtmlUnescapeSequence { "&#10;", "\n" },
HtmlUnescapeSequence { "&lt;", "<" },
HtmlUnescapeSequence { "&gt;", ">" },
HtmlUnescapeSequence { "&amp;", "&" } // 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<const char*, const char*> 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 <plugin> [--no-video] [--use-system-mpv-config] [--dir <directory>] [-e <window>] [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<HotExamplesLanguageSelectPage>(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "file-manager") == 0) {
auto file_manager_page = std::make_unique<FileManagerPage>(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<CommandArg> 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<CommandArg> 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 <quickmedia/HtmlSearch.h>
+
+namespace QuickMedia {
+ static std::shared_ptr<BodyItem> 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<Tab> &result_tabs) {
+ result_tabs.push_back({ create_body(), std::make_unique<HotExamplesSearchPage>(program, url), create_search_bar("Search...", 500) });
+ return PluginResult::OK;
+ }
+
+ SearchResult HotExamplesSearchPage::search(const std::string &str, BodyItems &result_items) {
+ std::vector<CommandArg> 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<Tab> &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<HotExamplesCodeExamplesPage>(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;