aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--icons/readm_launcher.pngbin0 -> 5951 bytes
-rw-r--r--images/readm_logo.pngbin0 -> 7797 bytes
-rw-r--r--plugins/ImageBoard.hpp1
-rw-r--r--plugins/MangaGeneric.hpp37
-rw-r--r--src/QuickMedia.cpp63
-rw-r--r--src/plugins/Fourchan.cpp7
-rw-r--r--src/plugins/MangaGeneric.cpp66
8 files changed, 143 insertions, 35 deletions
diff --git a/README.md b/README.md
index a7a4a2d..537b233 100644
--- a/README.md
+++ b/README.md
@@ -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
new file mode 100644
index 0000000..43eeccf
--- /dev/null
+++ b/icons/readm_launcher.png
Binary files differ
diff --git a/images/readm_logo.png b/images/readm_logo.png
new file mode 100644
index 0000000..2c76d3f
--- /dev/null
+++ b/images/readm_logo.png
Binary files differ
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;
}