diff options
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | icons/readm_launcher.png | bin | 0 -> 5951 bytes | |||
-rw-r--r-- | images/readm_logo.png | bin | 0 -> 7797 bytes | |||
-rw-r--r-- | plugins/ImageBoard.hpp | 1 | ||||
-rw-r--r-- | plugins/MangaGeneric.hpp | 37 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 63 | ||||
-rw-r--r-- | src/plugins/Fourchan.cpp | 7 | ||||
-rw-r--r-- | src/plugins/MangaGeneric.cpp | 66 |
8 files changed, 143 insertions, 35 deletions
@@ -1,6 +1,6 @@ # QuickMedia A dmenu-inspired native client for web services. -Currently supported web services: `youtube`, `spotify (podcasts)`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `4chan`, `matrix` and _others_.\ +Currently supported web services: `youtube`, `spotify (podcasts)`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `4chan`, `matrix` and _others_.\ **Note:** file-manager is early in progress.\ Config data, including manga progress is stored under `$HOME/.config/quickmedia`.\ Cache is stored under `$HOME/.cache/quickmedia`. @@ -8,7 +8,7 @@ Cache is stored under `$HOME/.cache/quickmedia`. ``` usage: quickmedia <plugin> [--use-system-mpv-config] [--dir <directory>] [-e <window>] OPTIONS: - plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, youtube, spotify, soundcloud, nyaa.si, matrix, file-manager or stdin + plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, youtube, spotify, soundcloud, nyaa.si, matrix, 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/icons/readm_launcher.png b/icons/readm_launcher.png Binary files differnew file mode 100644 index 0000000..43eeccf --- /dev/null +++ b/icons/readm_launcher.png diff --git a/images/readm_logo.png b/images/readm_logo.png Binary files differnew file mode 100644 index 0000000..2c76d3f --- /dev/null +++ b/images/readm_logo.png diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index b37448b..6b287b6 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -10,6 +10,7 @@ namespace QuickMedia { //FILE_TOO_LARGE, NO_SUCH_FILE, FILE_TYPE_NOT_ALLOWED, + UPLOAD_FAILED, ERR }; diff --git a/plugins/MangaGeneric.hpp b/plugins/MangaGeneric.hpp index e313c97..dcd3544 100644 --- a/plugins/MangaGeneric.hpp +++ b/plugins/MangaGeneric.hpp @@ -4,13 +4,25 @@ #include <functional> namespace QuickMedia { + struct MangaFormData { + const char *key; + const char *value; + }; + + struct MangaFormDataStr { + const char *key; + std::string value; + }; + + using SearchQueryJsonHandler = std::function<BodyItems(Json::Value&)>; struct SearchQuery { const char *search_template = nullptr; int page_start = 0; + std::vector<MangaFormData> form_data; + bool is_post = false; + SearchQueryJsonHandler json_handler = nullptr; }; - // If |url_contains| is null, then any matching query is added. If |title_field| is "text", then the inner text is used. - // If |url_field| is null, then the current page is used instead. struct TextQuery { const char *html_query = nullptr; const char *title_field = nullptr; @@ -98,44 +110,29 @@ namespace QuickMedia { const char* get_title() const override { return "Search"; } bool search_is_filter() override { return false; } SearchResult search(const std::string &str, BodyItems &result_items) override; - PluginResult get_page(const std::string &url, BodyItems &result_items); + PluginResult get_page(const std::string &url, bool is_post, const std::vector<MangaFormDataStr> &form_data, BodyItems &result_items); PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; - // Add a %s where the search query should be inserted into |search_template| and add a %p where the page number should be inserted, for example: - // example.com/search?q=%s&page=%p - // This is required. MangaGenericSearchPage& search_handler(const char *search_template, int page_start); - // This is required. + MangaGenericSearchPage& search_post_handler(const char *url, std::vector<MangaFormData> form_data, SearchQueryJsonHandler result_handler); MangaGenericSearchPage& text_handler(std::vector<TextQuery> queries); - // This is optional. MangaGenericSearchPage& description_handler(std::vector<DescriptionQuery> queries); - // This is optional. MangaGenericSearchPage& thumbnail_handler(std::vector<ThumbnailQuery> queries); - // This is optional. MangaGenericSearchPage& authors_handler(std::vector<AuthorsQuery> queries); - // If |url_contains| is null, then any matching query is added. If |title_field| is "text", then the inner text is used. - // This is required. + MangaGenericSearchPage& list_chapters_handler(const char *html_query, const char *title_field, const char *url_field, const char *url_contains); - // If |field_contains| is null, then any matching query is added. If |field_name| is "text", then the inner text is used. - // This is optional. MangaGenericSearchPage& list_chapters_uploaded_time_handler(const char *html_query, const char *field_name, const char *field_contains); - // If |field_contains| is null, then any matching query is added. If |field_name| is "text", then the inner text is used. - // This or |list_page_images_pagination_handler| or |list_page_images_custom_handler| is required. MangaGenericSearchPage& list_page_images_handler(const char *html_query, const char *field_name, const char *field_contains, ListPageImagesQueryPost post_handler = nullptr); - // If |image_field_contains| is null, then any matching query is added. If |pages_field_name| or |image_field_name| is "text", then the inner text is used. - // The last matching pages html query item is chosen as the number of pages. - // This or |list_page_images_handler| or |list_page_images_custom_handler| is required. MangaGenericSearchPage& list_page_images_pagination_handler( const char *pages_html_query, const char *pages_field_name, const char *image_html_query, const char *image_field_name, const char *image_field_contains, const char *next_page_html_query, const char *next_page_field_name, const char *next_page_field_contains); - // This or |list_page_images_handler| or |list_page_images_pagination_handler| is required. MangaGenericSearchPage& list_page_images_custom_handler(ListPageCustomHandler handler); // For example: mangasite.com/manga/204353&f=23 diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 64f326b..aebe86e 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -61,6 +61,7 @@ static const std::pair<const char*, const char*> valid_plugins[] = { std::make_pair("mangatown", "mangatown_logo.png"), std::make_pair("mangakatana", "mangakatana_logo.png"), std::make_pair("mangadex", "mangadex_logo.png"), + std::make_pair("readm", "readm_logo.png"), std::make_pair("manga", nullptr), std::make_pair("youtube", "yt_logo_rgb_dark_small.png"), std::make_pair("spotify", "spotify_logo.png"), @@ -446,7 +447,7 @@ namespace QuickMedia { static void usage() { fprintf(stderr, "usage: quickmedia <plugin> [--no-video] [--use-system-mpv-config] [--dir <directory>] [-e <window>]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, pornhub, spankbang, xvideos, xhamster, youtube, spotify, soundcloud, nyaa.si, matrix, file-manager or stdin\n"); + fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, youtube, spotify, soundcloud, nyaa.si, matrix, 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"); @@ -461,7 +462,7 @@ namespace QuickMedia { } static bool is_manga_plugin(const char *plugin_name) { - return strcmp(plugin_name, "manga") == 0 || strcmp(plugin_name, "manganelo") == 0 || strcmp(plugin_name, "manganelos") == 0 || strcmp(plugin_name, "mangatown") == 0 || strcmp(plugin_name, "mangakatana") == 0 || strcmp(plugin_name, "mangadex") == 0; + return strcmp(plugin_name, "manga") == 0 || strcmp(plugin_name, "manganelo") == 0 || strcmp(plugin_name, "manganelos") == 0 || strcmp(plugin_name, "mangatown") == 0 || strcmp(plugin_name, "mangakatana") == 0 || strcmp(plugin_name, "mangadex") == 0 || strcmp(plugin_name, "readm") == 0; } static std::shared_ptr<BodyItem> create_launcher_body_item(const char *title, const char *plugin_name, const std::string &thumbnail_url) { @@ -823,6 +824,42 @@ namespace QuickMedia { .manga_id_handler("/manga/", nullptr); } + static void add_readm_handlers(MangaGenericSearchPage *manga_generic_search_page) { + manga_generic_search_page->search_post_handler("https://readm.org/service/search", {{"dataType", "json"}, {"phrase", "%s"}}, + [](Json::Value &json_root) { + BodyItems result_items; + if(!json_root.isObject()) + return result_items; + + const Json::Value &manga_json = json_root["manga"]; + if(!manga_json.isArray()) + return result_items; + + for(const Json::Value &item_json : manga_json) { + if(!item_json.isObject()) + continue; + + const Json::Value &title_json = item_json["title"]; + const Json::Value &url_json = item_json["url"]; + const Json::Value &image_json = item_json["image"]; + if(!title_json.isString() || !url_json.isString()) + continue; + + auto body_item = BodyItem::create(strip(title_json.asString())); + body_item->url = strip(url_json.asString()); + if(image_json.isString()) + body_item->thumbnail_url = strip(image_json.asString()); + result_items.push_back(std::move(body_item)); + } + + return result_items; + }) + .list_chapters_handler("//div[class='episodes-list']//a", "text", "href", "/manga/") + .list_chapters_uploaded_time_handler("//div[class='episodes-list']//td[class='episode-date']", "text", nullptr) + .list_page_images_handler("//div[id='content']//img", "src", "/chapter_files/") + .manga_id_handler("/manga/", "/"); + } + static void add_pornhub_handlers(MediaGenericSearchPage *media_generic_search_page) { media_generic_search_page->search_handler("https://www.pornhub.com/video/search?search=%s&page=%p", 1) .text_handler({{"//div[class='nf-videos']//div[class='phimage']//a", "title", "href", "/view_video.php"}}) @@ -873,13 +910,14 @@ namespace QuickMedia { const Json::Value &title_json = json_item["tf"]; const Json::Value &url_json = json_item["u"]; const Json::Value &thumbnail_url_json = json_item["i"]; - if(!title_json.isString() || !url_json.isString() || !thumbnail_url_json.isString()) + if(!title_json.isString() || !url_json.isString()) continue; MediaRelatedItem related_item; related_item.title = title_json.asString(); related_item.url = url_json.asString(); - related_item.thumbnail_url = thumbnail_url_json.asString(); + if(thumbnail_url_json.isString()) + related_item.thumbnail_url = thumbnail_url_json.asString(); related_items.push_back(std::move(related_item)); } @@ -929,6 +967,7 @@ namespace QuickMedia { pipe_body->items.push_back(create_launcher_body_item("Manganelo", "manganelo", resources_root + "icons/manganelo_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("Manganelos", "manganelos", resources_root + "icons/manganelos_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("Mangatown", "mangatown", resources_root + "icons/mangatown_launcher.png")); + pipe_body->items.push_back(create_launcher_body_item("Readm", "readm", resources_root + "icons/readm_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("Matrix", "matrix", resources_root + "icons/matrix_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("Nyaa.si", "nyaa.si", resources_root + "icons/nyaa_si_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("Soundcloud", "soundcloud", resources_root + "icons/soundcloud_launcher.png")); @@ -972,6 +1011,14 @@ namespace QuickMedia { auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA); tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)}); + } else if(strcmp(plugin_name, "readm") == 0) { + auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "https://readm.org/"); + add_readm_handlers(search_page.get()); + tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); + + auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); + auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA); + tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)}); } else if(strcmp(plugin_name, "manga") == 0) { auto manganelo = std::make_unique<ManganeloSearchPage>(this); auto manganelos = std::make_unique<MangaGenericSearchPage>(this, "manganelos", "http://manganelos.com/"); @@ -980,12 +1027,15 @@ namespace QuickMedia { add_mangatown_handlers(mangatown.get()); auto mangakatana = std::make_unique<MangaGenericSearchPage>(this, "mangakatana", "https://mangakatana.com/", false); add_mangakatana_handlers(mangakatana.get()); + auto readm = std::make_unique<MangaGenericSearchPage>(this, "readm", "https://readm.org/"); + add_readm_handlers(readm.get()); std::vector<MangaPlugin> pages; pages.push_back({std::move(manganelo), "Manganelo", "manganelo", resources_root + "images/" + get_plugin_logo_name("manganelo")}); pages.push_back({std::move(manganelos), "Manganelos", "manganelos", resources_root + "images/" + get_plugin_logo_name("manganelos")}); pages.push_back({std::move(mangatown), "Mangatown", "mangatown", resources_root + "images/" + get_plugin_logo_name("mangatown")}); pages.push_back({std::move(mangakatana), "Mangakatana", "mangakatana", resources_root + "images/" + get_plugin_logo_name("mangakatana")}); + pages.push_back({std::move(readm), "Readm", "readm", resources_root + "images/" + get_plugin_logo_name("readm")}); // TODO: Add mangadex tabs.push_back(Tab{create_body(), std::make_unique<MangaCombinedSearchPage>(this, std::move(pages)), create_search_bar("Search...", 400)}); @@ -1271,6 +1321,8 @@ namespace QuickMedia { body_item->url = "https://mangatown.com/manga/" + base64_decode(filename.string()); else if(strcmp(plugin_name, "mangakatana") == 0) body_item->url = "https://mangakatana.com/manga/" + base64_decode(filename.string()); + else if(strcmp(plugin_name, "readm") == 0) + body_item->url = "https://readm.org/manga/" + base64_decode(filename.string()); else fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n"); history_items.push_back(std::move(body_item)); @@ -3102,6 +3154,9 @@ namespace QuickMedia { } else if(post_result == PostResult::FILE_TYPE_NOT_ALLOWED) { show_notification("QuickMedia", "Failed to post comment because you are trying to upload a file of a type that is not allowed", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; + } else if(post_result == PostResult::UPLOAD_FAILED) { + show_notification("QuickMedia", "Failed to post comment because file upload failed", Urgency::CRITICAL); + navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::ERR) { show_notification("QuickMedia", "Failed to post comment", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index 5e6a970..34cb8bf 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -540,12 +540,15 @@ 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}, + CommandArg{"-F", "com=" + comment_fixed}, CommandArg{"-F", "mode=regist"} }; @@ -583,6 +586,8 @@ namespace QuickMedia { return PostResult::TRY_AGAIN; if(response.find("Audio streams are not allowed") != std::string::npos) return PostResult::FILE_TYPE_NOT_ALLOWED; + if(response.find("Error: Upload failed") != std::string::npos) + return PostResult::UPLOAD_FAILED; return PostResult::ERR; } diff --git a/src/plugins/MangaGeneric.cpp b/src/plugins/MangaGeneric.cpp index 48533b8..a6df1c1 100644 --- a/src/plugins/MangaGeneric.cpp +++ b/src/plugins/MangaGeneric.cpp @@ -162,11 +162,32 @@ namespace QuickMedia { return plugin_result_to_search_result(get_page(str, 0, result_items)); } - PluginResult MangaGenericSearchPage::get_page(const std::string &url, BodyItems &result_items) { + PluginResult MangaGenericSearchPage::get_page(const std::string &url, bool is_post, const std::vector<MangaFormDataStr> &form_data, BodyItems &result_items) { std::vector<CommandArg> args; if(!website_url.empty()) args.push_back({ "-H", "referer: " + website_url }); + if(is_post) { + 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 }); + } + + assert(search_query.json_handler); + std::string err_msg; + Json::Value json_root; + if(download_json(json_root, url, args, true, fail_on_http_error ? nullptr : &err_msg) != DownloadResult::OK) + return PluginResult::NET_ERR; + + result_items = search_query.json_handler(json_root); + body_items_prepend_website_url(result_items, website_url); + return PluginResult::OK; + } + std::string target_url; std::string website_data; if(download_to_string(url, website_data, args, true, fail_on_http_error) != DownloadResult::OK) @@ -266,10 +287,27 @@ namespace QuickMedia { } PluginResult MangaGenericSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { - std::string url = search_query.search_template; - string_replace_all(url, "%s", url_param_encode(str)); - string_replace_all(url, "%p", std::to_string(search_query.page_start + page)); - return get_page(url, result_items); + if(search_query.is_post) { + if(page != 0) + return PluginResult::OK; + + std::vector<MangaFormDataStr> form_data(search_query.form_data.size()); + for(size_t i = 0; i < form_data.size(); ++i) { + MangaFormDataStr &form = form_data[i]; + form.key = search_query.form_data[i].key; + form.value = search_query.form_data[i].value; + string_replace_all(form.value, "%s", str); + } + return get_page(search_query.search_template, search_query.is_post, form_data, result_items); + } else { + std::string url = search_query.search_template; + string_replace_all(url, "%s", url_param_encode(str)); + size_t num_replaced = string_replace_all(url, "%p", std::to_string(search_query.page_start + page)); + if(num_replaced == 0 && page != 0) + return PluginResult::OK; + else + return get_page(url, false, {}, result_items); + } } PluginResult MangaGenericSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { @@ -396,7 +434,7 @@ namespace QuickMedia { } PluginResult MangaGenericCreatorPage::lazy_fetch(BodyItems &result_items) { - return search_page->get_page(creator.url, result_items); + return search_page->get_page(creator.url, false, {}, result_items); } static bool is_number(const char *str) { @@ -680,6 +718,18 @@ namespace QuickMedia { MangaGenericSearchPage& MangaGenericSearchPage::search_handler(const char *search_template, int page_start) { search_query.search_template = search_template; search_query.page_start = page_start; + search_query.form_data.clear(); + search_query.is_post = false; + search_query.json_handler = nullptr; + return *this; + } + + MangaGenericSearchPage& MangaGenericSearchPage::search_post_handler(const char *url, std::vector<MangaFormData> form_data, SearchQueryJsonHandler result_handler) { + search_query.search_template = url; + search_query.page_start = 1; + search_query.form_data = std::move(form_data); + search_query.is_post = true; + search_query.json_handler = std::move(result_handler); return *this; } @@ -723,7 +773,7 @@ namespace QuickMedia { list_page_query.images_query.html_query = html_query; list_page_query.images_query.field_name = field_name; list_page_query.images_query.field_contains = field_contains; - list_page_query.images_query.post_handler = post_handler; + list_page_query.images_query.post_handler = std::move(post_handler); return *this; } @@ -749,7 +799,7 @@ namespace QuickMedia { MangaGenericSearchPage& MangaGenericSearchPage::list_page_images_custom_handler(ListPageCustomHandler handler) { assert(handler); list_page_query.type = ListPageQueryType::CUSTOM; - list_page_query.custom_query.handler = handler; + list_page_query.custom_query.handler = std::move(handler); return *this; } |